diff --git a/README.md b/README.md index e588c2a..4f76b69 100644 --- a/README.md +++ b/README.md @@ -324,19 +324,21 @@ This is where the following section comes into play: ```yaml glue: - top: - left: - right: | num - bottom: - left: - right: | num - waypoints: - - percent: num - width: num | [num_left, num_right] - - ... - extra: - - - - ... + glue_name: + top: + left: + right: | num + bottom: + left: + right: | num + waypoints: + - percent: num + width: num | [num_left, num_right] + - ... + extra: + - + - ... + ... ``` ...where an `` is (mostly) the same as it was for points: @@ -348,7 +350,7 @@ rotate: num # default = 0 relative: boolean # default = true ``` -The section's `top` and `bottom` are both formatted the same, and describe the center line's top and bottom intersections, respectively. +The `top` and `bottom` fields in each glue's section are both formatted the same, and describe the center line's top and bottom intersections, respectively. In a one-piece case, this means that we project a line from a left-side reference point (optionally rotated and translated), another from the right, and converge them to where they meet. Split designs can specify `right` as a single number to mean the x coordinate where the side should be "cut off". The `relative` flag means that the `shift` is interpreted in layout size units instead of mms (see below). @@ -366,7 +368,7 @@ If this is insufficient (maybe because it would leave holes), the `waypoints` ca Here, `percent` means the y coordinate along the centerline (going from the top intersection to the bottom intersection), and `width` means the offset on the x axis. If this is somehow _still_ insufficient (or there were problems with the binding phase), we can specify additional primitive shapes under the `extra` key (similarly to how we would use them in the exports; see below). -These are then added to what we have so far to finish out the glue. +These are then added to what we have so far to finish out the glue. TODO!
@@ -381,6 +383,8 @@ Now we can configure what we want to "export" as outlines from this phase, given - `middle` means an "ideal" version of the glue (meaning that instead of the `outline.glue` we defined above, we get `both` - `left` - `right`, so the _exact_ middle piece we would have needed to glue everything together - `both` means both sides, held together by the glue - `glue` is just the raw glue shape we defined above under `outline.glue` + - `tag: ` : optional tags to filter which points to consider in this step, where tags can be specified as key-level attributes. + - `glue: ` : the name of the glue to use, if applicable - `size: num | [num_x, num_y]` : the width/height of the rectangles to lay onto the points. Note that the `relative` flag for the glue declaration above meant this size as the basis of the shift. So during a `keys` layout with a size of 18, for example, a relative shift of `[.5, .5]` actually means `[9, 9]` in mms. - `corner: num # default = 0)` : corner radius of the rectangles - `bevel: num # default = 0)` : corner bevel of the rectangles, can be combined with rounding @@ -395,7 +399,7 @@ Now we can configure what we want to "export" as outlines from this phase, given - `radius: num` : the radius of the circle - `polygon` : an independent polygon primitive. Parameters: - `points: [, ...]` : the points of the polygon. Each `` can have its own `ref` and `shift`, all of which are still the same as above. If `ref` is unspecified, the previous point's will be assumed. For the first, it's `[0, 0]` by default. -- `ref` : a previously defined outline, see below. +- `outline` : a previously defined outline, see below. - `name: outline_name` : the name of the referenced outline Using these, we define exports as follows: diff --git a/src/assert.js b/src/assert.js index 111e0ba..75e2cce 100644 --- a/src/assert.js +++ b/src/assert.js @@ -1,4 +1,5 @@ const m = require('makerjs') +const u = require('./utils') const Point = require('./point') const assert = exports.assert = (exp, msg) => { @@ -79,11 +80,53 @@ exports.anchor = (raw, name, points={}, check_unexpected=true, default_point=new point.shift(xyval, true) } if (raw.rotate !== undefined) { - let rot = sane(raw.rotate || 0, name + '.rotate', 'number') - if (point.meta.mirrored) { - rot = -rot - } - point.r += rot + point.r += sane(raw.rotate || 0, name + '.rotate', 'number') } return point +} + +const extend_pair = exports.extend_pair = (to, from) => { + const to_type = type(to) + const from_type = type(from) + if (from === undefined || from === null) return to + if (from === '!!unset') return undefined + if (to_type != from_type) return from + if (from_type == 'object') { + const res = u.deepcopy(to) + for (const key of Object.keys(from)) { + res[key] = extend_pair(to[key], from[key]) + } + return res + } else if (from_type == 'array') { + const res = u.deepcopy(to) + for (const [i, val] of from.entries()) { + res[i] = extend_pair(res[i], val) + } + return res + } else return from +} + +exports.extend = (...args) => { + let res = args[0] + for (const arg of args) { + if (res == arg) continue + res = extend_pair(res, arg) + } + return res +} + +const inherit = exports.inherit = (config, name_prefix, name, set) => { + let result = u.deepcopy(config) + if (config.extends !== undefined) { + let list = config.extends + if (type(list) !== 'array') list = [list] + for (const item of list) { + const other = set[item] + assert(other, `Field "${name_prefix}.${name}" does not name a valid target!`) + result = extend_pair(inherit(other, name_prefix, config.extends, set), result) + + } + delete result.extends + } + return result } \ No newline at end of file diff --git a/src/outline.js b/src/outline.js index cce3a3e..51584ea 100644 --- a/src/outline.js +++ b/src/outline.js @@ -35,27 +35,32 @@ const layout = exports._layout = (config = {}, points = {}) => { // Glue config sanitization - a.detect_unexpected(config, 'outline.glue', ['top', 'bottom', 'waypoints', 'extra']) - + const parsed_glue = u.deepcopy(a.sane(config, 'outline.glue', 'object')) + for (let [gkey, gval] of Object.entries(parsed_glue)) { + gval = a.inherit(gval, 'outline.glue', gkey, config) + a.detect_unexpected(gval, `outline.glue.${gkey}`, ['top', 'bottom', 'waypoints', 'extra']) - - for (const y of ['top', 'bottom']) { - a.detect_unexpected(config[y], `outline.glue.${y}`, ['left', 'right']) - config[y].left = relative_anchor(config[y].left, `outline.glue.${y}.left`, points) - if (a.type(config[y].right) != 'number') { - config[y].right = relative_anchor(config[y].right, `outline.glue.${y}.right`, points) + for (const y of ['top', 'bottom']) { + a.detect_unexpected(gval[y], `outline.glue.${gkey}.${y}`, ['left', 'right']) + gval[y].left = relative_anchor(gval[y].left, `outline.glue.${gkey}.${y}.left`, points) + if (a.type(gval[y].right) != 'number') { + gval[y].right = relative_anchor(gval[y].right, `outline.glue.${gkey}.${y}.right`, points) + } } + + gval.waypoints = a.sane(gval.waypoints || [], `outline.glue.${gkey}.waypoints`, 'array') + let wi = 0 + gval.waypoints = gval.waypoints.map(w => { + const name = `outline.glue.${gkey}.waypoints[${++wi}]` + a.detect_unexpected(w, name, ['percent', 'width']) + w.percent = a.sane(w.percent, name + '.percent', 'number') + w.width = a.wh(w.width, name + '.width') + return w + }) + + parsed_glue[gkey] = gval } - config.waypoints = a.sane(config.waypoints || [], 'outline.glue.waypoints', 'array') - let wi = 0 - config.waypoints = config.waypoints.map(w => { - const name = `outline.glue.waypoints[${++wi}]` - a.detect_unexpected(w, name, ['percent', 'width']) - w.percent = a.sane(w.percent, name + '.percent', 'number') - w.width = a.wh(w.width, name + '.width') - return w - }) // TODO: handle glue.extra (or revoke it from the docs) @@ -63,8 +68,12 @@ const layout = exports._layout = (config = {}, points = {}) => { // Layout params sanitization - a.detect_unexpected(params, `${export_name}`, expected.concat(['side', 'size', 'corner', 'bevel', 'bound'])) + a.detect_unexpected(params, `${export_name}`, expected.concat(['side', 'tags', 'glue', 'size', 'corner', 'bevel', 'bound'])) const side = a.in(params.side, `${export_name}.side`, ['left', 'right', 'middle', 'both', 'glue']) + const tags = a.sane(params.tags || [], `${export_name}.tags`, 'array') + const default_glue_name = Object.keys(parsed_glue)[0] + const glue_def = parsed_glue[a.sane(params.glue || default_glue_name, `${export_name}.glue`, 'string')] + a.assert(glue_def, `Field "${export_name}.glue" does not name a valid glue!`) const size = a.wh(params.size, `${export_name}.size`) const corner = a.sane(params.corner || 0, `${export_name}.corner`, 'number') const bevel = a.sane(params.bevel || 0, `${export_name}.bevel`, 'number') @@ -77,6 +86,14 @@ const layout = exports._layout = (config = {}, points = {}) => { if (['left', 'right', 'middle', 'both'].includes(side)) { for (const [pname, p] of Object.entries(points)) { + // filter by tags, if necessary + if (tags.length) { + const source = p.meta.tags || {} + const point_tags = Object.keys(source).filter(t => !!source[t]) + const relevant = point_tags.some(pt => tags.includes(pt)) + if (!relevant) continue + } + let from_x = -size[0] / 2, to_x = size[0] / 2 let from_y = -size[1] / 2, to_y = size[1] / 2 @@ -130,15 +147,15 @@ const layout = exports._layout = (config = {}, points = {}) => { return u.line(from.p, to.p) } - - const tll = get_line(config.top.left) - const trl = get_line(config.top.right) + + const tll = get_line(glue_def.top.left) + const trl = get_line(glue_def.top.right) const tip = m.path.converge(tll, trl) const tlp = u.eq(tll.origin, tip) ? tll.end : tll.origin const trp = u.eq(trl.origin, tip) ? trl.end : trl.origin - const bll = get_line(config.bottom.left) - const brl = get_line(config.bottom.right) + const bll = get_line(glue_def.bottom.left) + const brl = get_line(glue_def.bottom.right) const bip = m.path.converge(bll, brl) const blp = u.eq(bll.origin, bip) ? bll.end : bll.origin const brp = u.eq(brl.origin, bip) ? brl.end : brl.origin @@ -146,7 +163,7 @@ const layout = exports._layout = (config = {}, points = {}) => { const left_waypoints = [] const right_waypoints = [] - for (const w of config.waypoints) { + for (const w of glue_def.waypoints) { const percent = w.percent / 100 const center_x = tip[0] + percent * (bip[0] - tip[0]) const center_y = tip[1] + percent * (bip[1] - tip[1]) @@ -157,7 +174,7 @@ const layout = exports._layout = (config = {}, points = {}) => { } let waypoints - const is_split = a.type(config.top.right) == 'number' + const is_split = a.type(glue_def.top.right) == 'number' if (is_split) { waypoints = [tip, tlp] .concat(left_waypoints) @@ -198,7 +215,7 @@ exports.parse = (config = {}, points = {}) => { for (const part of parts) { const name = `outline.exports.${key}[${++index}]` const expected = ['type', 'operation'] - part.type = a.in(part.type, `${name}.type`, ['keys', 'rectangle', 'circle', 'polygon', 'ref']) + part.type = a.in(part.type, `${name}.type`, ['keys', 'rectangle', 'circle', 'polygon', 'outline']) part.operation = a.in(part.operation || 'add', `${name}.operation`, ['add', 'subtract', 'intersect', 'stack']) let op = u.union @@ -240,10 +257,12 @@ exports.parse = (config = {}, points = {}) => { } arg = u.poly(parsed_points) break - case 'ref': + case 'outline': a.assert(outlines[part.name], `Field "${name}.name" does not name an existing outline!`) arg = u.deepcopy(outlines[part.name]) break + default: + throw new Error(`Field "${name}.type" (${part.type}) does not name a valid outline part type!`) } result = op(result, arg) diff --git a/src/pcb.js b/src/pcb.js index 711aa79..d5fe20b 100644 --- a/src/pcb.js +++ b/src/pcb.js @@ -143,6 +143,8 @@ const makerjs2kicad = exports._makerjs2kicad = (model, layer='Edge.Cuts') => { const footprint_types = require('./footprints') const footprint = exports._footprint = (config, name, points, net_indexer, point) => { + + if (config === false) return '' // config sanitization a.detect_unexpected(config, name, ['type', 'anchor', 'nets', 'params']) diff --git a/src/points.js b/src/points.js index 69b7e4e..c27156d 100644 --- a/src/points.js +++ b/src/points.js @@ -2,35 +2,6 @@ const m = require('makerjs') const u = require('./utils') const a = require('./assert') -const extend_pair = exports._extend_pair = (to, from) => { - const to_type = a.type(to) - const from_type = a.type(from) - if (from === undefined || from === null) return to - if (to_type != from_type) return from - if (from_type == 'object') { - const res = u.deepcopy(to) - for (const key of Object.keys(from)) { - res[key] = extend_pair(to[key], from[key]) - } - return res - } else if (from_type == 'array') { - const res = u.deepcopy(to) - for (const [i, val] of from.entries()) { - res[i] = extend_pair(res[i], val) - } - return res - } else return from -} - -const extend = exports._extend = (...args) => { - let res = args[0] - for (const arg of args) { - if (res == arg) continue - res = extend_pair(res, arg) - } - return res -} - const push_rotation = exports._push_rotation = (list, angle, origin) => { let candidate = origin for (const r of list) { @@ -139,7 +110,7 @@ const render_zone = exports._render_zone = (zone_name, zone, anchor, global_key) asym: 'both' } for (const row of Object.keys(actual_rows)) { - const key = extend( + const key = a.extend( default_key, global_key, zone_wide_key, @@ -148,7 +119,8 @@ const render_zone = exports._render_zone = (zone_name, zone, anchor, global_key) col.rows[row] || {} ) - key.name = key.name || `${col_name}_${row}` + key.name = key.name || `${zone_name}_${col_name}_${row}` + key.colrow = `${col_name}_${row}` key.shift = a.xy(key.shift, `${key.name}.shift`) key.rotate = a.sane(key.rotate, `${key.name}.rotate`, 'number') key.padding = a.sane(key.padding, `${key.name}.padding`, 'number') @@ -195,8 +167,6 @@ const render_zone = exports._render_zone = (zone_name, zone, anchor, global_key) return points } - - exports.parse = (config = {}) => { a.detect_unexpected(config, 'points', ['zones', 'key', 'rotate', 'mirror']) @@ -207,7 +177,11 @@ exports.parse = (config = {}) => { const zones = a.sane(config.zones || {}, 'points.zones', 'object') const global_key = a.sane(config.key || {}, 'points.key', 'object') - for (const [zone_name, zone] of Object.entries(zones)) { + for (let [zone_name, zone] of Object.entries(zones)) { + + // handle zone-level `extends` clauses + zone = a.inherit(zone, 'points.zones', zone_name, zones) + const anchor = a.anchor(zone.anchor || {}, `points.zones.${zone_name}.anchor`, points) points = Object.assign(points, render_zone(zone_name, zone, anchor, global_key)) } @@ -238,9 +212,11 @@ exports.parse = (config = {}) => { for (const [name, p] of Object.entries(points)) { if (p.meta.asym == 'left') continue const mp = p.clone().mirror(axis) - mp.meta = extend(mp.meta, mp.meta.mirror || {}) + mp.meta = a.extend(mp.meta, mp.meta.mirror || {}) mp.meta.mirrored = true - mirrored_points[`mirror_${name}`] = mp + const new_name = `mirror_${name}` + mp.meta.name = new_name + mirrored_points[new_name] = mp if (p.meta.asym == 'right') { p.meta.skip = true } diff --git a/test/fixtures/absolem.yaml b/test/fixtures/absolem.yaml index 430c429..d4a1751 100644 --- a/test/fixtures/absolem.yaml +++ b/test/fixtures/absolem.yaml @@ -10,9 +10,9 @@ points: rows: bottom: home: - bind: [,10] + bind: [,15] top: - bind: [,10] + bind: [,15] key: column_net: P1 ring: @@ -61,21 +61,35 @@ points: rows: bottom: bind: [10] - row_net: P7 + row_net: P16 mirror: - row_net: P16 + row_net: P7 home: bind: [10] - row_net: P6 + row_net: P14 mirror: - row_net: P14 + row_net: P6 top: - row_net: P5 + row_net: P15 mirror: - row_net: P15 + row_net: P5 + key: + tags: + s19: true + choc: + extends: matrix + columns: + pinky: + stagger: 1 + origin: [7, -8] + key: + padding: 18 + tags: + s19: false + s18: true thumbfan: anchor: - ref: inner_bottom + ref: matrix_inner_bottom shift: [-7, -19] columns: near: @@ -84,9 +98,11 @@ points: origin: [9.5, -9] rows: thumb: - bind: [10,1,,] + bind: [10,5,,] key: column_net: P2 + tags: + classic: true home: spread: 21.25 rotate: -28 @@ -96,17 +112,71 @@ points: bind: [,10,,15] key: column_net: P3 + tags: + classic: true + uniform: true far: rows: thumb: bind: [-1,,,5] key: column_net: P4 + tags: + classic: true rows: thumb: - row_net: P8 + row_net: P10 mirror: - row_net: P10 + row_net: P8 + unifar: + anchor: + ref: thumbfan_home_thumb + columns: + home_again: + rotate: -28 + origin: [9.5, -9] + key: + skip: true + far1u: + rows: + thumb: + bind: [-1,,,5] + key: + column_net: P4 + footprints: + diode: false + tags: + uniform: true + rows: + thumb: + row_net: P10 + mirror: + row_net: P8 + uninear: + anchor: + ref: thumbfan_home_thumb + columns: + home_again: + spread: -19 + rotate: 28 + origin: [-9.5, -9] + key: + skip: true + near1u: + rows: + thumb: + bind: [10,5,,] + key: + column_net: P2 + footprints: + diode: false + tags: + uniform: true + rows: + thumb: + row_net: P10 + mirror: + row_net: P8 key: bind: [0,0,0,0] footprints: @@ -114,95 +184,145 @@ points: type: mx nets: from: '!column_net' - to: '!name' + to: '!colrow' diode: type: diode anchor: rotate: 90 - shift: [-8, 0] + shift: [8, 0] nets: from: '!name' to: '!row_net' + mirror: + footprints: + mx: + nets: + from: '!colrow' + to: '!column_net' rotate: -20 mirror: - ref: pinky_home + ref: matrix_pinky_home distance: 223.7529778 outline: glue: - top: - left: - ref: inner_top - shift: [, 0.5] - right: - ref: mirror_inner_top - shift: [, 0.5] - bottom: - left: - ref: far_thumb - shift: [0.5, 0] - rotate: 90 - right: - ref: mirror_far_thumb - shift: [0.5, 0] - rotate: 90 - waypoints: - - percent: 50 - width: 50 - - percent: 90 - width: 25 + classic: + top: + left: + ref: matrix_inner_top + shift: [, 0.5] + right: + ref: mirror_matrix_inner_top + shift: [, 0.5] + bottom: + left: + ref: thumbfan_far_thumb + shift: [0.5, 0] + rotate: 90 + right: + ref: mirror_thumbfan_far_thumb + shift: [0.5, 0] + rotate: 90 + waypoints: + - percent: 50 + width: 50 + - percent: 90 + width: 25 + uniform: + extends: classic + bottom: + left: + ref: unifar_far1u_thumb + right: + ref: mirror_unifar_far1u_thumb + choc: + extends: classic + top: + left: + ref: choc_inner_top + right: + ref: mirror_choc_inner_top + uniform_choc: + extends: + - uniform + - choc exports: - outline: + classic_outline: - type: keys side: both - size: 18 + tags: + - s19 + - classic + glue: classic + size: 13.5 corner: .5 - holes: + uniform_outline: - type: keys - operation: stack side: both + tags: + - s19 + - uniform + glue: uniform + size: 13.5 + corner: .5 + intersected_outline: + - type: outline + name: classic_outline + - type: outline + name: uniform_outline + operation: intersect + classic_holes: + - type: keys + side: both + tags: + - s19 + - classic + glue: classic size: 14 bound: false - middle: + classic_middle: - type: keys - operation: stack side: middle + tags: + - s19 + - classic + glue: classic size: 24 - type: rectangle size: [25, 5] - ref: home_thumb + ref: thumbfan_home_thumb shift: [0, 12] - type: rectangle size: [25, 5] - ref: far_thumb + ref: thumbfan_far_thumb shift: [25, 12] - type: rectangle size: [25, 5] - ref: mirror_home_thumb + ref: mirror_thumbfan_home_thumb shift: [25, 12] - type: rectangle size: [25, 5] - ref: mirror_far_thumb + ref: mirror_thumbfan_far_thumb shift: [0, 12] - - type: ref - name: outline + - type: outline + name: classic_outline operation: intersect complex: - - type: ref - name: outline - - type: ref - name: holes + - type: outline + name: classic_outline + - type: outline + name: classic_holes operation: stack - - type: ref - name: middle + - type: outline + name: classic_middle operation: stack pcb: - edge: outline + edge: intersected_outline footprints: mcu: type: promicro anchor: ref: - - inner_top - - mirror_inner_top + - choc_inner_top + - mirror_choc_inner_top shift: [0, -20] rotate: 270