diff --git a/README.md b/README.md index 4f76b69..16215e8 100644 --- a/README.md +++ b/README.md @@ -36,15 +36,15 @@ The important thing is that the data should contain the following keys: ```yaml points: -outline: -case: -pcb: +outlines: +cases: +pcbs: ``` The `points` section describes the core of the layout: the positions of the keys. -The `outline` section then uses these points to generate plate, case, and PCB outlines. -The `case` section details how the case outlines are to be 3D-ized to form a 3D-printable object. -Finally, the `pcb` section is used to configure a KiCAD PCB template. +The `outlines` section then uses these points to generate plate, case, and PCB outlines. +The `cases` section details how the case outlines are to be 3D-ized to form a 3D-printable object. +Finally, the `pcbs` section is used to configure KiCAD PCB templates. In the following, we'll have an in-depth discussion about each of these, with an additional running example of how the [Absolem](#TODO-link-to-config-yaml)'s config was created. @@ -305,7 +305,7 @@ TODO: Absolem points here, with pics -## Outline +## Outlines Once the raw points are available, we want to turn them into solid, continuous outlines. The points are enough to create properly positioned and rotated rectangles (with parametric side lengths), but they won't combine since there won't be any overlap. @@ -480,7 +480,7 @@ If we only want to use it as a building block for further exports, we can start -## Case +## Cases Cases add a pretty basic and minimal 3D aspect to the generation process. In this phase, we take different outlines (exported from the above section, even the "private" ones), extrude and position them in space, and combine them into one 3D-printable object. @@ -488,7 +488,7 @@ That's it. Declarations might look like this: ```yaml -case: +cases: case_name: - outline: extrude: num # default = 1 @@ -575,7 +575,7 @@ If we only want to use an object as a building block for further objects, we can -## PCB +## PCBs Everything should be ready for a handwire, but if you'd like the design to be more accessible and easily replicable, you probably want a PCB as well. To help you get started, the necessary footprints and an edge cut can be automatically positioned so that all you need to do manually is the routing. @@ -589,14 +589,16 @@ The differences between the two footprint types are: Additionally, the edge cut of the PCB can be specified using a previously defined outline name under the `edge` key. ```yaml -pcb: - edge: - footprints: - - type: - anchor: - nets: - params: - - ... +pcbs: + pcb_name: + edge: + footprints: + - type: + anchor: + nets: + params: + - ... + ... ``` Currently, the following footprint types are supported: diff --git a/src/assert.js b/src/assert.js index 75e2cce..fb0f80b 100644 --- a/src/assert.js +++ b/src/assert.js @@ -106,7 +106,7 @@ const extend_pair = exports.extend_pair = (to, from) => { } else return from } -exports.extend = (...args) => { +const extend = exports.extend = (...args) => { let res = args[0] for (const arg of args) { if (res == arg) continue @@ -115,18 +115,22 @@ exports.extend = (...args) => { 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) - +const inherit = exports.inherit = (name_prefix, name, set) => { + let result = u.deepcopy(set[name]) + if (result.extends !== undefined) { + let candidates = [name] + const list = [] + while (candidates.length) { + const item = candidates.shift() + const other = u.deepcopy(set[item]) + assert(other, `"${item}" (reached from "${name_prefix}.${name}.extends") does not name a valid target!`) + let parents = other.extends || [] + if (type(parents) !== 'array') parents = [parents] + candidates = candidates.concat(parents) + delete other.extends + list.unshift(other) } - delete result.extends + result = extend.apply(this, list) } return result } \ No newline at end of file diff --git a/src/cli.js b/src/cli.js index 31459b8..61c7f07 100644 --- a/src/cli.js +++ b/src/cli.js @@ -12,8 +12,8 @@ const yargs = require('yargs') const u = require('./utils') const io = require('./io') const points_lib = require('./points') -const outline_lib = require('./outline') -const pcb_lib = require('./pcb') +const outlines_lib = require('./outlines') +const pcbs_lib = require('./pcbs') // command line args @@ -68,19 +68,21 @@ if (args.debug) { // outlines console.log('Generating outlines...') -const outlines = outline_lib.parse(config.outline, points) +const outlines = outlines_lib.parse(config.outlines, points) for (const [name, outline] of Object.entries(outlines)) { if (!args.debug && name.startsWith('_')) continue - io.dump_model(outline, path.join(args.o, `outline/${name}`), args.debug) + io.dump_model(outline, path.join(args.o, `outlines/${name}`), args.debug) } -// pcb +// pcbs -console.log('Scaffolding PCB...') -const pcb = pcb_lib.parse(config.pcb, points, outlines) -const pcb_file = path.join(args.o, `pcb/pcb.kicad_pcb`) -fs.mkdirpSync(path.dirname(pcb_file)) -fs.writeFileSync(pcb_file, pcb) +console.log('Scaffolding PCBs...') +const pcbs = pcbs_lib.parse(config.pcbs, points, outlines) +for (const [pcb_name, pcb_text] of Object.entries(pcbs)) { + const pcb_file = path.join(args.o, `pcbs/${pcb_name}.kicad_pcb`) + fs.mkdirpSync(path.dirname(pcb_file)) + fs.writeFileSync(pcb_file, pcb_text) +} // goodbye diff --git a/src/outline.js b/src/outlines.js similarity index 93% rename from src/outline.js rename to src/outlines.js index 51584ea..906b010 100644 --- a/src/outline.js +++ b/src/outlines.js @@ -35,23 +35,23 @@ const layout = exports._layout = (config = {}, points = {}) => { // Glue config sanitization - const parsed_glue = u.deepcopy(a.sane(config, 'outline.glue', 'object')) + const parsed_glue = u.deepcopy(a.sane(config, 'outlines.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']) + gval = a.inherit('outlines.glue', gkey, config) + a.detect_unexpected(gval, `outlines.glue.${gkey}`, ['top', 'bottom', 'waypoints', 'extra']) 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) + a.detect_unexpected(gval[y], `outlines.glue.${gkey}.${y}`, ['left', 'right']) + gval[y].left = relative_anchor(gval[y].left, `outlines.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[y].right = relative_anchor(gval[y].right, `outlines.glue.${gkey}.${y}.right`, points) } } - gval.waypoints = a.sane(gval.waypoints || [], `outline.glue.${gkey}.waypoints`, 'array') + gval.waypoints = a.sane(gval.waypoints || [], `outlines.glue.${gkey}.waypoints`, 'array') let wi = 0 gval.waypoints = gval.waypoints.map(w => { - const name = `outline.glue.${gkey}.waypoints[${++wi}]` + const name = `outlines.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') @@ -208,12 +208,12 @@ exports.parse = (config = {}, points = {}) => { const outlines = {} - const ex = a.sane(config.exports, 'outline.exports', 'object') - for (const [key, parts] of Object.entries(ex)) { - let index = 0 + const ex = a.sane(config.exports, 'outlines.exports', 'object') + for (let [key, parts] of Object.entries(ex)) { + parts = a.inherit('outlines.exports', key, ex) let result = {models: {}} - for (const part of parts) { - const name = `outline.exports.${key}[${++index}]` + for (const [part_name, part] of Object.entries(parts)) { + const name = `outlines.exports.${key}.${part_name}` const expected = ['type', 'operation'] 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']) diff --git a/src/pcb.js b/src/pcbs.js similarity index 75% rename from src/pcb.js rename to src/pcbs.js index d5fe20b..712aea2 100644 --- a/src/pcb.js +++ b/src/pcbs.js @@ -204,56 +204,62 @@ const footprint = exports._footprint = (config, name, points, net_indexer, point exports.parse = (config, points, outlines) => { - // config sanitization - a.detect_unexpected(config, 'pcb', ['edge', 'footprints']) - const edge = outlines[config.edge] - if (!edge) throw new Error(`Field "pcb.edge" doesn't name a valid outline!`) + const pcbs = a.sane(config, 'pcb', 'object') + const results = {} - // Edge.Cuts conversion - const kicad_edge = makerjs2kicad(edge) + for (const [pcb_name, pcb_config] of Object.entries(pcbs)) { - // making a global net index registry - const nets = {"": 0} - const net_indexer = net => { - if (nets[net] !== undefined) return nets[net] - const index = Object.keys(nets).length - return nets[net] = index - } + // config sanitization + a.detect_unexpected(pcb_config, `pcb.${pcb_name}`, ['edge', 'footprints']) + const edge = outlines[pcb_config.edge] + if (!edge) throw new Error(`Field "pcb.${pcb_name}.edge" doesn't name a valid outline!`) - const footprints = [] + // Edge.Cuts conversion + const kicad_edge = makerjs2kicad(edge) - // key-level footprints - for (const [pname, point] of Object.entries(points)) { - for (const [f_name, f] of Object.entries(point.meta.footprints || {})) { - footprints.push(footprint(f, `${pname}.footprints.${f_name}`, points, net_indexer, point)) + // making a global net index registry + const nets = {"": 0} + const net_indexer = net => { + if (nets[net] !== undefined) return nets[net] + const index = Object.keys(nets).length + return nets[net] = index } + + const footprints = [] + + // key-level footprints + for (const [p_name, point] of Object.entries(points)) { + for (const [f_name, f] of Object.entries(point.meta.footprints || {})) { + footprints.push(footprint(f, `${p_name}.footprints.${f_name}`, points, net_indexer, point)) + } + } + + // global one-off footprints + const global_footprints = a.sane(pcb_config.footprints || {}, `pcb.${pcb_name}.footprints`, 'object') + for (const [gf_name, gf] of Object.entries(global_footprints)) { + footprints.push(footprint(gf, `pcb.${pcb_name}.footprints.${gf_name}`, points, net_indexer)) + } + + // finalizing nets + const nets_arr = [] + const add_nets_arr = [] + for (const [net, index] of Object.entries(nets)) { + nets_arr.push(`(net ${index} "${net}")`) + add_nets_arr.push(`(add_net "${net}")`) + } + + const netclass = kicad_netclass.replace('__ADD_NET', add_nets_arr.join('\n')) + const nets_text = nets_arr.join('\n') + const footprint_text = footprints.join('\n') + results[pcb_name] = ` + ${kicad_prefix} + ${nets_text} + ${netclass} + ${footprint_text} + ${kicad_edge} + ${kicad_suffix} + ` } - // global one-off footprints - const global_footprints = a.sane(config.footprints || {}, 'pcb.footprints', 'object') - for (const [gf_name, gf] of Object.entries(global_footprints)) { - footprints.push(footprint(gf, `pcb.footprints.${gf_name}`, points, net_indexer)) - } - - // finalizing nets - const nets_arr = [] - const add_nets_arr = [] - for (const [net, index] of Object.entries(nets)) { - nets_arr.push(`(net ${index} "${net}")`) - add_nets_arr.push(`(add_net "${net}")`) - } - - const netclass = kicad_netclass.replace('__ADD_NET', add_nets_arr.join('\n')) - const nets_text = nets_arr.join('\n') - const footprint_text = footprints.join('\n') - return ` - - ${kicad_prefix} - ${nets_text} - ${netclass} - ${footprint_text} - ${kicad_edge} - ${kicad_suffix} - - ` + return results } \ No newline at end of file diff --git a/src/points.js b/src/points.js index c27156d..4651ffa 100644 --- a/src/points.js +++ b/src/points.js @@ -180,7 +180,7 @@ exports.parse = (config = {}) => { for (let [zone_name, zone] of Object.entries(zones)) { // handle zone-level `extends` clauses - zone = a.inherit(zone, 'points.zones', zone_name, zones) + zone = a.inherit('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)) diff --git a/test/fixtures/absolem.yaml b/test/fixtures/absolem.yaml index d4a1751..4785e28 100644 --- a/test/fixtures/absolem.yaml +++ b/test/fixtures/absolem.yaml @@ -10,9 +10,9 @@ points: rows: bottom: home: - bind: [,15] + bind: [,15,-1] top: - bind: [,15] + bind: [,15,-1] key: column_net: P1 ring: @@ -70,6 +70,10 @@ points: mirror: row_net: P6 top: + footprints: + mx: + anchor: + rotate: 180 row_net: P15 mirror: row_net: P5 @@ -87,6 +91,8 @@ points: tags: s19: false s18: true + footprints: + diode: '!!unset' thumbfan: anchor: ref: matrix_inner_bottom @@ -203,9 +209,9 @@ points: mirror: ref: matrix_pinky_home distance: 223.7529778 -outline: +outlines: glue: - classic: + classic_s19: top: left: ref: matrix_inner_top @@ -227,102 +233,117 @@ outline: width: 50 - percent: 90 width: 25 - uniform: - extends: classic + uniform_s19: + extends: classic_s19 bottom: left: ref: unifar_far1u_thumb right: ref: mirror_unifar_far1u_thumb - choc: - extends: classic + classic_s18: + extends: classic_s19 top: left: ref: choc_inner_top right: ref: mirror_choc_inner_top - uniform_choc: + uniform_s18: extends: - - uniform - - choc + - uniform_s19 + - classic_s18 exports: - classic_outline: - - type: keys + classic_s19_outline: + main: + type: keys side: both tags: - s19 - classic - glue: classic + glue: classic_s19 size: 13.5 corner: .5 - uniform_outline: - - type: keys - side: both + uniform_s19_outline: + extends: classic_s19_outline + main: tags: - s19 - uniform - glue: uniform - size: 13.5 - corner: .5 + glue: uniform_s19 + uniform_s18_outline: + extends: uniform_s19_outline + main: + tags: + - s18 + - uniform + glue: uniform_s18 intersected_outline: - - type: outline - name: classic_outline - - type: outline - name: uniform_outline + one: + type: outline + name: classic_s19_outline + two: + type: outline + name: uniform_s18_outline operation: intersect - classic_holes: - - type: keys + classic_s19_switches: + main: + type: keys side: both tags: - - s19 - classic - glue: classic + glue: classic_s19 size: 14 bound: false - classic_middle: - - type: keys + uniform_s19_switches: + main: + type: keys + side: both + tags: + - uniform + glue: uniform_s19 + size: 14 + bound: false + classic_s19_middle: + raw: + type: keys side: middle tags: - s19 - classic - glue: classic + glue: classic_s19 size: 24 - - type: rectangle + helper1: + type: rectangle size: [25, 5] ref: thumbfan_home_thumb shift: [0, 12] - - type: rectangle + helper2: + type: rectangle size: [25, 5] ref: thumbfan_far_thumb - shift: [25, 12] - - type: rectangle + shift: [-25, 12] + helper3: + type: rectangle size: [25, 5] ref: mirror_thumbfan_home_thumb shift: [25, 12] - - type: rectangle + helper4: + type: rectangle size: [25, 5] ref: mirror_thumbfan_far_thumb shift: [0, 12] - - type: outline - name: classic_outline + outer_bounds: + type: outline + name: classic_s19_outline operation: intersect - complex: - - type: outline - name: classic_outline - - type: outline - name: classic_holes - operation: stack - - type: outline - name: classic_middle - operation: stack -pcb: - edge: intersected_outline - footprints: - mcu: - type: promicro - anchor: - ref: - - choc_inner_top - - mirror_choc_inner_top - shift: [0, -20] - rotate: 270 +pcbs: + main: + edge: intersected_outline + footprints: + mcu: + type: promicro + anchor: + ref: + - choc_inner_top + - mirror_choc_inner_top + shift: [0, -20] + rotate: 270