diff --git a/roadmap.md b/roadmap.md index 4d90f7e..a1af41b 100644 --- a/roadmap.md +++ b/roadmap.md @@ -6,9 +6,6 @@ ### Major -- Key-level access to full anchors - - this could provide extra variables `padding`, `spread`, `splay` for custom layout purposes - - make row anchors cumulative, too (like columns), so fingers arcs and other edits can happen - Restructure pcb point/footprint filtering - Use the same `what`/`where` infrastructure as outlines - Collapse params/nets/anchors into a single hierarchy from the user's POV @@ -30,12 +27,8 @@ - Allow footprints to publish outlines - Make these usable in the `outlines` section through a new `what` - 3D orient for cases -- Allow a generic `adjust` field for outlines that accepts an anchor - - This could swallow `origin` from `outline` -- Post-process anchor for global (post-mirror!) orient/shift/rotate for everything - Even more extreme anchor stuff - Checkpoints, intersects, distances, weighted combinations? -- Allow both object (as well as arrays) in multiple anchor refs - SVG input (for individual outlines, or even combinations parsed by line color, etc.) - And once that's done, possibly even STL or other input for cases or pcb renders - Support text silk output to PCBs (in configurable fonts, through SVG?) @@ -44,7 +37,6 @@ - Support curves (arcs as well as Béziers) in polygons - Add snappable line footprint - Figure out a manual, but still reasonably comfortable routing method directly from the config -- Add filleting syntax with `@`? - Eeschema support for pcbs - Generate ZMK shield from config - Export **to** KLE? @@ -53,11 +45,7 @@ - Look into kicad 5 vs. 6 output format - Update json schema and add syntax highlight to editors - Support different netclasses -- `round`, `pointy` and `beveled` symbolic constants for expand joint types - - Also, string shorthands like `3)`, `5>` and `10]` - Allow a potential filter for filleting (only on angles =90°, <45°, left turn vs. right turn when going clockwise, etc.) -- Support cumulative handling of outline parts (i.e., add `fillet` as an generic option that applies to all the parts up to that point) - - Similar with adjust ### Patch diff --git a/src/anchor.js b/src/anchor.js index 0d5ae4d..73fde64 100644 --- a/src/anchor.js +++ b/src/anchor.js @@ -69,7 +69,6 @@ const anchor = exports.parse = (raw, name, points={}, default_point=new Point(), } else { point = anchor(raw.ref, `${name}.ref`, points, default_point, mirror)(units) } - } if (raw.aggregate !== undefined) { @@ -95,10 +94,7 @@ const anchor = exports.parse = (raw, name, points={}, default_point=new Point(), // simple case: number gets added to point rotation if (a.type(config)(units) == 'number') { let angle = a.sane(config, name, 'number')(units) - if (point.meta.mirrored) { - angle = -angle - } - point.r += angle + point.rotate(angle, false) // recursive case: points turns "towards" target anchor } else { const target = anchor(config, name, points, default_point, mirror)(units) @@ -111,10 +107,7 @@ const anchor = exports.parse = (raw, name, points={}, default_point=new Point(), } if (raw.shift !== undefined) { let xyval = a.wh(raw.shift, `${name}.shift`)(units) - if (point.meta.mirrored) { - xyval[0] = -xyval[0] - } - point.shift(xyval, true) + point.shift(xyval) } if (raw.rotate !== undefined) { rotator(raw.rotate, `${name}.rotate`, point) diff --git a/src/operation.js b/src/operation.js index c8edc36..fa210fb 100644 --- a/src/operation.js +++ b/src/operation.js @@ -1,10 +1,16 @@ const op_prefix = exports.op_prefix = str => { + + const prefix = str[0] const suffix = str.slice(1) - if (str.startsWith('+')) return {name: suffix, operation: 'add'} - if (str.startsWith('-')) return {name: suffix, operation: 'subtract'} - if (str.startsWith('~')) return {name: suffix, operation: 'intersect'} - if (str.startsWith('^')) return {name: suffix, operation: 'stack'} - return {name: str, operation: 'add'} + const result = {name: suffix, operation: 'add'} + + if (prefix == '+') ; // noop + else if (prefix == '-') result.operation = 'subtract' + else if (prefix == '~') result.operation = 'intersect' + else if (prefix == '^') result.operation = 'stack' + else result.name = str // no prefix, so the name was the whole string + + return result } exports.operation = (str, choices={}, order=Object.keys(choices)) => { diff --git a/src/outlines.js b/src/outlines.js index d2e417f..8afcd9b 100644 --- a/src/outlines.js +++ b/src/outlines.js @@ -41,7 +41,7 @@ const rectangle = (config, name, points, outlines, units) => { const bevel = a.sane(config.bevel || 0, `${name}.bevel`, 'number')(rec_units) // return shape function and its units - return [(point, bound) => { + return [() => { const error = (dim, val) => `Rectangle for "${name}" isn't ${dim} enough for its corner and bevel (${val} - 2 * ${corner} - 2 * ${bevel} <= 0)!` const [w, h] = size @@ -66,13 +66,9 @@ const rectangle = (config, name, points, outlines, units) => { } if (corner > 0) rect = m.model.outline(rect, corner, 0) rect = m.model.moveRelative(rect, [-cw/2, -ch/2]) - if (bound) { - const bbox = {high: [w/2, h/2], low: [-w/2, -h/2]} - rect = binding(rect, bbox, point, rec_units) - } - rect = point.position(rect) + const bbox = {high: [w/2, h/2], low: [-w/2, -h/2]} - return rect + return [rect, bbox] }, rec_units] } @@ -86,14 +82,10 @@ const circle = (config, name, points, outlines, units) => { }, units) // return shape function and its units - return [(point, bound) => { + return [() => { let circle = u.circle([0, 0], radius) - if (bound) { - const bbox = {high: [radius, radius], low: [-radius, -radius]} - circle = binding(circle, bbox, point, circ_units) - } - circle = point.position(circle) - return circle + const bbox = {high: [radius, radius], low: [-radius, -radius]} + return [circle, bbox] }, circ_units] } @@ -104,10 +96,10 @@ const polygon = (config, name, points, outlines, units) => { const poly_points = a.sane(config.points, `${name}.points`, 'array')() // return shape function and its units - return [(point, bound) => { + return [point => { const parsed_points = [] - // the point starts at [0, 0] as it will be positioned later - // but we keep the metadata for potential mirroring purposes + // the poly starts at [0, 0] as it will be positioned later + // but we keep the point metadata for potential mirroring purposes let last_anchor = new Point(0, 0, 0, point.meta) let poly_index = -1 for (const poly_point of poly_points) { @@ -116,52 +108,24 @@ const polygon = (config, name, points, outlines, units) => { parsed_points.push(last_anchor.p) } let poly = u.poly(parsed_points) - if (bound) { - const bbox = u.bbox(parsed_points) - poly = binding(poly, bbox, point, units) - } - poly = point.position(poly) - return poly + const bbox = u.bbox(parsed_points) + return [poly, bbox] }, units] } const outline = (config, name, points, outlines, units) => { // prepare params - a.unexpected(config, `${name}`, ['name', 'fillet', 'expand', 'origin', 'scale']) + a.unexpected(config, `${name}`, ['name', 'origin']) a.assert(outlines[config.name], `Field "${name}.name" does not name an existing outline!`) - const fillet = a.sane(config.fillet || 0, `${name}.fillet`, 'number')(units) - const expand = a.sane(config.expand || 0, `${name}.expand`, 'number')(units) - const joints = a.in(a.sane(config.joints || 0, `${name}.joints`, 'number')(units), `${name}.joints`, [0, 1, 2]) const origin = anchor(config.origin || {}, `${name}.origin`, points)(units) - const scale = a.sane(config.scale || 1, `${name}.scale`, 'number')(units) - + // return shape function and its units - return [(point, bound) => { + return [() => { let o = u.deepcopy(outlines[config.name]) o = origin.unposition(o) - - if (scale !== 1) { - o = m.model.scale(o, scale) - } - - if (fillet) { - for (const [index, chain] of m.model.findChains(o).entries()) { - o.models[`fillet_${index}`] = m.chain.fillet(chain, fillet) - } - } - - if (expand) { - o = m.model.outline(o, Math.abs(expand), joints, (expand < 0), {farPoint: u.farPoint}) - } - - if (bound) { - const bbox = m.measure.modelExtents(o) - o = binding(o, bbox, point, units) - } - - o = point.position(o) - return o + const bbox = m.measure.modelExtents(o) + return [o, bbox] }, units] } @@ -172,6 +136,29 @@ const whats = { outline } +const expand_shorthand = (config, units) => { + if (a.type(config.expand)(units) == 'string') { + const prefix = config.expand.slice(0, -1) + const suffix = config.expand.slice(-1) + let expand = suffix + let joints = 0 + + if (suffix == ')') ; // noop + else if (suffix == '>') joints = 1 + else if (suffix == ']') joints = 2 + else expand = config.expand + + config.expand = parseFloat(expand) + config.joints = config.joints || joints + } + + if (a.type(config.joints)(units) == 'string') { + if (config.joints == 'round') config.joints = 0 + if (config.joints == 'pointy') config.joints = 1 + if (config.joints == 'beveled') config.joints = 2 + } +} + exports.parse = (config = {}, points = {}, units = {}) => { // output outlines will be collected here @@ -205,10 +192,18 @@ exports.parse = (config = {}, points = {}, units = {}) => { const what = a.in(part.what || 'outline', `${name}.what`, ['rectangle', 'circle', 'polygon', 'outline']) const bound = !!part.bound const mirror = a.sane(part.mirror || false, `${name}.mirror`, 'boolean')() + // `where` is delayed until we have all, potentially what-dependent units // default where is [0, 0], as per filter parsing const original_where = part.where // need to save, so the delete's don't get rid of it below const where = units => filter(original_where, `${name}.where`, points, units, mirror) + + const adjust = anchor(part.adjust || {}, `${name}.adjust`, points)(units) + const fillet = a.sane(part.fillet || 0, `${name}.fillet`, 'number')(units) + expand_shorthand(part, units) + const expand = a.sane(part.expand || 0, `${name}.expand`, 'number')(units) + const joints = a.in(a.sane(part.joints || 0, `${name}.joints`, 'number')(units), `${name}.joints`, [0, 1, 2]) + const scale = a.sane(part.scale || 1, `${name}.scale`, 'number')(units) // these keys are then removed, so ops can check their own unexpected keys without interference delete part.operation @@ -216,15 +211,41 @@ exports.parse = (config = {}, points = {}, units = {}) => { delete part.bound delete part.mirror delete part.where + delete part.adjust + delete part.fillet + delete part.expand + delete part.joints + delete part.scale // a prototype "shape" maker (and its units) are computed const [shape_maker, shape_units] = whats[what](part, name, points, outlines, units) // and then the shape is repeated for all where positions for (const w of where(shape_units)) { - const shape = shape_maker(w, bound) + const point = w.clone().shift(adjust.p).rotate(adjust.r, false) + let [shape, bbox] = shape_maker(point) // point is passed for mirroring metadata only... + if (bound) { + shape = binding(shape, bbox, point, shape_units) + } + shape = point.position(shape) // ...actual positioning happens here outlines[outline_name] = operation(outlines[outline_name], shape) } + + if (scale !== 1) { + outlines[outline_name] = m.model.scale(outlines[outline_name], scale) + } + + if (expand) { + outlines[outline_name] = m.model.outline( + outlines[outline_name], Math.abs(expand), joints, (expand < 0), {farPoint: u.farPoint} + ) + } + + if (fillet) { + for (const [index, chain] of m.model.findChains(outlines[outline_name]).entries()) { + outlines[outline_name].models[`fillet_${part_name}_${index}`] = m.chain.fillet(chain, fillet) + } + } } // final adjustments diff --git a/src/point.js b/src/point.js index 9e32b7c..d6d5e12 100644 --- a/src/point.js +++ b/src/point.js @@ -25,6 +25,7 @@ module.exports = class Point { } shift(s, relative=true) { + s[0] *= this.meta.mirrored ? -1 : 1 if (relative) { s = m.point.rotate(s, this.r) } @@ -34,7 +35,10 @@ module.exports = class Point { } rotate(angle, origin=[0, 0]) { - this.p = m.point.rotate(this.p, angle, origin) + angle *= this.meta.mirrored ? -1 : 1 + if (origin) { + this.p = m.point.rotate(this.p, angle, origin) + } this.r += angle return this }