├── config ├── flipper.yaml ├── input.yaml └── adux.yaml ├── test ├── cli │ ├── bad_input │ │ ├── bad.yaml │ │ ├── error │ │ └── command │ ├── missing_input │ │ ├── command │ │ └── error │ ├── minimal │ │ ├── command │ │ ├── reference │ │ │ ├── source │ │ │ │ ├── raw.txt │ │ │ │ └── canonical.yaml │ │ │ └── points │ │ │ │ ├── units.yaml │ │ │ │ ├── demo.svg │ │ │ │ ├── demo.dxf │ │ │ │ ├── points.yaml │ │ │ │ └── demo.yaml │ │ └── log │ ├── medium │ │ ├── command │ │ ├── log │ │ └── reference │ │ │ ├── outlines │ │ │ └── export.dxf │ │ │ └── pcbs │ │ │ └── export.kicad_pcb │ ├── nonexistent_input │ │ ├── command │ │ └── error │ └── big │ │ ├── command │ │ ├── reference │ │ ├── points │ │ │ ├── units.yaml │ │ │ ├── demo.svg │ │ │ ├── points.yaml │ │ │ ├── demo.dxf │ │ │ └── demo.yaml │ │ ├── outlines │ │ │ ├── _export.svg │ │ │ ├── export.svg │ │ │ ├── _export.dxf │ │ │ ├── export.dxf │ │ │ ├── export.yaml │ │ │ └── _export.yaml │ │ ├── source │ │ │ ├── raw.txt │ │ │ └── canonical.yaml │ │ ├── cases │ │ │ ├── export.jscad │ │ │ └── _export.jscad │ │ └── pcbs │ │ │ ├── _export.kicad_pcb │ │ │ └── export.kicad_pcb │ │ └── log ├── fixtures │ ├── minimal.yaml │ ├── medium.yaml │ ├── big.yaml │ └── atreus_kle.json ├── points │ ├── default.yaml │ ├── basic_2x2.yaml │ ├── units.yaml │ ├── overrides.yaml │ ├── units___units.json │ ├── adjustments.yaml │ ├── default___demo_dxf.dxf │ ├── basic_2x2___demo_dxf.dxf │ ├── adjustments___demo_dxf.dxf │ ├── default___points.json │ └── overrides___demo_dxf.dxf ├── helpers │ ├── register.js │ ├── point.js │ └── mock_footprints.js ├── cases │ ├── cube.yaml │ └── cube___cases_cube_jscad.jscad ├── pcbs │ ├── references.yaml │ ├── mock_footprints.yaml │ ├── references___pcbs.json │ └── mock_footprints___pcbs.json ├── outlines │ ├── outlines.yaml │ ├── basic.yaml │ ├── circles.yaml │ ├── rectangles.yaml │ ├── polygons.yaml │ ├── affect_mirror.yaml │ ├── circles___outlines_outline_dxf.dxf │ ├── outlines___outlines_scale_dxf.dxf │ ├── outlines___outlines_fillet_dxf.dxf │ ├── polygons___outlines_outline_dxf.dxf │ ├── basic___outlines_outline_dxf.dxf │ ├── rectangles___outlines_outline_dxf.dxf │ └── affect_mirror___outlines_test_dxf.dxf └── unit │ ├── operation.js │ ├── units.js │ ├── filter.js │ ├── prepare.js │ ├── assert.js │ ├── point.js │ ├── interface.js │ └── anchor.js ├── scripts ├── build.sh └── render_outlines.R ├── .npmrc ├── rollup.config.js ├── src ├── footprints │ ├── index.js │ ├── via.js │ ├── jumper.js │ ├── oled.js │ ├── alps.js │ ├── omron.js │ ├── pad.js │ ├── jstph.js │ ├── button.js │ ├── diode.js │ ├── rgb.js │ ├── slider.js │ ├── trrs.js │ ├── choc.js │ ├── mx.js │ ├── scrollwheel.js │ ├── rotary.js │ └── chocmini.js ├── operation.js ├── units.js ├── point.js ├── io.js ├── assert.js ├── kle.js ├── cli.js ├── ergogen.js ├── utils.js ├── prepare.js ├── filter.js └── anchor.js ├── license.md ├── package.json ├── readme.md ├── .gitignore └── roadmap.md /config/flipper.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/cli/bad_input/bad.yaml: -------------------------------------------------------------------------------- 1 | 'bad input' -------------------------------------------------------------------------------- /test/cli/missing_input/command: -------------------------------------------------------------------------------- 1 | node src/cli.js 2 | -------------------------------------------------------------------------------- /test/cli/bad_input/error: -------------------------------------------------------------------------------- 1 | Error: Input doesn't resolve into an object! -------------------------------------------------------------------------------- /test/cli/minimal/command: -------------------------------------------------------------------------------- 1 | node src/cli.js test/fixtures/minimal.yaml -------------------------------------------------------------------------------- /test/cli/missing_input/error: -------------------------------------------------------------------------------- 1 | Usage: ergogen [options] -------------------------------------------------------------------------------- /test/cli/medium/command: -------------------------------------------------------------------------------- 1 | node src/cli.js test/fixtures/medium.yaml 2 | -------------------------------------------------------------------------------- /test/cli/nonexistent_input/command: -------------------------------------------------------------------------------- 1 | node src/cli.js nonexistent.yaml 2 | -------------------------------------------------------------------------------- /test/cli/bad_input/command: -------------------------------------------------------------------------------- 1 | node src/cli.js test/cli/bad_input/bad.yaml 2 | -------------------------------------------------------------------------------- /test/cli/big/command: -------------------------------------------------------------------------------- 1 | node src/cli.js test/fixtures/big.yaml --debug --clean 2 | -------------------------------------------------------------------------------- /test/fixtures/minimal.yaml: -------------------------------------------------------------------------------- 1 | points.zones.matrix: 2 | columns.col: {} 3 | rows.row: {} -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | ergogen config/input.yaml -o output 2 | Rscript scripts/render_outlines.R -------------------------------------------------------------------------------- /test/cli/minimal/reference/source/raw.txt: -------------------------------------------------------------------------------- 1 | points.zones.matrix: 2 | columns.col: {} 3 | rows.row: {} -------------------------------------------------------------------------------- /test/points/default.yaml: -------------------------------------------------------------------------------- 1 | points.zones: 2 | matrix: 3 | single_key_column: 4 | columns: 5 | named: -------------------------------------------------------------------------------- /test/cli/nonexistent_input/error: -------------------------------------------------------------------------------- 1 | Could not read config file "nonexistent.yaml": Error: ENOENT: no such file or directory, open 'nonexistent.yaml' -------------------------------------------------------------------------------- /test/points/basic_2x2.yaml: -------------------------------------------------------------------------------- 1 | points: 2 | zones: 3 | matrix: 4 | columns: 5 | left: 6 | right: 7 | rows: 8 | bottom: 9 | top: -------------------------------------------------------------------------------- /test/helpers/register.js: -------------------------------------------------------------------------------- 1 | global.chai = require('chai') 2 | global.chai.use(require('chai-as-promised')) 3 | global.expect = global.chai.expect 4 | global.should = global.chai.should() -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # Suppress deprecation warnings from the old JSCAD libs 2 | # And either way, end users don't need to see warnings 3 | # Can be deleted locally for development purposes... 4 | loglevel=error -------------------------------------------------------------------------------- /test/cli/minimal/reference/source/canonical.yaml: -------------------------------------------------------------------------------- 1 | points: 2 | zones: 3 | matrix: 4 | columns: 5 | col: {} 6 | rows: 7 | row: {} 8 | -------------------------------------------------------------------------------- /test/points/units.yaml: -------------------------------------------------------------------------------- 1 | units: 2 | a: cy - 7 3 | variables: 4 | b: a * 1.5 5 | points: 6 | zones: 7 | matrix: 8 | columns: 9 | col: 10 | rows: 11 | row: -------------------------------------------------------------------------------- /test/fixtures/medium.yaml: -------------------------------------------------------------------------------- 1 | points.zones.matrix: 2 | columns.col: {} 3 | outlines: 4 | export: 5 | - what: rectangle 6 | where: true 7 | size: 18 8 | pcbs: 9 | export: {} 10 | -------------------------------------------------------------------------------- /test/cases/cube.yaml: -------------------------------------------------------------------------------- 1 | points.zones.matrix: {} 2 | outlines: 3 | square: 4 | - what: rectangle 5 | where: true 6 | size: [5, 5] 7 | cases: 8 | cube: 9 | - name: square 10 | extrude: 5 -------------------------------------------------------------------------------- /test/pcbs/references.yaml: -------------------------------------------------------------------------------- 1 | points.zones.matrix: 2 | columns.one: 3 | rows.only: 4 | pcbs: 5 | shown: 6 | references: true 7 | footprints: 8 | - type: references_test 9 | hidden: 10 | footprints: 11 | - type: references_test -------------------------------------------------------------------------------- /test/helpers/point.js: -------------------------------------------------------------------------------- 1 | exports.check = (point, expected=[]) => { 2 | point.x.should.equal(expected[0] || 0) 3 | point.y.should.equal(expected[1] || 0) 4 | point.r.should.equal(expected[2] || 0) 5 | point.meta.should.deep.equal(expected[3] || {}) 6 | } -------------------------------------------------------------------------------- /test/cli/minimal/reference/points/units.yaml: -------------------------------------------------------------------------------- 1 | U: 19.05 2 | u: 19 3 | cx: 18 4 | cy: 17 5 | $default_stagger: 0 6 | $default_spread: 19 7 | $default_splay: 0 8 | $default_height: 18 9 | $default_width: 18 10 | $default_padding: 19 11 | $default_autobind: 10 12 | -------------------------------------------------------------------------------- /test/cli/big/reference/points/units.yaml: -------------------------------------------------------------------------------- 1 | U: 19.05 2 | u: 19 3 | cx: 18 4 | cy: 17 5 | $default_stagger: 0 6 | $default_spread: 19 7 | $default_splay: 0 8 | $default_height: 18 9 | $default_width: 18 10 | $default_padding: 19 11 | $default_autobind: 10 12 | a: 47 13 | -------------------------------------------------------------------------------- /test/cli/medium/log: -------------------------------------------------------------------------------- 1 | Ergogen CLI 2 | 3 | Interpreting format: YAML 4 | Preprocessing input... 5 | Calculating variables... 6 | Parsing points... 7 | Generating outlines... 8 | Modeling cases... 9 | Scaffolding PCBs... 10 | Writing output to disk... 11 | Done. 12 | 13 | -------------------------------------------------------------------------------- /test/points/overrides.yaml: -------------------------------------------------------------------------------- 1 | points.zones.matrix: 2 | columns: 3 | left: 4 | middle: 5 | rows: 6 | top: 7 | right: 8 | key.stagger: u 9 | rows: 10 | bottom: $unset 11 | home: 12 | top: 13 | rows: 14 | bottom: 15 | home: -------------------------------------------------------------------------------- /scripts/render_outlines.R: -------------------------------------------------------------------------------- 1 | library(rgdal) 2 | library(sf) 3 | library(ggplot2) 4 | outline <- st_read("output/outlines/cutout.dxf") 5 | keys <- st_read("output/outlines/keycap_outlines.dxf") 6 | 7 | ggplot() + 8 | geom_sf(data = outline) + 9 | geom_sf(data = keys) 10 | ggsave("output/outlines/composite.png") -------------------------------------------------------------------------------- /test/cli/big/log: -------------------------------------------------------------------------------- 1 | Ergogen CLI (Debug Mode) 2 | 3 | Interpreting format: YAML 4 | Preprocessing input... 5 | Calculating variables... 6 | Parsing points... 7 | Generating outlines... 8 | Modeling cases... 9 | Scaffolding PCBs... 10 | Cleaning output folder... 11 | Writing output to disk... 12 | Done. 13 | 14 | -------------------------------------------------------------------------------- /test/cli/minimal/log: -------------------------------------------------------------------------------- 1 | Ergogen CLI 2 | 3 | Interpreting format: YAML 4 | Preprocessing input... 5 | Calculating variables... 6 | Parsing points... 7 | Generating outlines... 8 | Modeling cases... 9 | Scaffolding PCBs... 10 | Output would be empty, rerunning in debug mode... 11 | Writing output to disk... 12 | Done. 13 | 14 | -------------------------------------------------------------------------------- /test/points/units___units.json: -------------------------------------------------------------------------------- 1 | { 2 | "U": 19.05, 3 | "u": 19, 4 | "cx": 18, 5 | "cy": 17, 6 | "$default_stagger": 0, 7 | "$default_spread": 19, 8 | "$default_splay": 0, 9 | "$default_height": 18, 10 | "$default_width": 18, 11 | "$default_padding": 19, 12 | "$default_autobind": 10, 13 | "a": 10, 14 | "b": 15 15 | } 16 | -------------------------------------------------------------------------------- /test/cli/big/reference/points/demo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/cli/big/reference/outlines/_export.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/cli/big/reference/outlines/export.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/cli/minimal/reference/points/demo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/big.yaml: -------------------------------------------------------------------------------- 1 | units: 2 | a: 28 + u 3 | points.zones.matrix: 4 | outlines: 5 | export: 6 | - what: rectangle 7 | where: true 8 | size: 18 9 | _export: 10 | - what: rectangle 11 | where: true 12 | size: 18 13 | cases: 14 | export: 15 | - name: export 16 | extrude: 1 17 | _export: 18 | - name: export 19 | extrude: 1 20 | pcbs: 21 | export: {} 22 | _export: {} 23 | -------------------------------------------------------------------------------- /test/points/adjustments.yaml: -------------------------------------------------------------------------------- 1 | points: 2 | zones: 3 | matrix: 4 | columns: 5 | left: 6 | right: 7 | key: 8 | stagger: 5 9 | spread: 25 10 | splay: 5 11 | origin: [-9, -9] 12 | rows: 13 | top: 14 | orient: -90 15 | shift: [0, 10] 16 | rotate: 90 17 | rows: 18 | bottom: 19 | top: -------------------------------------------------------------------------------- /test/cli/big/reference/source/raw.txt: -------------------------------------------------------------------------------- 1 | units: 2 | a: 28 + u 3 | points.zones.matrix: 4 | outlines: 5 | export: 6 | - what: rectangle 7 | where: true 8 | size: 18 9 | _export: 10 | - what: rectangle 11 | where: true 12 | size: 18 13 | cases: 14 | export: 15 | - name: export 16 | extrude: 1 17 | _export: 18 | - name: export 19 | extrude: 1 20 | pcbs: 21 | export: {} 22 | _export: {} 23 | -------------------------------------------------------------------------------- /test/outlines/outlines.yaml: -------------------------------------------------------------------------------- 1 | points: 2 | key: 3 | padding: cy 4 | bind: 0.1 5 | zones: 6 | matrix: 7 | columns: 8 | one: 9 | two: 10 | rows: 11 | bottom: 12 | top: 13 | outlines: 14 | base: 15 | - what: rectangle 16 | where: true 17 | size: cy 18 | bound: true 19 | fillet: 20 | - what: outline 21 | name: base 22 | fillet: 2 23 | scale: 24 | - what: outline 25 | name: fillet 26 | scale: 0.5 -------------------------------------------------------------------------------- /test/outlines/basic.yaml: -------------------------------------------------------------------------------- 1 | points: 2 | zones: 3 | matrix: 4 | columns: 5 | left.key.bind: [,10,,] 6 | right.key.bind: [,,,10] 7 | rows: 8 | bottom.key.bind: [10,,,] 9 | top.key.bind: [,,10,] 10 | key: 11 | bind: [0, 0, 0, 0] 12 | outlines: 13 | outline: 14 | main: 15 | what: rectangle 16 | where: true 17 | size: 20 18 | min: 19 | what: rectangle 20 | where: true 21 | bound: false 22 | size: 14 23 | operation: subtract -------------------------------------------------------------------------------- /test/outlines/circles.yaml: -------------------------------------------------------------------------------- 1 | points: 2 | zones: 3 | matrix: {} 4 | mirror: 20 5 | outlines: 6 | outline: 7 | main: 8 | what: rectangle 9 | where: true 10 | size: 20 11 | bound: false 12 | middle_circle: 13 | what: circle 14 | where: 15 | aggregate.parts: 16 | - matrix 17 | - mirror_matrix 18 | radius: 15 19 | outside_circles: 20 | what: circle 21 | where: 22 | ref: matrix 23 | shift: [-10, 10] 24 | radius: 5 25 | mirror: true -------------------------------------------------------------------------------- /test/outlines/rectangles.yaml: -------------------------------------------------------------------------------- 1 | points: 2 | zones: 3 | matrix: {} 4 | mirror: 20 5 | outlines: 6 | outline: 7 | main: 8 | what: rectangle 9 | where: true 10 | size: 20 11 | bound: false 12 | middle_rect: 13 | what: rectangle 14 | where: 15 | aggregate.parts: 16 | - matrix 17 | - mirror_matrix 18 | shift: [0, sy/2] 19 | size: [20, 40] 20 | outside_rects: 21 | what: rectangle 22 | where: 23 | ref: matrix 24 | shift: [-10, 10] 25 | size: 10 26 | mirror: true -------------------------------------------------------------------------------- /test/cli/big/reference/source/canonical.yaml: -------------------------------------------------------------------------------- 1 | units: 2 | a: 28 + u 3 | points: 4 | zones: 5 | matrix: null 6 | outlines: 7 | export: 8 | - 9 | what: rectangle 10 | where: true 11 | size: 18 12 | _export: 13 | - 14 | what: rectangle 15 | where: true 16 | size: 18 17 | cases: 18 | export: 19 | - 20 | name: export 21 | extrude: 1 22 | _export: 23 | - 24 | name: export 25 | extrude: 1 26 | pcbs: 27 | export: {} 28 | _export: {} 29 | -------------------------------------------------------------------------------- /test/outlines/polygons.yaml: -------------------------------------------------------------------------------- 1 | points: 2 | zones: 3 | matrix: {} 4 | mirror: 20 5 | outlines: 6 | outline: 7 | main: 8 | what: rectangle 9 | where: true 10 | size: 20 11 | bound: false 12 | middle_poly: 13 | what: polygon 14 | where.aggregate.parts: 15 | - matrix 16 | - mirror_matrix 17 | points: 18 | - shift: [0, 20] 19 | - shift: [20, -40] 20 | - shift: [-40, 0] 21 | outside_polys: 22 | what: polygon 23 | where.ref: matrix 24 | points: 25 | - shift: [-10, 15] 26 | - shift: [5, -10] 27 | - shift: [-10, 0] 28 | mirror: true -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import pkg from './package.json' 2 | import json from '@rollup/plugin-json' 3 | import commonjs from '@rollup/plugin-commonjs' 4 | 5 | export default { 6 | input: 'src/ergogen.js', 7 | external: ['makerjs', 'js-yaml', 'mathjs', 'kle-serial'], 8 | output: { 9 | name: 'ergogen', 10 | file: 'dist/ergogen.js', 11 | format: 'umd', 12 | banner: `/*!\n * Ergogen v${pkg.version}\n * https://ergogen.xyz\n */\n`, 13 | globals: { 14 | 'makerjs': 'makerjs', 15 | 'js-yaml': 'jsyaml', 16 | 'mathjs': 'math', 17 | 'kle-serial': 'kle' 18 | } 19 | }, 20 | plugins: [ 21 | json(), 22 | commonjs() 23 | ] 24 | } -------------------------------------------------------------------------------- /src/footprints/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | alps: require('./alps'), 3 | button: require('./button'), 4 | choc: require('./choc'), 5 | chocmini: require('./chocmini'), 6 | diode: require('./diode'), 7 | jstph: require('./jstph'), 8 | jumper: require('./jumper'), 9 | mx: require('./mx'), 10 | oled: require('./oled'), 11 | omron: require('./omron'), 12 | pad: require('./pad'), 13 | promicro: require('./promicro'), 14 | rgb: require('./rgb'), 15 | rotary: require('./rotary'), 16 | scrollwheel: require('./scrollwheel'), 17 | slider: require('./slider'), 18 | trrs: require('./trrs'), 19 | via: require('./via'), 20 | } -------------------------------------------------------------------------------- /test/pcbs/mock_footprints.yaml: -------------------------------------------------------------------------------- 1 | points.zones.matrix: 2 | outlines: 3 | edge: 4 | - what: rectangle 5 | where: true 6 | size: u 7 | pcbs: 8 | main: 9 | outlines: 10 | edge: 11 | outline: edge 12 | footprints: 13 | trace: 14 | type: trace_test 15 | anchor: 16 | shift: [1, 1] 17 | rotate: 30 18 | zone: 19 | type: zone_test 20 | anchor: 21 | shift: [1, 1] 22 | rotate: 30 23 | dyn: 24 | type: dynamic_net_test 25 | anc: 26 | type: anchor_test 27 | anchors: 28 | end: 29 | ref: matrix 30 | shift: [10, 10] -------------------------------------------------------------------------------- /src/operation.js: -------------------------------------------------------------------------------- 1 | const op_prefix = exports.op_prefix = str => { 2 | const suffix = str.slice(1) 3 | if (str.startsWith('+')) return {name: suffix, operation: 'add'} 4 | if (str.startsWith('-')) return {name: suffix, operation: 'subtract'} 5 | if (str.startsWith('~')) return {name: suffix, operation: 'intersect'} 6 | if (str.startsWith('^')) return {name: suffix, operation: 'stack'} 7 | return {name: str, operation: 'add'} 8 | } 9 | 10 | exports.operation = (str, choices={}, order=Object.keys(choices)) => { 11 | let res = op_prefix(str) 12 | for (const key of order) { 13 | if (choices[key].includes(res.name)) { 14 | res.type = key 15 | break 16 | } 17 | } 18 | return res 19 | } -------------------------------------------------------------------------------- /src/footprints/via.js: -------------------------------------------------------------------------------- 1 | // Via 2 | // Nets 3 | // net: the net this via should be connected to 4 | 5 | module.exports = { 6 | nets: { 7 | net: undefined 8 | }, 9 | body: p => ` 10 | (module VIA-0.6mm (layer F.Cu) (tedit 591DBFB0) 11 | ${p.at /* parametric position */} 12 | ${'' /* footprint reference */} 13 | (fp_text reference REF** (at 0 1.4) (layer F.SilkS) hide (effects (font (size 1 1) (thickness 0.15)))) 14 | (fp_text value VIA-0.6mm (at 0 -1.4) (layer F.Fab) hide (effects (font (size 1 1) (thickness 0.15)))) 15 | 16 | ${'' /* via */} 17 | (pad 1 thru_hole circle (at 0 0) (size 0.6 0.6) (drill 0.3) (layers *.Cu) (zone_connect 2) ${p.net.net.str}) 18 | ) 19 | ` 20 | } -------------------------------------------------------------------------------- /src/units.js: -------------------------------------------------------------------------------- 1 | const a = require('./assert') 2 | const prep = require('./prepare') 3 | 4 | const default_units = { 5 | U: 19.05, 6 | u: 19, 7 | cx: 18, 8 | cy: 17, 9 | $default_stagger: 0, 10 | $default_spread: 'u', 11 | $default_splay: 0, 12 | $default_height: 'u-1', 13 | $default_width: 'u-1', 14 | $default_padding: 'u', 15 | $default_autobind: 10 16 | } 17 | 18 | exports.parse = (config = {}) => { 19 | const raw_units = prep.extend( 20 | default_units, 21 | a.sane(config.units || {}, 'units', 'object')(), 22 | a.sane(config.variables || {}, 'variables', 'object')() 23 | ) 24 | const units = {} 25 | for (const [key, val] of Object.entries(raw_units)) { 26 | units[key] = a.mathnum(val)(units) 27 | } 28 | return units 29 | } -------------------------------------------------------------------------------- /test/cli/big/reference/points/points.yaml: -------------------------------------------------------------------------------- 1 | matrix: 2 | x: 0 3 | 'y': 0 4 | r: 0 5 | meta: 6 | stagger: 0 7 | spread: 0 8 | splay: 0 9 | origin: 10 | - 0 11 | - 0 12 | orient: 0 13 | shift: 14 | - 0 15 | - 0 16 | rotate: 0 17 | width: 18 18 | height: 18 19 | padding: 19 20 | autobind: 10 21 | skip: false 22 | asym: both 23 | colrow: default_default 24 | name: matrix 25 | zone: 26 | name: matrix 27 | col: 28 | rows: {} 29 | key: {} 30 | name: default 31 | row: default 32 | bind: 33 | - 0 34 | - 0 35 | - 0 36 | - 0 37 | -------------------------------------------------------------------------------- /test/cli/big/reference/points/demo.dxf: -------------------------------------------------------------------------------- 1 | 0 2 | SECTION 3 | 2 4 | HEADER 5 | 9 6 | $INSUNITS 7 | 70 8 | 4 9 | 0 10 | ENDSEC 11 | 0 12 | SECTION 13 | 2 14 | TABLES 15 | 0 16 | TABLE 17 | 2 18 | LTYPE 19 | 0 20 | LTYPE 21 | 72 22 | 65 23 | 70 24 | 64 25 | 2 26 | CONTINUOUS 27 | 3 28 | ______ 29 | 73 30 | 0 31 | 40 32 | 0 33 | 0 34 | ENDTAB 35 | 0 36 | TABLE 37 | 2 38 | LAYER 39 | 0 40 | ENDTAB 41 | 0 42 | ENDSEC 43 | 0 44 | SECTION 45 | 2 46 | ENTITIES 47 | 0 48 | LINE 49 | 8 50 | 0 51 | 10 52 | -9 53 | 20 54 | 9 55 | 11 56 | 9 57 | 21 58 | 9 59 | 0 60 | LINE 61 | 8 62 | 0 63 | 10 64 | 9 65 | 20 66 | 9 67 | 11 68 | 9 69 | 21 70 | -9 71 | 0 72 | LINE 73 | 8 74 | 0 75 | 10 76 | 9 77 | 20 78 | -9 79 | 11 80 | -9 81 | 21 82 | -9 83 | 0 84 | LINE 85 | 8 86 | 0 87 | 10 88 | -9 89 | 20 90 | -9 91 | 11 92 | -9 93 | 21 94 | 9 95 | 0 96 | ENDSEC 97 | 0 98 | EOF -------------------------------------------------------------------------------- /test/cli/big/reference/outlines/_export.dxf: -------------------------------------------------------------------------------- 1 | 0 2 | SECTION 3 | 2 4 | HEADER 5 | 9 6 | $INSUNITS 7 | 70 8 | 4 9 | 0 10 | ENDSEC 11 | 0 12 | SECTION 13 | 2 14 | TABLES 15 | 0 16 | TABLE 17 | 2 18 | LTYPE 19 | 0 20 | LTYPE 21 | 72 22 | 65 23 | 70 24 | 64 25 | 2 26 | CONTINUOUS 27 | 3 28 | ______ 29 | 73 30 | 0 31 | 40 32 | 0 33 | 0 34 | ENDTAB 35 | 0 36 | TABLE 37 | 2 38 | LAYER 39 | 0 40 | ENDTAB 41 | 0 42 | ENDSEC 43 | 0 44 | SECTION 45 | 2 46 | ENTITIES 47 | 0 48 | LINE 49 | 8 50 | 0 51 | 10 52 | -9 53 | 20 54 | -9 55 | 11 56 | 9 57 | 21 58 | -9 59 | 0 60 | LINE 61 | 8 62 | 0 63 | 10 64 | 9 65 | 20 66 | -9 67 | 11 68 | 9 69 | 21 70 | 9 71 | 0 72 | LINE 73 | 8 74 | 0 75 | 10 76 | 9 77 | 20 78 | 9 79 | 11 80 | -9 81 | 21 82 | 9 83 | 0 84 | LINE 85 | 8 86 | 0 87 | 10 88 | -9 89 | 20 90 | 9 91 | 11 92 | -9 93 | 21 94 | -9 95 | 0 96 | ENDSEC 97 | 0 98 | EOF -------------------------------------------------------------------------------- /test/cli/big/reference/outlines/export.dxf: -------------------------------------------------------------------------------- 1 | 0 2 | SECTION 3 | 2 4 | HEADER 5 | 9 6 | $INSUNITS 7 | 70 8 | 4 9 | 0 10 | ENDSEC 11 | 0 12 | SECTION 13 | 2 14 | TABLES 15 | 0 16 | TABLE 17 | 2 18 | LTYPE 19 | 0 20 | LTYPE 21 | 72 22 | 65 23 | 70 24 | 64 25 | 2 26 | CONTINUOUS 27 | 3 28 | ______ 29 | 73 30 | 0 31 | 40 32 | 0 33 | 0 34 | ENDTAB 35 | 0 36 | TABLE 37 | 2 38 | LAYER 39 | 0 40 | ENDTAB 41 | 0 42 | ENDSEC 43 | 0 44 | SECTION 45 | 2 46 | ENTITIES 47 | 0 48 | LINE 49 | 8 50 | 0 51 | 10 52 | -9 53 | 20 54 | -9 55 | 11 56 | 9 57 | 21 58 | -9 59 | 0 60 | LINE 61 | 8 62 | 0 63 | 10 64 | 9 65 | 20 66 | -9 67 | 11 68 | 9 69 | 21 70 | 9 71 | 0 72 | LINE 73 | 8 74 | 0 75 | 10 76 | 9 77 | 20 78 | 9 79 | 11 80 | -9 81 | 21 82 | 9 83 | 0 84 | LINE 85 | 8 86 | 0 87 | 10 88 | -9 89 | 20 90 | 9 91 | 11 92 | -9 93 | 21 94 | -9 95 | 0 96 | ENDSEC 97 | 0 98 | EOF -------------------------------------------------------------------------------- /test/cli/medium/reference/outlines/export.dxf: -------------------------------------------------------------------------------- 1 | 0 2 | SECTION 3 | 2 4 | HEADER 5 | 9 6 | $INSUNITS 7 | 70 8 | 4 9 | 0 10 | ENDSEC 11 | 0 12 | SECTION 13 | 2 14 | TABLES 15 | 0 16 | TABLE 17 | 2 18 | LTYPE 19 | 0 20 | LTYPE 21 | 72 22 | 65 23 | 70 24 | 64 25 | 2 26 | CONTINUOUS 27 | 3 28 | ______ 29 | 73 30 | 0 31 | 40 32 | 0 33 | 0 34 | ENDTAB 35 | 0 36 | TABLE 37 | 2 38 | LAYER 39 | 0 40 | ENDTAB 41 | 0 42 | ENDSEC 43 | 0 44 | SECTION 45 | 2 46 | ENTITIES 47 | 0 48 | LINE 49 | 8 50 | 0 51 | 10 52 | -9 53 | 20 54 | -9 55 | 11 56 | 9 57 | 21 58 | -9 59 | 0 60 | LINE 61 | 8 62 | 0 63 | 10 64 | 9 65 | 20 66 | -9 67 | 11 68 | 9 69 | 21 70 | 9 71 | 0 72 | LINE 73 | 8 74 | 0 75 | 10 76 | 9 77 | 20 78 | 9 79 | 11 80 | -9 81 | 21 82 | 9 83 | 0 84 | LINE 85 | 8 86 | 0 87 | 10 88 | -9 89 | 20 90 | 9 91 | 11 92 | -9 93 | 21 94 | -9 95 | 0 96 | ENDSEC 97 | 0 98 | EOF -------------------------------------------------------------------------------- /test/cli/minimal/reference/points/demo.dxf: -------------------------------------------------------------------------------- 1 | 0 2 | SECTION 3 | 2 4 | HEADER 5 | 9 6 | $INSUNITS 7 | 70 8 | 4 9 | 0 10 | ENDSEC 11 | 0 12 | SECTION 13 | 2 14 | TABLES 15 | 0 16 | TABLE 17 | 2 18 | LTYPE 19 | 0 20 | LTYPE 21 | 72 22 | 65 23 | 70 24 | 64 25 | 2 26 | CONTINUOUS 27 | 3 28 | ______ 29 | 73 30 | 0 31 | 40 32 | 0 33 | 0 34 | ENDTAB 35 | 0 36 | TABLE 37 | 2 38 | LAYER 39 | 0 40 | ENDTAB 41 | 0 42 | ENDSEC 43 | 0 44 | SECTION 45 | 2 46 | ENTITIES 47 | 0 48 | LINE 49 | 8 50 | 0 51 | 10 52 | -9 53 | 20 54 | 9 55 | 11 56 | 9 57 | 21 58 | 9 59 | 0 60 | LINE 61 | 8 62 | 0 63 | 10 64 | 9 65 | 20 66 | 9 67 | 11 68 | 9 69 | 21 70 | -9 71 | 0 72 | LINE 73 | 8 74 | 0 75 | 10 76 | 9 77 | 20 78 | -9 79 | 11 80 | -9 81 | 21 82 | -9 83 | 0 84 | LINE 85 | 8 86 | 0 87 | 10 88 | -9 89 | 20 90 | -9 91 | 11 92 | -9 93 | 21 94 | 9 95 | 0 96 | ENDSEC 97 | 0 98 | EOF -------------------------------------------------------------------------------- /test/cli/minimal/reference/points/points.yaml: -------------------------------------------------------------------------------- 1 | matrix_col_row: 2 | x: 0 3 | 'y': 0 4 | r: 0 5 | meta: 6 | stagger: 0 7 | spread: 0 8 | splay: 0 9 | origin: 10 | - 0 11 | - 0 12 | orient: 0 13 | shift: 14 | - 0 15 | - 0 16 | rotate: 0 17 | width: 18 18 | height: 18 19 | padding: 19 20 | autobind: 10 21 | skip: false 22 | asym: both 23 | colrow: col_row 24 | name: matrix_col_row 25 | zone: 26 | columns: 27 | col: &ref_0 28 | rows: {} 29 | key: {} 30 | name: col 31 | rows: 32 | row: {} 33 | name: matrix 34 | col: *ref_0 35 | row: row 36 | bind: 37 | - 0 38 | - 0 39 | - 0 40 | - 0 41 | -------------------------------------------------------------------------------- /src/footprints/jumper.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | nets: { 3 | from: undefined, 4 | to: undefined 5 | }, 6 | params: { 7 | class: 'J', 8 | side: 'F' 9 | }, 10 | body: p => ` 11 | (module lib:Jumper (layer F.Cu) (tedit 5E1ADAC2) 12 | ${p.at /* parametric position */} 13 | 14 | ${'' /* footprint reference */} 15 | (fp_text reference "${p.ref}" (at 0 0) (layer F.SilkS) ${p.ref_hide} (effects (font (size 1.27 1.27) (thickness 0.15)))) 16 | (fp_text value Jumper (at 0 -7.3) (layer F.Fab) (effects (font (size 1 1) (thickness 0.15)))) 17 | 18 | ${'' /* pins */} 19 | (pad 1 smd rect (at -0.50038 0 ${p.rot}) (size 0.635 1.143) (layers ${p.param.side}.Cu ${p.param.side}.Paste ${p.param.side}.Mask) 20 | (clearance 0.1905) ${p.net.from.str}) 21 | (pad 2 smd rect (at 0.50038 0 ${p.rot}) (size 0.635 1.143) (layers ${p.param.side}.Cu ${p.param.side}.Paste ${p.param.side}.Mask) 22 | (clearance 0.1905) ${p.net.to.str})) 23 | ` 24 | } -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Bán Dénes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/unit/operation.js: -------------------------------------------------------------------------------- 1 | const o = require('../../src/operation') 2 | 3 | describe('Operation', function() { 4 | 5 | it('op_prefix', function() { 6 | o.op_prefix('arst').should.deep.equal({name: 'arst', operation: 'add'}) 7 | o.op_prefix('+arst').should.deep.equal({name: 'arst', operation: 'add'}) 8 | o.op_prefix('-arst').should.deep.equal({name: 'arst', operation: 'subtract'}) 9 | o.op_prefix('~arst').should.deep.equal({name: 'arst', operation: 'intersect'}) 10 | o.op_prefix('^arst').should.deep.equal({name: 'arst', operation: 'stack'}) 11 | }) 12 | 13 | it('operation', function() { 14 | // without choices, it's the same as op_prefix 15 | o.operation('arst').should.deep.equal({name: 'arst', operation: 'add'}) 16 | // with choices, it propagates type where it found the name 17 | o.operation('arst', {bad: [], good: ['arst']}).should.deep.equal({name: 'arst', operation: 'add', type: 'good'}) 18 | // it also respects order when overridden 19 | o.operation('arst', {first: ['arst'], second: ['arst']}, ['second', 'first']).should.deep.equal({name: 'arst', operation: 'add', type: 'second'}) 20 | }) 21 | 22 | }) -------------------------------------------------------------------------------- /src/footprints/oled.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | nets: { 3 | SDA: undefined, 4 | SCL: undefined, 5 | VCC: 'VCC', 6 | GND: 'GND' 7 | }, 8 | params: { 9 | class: 'OLED', 10 | side: 'F' 11 | }, 12 | body: p => ` 13 | (module lib:OLED_headers (layer F.Cu) (tedit 5E1ADAC2) 14 | ${p.at /* parametric position */} 15 | 16 | ${'' /* footprint reference */} 17 | (fp_text reference "${p.ref}" (at 0 0) (layer F.SilkS) ${p.ref_hide} (effects (font (size 1.27 1.27) (thickness 0.15)))) 18 | (fp_text value OLED (at 0 -7.3) (layer F.Fab) (effects (font (size 1 1) (thickness 0.15)))) 19 | 20 | ${'' /* pins */} 21 | (pad 4 thru_hole oval (at 1.6 2.18 ${p.rot+270}) (size 1.7 1.7) (drill 1) (layers *.Cu *.Mask) 22 | ${p.net.SDA.str}) 23 | (pad 3 thru_hole oval (at 1.6 4.72 ${p.rot+270}) (size 1.7 1.7) (drill 1) (layers *.Cu *.Mask) 24 | ${p.net.SCL.str}) 25 | (pad 2 thru_hole oval (at 1.6 7.26 ${p.rot+270}) (size 1.7 1.7) (drill 1) (layers *.Cu *.Mask) 26 | ${p.net.VCC.str}) 27 | (pad 1 thru_hole rect (at 1.6 9.8 ${p.rot+270}) (size 1.7 1.7) (drill 1) (layers *.Cu *.Mask) 28 | ${p.net.GND.str}) 29 | ) 30 | ` 31 | } -------------------------------------------------------------------------------- /test/outlines/affect_mirror.yaml: -------------------------------------------------------------------------------- 1 | points: 2 | zones: 3 | matrix: 4 | columns: 5 | only: 6 | rows: 7 | bottom: 8 | top: 9 | mirror: 10 | ref: matrix_only_top 11 | distance: 30 12 | outlines: 13 | test: 14 | keys: 15 | what: rectangle 16 | where: true 17 | size: 14 18 | bound: false 19 | rb: 20 | what: rectangle 21 | where: 22 | - ref: mirror_matrix_only_bottom 23 | # we do NOT specify `affect: xyr` here 24 | - shift: [-3,-3] 25 | orient: 30 26 | size: [6,6] 27 | operation: stack 28 | rt: 29 | what: rectangle 30 | where: 31 | - ref: mirror_matrix_only_top 32 | affect: xyr 33 | - shift: [-3,-3] 34 | orient: 30 35 | size: [6,6] 36 | operation: stack 37 | lb: 38 | what: rectangle 39 | where: 40 | - ref: matrix_only_bottom 41 | # again, no `affect: xyr` 42 | - shift: [-3,-3] 43 | orient: 30 44 | size: [6,6] 45 | operation: stack 46 | lt: 47 | what: rectangle 48 | where: 49 | - ref: matrix_only_top 50 | affect: xyr 51 | - shift: [-3,-3] 52 | orient: 30 53 | size: [6,6] 54 | operation: stack -------------------------------------------------------------------------------- /test/points/default___demo_dxf.dxf: -------------------------------------------------------------------------------- 1 | 0 2 | SECTION 3 | 2 4 | HEADER 5 | 9 6 | $INSUNITS 7 | 70 8 | 4 9 | 0 10 | ENDSEC 11 | 0 12 | SECTION 13 | 2 14 | TABLES 15 | 0 16 | TABLE 17 | 2 18 | LTYPE 19 | 0 20 | LTYPE 21 | 72 22 | 65 23 | 70 24 | 64 25 | 2 26 | CONTINUOUS 27 | 3 28 | ______ 29 | 73 30 | 0 31 | 40 32 | 0 33 | 0 34 | ENDTAB 35 | 0 36 | TABLE 37 | 2 38 | LAYER 39 | 0 40 | ENDTAB 41 | 0 42 | ENDSEC 43 | 0 44 | SECTION 45 | 2 46 | ENTITIES 47 | 0 48 | LINE 49 | 8 50 | 0 51 | 10 52 | -9 53 | 20 54 | 9 55 | 11 56 | 9 57 | 21 58 | 9 59 | 0 60 | LINE 61 | 8 62 | 0 63 | 10 64 | 9 65 | 20 66 | 9 67 | 11 68 | 9 69 | 21 70 | -9 71 | 0 72 | LINE 73 | 8 74 | 0 75 | 10 76 | 9 77 | 20 78 | -9 79 | 11 80 | -9 81 | 21 82 | -9 83 | 0 84 | LINE 85 | 8 86 | 0 87 | 10 88 | -9 89 | 20 90 | -9 91 | 11 92 | -9 93 | 21 94 | 9 95 | 0 96 | LINE 97 | 8 98 | 0 99 | 10 100 | -9 101 | 20 102 | 9 103 | 11 104 | 9 105 | 21 106 | 9 107 | 0 108 | LINE 109 | 8 110 | 0 111 | 10 112 | 9 113 | 20 114 | 9 115 | 11 116 | 9 117 | 21 118 | -9 119 | 0 120 | LINE 121 | 8 122 | 0 123 | 10 124 | 9 125 | 20 126 | -9 127 | 11 128 | -9 129 | 21 130 | -9 131 | 0 132 | LINE 133 | 8 134 | 0 135 | 10 136 | -9 137 | 20 138 | -9 139 | 11 140 | -9 141 | 21 142 | 9 143 | 0 144 | ENDSEC 145 | 0 146 | EOF -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ergogen", 3 | "version": "4.0.0-develop", 4 | "description": "Ergonomic keyboard layout generator", 5 | "author": "Bán Dénes ", 6 | "license": "MIT", 7 | "homepage": "https://ergogen.xyz", 8 | "repository": "github:ergogen/ergogen", 9 | "bugs": "https://github.com/ergogen/ergogen/issues", 10 | "main": "./src/ergogen.js", 11 | "bin": "./src/cli.js", 12 | "scripts": { 13 | "build": "rollup -c", 14 | "test": "mocha -r test/helpers/register test/index.js", 15 | "coverage": "nyc --reporter=html --reporter=text npm test" 16 | }, 17 | "dependencies": { 18 | "fs-extra": "^10.0.1", 19 | "js-yaml": "^3.14.1", 20 | "kle-serial": "github:ergogen/kle-serial#ergogen", 21 | "makerjs": "github:ergogen/maker.js#ergogen", 22 | "mathjs": "^10.1.1", 23 | "yargs": "^17.3.1" 24 | }, 25 | "devDependencies": { 26 | "@rollup/plugin-commonjs": "^21.0.2", 27 | "@rollup/plugin-json": "^4.1.0", 28 | "chai": "^4.3.6", 29 | "chai-as-promised": "^7.1.1", 30 | "dir-compare": "^4.0.0", 31 | "glob": "^7.2.0", 32 | "mocha": "^9.2.1", 33 | "nyc": "^15.1.0", 34 | "rollup": "^2.68.0" 35 | }, 36 | "nyc": { 37 | "all": true, 38 | "include": [ 39 | "src/**/*.js" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/cli/big/reference/points/demo.yaml: -------------------------------------------------------------------------------- 1 | models: 2 | export: 3 | models: 4 | matrix: 5 | paths: 6 | top: 7 | type: line 8 | origin: 9 | - -9 10 | - 9 11 | end: 12 | - 9 13 | - 9 14 | right: 15 | type: line 16 | origin: 17 | - 9 18 | - 9 19 | end: 20 | - 9 21 | - -9 22 | bottom: 23 | type: line 24 | origin: 25 | - 9 26 | - -9 27 | end: 28 | - -9 29 | - -9 30 | left: 31 | type: line 32 | origin: 33 | - -9 34 | - -9 35 | end: 36 | - -9 37 | - 9 38 | origin: 39 | - 0 40 | - 0 41 | origin: 42 | - 0 43 | - 0 44 | units: mm 45 | origin: 46 | - 0 47 | - 0 48 | -------------------------------------------------------------------------------- /test/cli/minimal/reference/points/demo.yaml: -------------------------------------------------------------------------------- 1 | models: 2 | export: 3 | models: 4 | matrix_col_row: 5 | paths: 6 | top: 7 | type: line 8 | origin: 9 | - -9 10 | - 9 11 | end: 12 | - 9 13 | - 9 14 | right: 15 | type: line 16 | origin: 17 | - 9 18 | - 9 19 | end: 20 | - 9 21 | - -9 22 | bottom: 23 | type: line 24 | origin: 25 | - 9 26 | - -9 27 | end: 28 | - -9 29 | - -9 30 | left: 31 | type: line 32 | origin: 33 | - -9 34 | - -9 35 | end: 36 | - -9 37 | - 9 38 | origin: 39 | - 0 40 | - 0 41 | origin: 42 | - 0 43 | - 0 44 | units: mm 45 | origin: 46 | - 0 47 | - 0 48 | -------------------------------------------------------------------------------- /test/cases/cube___cases_cube_jscad.jscad: -------------------------------------------------------------------------------- 1 | function square_outline_fn(){ 2 | return new CSG.Path2D([[-2.5,-2.5],[2.5,-2.5]]).appendPoint([2.5,2.5]).appendPoint([-2.5,2.5]).appendPoint([-2.5,-2.5]).close().innerToCAG() 3 | .extrude({ offset: [0, 0, 5] }); 4 | } 5 | 6 | 7 | 8 | 9 | function cube_case_fn() { 10 | 11 | 12 | // creating part 0 of case cube 13 | let cube__part_0 = square_outline_fn(); 14 | 15 | // make sure that rotations are relative 16 | let cube__part_0_bounds = cube__part_0.getBounds(); 17 | let cube__part_0_x = cube__part_0_bounds[0].x + (cube__part_0_bounds[1].x - cube__part_0_bounds[0].x) / 2 18 | let cube__part_0_y = cube__part_0_bounds[0].y + (cube__part_0_bounds[1].y - cube__part_0_bounds[0].y) / 2 19 | cube__part_0 = translate([-cube__part_0_x, -cube__part_0_y, 0], cube__part_0); 20 | cube__part_0 = rotate([0,0,0], cube__part_0); 21 | cube__part_0 = translate([cube__part_0_x, cube__part_0_y, 0], cube__part_0); 22 | 23 | cube__part_0 = translate([0,0,0], cube__part_0); 24 | let result = cube__part_0; 25 | 26 | 27 | return result; 28 | } 29 | 30 | 31 | 32 | function main() { 33 | return cube_case_fn(); 34 | } 35 | 36 | -------------------------------------------------------------------------------- /test/cli/big/reference/cases/export.jscad: -------------------------------------------------------------------------------- 1 | function export_outline_fn(){ 2 | return new CSG.Path2D([[-9,-9],[9,-9]]).appendPoint([9,9]).appendPoint([-9,9]).appendPoint([-9,-9]).close().innerToCAG() 3 | .extrude({ offset: [0, 0, 1] }); 4 | } 5 | 6 | 7 | 8 | 9 | function export_case_fn() { 10 | 11 | 12 | // creating part 0 of case export 13 | let export__part_0 = export_outline_fn(); 14 | 15 | // make sure that rotations are relative 16 | let export__part_0_bounds = export__part_0.getBounds(); 17 | let export__part_0_x = export__part_0_bounds[0].x + (export__part_0_bounds[1].x - export__part_0_bounds[0].x) / 2 18 | let export__part_0_y = export__part_0_bounds[0].y + (export__part_0_bounds[1].y - export__part_0_bounds[0].y) / 2 19 | export__part_0 = translate([-export__part_0_x, -export__part_0_y, 0], export__part_0); 20 | export__part_0 = rotate([0,0,0], export__part_0); 21 | export__part_0 = translate([export__part_0_x, export__part_0_y, 0], export__part_0); 22 | 23 | export__part_0 = translate([0,0,0], export__part_0); 24 | let result = export__part_0; 25 | 26 | 27 | return result; 28 | } 29 | 30 | 31 | 32 | function main() { 33 | return export_case_fn(); 34 | } 35 | 36 | -------------------------------------------------------------------------------- /test/cli/big/reference/cases/_export.jscad: -------------------------------------------------------------------------------- 1 | function export_outline_fn(){ 2 | return new CSG.Path2D([[-9,-9],[9,-9]]).appendPoint([9,9]).appendPoint([-9,9]).appendPoint([-9,-9]).close().innerToCAG() 3 | .extrude({ offset: [0, 0, 1] }); 4 | } 5 | 6 | 7 | 8 | 9 | function _export_case_fn() { 10 | 11 | 12 | // creating part 0 of case _export 13 | let _export__part_0 = export_outline_fn(); 14 | 15 | // make sure that rotations are relative 16 | let _export__part_0_bounds = _export__part_0.getBounds(); 17 | let _export__part_0_x = _export__part_0_bounds[0].x + (_export__part_0_bounds[1].x - _export__part_0_bounds[0].x) / 2 18 | let _export__part_0_y = _export__part_0_bounds[0].y + (_export__part_0_bounds[1].y - _export__part_0_bounds[0].y) / 2 19 | _export__part_0 = translate([-_export__part_0_x, -_export__part_0_y, 0], _export__part_0); 20 | _export__part_0 = rotate([0,0,0], _export__part_0); 21 | _export__part_0 = translate([_export__part_0_x, _export__part_0_y, 0], _export__part_0); 22 | 23 | _export__part_0 = translate([0,0,0], _export__part_0); 24 | let result = _export__part_0; 25 | 26 | 27 | return result; 28 | } 29 | 30 | 31 | 32 | function main() { 33 | return _export_case_fn(); 34 | } 35 | 36 | -------------------------------------------------------------------------------- /src/footprints/alps.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | nets: { 3 | from: undefined, 4 | to: undefined 5 | }, 6 | params: { 7 | class: 'S' 8 | }, 9 | body: p => ` 10 | 11 | (module ALPS (layer F.Cu) (tedit 5CF31DEF) 12 | 13 | ${p.at /* parametric position */} 14 | 15 | ${'' /* footprint reference */} 16 | (fp_text reference "${p.ref}" (at 0 0) (layer F.SilkS) ${p.ref_hide} (effects (font (size 1.27 1.27) (thickness 0.15)))) 17 | (fp_text value "" (at 0 0) (layer F.SilkS) hide (effects (font (size 1.27 1.27) (thickness 0.15)))) 18 | 19 | ${''/* corner marks */} 20 | (fp_line (start -7 -6) (end -7 -7) (layer Dwgs.User) (width 0.15)) 21 | (fp_line (start -7 7) (end -6 7) (layer Dwgs.User) (width 0.15)) 22 | (fp_line (start -6 -7) (end -7 -7) (layer Dwgs.User) (width 0.15)) 23 | (fp_line (start -7 7) (end -7 6) (layer Dwgs.User) (width 0.15)) 24 | (fp_line (start 7 6) (end 7 7) (layer Dwgs.User) (width 0.15)) 25 | (fp_line (start 7 -7) (end 6 -7) (layer Dwgs.User) (width 0.15)) 26 | (fp_line (start 6 7) (end 7 7) (layer Dwgs.User) (width 0.15)) 27 | (fp_line (start 7 -7) (end 7 -6) (layer Dwgs.User) (width 0.15)) 28 | 29 | ${''/* pins */} 30 | (pad 1 thru_hole circle (at 2.5 -4.5) (size 2.25 2.25) (drill 1.47) (layers *.Cu *.Mask) ${p.net.from.str}) 31 | (pad 2 thru_hole circle (at -2.5 -4) (size 2.25 2.25) (drill 1.47) (layers *.Cu *.Mask) ${p.net.to.str}) 32 | ) 33 | 34 | ` 35 | } -------------------------------------------------------------------------------- /test/unit/units.js: -------------------------------------------------------------------------------- 1 | const u = require('../../src/units') 2 | const public = key => !key.startsWith('$') 3 | 4 | describe('Units', function() { 5 | 6 | it('defaults', function() { 7 | // check that an empty config has the default units (and nothing more) 8 | const def = u.parse({}) 9 | Object.keys(def).filter(public).length.should.equal(4) 10 | def.U.should.equal(19.05) 11 | def.u.should.equal(19) 12 | def.cx.should.equal(18) 13 | def.cy.should.equal(17) 14 | }) 15 | 16 | it('units', function() { 17 | // check that units can contain formulas, and reference each other 18 | const res = u.parse({ 19 | units: { 20 | a: 'cx / 2', 21 | b: 'a + 1' 22 | } 23 | }) 24 | Object.keys(res).filter(public).length.should.equal(6) 25 | res.a.should.equal(9) 26 | res.b.should.equal(10) 27 | // also check that order matters, which it should 28 | u.parse.bind(this, { 29 | units: { 30 | a: 'b + 1', 31 | b: 'cx / 2' 32 | } 33 | }).should.throw() 34 | }) 35 | 36 | it('variables', function() { 37 | // check that variables work, and can override units 38 | const res = u.parse({ 39 | units: { 40 | a: 'cx / 2', 41 | }, 42 | variables: { 43 | a: 'U + 1' 44 | } 45 | }) 46 | Object.keys(res).filter(public).length.should.equal(5) 47 | res.a.should.equal(20.05) 48 | }) 49 | 50 | }) -------------------------------------------------------------------------------- /test/cli/big/reference/outlines/export.yaml: -------------------------------------------------------------------------------- 1 | models: 2 | export: 3 | models: 4 | a: 5 | models: {} 6 | origin: 7 | - 0 8 | - 0 9 | b: 10 | paths: 11 | ShapeLine1: 12 | type: line 13 | origin: 14 | - -9 15 | - -9 16 | end: 17 | - 9 18 | - -9 19 | ShapeLine2: 20 | type: line 21 | origin: 22 | - 9 23 | - -9 24 | end: 25 | - 9 26 | - 9 27 | ShapeLine3: 28 | type: line 29 | origin: 30 | - 9 31 | - 9 32 | end: 33 | - -9 34 | - 9 35 | ShapeLine4: 36 | type: line 37 | origin: 38 | - -9 39 | - 9 40 | end: 41 | - -9 42 | - -9 43 | origin: 44 | - 0 45 | - 0 46 | origin: 47 | - 0 48 | - 0 49 | units: mm 50 | origin: 51 | - 0 52 | - 0 53 | -------------------------------------------------------------------------------- /test/cli/big/reference/outlines/_export.yaml: -------------------------------------------------------------------------------- 1 | models: 2 | export: 3 | models: 4 | a: 5 | models: {} 6 | origin: 7 | - 0 8 | - 0 9 | b: 10 | paths: 11 | ShapeLine1: 12 | type: line 13 | origin: 14 | - -9 15 | - -9 16 | end: 17 | - 9 18 | - -9 19 | ShapeLine2: 20 | type: line 21 | origin: 22 | - 9 23 | - -9 24 | end: 25 | - 9 26 | - 9 27 | ShapeLine3: 28 | type: line 29 | origin: 30 | - 9 31 | - 9 32 | end: 33 | - -9 34 | - 9 35 | ShapeLine4: 36 | type: line 37 | origin: 38 | - -9 39 | - 9 40 | end: 41 | - -9 42 | - -9 43 | origin: 44 | - 0 45 | - 0 46 | origin: 47 | - 0 48 | - 0 49 | units: mm 50 | origin: 51 | - 0 52 | - 0 53 | -------------------------------------------------------------------------------- /test/outlines/circles___outlines_outline_dxf.dxf: -------------------------------------------------------------------------------- 1 | 0 2 | SECTION 3 | 2 4 | HEADER 5 | 9 6 | $INSUNITS 7 | 70 8 | 4 9 | 0 10 | ENDSEC 11 | 0 12 | SECTION 13 | 2 14 | TABLES 15 | 0 16 | TABLE 17 | 2 18 | LTYPE 19 | 0 20 | LTYPE 21 | 72 22 | 65 23 | 70 24 | 64 25 | 2 26 | CONTINUOUS 27 | 3 28 | ______ 29 | 73 30 | 0 31 | 40 32 | 0 33 | 0 34 | ENDTAB 35 | 0 36 | TABLE 37 | 2 38 | LAYER 39 | 0 40 | ENDTAB 41 | 0 42 | ENDSEC 43 | 0 44 | SECTION 45 | 2 46 | ENTITIES 47 | 0 48 | LINE 49 | 8 50 | 0 51 | 10 52 | -10 53 | 20 54 | -10 55 | 11 56 | 8.8196601 57 | 21 58 | -10 59 | 0 60 | LINE 61 | 8 62 | 0 63 | 10 64 | 8.8196601 65 | 20 66 | 10 67 | 11 68 | -5 69 | 21 70 | 10 71 | 0 72 | LINE 73 | 8 74 | 0 75 | 10 76 | -10 77 | 20 78 | 5 79 | 11 80 | -10 81 | 21 82 | -10 83 | 0 84 | LINE 85 | 8 86 | 0 87 | 10 88 | 31.1803399 89 | 20 90 | -10 91 | 11 92 | 50 93 | 21 94 | -10 95 | 0 96 | LINE 97 | 8 98 | 0 99 | 10 100 | 50 101 | 20 102 | -10 103 | 11 104 | 50 105 | 21 106 | 5 107 | 0 108 | LINE 109 | 8 110 | 0 111 | 10 112 | 45 113 | 20 114 | 10 115 | 11 116 | 31.1803399 117 | 21 118 | 10 119 | 0 120 | ARC 121 | 8 122 | 0 123 | 10 124 | 20 125 | 20 126 | 0 127 | 40 128 | 15 129 | 50 130 | 221.8103149 131 | 51 132 | 318.1896851 133 | 0 134 | ARC 135 | 8 136 | 0 137 | 10 138 | 20 139 | 20 140 | 0 141 | 40 142 | 15 143 | 50 144 | 401.8103149 145 | 51 146 | 498.1896851 147 | 0 148 | ARC 149 | 8 150 | 0 151 | 10 152 | -10 153 | 20 154 | 10 155 | 40 156 | 5 157 | 50 158 | 0 159 | 51 160 | 270 161 | 0 162 | ARC 163 | 8 164 | 0 165 | 10 166 | 50 167 | 20 168 | 10 169 | 40 170 | 5 171 | 50 172 | 270 173 | 51 174 | 540 175 | 0 176 | ENDSEC 177 | 0 178 | EOF -------------------------------------------------------------------------------- /src/footprints/omron.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | nets: { 3 | from: undefined, 4 | to: undefined 5 | }, 6 | params: { 7 | class: 'S' 8 | }, 9 | body: p => ` 10 | 11 | (module OMRON_B3F-4055 (layer F.Cu) (tstamp 5BF2CC94) 12 | 13 | ${p.at /* parametric position */} 14 | ${'' /* footprint reference */} 15 | (fp_text reference "${p.ref}" (at 0 0) (layer F.SilkS) ${p.ref_hide} (effects (font (size 1.27 1.27) (thickness 0.15)))) 16 | (fp_text value "" (at 0 0) (layer F.SilkS) hide (effects (font (size 1.27 1.27) (thickness 0.15)))) 17 | 18 | ${'' /* stabilizers */} 19 | (pad "" np_thru_hole circle (at 0 -4.5) (size 1.8 1.8) (drill 1.8) (layers *.Cu *.Mask)) 20 | (pad "" np_thru_hole circle (at 0 4.5) (size 1.8 1.8) (drill 1.8) (layers *.Cu *.Mask)) 21 | 22 | ${'' /* switch marks */} 23 | (fp_line (start -6 -6) (end 6 -6) (layer Dwgs.User) (width 0.15)) 24 | (fp_line (start 6 -6) (end 6 6) (layer Dwgs.User) (width 0.15)) 25 | (fp_line (start 6 6) (end -6 6) (layer Dwgs.User) (width 0.15)) 26 | (fp_line (start -6 6) (end -6 -6) (layer Dwgs.User) (width 0.15)) 27 | 28 | ${'' /* pins */} 29 | (pad 1 np_thru_hole circle (at 6.25 -2.5) (size 1.2 1.2) (drill 1.2) (layers *.Cu *.Mask) ${p.net.from.str}) 30 | (pad 2 np_thru_hole circle (at -6.25 -2.5) (size 1.2 1.2) (drill 1.2) (layers *.Cu *.Mask) ${p.net.from.str}) 31 | (pad 3 np_thru_hole circle (at 6.25 2.5) (size 1.2 1.2) (drill 1.2) (layers *.Cu *.Mask) ${p.net.to.str}) 32 | (pad 4 np_thru_hole circle (at -6.25 2.5 ) (size 1.2 1.2) (drill 1.2) (layers *.Cu *.Mask) ${p.net.to.str}) 33 | ) 34 | 35 | ` 36 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Ergogen 2 | 3 | Ergogen is a keyboard generator that aims to provide a common configuration format to describe **ergonomic** 2D layouts, and generate automatic plates, cases, and (un-routed) PCBs for them. 4 | The project grew out of (and is an integral part of) the [Absolem keyboard](https://zealot.hu/absolem), and shares its [Discord server](https://discord.gg/nbKcAZB) as well. 5 | 6 | ## Getting started 7 | 8 | Until there's a proper "Getting started" guide, try getting acquainted with ergogen by following these steps in order: 9 | 10 | 1. Read the [docs](https://docs.ergogen.xyz). D'uuh. They're not complete by any measure, but should give you a fairly good idea what you're dealing with here. 11 | 12 | 1. Try one of the web-based deployments ([official](https://ergogen.xyz); [unofficial](https://ergogen.cache.works/) but probably better and soon to be official) - no need to download the CLI unless you want to A) preview in-development features, B) use custom modifications, or C) contribute code. Click things, look at outputs; see if things start to make sense. 13 | 14 | 1. Search the [`#ergogen`](https://github.com/topics/ergogen) topic on GitHub to look at (and reverse engineer) a variety of real life configs using ergogen. Pop them into the web UI, see what they do, tinker with them; things should start to make more and more sense. 15 | 16 | 1. If a question persists after all of the above, feel free to ask it over on [Discord](https://discord.gg/nbKcAZB) and we'll do our best to help you out. 17 | 18 | 19 | ## Contributions 20 | 21 | Feature ideas, documentation improvements, examples, tests, or pull requests welcome! 22 | Get in touch [on Discord](https://discord.gg/nbKcAZB), and we can definitely find something you can help with, if you'd like to. 23 | -------------------------------------------------------------------------------- /src/point.js: -------------------------------------------------------------------------------- 1 | const m = require('makerjs') 2 | const u = require('./utils') 3 | 4 | module.exports = class Point { 5 | constructor(x=0, y=0, r=0, meta={}) { 6 | if (Array.isArray(x)) { 7 | this.x = x[0] 8 | this.y = x[1] 9 | this.r = 0 10 | this.meta = {} 11 | } else { 12 | this.x = x 13 | this.y = y 14 | this.r = r 15 | this.meta = meta 16 | } 17 | } 18 | 19 | get p() { 20 | return [this.x, this.y] 21 | } 22 | 23 | set p(val) { 24 | [this.x, this.y] = val 25 | } 26 | 27 | shift(s, relative=true) { 28 | if (relative) { 29 | s = m.point.rotate(s, this.r) 30 | } 31 | this.x += s[0] 32 | this.y += s[1] 33 | return this 34 | } 35 | 36 | rotate(angle, origin=[0, 0]) { 37 | this.p = m.point.rotate(this.p, angle, origin) 38 | this.r += angle 39 | return this 40 | } 41 | 42 | mirror(x) { 43 | this.x = 2 * x - this.x 44 | this.r = -this.r 45 | return this 46 | } 47 | 48 | clone() { 49 | return new Point( 50 | this.x, 51 | this.y, 52 | this.r, 53 | u.deepcopy(this.meta) 54 | ) 55 | } 56 | 57 | position(model) { 58 | return m.model.moveRelative(m.model.rotate(model, this.r), this.p) 59 | } 60 | 61 | unposition(model) { 62 | return m.model.rotate(m.model.moveRelative(model, [-this.x, -this.y]), -this.r) 63 | } 64 | 65 | rect(size=14) { 66 | let rect = u.rect(size, size, [-size/2, -size/2]) 67 | return this.position(rect) 68 | } 69 | 70 | angle(other) { 71 | const dx = other.x - this.x 72 | const dy = other.y - this.y 73 | return -Math.atan2(dx, dy) * (180 / Math.PI) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/io.js: -------------------------------------------------------------------------------- 1 | const yaml = require('js-yaml') 2 | const makerjs = require('makerjs') 3 | 4 | const u = require('./utils') 5 | const a = require('./assert') 6 | const kle = require('./kle') 7 | 8 | exports.interpret = (raw, logger) => { 9 | let config = raw 10 | let format = 'OBJ' 11 | if (a.type(raw)() == 'string') { 12 | try { 13 | config = yaml.safeLoad(raw) 14 | format = 'YAML' 15 | } catch (yamlex) { 16 | try { 17 | config = new Function(raw)() 18 | a.assert( 19 | a.type(config)() == 'object', 20 | 'Input JS Code doesn\'t resolve into an object!' 21 | ) 22 | format = 'JS' 23 | } catch (codeex) { 24 | logger('YAML exception:', yamlex) 25 | logger('Code exception:', codeex) 26 | throw new Error('Input is not valid YAML, JSON, or JS Code!') 27 | } 28 | } 29 | } 30 | 31 | try { 32 | // assume it's KLE and try to convert it 33 | config = kle.convert(config, logger) 34 | format = 'KLE' 35 | } catch (kleex) { 36 | // nope... nevermind 37 | } 38 | 39 | if (a.type(config)() != 'object') { 40 | throw new Error('Input doesn\'t resolve into an object!') 41 | } 42 | 43 | if (!Object.keys(config).length) { 44 | throw new Error('Input appears to be empty!') 45 | } 46 | 47 | return [config, format] 48 | } 49 | 50 | exports.twodee = (model, debug) => { 51 | const assembly = makerjs.model.originate({ 52 | models: { 53 | export: u.deepcopy(model) 54 | }, 55 | units: 'mm' 56 | }) 57 | 58 | const result = { 59 | dxf: makerjs.exporter.toDXF(assembly), 60 | } 61 | if (debug) { 62 | result.yaml = assembly 63 | result.svg = makerjs.exporter.toSVG(assembly) 64 | } 65 | return result 66 | } 67 | -------------------------------------------------------------------------------- /src/footprints/pad.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | nets: { 3 | net: undefined 4 | }, 5 | params: { 6 | class: 'PAD', 7 | width: 1, 8 | height: 1, 9 | front: true, 10 | back: true, 11 | text: '', 12 | align: 'left', 13 | mirrored: '=mirrored' 14 | }, 15 | body: p => { 16 | 17 | const layout = (toggle, side) => { 18 | if (!toggle) return '' 19 | let x = 0, y = 0 20 | const mirror = side == 'B' ? '(justify mirror)' : '' 21 | const plus = (p.param.text.length + 1) * 0.5 22 | let align = p.param.align 23 | if (p.param.mirrored === true) { 24 | if (align == 'left') align = 'right' 25 | else if (align == 'right') align = 'left' 26 | } 27 | if (align == 'left') x -= p.param.width / 2 + plus 28 | if (align == 'right') x += p.param.width / 2 + plus 29 | if (align == 'up') y += p.param.height / 2 + plus 30 | if (align == 'down') y -= p.param.height / 2 + plus 31 | const text = `(fp_text user ${p.param.text} (at ${x} ${y} ${p.rot}) (layer ${side}.SilkS) (effects (font (size 0.8 0.8) (thickness 0.15)) ${mirror}))` 32 | return `(pad 1 smd rect (at 0 0 ${p.rot}) (size ${p.param.width} ${p.param.height}) (layers ${side}.Cu ${side}.Paste ${side}.Mask) ${p.net.net.str})\n${text}` 33 | } 34 | 35 | return ` 36 | 37 | (module SMDPad (layer F.Cu) (tedit 5B24D78E) 38 | 39 | ${p.at /* parametric position */} 40 | 41 | ${'' /* footprint reference */} 42 | (fp_text reference "${p.ref}" (at 0 0) (layer F.SilkS) ${p.ref_hide} (effects (font (size 1.27 1.27) (thickness 0.15)))) 43 | (fp_text value "" (at 0 0) (layer F.SilkS) hide (effects (font (size 1.27 1.27) (thickness 0.15)))) 44 | 45 | ${''/* SMD pads */} 46 | ${layout(p.param.front, 'F')} 47 | ${layout(p.param.back, 'B')} 48 | 49 | ) 50 | 51 | ` 52 | } 53 | } -------------------------------------------------------------------------------- /src/footprints/jstph.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | nets: { 3 | pos: undefined, 4 | neg: undefined 5 | }, 6 | params: { 7 | class: 'JST', 8 | side: 'F' 9 | }, 10 | body: p => ` 11 | 12 | (module JST_PH_S2B-PH-K_02x2.00mm_Angled (layer F.Cu) (tedit 58D3FE32) 13 | 14 | (descr "JST PH series connector, S2B-PH-K, side entry type, through hole, Datasheet: http://www.jst-mfg.com/product/pdf/eng/ePH.pdf") 15 | (tags "connector jst ph") 16 | 17 | ${p.at /* parametric position */} 18 | 19 | ${'' /* footprint reference */} 20 | (fp_text reference "${p.ref}" (at 0 0) (layer F.SilkS) ${p.ref_hide} (effects (font (size 1.27 1.27) (thickness 0.15)))) 21 | (fp_text value "" (at 0 0) (layer F.SilkS) hide (effects (font (size 1.27 1.27) (thickness 0.15)))) 22 | 23 | (fp_line (start -2.25 0.25) (end -2.25 -1.35) (layer ${p.param.side}.SilkS) (width 0.15)) 24 | (fp_line (start -2.25 -1.35) (end -2.95 -1.35) (layer ${p.param.side}.SilkS) (width 0.15)) 25 | (fp_line (start -2.95 -1.35) (end -2.95 6.25) (layer ${p.param.side}.SilkS) (width 0.15)) 26 | (fp_line (start -2.95 6.25) (end 2.95 6.25) (layer ${p.param.side}.SilkS) (width 0.15)) 27 | (fp_line (start 2.95 6.25) (end 2.95 -1.35) (layer ${p.param.side}.SilkS) (width 0.15)) 28 | (fp_line (start 2.95 -1.35) (end 2.25 -1.35) (layer ${p.param.side}.SilkS) (width 0.15)) 29 | (fp_line (start 2.25 -1.35) (end 2.25 0.25) (layer ${p.param.side}.SilkS) (width 0.15)) 30 | (fp_line (start 2.25 0.25) (end -2.25 0.25) (layer ${p.param.side}.SilkS) (width 0.15)) 31 | 32 | (fp_line (start -1 1.5) (end -1 2.0) (layer ${p.param.side}.SilkS) (width 0.15)) 33 | (fp_line (start -1.25 1.75) (end -0.75 1.75) (layer ${p.param.side}.SilkS) (width 0.15)) 34 | 35 | (pad 1 thru_hole rect (at -1 0 ${p.rot}) (size 1.2 1.7) (drill 0.75) (layers *.Cu *.Mask) ${p.net.pos.str}) 36 | (pad 2 thru_hole oval (at 1 0 ${p.rot}) (size 1.2 1.7) (drill 0.75) (layers *.Cu *.Mask) ${p.net.neg.str}) 37 | 38 | ) 39 | 40 | ` 41 | } -------------------------------------------------------------------------------- /test/points/basic_2x2___demo_dxf.dxf: -------------------------------------------------------------------------------- 1 | 0 2 | SECTION 3 | 2 4 | HEADER 5 | 9 6 | $INSUNITS 7 | 70 8 | 4 9 | 0 10 | ENDSEC 11 | 0 12 | SECTION 13 | 2 14 | TABLES 15 | 0 16 | TABLE 17 | 2 18 | LTYPE 19 | 0 20 | LTYPE 21 | 72 22 | 65 23 | 70 24 | 64 25 | 2 26 | CONTINUOUS 27 | 3 28 | ______ 29 | 73 30 | 0 31 | 40 32 | 0 33 | 0 34 | ENDTAB 35 | 0 36 | TABLE 37 | 2 38 | LAYER 39 | 0 40 | ENDTAB 41 | 0 42 | ENDSEC 43 | 0 44 | SECTION 45 | 2 46 | ENTITIES 47 | 0 48 | LINE 49 | 8 50 | 0 51 | 10 52 | -9 53 | 20 54 | 9 55 | 11 56 | 9 57 | 21 58 | 9 59 | 0 60 | LINE 61 | 8 62 | 0 63 | 10 64 | 9 65 | 20 66 | 9 67 | 11 68 | 9 69 | 21 70 | -9 71 | 0 72 | LINE 73 | 8 74 | 0 75 | 10 76 | 9 77 | 20 78 | -9 79 | 11 80 | -9 81 | 21 82 | -9 83 | 0 84 | LINE 85 | 8 86 | 0 87 | 10 88 | -9 89 | 20 90 | -9 91 | 11 92 | -9 93 | 21 94 | 9 95 | 0 96 | LINE 97 | 8 98 | 0 99 | 10 100 | -9 101 | 20 102 | 28 103 | 11 104 | 9 105 | 21 106 | 28 107 | 0 108 | LINE 109 | 8 110 | 0 111 | 10 112 | 9 113 | 20 114 | 28 115 | 11 116 | 9 117 | 21 118 | 10 119 | 0 120 | LINE 121 | 8 122 | 0 123 | 10 124 | 9 125 | 20 126 | 10 127 | 11 128 | -9 129 | 21 130 | 10 131 | 0 132 | LINE 133 | 8 134 | 0 135 | 10 136 | -9 137 | 20 138 | 10 139 | 11 140 | -9 141 | 21 142 | 28 143 | 0 144 | LINE 145 | 8 146 | 0 147 | 10 148 | 10 149 | 20 150 | 9 151 | 11 152 | 28 153 | 21 154 | 9 155 | 0 156 | LINE 157 | 8 158 | 0 159 | 10 160 | 28 161 | 20 162 | 9 163 | 11 164 | 28 165 | 21 166 | -9 167 | 0 168 | LINE 169 | 8 170 | 0 171 | 10 172 | 28 173 | 20 174 | -9 175 | 11 176 | 10 177 | 21 178 | -9 179 | 0 180 | LINE 181 | 8 182 | 0 183 | 10 184 | 10 185 | 20 186 | -9 187 | 11 188 | 10 189 | 21 190 | 9 191 | 0 192 | LINE 193 | 8 194 | 0 195 | 10 196 | 10 197 | 20 198 | 28 199 | 11 200 | 28 201 | 21 202 | 28 203 | 0 204 | LINE 205 | 8 206 | 0 207 | 10 208 | 28 209 | 20 210 | 28 211 | 11 212 | 28 213 | 21 214 | 10 215 | 0 216 | LINE 217 | 8 218 | 0 219 | 10 220 | 28 221 | 20 222 | 10 223 | 11 224 | 10 225 | 21 226 | 10 227 | 0 228 | LINE 229 | 8 230 | 0 231 | 10 232 | 10 233 | 20 234 | 10 235 | 11 236 | 10 237 | 21 238 | 28 239 | 0 240 | ENDSEC 241 | 0 242 | EOF -------------------------------------------------------------------------------- /src/footprints/button.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | nets: { 3 | from: undefined, 4 | to: undefined 5 | }, 6 | params: { 7 | class: 'B', // for Button 8 | side: 'F' 9 | }, 10 | body: p => ` 11 | 12 | (module E73:SW_TACT_ALPS_SKQGABE010 (layer F.Cu) (tstamp 5BF2CC94) 13 | 14 | (descr "Low-profile SMD Tactile Switch, https://www.e-switch.com/product-catalog/tact/product-lines/tl3342-series-low-profile-smt-tact-switch") 15 | (tags "SPST Tactile Switch") 16 | 17 | ${p.at /* parametric position */} 18 | ${'' /* footprint reference */} 19 | (fp_text reference "${p.ref}" (at 0 0) (layer F.SilkS) ${p.ref_hide} (effects (font (size 1.27 1.27) (thickness 0.15)))) 20 | (fp_text value "" (at 0 0) (layer F.SilkS) hide (effects (font (size 1.27 1.27) (thickness 0.15)))) 21 | 22 | ${'' /* outline */} 23 | (fp_line (start 2.75 1.25) (end 1.25 2.75) (layer ${p.param.side}.SilkS) (width 0.15)) 24 | (fp_line (start 2.75 -1.25) (end 1.25 -2.75) (layer ${p.param.side}.SilkS) (width 0.15)) 25 | (fp_line (start 2.75 -1.25) (end 2.75 1.25) (layer ${p.param.side}.SilkS) (width 0.15)) 26 | (fp_line (start -1.25 2.75) (end 1.25 2.75) (layer ${p.param.side}.SilkS) (width 0.15)) 27 | (fp_line (start -1.25 -2.75) (end 1.25 -2.75) (layer ${p.param.side}.SilkS) (width 0.15)) 28 | (fp_line (start -2.75 1.25) (end -1.25 2.75) (layer ${p.param.side}.SilkS) (width 0.15)) 29 | (fp_line (start -2.75 -1.25) (end -1.25 -2.75) (layer ${p.param.side}.SilkS) (width 0.15)) 30 | (fp_line (start -2.75 -1.25) (end -2.75 1.25) (layer ${p.param.side}.SilkS) (width 0.15)) 31 | 32 | ${'' /* pins */} 33 | (pad 1 smd rect (at -3.1 -1.85 ${p.rot}) (size 1.8 1.1) (layers ${p.param.side}.Cu ${p.param.side}.Paste ${p.param.side}.Mask) ${p.net.from.str}) 34 | (pad 1 smd rect (at 3.1 -1.85 ${p.rot}) (size 1.8 1.1) (layers ${p.param.side}.Cu ${p.param.side}.Paste ${p.param.side}.Mask) ${p.net.from.str}) 35 | (pad 2 smd rect (at -3.1 1.85 ${p.rot}) (size 1.8 1.1) (layers ${p.param.side}.Cu ${p.param.side}.Paste ${p.param.side}.Mask) ${p.net.to.str}) 36 | (pad 2 smd rect (at 3.1 1.85 ${p.rot}) (size 1.8 1.1) (layers ${p.param.side}.Cu ${p.param.side}.Paste ${p.param.side}.Mask) ${p.net.to.str}) 37 | ) 38 | 39 | ` 40 | } 41 | -------------------------------------------------------------------------------- /test/points/adjustments___demo_dxf.dxf: -------------------------------------------------------------------------------- 1 | 0 2 | SECTION 3 | 2 4 | HEADER 5 | 9 6 | $INSUNITS 7 | 70 8 | 4 9 | 0 10 | ENDSEC 11 | 0 12 | SECTION 13 | 2 14 | TABLES 15 | 0 16 | TABLE 17 | 2 18 | LTYPE 19 | 0 20 | LTYPE 21 | 72 22 | 65 23 | 70 24 | 64 25 | 2 26 | CONTINUOUS 27 | 3 28 | ______ 29 | 73 30 | 0 31 | 40 32 | 0 33 | 0 34 | ENDTAB 35 | 0 36 | TABLE 37 | 2 38 | LAYER 39 | 0 40 | ENDTAB 41 | 0 42 | ENDSEC 43 | 0 44 | SECTION 45 | 2 46 | ENTITIES 47 | 0 48 | LINE 49 | 8 50 | 0 51 | 10 52 | -9 53 | 20 54 | 9 55 | 11 56 | 9 57 | 21 58 | 9 59 | 0 60 | LINE 61 | 8 62 | 0 63 | 10 64 | 9 65 | 20 66 | 9 67 | 11 68 | 9 69 | 21 70 | -9 71 | 0 72 | LINE 73 | 8 74 | 0 75 | 10 76 | 9 77 | 20 78 | -9 79 | 11 80 | -9 81 | 21 82 | -9 83 | 0 84 | LINE 85 | 8 86 | 0 87 | 10 88 | -9 89 | 20 90 | -9 91 | 11 92 | -9 93 | 21 94 | 9 95 | 0 96 | LINE 97 | 8 98 | 0 99 | 10 100 | -9 101 | 20 102 | 28 103 | 11 104 | 9 105 | 21 106 | 28 107 | 0 108 | LINE 109 | 8 110 | 0 111 | 10 112 | 9 113 | 20 114 | 28 115 | 11 116 | 9 117 | 21 118 | 10 119 | 0 120 | LINE 121 | 8 122 | 0 123 | 10 124 | 9 125 | 20 126 | 10 127 | 11 128 | -9 129 | 21 130 | 10 131 | 0 132 | LINE 133 | 8 134 | 0 135 | 10 136 | -9 137 | 20 138 | 10 139 | 11 140 | -9 141 | 21 142 | 28 143 | 0 144 | LINE 145 | 8 146 | 0 147 | 10 148 | 14.4311966 149 | 20 150 | 13.9315046 151 | 11 152 | 32.3627012 153 | 21 154 | 15.500308 155 | 0 156 | LINE 157 | 8 158 | 0 159 | 10 160 | 32.3627012 161 | 20 162 | 15.500308 163 | 11 164 | 33.9315046 165 | 21 166 | -2.4311966 167 | 0 168 | LINE 169 | 8 170 | 0 171 | 10 172 | 33.9315046 173 | 20 174 | -2.4311966 175 | 11 176 | 16 177 | 21 178 | -4 179 | 0 180 | LINE 181 | 8 182 | 0 183 | 10 184 | 16 185 | 20 186 | -4 187 | 11 188 | 14.4311966 189 | 21 190 | 13.9315046 191 | 0 192 | LINE 193 | 8 194 | 0 195 | 10 196 | 22.7371845 197 | 20 198 | 33.7307612 199 | 11 200 | 40.6686891 201 | 21 202 | 35.2995646 203 | 0 204 | LINE 205 | 8 206 | 0 207 | 10 208 | 40.6686891 209 | 20 210 | 35.2995646 211 | 11 212 | 42.2374925 213 | 21 214 | 17.36806 215 | 0 216 | LINE 217 | 8 218 | 0 219 | 10 220 | 42.2374925 221 | 20 222 | 17.36806 223 | 11 224 | 24.3059879 225 | 21 226 | 15.7992566 227 | 0 228 | LINE 229 | 8 230 | 0 231 | 10 232 | 24.3059879 233 | 20 234 | 15.7992566 235 | 11 236 | 22.7371845 237 | 21 238 | 33.7307612 239 | 0 240 | ENDSEC 241 | 0 242 | EOF -------------------------------------------------------------------------------- /test/outlines/outlines___outlines_scale_dxf.dxf: -------------------------------------------------------------------------------- 1 | 0 2 | SECTION 3 | 2 4 | HEADER 5 | 9 6 | $INSUNITS 7 | 70 8 | 4 9 | 0 10 | ENDSEC 11 | 0 12 | SECTION 13 | 2 14 | TABLES 15 | 0 16 | TABLE 17 | 2 18 | LTYPE 19 | 0 20 | LTYPE 21 | 72 22 | 65 23 | 70 24 | 64 25 | 2 26 | CONTINUOUS 27 | 3 28 | ______ 29 | 73 30 | 0 31 | 40 32 | 0 33 | 0 34 | ENDTAB 35 | 0 36 | TABLE 37 | 2 38 | LAYER 39 | 0 40 | ENDTAB 41 | 0 42 | ENDSEC 43 | 0 44 | SECTION 45 | 2 46 | ENTITIES 47 | 0 48 | LINE 49 | 8 50 | 0 51 | 10 52 | 4.3 53 | 20 54 | -3.3 55 | 11 56 | 4.3 57 | 21 58 | 11.8 59 | 0 60 | LINE 61 | 8 62 | 0 63 | 10 64 | -3.3 65 | 20 66 | -4.3 67 | 11 68 | 3.3 69 | 21 70 | -4.3 71 | 0 72 | LINE 73 | 8 74 | 0 75 | 10 76 | -4.3 77 | 20 78 | -3.3 79 | 11 80 | -4.3 81 | 21 82 | 11.8 83 | 0 84 | LINE 85 | 8 86 | 0 87 | 10 88 | -3.3 89 | 20 90 | 12.8 91 | 11 92 | 3.3 93 | 21 94 | 12.8 95 | 0 96 | LINE 97 | 8 98 | 0 99 | 10 100 | 13.8 101 | 20 102 | -3.3 103 | 11 104 | 13.8 105 | 21 106 | 11.8 107 | 0 108 | LINE 109 | 8 110 | 0 111 | 10 112 | 6.2 113 | 20 114 | -4.3 115 | 11 116 | 12.8 117 | 21 118 | -4.3 119 | 0 120 | LINE 121 | 8 122 | 0 123 | 10 124 | 5.2 125 | 20 126 | -3.3 127 | 11 128 | 5.2 129 | 21 130 | 11.8 131 | 0 132 | LINE 133 | 8 134 | 0 135 | 10 136 | 6.2 137 | 20 138 | 12.8 139 | 11 140 | 12.8 141 | 21 142 | 12.8 143 | 0 144 | ARC 145 | 8 146 | 0 147 | 10 148 | 3.3 149 | 20 150 | 11.8 151 | 40 152 | 1 153 | 50 154 | 0 155 | 51 156 | 90 157 | 0 158 | ARC 159 | 8 160 | 0 161 | 10 162 | -3.3 163 | 20 164 | 11.8 165 | 40 166 | 1 167 | 50 168 | 90 169 | 51 170 | 180 171 | 0 172 | ARC 173 | 8 174 | 0 175 | 10 176 | -3.3 177 | 20 178 | -3.3 179 | 40 180 | 1 181 | 50 182 | 180 183 | 51 184 | 270 185 | 0 186 | ARC 187 | 8 188 | 0 189 | 10 190 | 3.3 191 | 20 192 | -3.3 193 | 40 194 | 1 195 | 50 196 | 270 197 | 51 198 | 0 199 | 0 200 | ARC 201 | 8 202 | 0 203 | 10 204 | 12.8 205 | 20 206 | 11.8 207 | 40 208 | 1 209 | 50 210 | 0 211 | 51 212 | 90 213 | 0 214 | ARC 215 | 8 216 | 0 217 | 10 218 | 6.2 219 | 20 220 | 11.8 221 | 40 222 | 1 223 | 50 224 | 90 225 | 51 226 | 180 227 | 0 228 | ARC 229 | 8 230 | 0 231 | 10 232 | 6.2 233 | 20 234 | -3.3 235 | 40 236 | 1 237 | 50 238 | 180 239 | 51 240 | 270 241 | 0 242 | ARC 243 | 8 244 | 0 245 | 10 246 | 12.8 247 | 20 248 | -3.3 249 | 40 250 | 1 251 | 50 252 | 270 253 | 51 254 | 0 255 | 0 256 | ENDSEC 257 | 0 258 | EOF -------------------------------------------------------------------------------- /test/outlines/outlines___outlines_fillet_dxf.dxf: -------------------------------------------------------------------------------- 1 | 0 2 | SECTION 3 | 2 4 | HEADER 5 | 9 6 | $INSUNITS 7 | 70 8 | 4 9 | 0 10 | ENDSEC 11 | 0 12 | SECTION 13 | 2 14 | TABLES 15 | 0 16 | TABLE 17 | 2 18 | LTYPE 19 | 0 20 | LTYPE 21 | 72 22 | 65 23 | 70 24 | 64 25 | 2 26 | CONTINUOUS 27 | 3 28 | ______ 29 | 73 30 | 0 31 | 40 32 | 0 33 | 0 34 | ENDTAB 35 | 0 36 | TABLE 37 | 2 38 | LAYER 39 | 0 40 | ENDTAB 41 | 0 42 | ENDSEC 43 | 0 44 | SECTION 45 | 2 46 | ENTITIES 47 | 0 48 | LINE 49 | 8 50 | 0 51 | 10 52 | 8.6 53 | 20 54 | -6.6 55 | 11 56 | 8.6 57 | 21 58 | 23.6 59 | 0 60 | LINE 61 | 8 62 | 0 63 | 10 64 | -6.6 65 | 20 66 | -8.6 67 | 11 68 | 6.6 69 | 21 70 | -8.6 71 | 0 72 | LINE 73 | 8 74 | 0 75 | 10 76 | -8.6 77 | 20 78 | -6.6 79 | 11 80 | -8.6 81 | 21 82 | 23.6 83 | 0 84 | LINE 85 | 8 86 | 0 87 | 10 88 | -6.6 89 | 20 90 | 25.6 91 | 11 92 | 6.6 93 | 21 94 | 25.6 95 | 0 96 | LINE 97 | 8 98 | 0 99 | 10 100 | 27.6 101 | 20 102 | -6.6 103 | 11 104 | 27.6 105 | 21 106 | 23.6 107 | 0 108 | LINE 109 | 8 110 | 0 111 | 10 112 | 12.4 113 | 20 114 | -8.6 115 | 11 116 | 25.6 117 | 21 118 | -8.6 119 | 0 120 | LINE 121 | 8 122 | 0 123 | 10 124 | 10.4 125 | 20 126 | -6.6 127 | 11 128 | 10.4 129 | 21 130 | 23.6 131 | 0 132 | LINE 133 | 8 134 | 0 135 | 10 136 | 12.4 137 | 20 138 | 25.6 139 | 11 140 | 25.6 141 | 21 142 | 25.6 143 | 0 144 | ARC 145 | 8 146 | 0 147 | 10 148 | 6.6 149 | 20 150 | 23.6 151 | 40 152 | 2 153 | 50 154 | 0 155 | 51 156 | 90 157 | 0 158 | ARC 159 | 8 160 | 0 161 | 10 162 | -6.6 163 | 20 164 | 23.6 165 | 40 166 | 2 167 | 50 168 | 90 169 | 51 170 | 180 171 | 0 172 | ARC 173 | 8 174 | 0 175 | 10 176 | -6.6 177 | 20 178 | -6.6 179 | 40 180 | 2 181 | 50 182 | 180 183 | 51 184 | 270 185 | 0 186 | ARC 187 | 8 188 | 0 189 | 10 190 | 6.6 191 | 20 192 | -6.6 193 | 40 194 | 2 195 | 50 196 | 270 197 | 51 198 | 0 199 | 0 200 | ARC 201 | 8 202 | 0 203 | 10 204 | 25.6 205 | 20 206 | 23.6 207 | 40 208 | 2 209 | 50 210 | 0 211 | 51 212 | 90 213 | 0 214 | ARC 215 | 8 216 | 0 217 | 10 218 | 12.4 219 | 20 220 | 23.6 221 | 40 222 | 2 223 | 50 224 | 90 225 | 51 226 | 180 227 | 0 228 | ARC 229 | 8 230 | 0 231 | 10 232 | 12.4 233 | 20 234 | -6.6 235 | 40 236 | 2 237 | 50 238 | 180 239 | 51 240 | 270 241 | 0 242 | ARC 243 | 8 244 | 0 245 | 10 246 | 25.6 247 | 20 248 | -6.6 249 | 40 250 | 2 251 | 50 252 | 270 253 | 51 254 | 0 255 | 0 256 | ENDSEC 257 | 0 258 | EOF -------------------------------------------------------------------------------- /test/points/default___points.json: -------------------------------------------------------------------------------- 1 | { 2 | "matrix": { 3 | "x": 0, 4 | "y": 0, 5 | "r": 0, 6 | "meta": { 7 | "stagger": 0, 8 | "spread": 0, 9 | "splay": 0, 10 | "origin": [ 11 | 0, 12 | 0 13 | ], 14 | "orient": 0, 15 | "shift": [ 16 | 0, 17 | 0 18 | ], 19 | "rotate": 0, 20 | "width": 18, 21 | "height": 18, 22 | "padding": 19, 23 | "autobind": 10, 24 | "skip": false, 25 | "asym": "both", 26 | "colrow": "default_default", 27 | "name": "matrix", 28 | "zone": { 29 | "name": "matrix" 30 | }, 31 | "col": { 32 | "rows": {}, 33 | "key": {}, 34 | "name": "default" 35 | }, 36 | "row": "default", 37 | "bind": [ 38 | 0, 39 | 0, 40 | 0, 41 | 0 42 | ] 43 | } 44 | }, 45 | "single_key_column_named": { 46 | "x": 0, 47 | "y": 0, 48 | "r": 0, 49 | "meta": { 50 | "stagger": 0, 51 | "spread": 0, 52 | "splay": 0, 53 | "origin": [ 54 | 0, 55 | 0 56 | ], 57 | "orient": 0, 58 | "shift": [ 59 | 0, 60 | 0 61 | ], 62 | "rotate": 0, 63 | "width": 18, 64 | "height": 18, 65 | "padding": 19, 66 | "autobind": 10, 67 | "skip": false, 68 | "asym": "both", 69 | "colrow": "named_default", 70 | "name": "single_key_column_named", 71 | "zone": { 72 | "columns": { 73 | "named": null 74 | }, 75 | "name": "single_key_column" 76 | }, 77 | "col": { 78 | "rows": {}, 79 | "key": {}, 80 | "name": "named" 81 | }, 82 | "row": "default", 83 | "bind": [ 84 | 0, 85 | 0, 86 | 0, 87 | 0 88 | ] 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /test/outlines/polygons___outlines_outline_dxf.dxf: -------------------------------------------------------------------------------- 1 | 0 2 | SECTION 3 | 2 4 | HEADER 5 | 9 6 | $INSUNITS 7 | 70 8 | 4 9 | 0 10 | ENDSEC 11 | 0 12 | SECTION 13 | 2 14 | TABLES 15 | 0 16 | TABLE 17 | 2 18 | LTYPE 19 | 0 20 | LTYPE 21 | 72 22 | 65 23 | 70 24 | 64 25 | 2 26 | CONTINUOUS 27 | 3 28 | ______ 29 | 73 30 | 0 31 | 40 32 | 0 33 | 0 34 | ENDTAB 35 | 0 36 | TABLE 37 | 2 38 | LAYER 39 | 0 40 | ENDTAB 41 | 0 42 | ENDSEC 43 | 0 44 | SECTION 45 | 2 46 | ENTITIES 47 | 0 48 | LINE 49 | 8 50 | 0 51 | 10 52 | -10 53 | 20 54 | -10 55 | 11 56 | 5 57 | 21 58 | -10 59 | 0 60 | LINE 61 | 8 62 | 0 63 | 10 64 | 10 65 | 20 66 | 0 67 | 11 68 | 10 69 | 21 70 | 10 71 | 0 72 | LINE 73 | 8 74 | 0 75 | 10 76 | 10 77 | 20 78 | 10 79 | 11 80 | -7.5 81 | 21 82 | 10 83 | 0 84 | LINE 85 | 8 86 | 0 87 | 10 88 | -10 89 | 20 90 | 5 91 | 11 92 | -10 93 | 21 94 | -10 95 | 0 96 | LINE 97 | 8 98 | 0 99 | 10 100 | 35 101 | 20 102 | -10 103 | 11 104 | 50 105 | 21 106 | -10 107 | 0 108 | LINE 109 | 8 110 | 0 111 | 10 112 | 50 113 | 20 114 | -10 115 | 11 116 | 50 117 | 21 118 | 5 119 | 0 120 | LINE 121 | 8 122 | 0 123 | 10 124 | 47.5 125 | 20 126 | 10 127 | 11 128 | 30 129 | 21 130 | 10 131 | 0 132 | LINE 133 | 8 134 | 0 135 | 10 136 | 30 137 | 20 138 | 10 139 | 11 140 | 30 141 | 21 142 | 0 143 | 0 144 | LINE 145 | 8 146 | 0 147 | 10 148 | 0 149 | 20 150 | -20 151 | 11 152 | 5 153 | 21 154 | -10 155 | 0 156 | LINE 157 | 8 158 | 0 159 | 10 160 | 10 161 | 20 162 | 0 163 | 11 164 | 20 165 | 21 166 | 20 167 | 0 168 | LINE 169 | 8 170 | 0 171 | 10 172 | 20 173 | 20 174 | 20 175 | 11 176 | 30 177 | 21 178 | 0 179 | 0 180 | LINE 181 | 8 182 | 0 183 | 10 184 | 35 185 | 20 186 | -10 187 | 11 188 | 40 189 | 21 190 | -20 191 | 0 192 | LINE 193 | 8 194 | 0 195 | 10 196 | 40 197 | 20 198 | -20 199 | 11 200 | 0 201 | 21 202 | -20 203 | 0 204 | LINE 205 | 8 206 | 0 207 | 10 208 | -15 209 | 20 210 | 5 211 | 11 212 | -10 213 | 21 214 | 15 215 | 0 216 | LINE 217 | 8 218 | 0 219 | 10 220 | -10 221 | 20 222 | 15 223 | 11 224 | -7.5 225 | 21 226 | 10 227 | 0 228 | LINE 229 | 8 230 | 0 231 | 10 232 | -10 233 | 20 234 | 5 235 | 11 236 | -15 237 | 21 238 | 5 239 | 0 240 | LINE 241 | 8 242 | 0 243 | 10 244 | 55 245 | 20 246 | 5 247 | 11 248 | 50 249 | 21 250 | 15 251 | 0 252 | LINE 253 | 8 254 | 0 255 | 10 256 | 50 257 | 20 258 | 15 259 | 11 260 | 47.5 261 | 21 262 | 10 263 | 0 264 | LINE 265 | 8 266 | 0 267 | 10 268 | 50 269 | 20 270 | 5 271 | 11 272 | 55 273 | 21 274 | 5 275 | 0 276 | ENDSEC 277 | 0 278 | EOF -------------------------------------------------------------------------------- /test/unit/filter.js: -------------------------------------------------------------------------------- 1 | const filter = require('../../src/filter').parse 2 | const anchor = require('../../src/anchor').parse 3 | const Point = require('../../src/point') 4 | 5 | describe('Filter', function() { 6 | 7 | const points = { 8 | one: new Point(0, 1, 0, {name: 'one', tags: ['odd']}), 9 | two: new Point(0, 2, 0, {name: 'two', tags: ['even', 'prime']}), 10 | three: new Point(0, 3, 0, {name: 'three', tags: {odd: 'yes', prime: 'yupp'}}) 11 | } 12 | 13 | const names = points => points.map(p => p.meta.name) 14 | 15 | it('similar', function() { 16 | // an undefined config leads to a default point 17 | filter(undefined, '', points).should.deep.equal([new Point()]) 18 | // true shouldn't filter anything, while false should filter everything 19 | filter(true, '', points).should.deep.equal(Object.values(points)) 20 | filter(false, '', points).should.deep.equal([]) 21 | // objects just propagate to anchor (and then wrap in array for consistency) 22 | filter({}, '', points).should.deep.equal([anchor({}, '', points)()]) 23 | // simple name string 24 | names(filter('one', '', points)).should.deep.equal(['one']) 25 | // simple name regex 26 | names(filter('/^t/', '', points)).should.deep.equal(['two', 'three']) 27 | // tags should count, too (one for the name, three for the odd tag) 28 | names(filter('/^o/', '', points)).should.deep.equal(['one', 'three']) 29 | // middle spec, should be the same as above, only explicit 30 | names(filter('~ /^o/', '', points)).should.deep.equal(['one', 'three']) 31 | // full spec (n would normally match both one and even, but on the tags level, it's just even) 32 | names(filter('meta.tags ~ /n/', '', points)).should.deep.equal(['two']) 33 | // negation 34 | names(filter('meta.tags ~ -/n/', '', points)).should.deep.equal(['one', 'three']) 35 | // arrays OR by default at odd levels levels (including top level)... 36 | names(filter(['one', 'two'], '', points)).should.deep.equal(['one', 'two']) 37 | // ...and AND at even levels 38 | names(filter([['even', 'prime']], '', points)).should.deep.equal(['two']) 39 | // arbitrary nesting should be possible 40 | names(filter([[['even', 'odd'], 'prime']], '', points)).should.deep.equal(['two', 'three']) 41 | // anything other than string/array/object/undefined is an error 42 | filter.bind(this, 28, '', points).should.throw('Unexpected type') 43 | }) 44 | 45 | }) -------------------------------------------------------------------------------- /test/outlines/basic___outlines_outline_dxf.dxf: -------------------------------------------------------------------------------- 1 | 0 2 | SECTION 3 | 2 4 | HEADER 5 | 9 6 | $INSUNITS 7 | 70 8 | 4 9 | 0 10 | ENDSEC 11 | 0 12 | SECTION 13 | 2 14 | TABLES 15 | 0 16 | TABLE 17 | 2 18 | LTYPE 19 | 0 20 | LTYPE 21 | 72 22 | 65 23 | 70 24 | 64 25 | 2 26 | CONTINUOUS 27 | 3 28 | ______ 29 | 73 30 | 0 31 | 40 32 | 0 33 | 0 34 | ENDTAB 35 | 0 36 | TABLE 37 | 2 38 | LAYER 39 | 0 40 | ENDTAB 41 | 0 42 | ENDSEC 43 | 0 44 | SECTION 45 | 2 46 | ENTITIES 47 | 0 48 | LINE 49 | 8 50 | 0 51 | 10 52 | -10 53 | 20 54 | -10 55 | 11 56 | 29 57 | 21 58 | -10 59 | 0 60 | LINE 61 | 8 62 | 0 63 | 10 64 | -10 65 | 20 66 | -10 67 | 11 68 | -10 69 | 21 70 | 29 71 | 0 72 | LINE 73 | 8 74 | 0 75 | 10 76 | -10 77 | 20 78 | 29 79 | 11 80 | 29 81 | 21 82 | 29 83 | 0 84 | LINE 85 | 8 86 | 0 87 | 10 88 | 29 89 | 20 90 | -10 91 | 11 92 | 29 93 | 21 94 | 29 95 | 0 96 | LINE 97 | 8 98 | 0 99 | 10 100 | -7 101 | 20 102 | -7 103 | 11 104 | 7 105 | 21 106 | -7 107 | 0 108 | LINE 109 | 8 110 | 0 111 | 10 112 | 7 113 | 20 114 | -7 115 | 11 116 | 7 117 | 21 118 | 7 119 | 0 120 | LINE 121 | 8 122 | 0 123 | 10 124 | 7 125 | 20 126 | 7 127 | 11 128 | -7 129 | 21 130 | 7 131 | 0 132 | LINE 133 | 8 134 | 0 135 | 10 136 | -7 137 | 20 138 | 7 139 | 11 140 | -7 141 | 21 142 | -7 143 | 0 144 | LINE 145 | 8 146 | 0 147 | 10 148 | -7 149 | 20 150 | 12 151 | 11 152 | 7 153 | 21 154 | 12 155 | 0 156 | LINE 157 | 8 158 | 0 159 | 10 160 | 7 161 | 20 162 | 12 163 | 11 164 | 7 165 | 21 166 | 26 167 | 0 168 | LINE 169 | 8 170 | 0 171 | 10 172 | 7 173 | 20 174 | 26 175 | 11 176 | -7 177 | 21 178 | 26 179 | 0 180 | LINE 181 | 8 182 | 0 183 | 10 184 | -7 185 | 20 186 | 26 187 | 11 188 | -7 189 | 21 190 | 12 191 | 0 192 | LINE 193 | 8 194 | 0 195 | 10 196 | 12 197 | 20 198 | -7 199 | 11 200 | 26 201 | 21 202 | -7 203 | 0 204 | LINE 205 | 8 206 | 0 207 | 10 208 | 26 209 | 20 210 | -7 211 | 11 212 | 26 213 | 21 214 | 7 215 | 0 216 | LINE 217 | 8 218 | 0 219 | 10 220 | 26 221 | 20 222 | 7 223 | 11 224 | 12 225 | 21 226 | 7 227 | 0 228 | LINE 229 | 8 230 | 0 231 | 10 232 | 12 233 | 20 234 | 7 235 | 11 236 | 12 237 | 21 238 | -7 239 | 0 240 | LINE 241 | 8 242 | 0 243 | 10 244 | 12 245 | 20 246 | 12 247 | 11 248 | 26 249 | 21 250 | 12 251 | 0 252 | LINE 253 | 8 254 | 0 255 | 10 256 | 26 257 | 20 258 | 12 259 | 11 260 | 26 261 | 21 262 | 26 263 | 0 264 | LINE 265 | 8 266 | 0 267 | 10 268 | 26 269 | 20 270 | 26 271 | 11 272 | 12 273 | 21 274 | 26 275 | 0 276 | LINE 277 | 8 278 | 0 279 | 10 280 | 12 281 | 20 282 | 26 283 | 11 284 | 12 285 | 21 286 | 12 287 | 0 288 | ENDSEC 289 | 0 290 | EOF -------------------------------------------------------------------------------- /src/footprints/diode.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | nets: { 3 | from: undefined, 4 | to: undefined 5 | }, 6 | params: { 7 | class: 'D' 8 | }, 9 | body: p => ` 10 | 11 | (module ComboDiode (layer F.Cu) (tedit 5B24D78E) 12 | 13 | 14 | ${p.at /* parametric position */} 15 | 16 | ${'' /* footprint reference */} 17 | (fp_text reference "${p.ref}" (at 0 0) (layer F.SilkS) ${p.ref_hide} (effects (font (size 1.27 1.27) (thickness 0.15)))) 18 | (fp_text value "" (at 0 0) (layer F.SilkS) hide (effects (font (size 1.27 1.27) (thickness 0.15)))) 19 | 20 | ${''/* diode symbols */} 21 | (fp_line (start 0.25 0) (end 0.75 0) (layer F.SilkS) (width 0.1)) 22 | (fp_line (start 0.25 0.4) (end -0.35 0) (layer F.SilkS) (width 0.1)) 23 | (fp_line (start 0.25 -0.4) (end 0.25 0.4) (layer F.SilkS) (width 0.1)) 24 | (fp_line (start -0.35 0) (end 0.25 -0.4) (layer F.SilkS) (width 0.1)) 25 | (fp_line (start -0.35 0) (end -0.35 0.55) (layer F.SilkS) (width 0.1)) 26 | (fp_line (start -0.35 0) (end -0.35 -0.55) (layer F.SilkS) (width 0.1)) 27 | (fp_line (start -0.75 0) (end -0.35 0) (layer F.SilkS) (width 0.1)) 28 | (fp_line (start 0.25 0) (end 0.75 0) (layer B.SilkS) (width 0.1)) 29 | (fp_line (start 0.25 0.4) (end -0.35 0) (layer B.SilkS) (width 0.1)) 30 | (fp_line (start 0.25 -0.4) (end 0.25 0.4) (layer B.SilkS) (width 0.1)) 31 | (fp_line (start -0.35 0) (end 0.25 -0.4) (layer B.SilkS) (width 0.1)) 32 | (fp_line (start -0.35 0) (end -0.35 0.55) (layer B.SilkS) (width 0.1)) 33 | (fp_line (start -0.35 0) (end -0.35 -0.55) (layer B.SilkS) (width 0.1)) 34 | (fp_line (start -0.75 0) (end -0.35 0) (layer B.SilkS) (width 0.1)) 35 | 36 | ${''/* SMD pads on both sides */} 37 | (pad 1 smd rect (at -1.65 0 ${p.rot}) (size 0.9 1.2) (layers F.Cu F.Paste F.Mask) ${p.net.to.str}) 38 | (pad 2 smd rect (at 1.65 0 ${p.rot}) (size 0.9 1.2) (layers B.Cu B.Paste B.Mask) ${p.net.from.str}) 39 | (pad 1 smd rect (at -1.65 0 ${p.rot}) (size 0.9 1.2) (layers B.Cu B.Paste B.Mask) ${p.net.to.str}) 40 | (pad 2 smd rect (at 1.65 0 ${p.rot}) (size 0.9 1.2) (layers F.Cu F.Paste F.Mask) ${p.net.from.str}) 41 | 42 | ${''/* THT terminals */} 43 | (pad 1 thru_hole circle (at 3.81 0 ${p.rot}) (size 1.905 1.905) (drill 0.9906) (layers *.Cu *.Mask) ${p.net.from.str}) 44 | (pad 2 thru_hole rect (at -3.81 0 ${p.rot}) (size 1.778 1.778) (drill 0.9906) (layers *.Cu *.Mask) ${p.net.to.str}) 45 | ) 46 | 47 | ` 48 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | out 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and not Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Stores VSCode versions used for testing VSCode extensions 109 | .vscode-test 110 | 111 | # yarn v2 112 | .yarn/cache 113 | .yarn/unplugged 114 | .yarn/build-state.yml 115 | .yarn/install-state.gz 116 | .pnp.* 117 | 118 | # Project specific 119 | output 120 | temp* -------------------------------------------------------------------------------- /test/outlines/rectangles___outlines_outline_dxf.dxf: -------------------------------------------------------------------------------- 1 | 0 2 | SECTION 3 | 2 4 | HEADER 5 | 9 6 | $INSUNITS 7 | 70 8 | 4 9 | 0 10 | ENDSEC 11 | 0 12 | SECTION 13 | 2 14 | TABLES 15 | 0 16 | TABLE 17 | 2 18 | LTYPE 19 | 0 20 | LTYPE 21 | 72 22 | 65 23 | 70 24 | 64 25 | 2 26 | CONTINUOUS 27 | 3 28 | ______ 29 | 73 30 | 0 31 | 40 32 | 0 33 | 0 34 | ENDTAB 35 | 0 36 | TABLE 37 | 2 38 | LAYER 39 | 0 40 | ENDTAB 41 | 0 42 | ENDSEC 43 | 0 44 | SECTION 45 | 2 46 | ENTITIES 47 | 0 48 | LINE 49 | 8 50 | 0 51 | 10 52 | -10 53 | 20 54 | -10 55 | 11 56 | 10 57 | 21 58 | -10 59 | 0 60 | LINE 61 | 8 62 | 0 63 | 10 64 | 10 65 | 20 66 | -10 67 | 11 68 | 10 69 | 21 70 | 0 71 | 0 72 | LINE 73 | 8 74 | 0 75 | 10 76 | 10 77 | 20 78 | 10 79 | 11 80 | -5 81 | 21 82 | 10 83 | 0 84 | LINE 85 | 8 86 | 0 87 | 10 88 | -10 89 | 20 90 | 5 91 | 11 92 | -10 93 | 21 94 | -10 95 | 0 96 | LINE 97 | 8 98 | 0 99 | 10 100 | 30 101 | 20 102 | -10 103 | 11 104 | 50 105 | 21 106 | -10 107 | 0 108 | LINE 109 | 8 110 | 0 111 | 10 112 | 50 113 | 20 114 | -10 115 | 11 116 | 50 117 | 21 118 | 5 119 | 0 120 | LINE 121 | 8 122 | 0 123 | 10 124 | 45 125 | 20 126 | 10 127 | 11 128 | 30 129 | 21 130 | 10 131 | 0 132 | LINE 133 | 8 134 | 0 135 | 10 136 | 30 137 | 20 138 | 0 139 | 11 140 | 30 141 | 21 142 | -10 143 | 0 144 | LINE 145 | 8 146 | 0 147 | 10 148 | 10 149 | 20 150 | 0 151 | 11 152 | 30 153 | 21 154 | 0 155 | 0 156 | LINE 157 | 8 158 | 0 159 | 10 160 | 30 161 | 20 162 | 10 163 | 11 164 | 30 165 | 21 166 | 40 167 | 0 168 | LINE 169 | 8 170 | 0 171 | 10 172 | 30 173 | 20 174 | 40 175 | 11 176 | 10 177 | 21 178 | 40 179 | 0 180 | LINE 181 | 8 182 | 0 183 | 10 184 | 10 185 | 20 186 | 40 187 | 11 188 | 10 189 | 21 190 | 10 191 | 0 192 | LINE 193 | 8 194 | 0 195 | 10 196 | -15 197 | 20 198 | 5 199 | 11 200 | -10 201 | 21 202 | 5 203 | 0 204 | LINE 205 | 8 206 | 0 207 | 10 208 | -5 209 | 20 210 | 10 211 | 11 212 | -5 213 | 21 214 | 15 215 | 0 216 | LINE 217 | 8 218 | 0 219 | 10 220 | -5 221 | 20 222 | 15 223 | 11 224 | -15 225 | 21 226 | 15 227 | 0 228 | LINE 229 | 8 230 | 0 231 | 10 232 | -15 233 | 20 234 | 15 235 | 11 236 | -15 237 | 21 238 | 5 239 | 0 240 | LINE 241 | 8 242 | 0 243 | 10 244 | 50 245 | 20 246 | 5 247 | 11 248 | 55 249 | 21 250 | 5 251 | 0 252 | LINE 253 | 8 254 | 0 255 | 10 256 | 55 257 | 20 258 | 5 259 | 11 260 | 55 261 | 21 262 | 15 263 | 0 264 | LINE 265 | 8 266 | 0 267 | 10 268 | 55 269 | 20 270 | 15 271 | 11 272 | 45 273 | 21 274 | 15 275 | 0 276 | LINE 277 | 8 278 | 0 279 | 10 280 | 45 281 | 20 282 | 15 283 | 11 284 | 45 285 | 21 286 | 10 287 | 0 288 | ENDSEC 289 | 0 290 | EOF -------------------------------------------------------------------------------- /src/assert.js: -------------------------------------------------------------------------------- 1 | const m = require('makerjs') 2 | const u = require('./utils') 3 | const Point = require('./point') 4 | const mathjs = require('mathjs') 5 | 6 | const mathnum = exports.mathnum = raw => units => { 7 | return mathjs.evaluate(`${raw}`, units || {}) 8 | } 9 | 10 | const assert = exports.assert = (exp, msg) => { 11 | if (!exp) { 12 | throw new Error(msg) 13 | } 14 | } 15 | 16 | const type = exports.type = val => units => { 17 | if (Array.isArray(val)) return 'array' 18 | if (val === null) return 'null' 19 | try { 20 | const num = mathnum(val)(units) 21 | if (typeof num === 'number') return 'number' 22 | } catch (err) {} 23 | return typeof val 24 | } 25 | 26 | const sane = exports.sane = (val, name, _type) => units => { 27 | assert(type(val)(units) == _type, `Field "${name}" should be of type ${_type}!`) 28 | if (_type == 'number') return mathnum(val)(units) 29 | return val 30 | } 31 | 32 | const unexpected = exports.unexpected = (obj, name, expected) => { 33 | const sane_obj = sane(obj, name, 'object')() 34 | for (const key of Object.keys(sane_obj)) { 35 | assert(expected.includes(key), `Unexpected key "${key}" within field "${name}"!`) 36 | } 37 | } 38 | 39 | const _in = exports.in = (raw, name, arr) => { 40 | assert(arr.includes(raw), `Field "${name}" should be one of [${arr.join(', ')}]!`) 41 | return raw 42 | } 43 | 44 | const arr = exports.arr = (raw, name, length, _type, _default) => units => { 45 | assert(type(raw)(units) == 'array', `Field "${name}" should be an array!`) 46 | assert(length == 0 || raw.length == length, `Field "${name}" should be an array of length ${length}!`) 47 | raw = raw.map(val => val === undefined ? _default : val) 48 | raw.map(val => assert(type(val)(units) == _type, `Field "${name}" should contain ${_type}s!`)) 49 | if (_type == 'number') { 50 | raw = raw.map(val => mathnum(val)(units)) 51 | } 52 | return raw 53 | } 54 | 55 | const numarr = exports.numarr = (raw, name, length) => units => arr(raw, name, length, 'number', 0)(units) 56 | const strarr = exports.strarr = (raw, name) => arr(raw, name, 0, 'string', '')() 57 | 58 | const xy = exports.xy = (raw, name) => units => numarr(raw, name, 2)(units) 59 | 60 | const wh = exports.wh = (raw, name) => units => { 61 | if (!Array.isArray(raw)) raw = [raw, raw] 62 | return xy(raw, name)(units) 63 | } 64 | 65 | exports.trbl = (raw, name, _default=0) => units => { 66 | if (!Array.isArray(raw)) raw = [raw, raw, raw, raw] 67 | if (raw.length == 2) raw = [raw[1], raw[0], raw[1], raw[0]] 68 | return arr(raw, name, 4, 'number', _default)(units) 69 | } 70 | -------------------------------------------------------------------------------- /src/kle.js: -------------------------------------------------------------------------------- 1 | const u = require('./utils') 2 | const kle = require('kle-serial') 3 | const yaml = require('js-yaml') 4 | 5 | exports.convert = (config, logger) => { 6 | const keyboard = kle.Serial.deserialize(config) 7 | const result = {points: {zones: {}}, pcbs: {main: {}}} 8 | 9 | // if the keyboard notes are valid YAML/JSON, they get added to each key as metadata 10 | let meta 11 | try { 12 | meta = yaml.load(keyboard.meta.notes) 13 | } catch (ex) { 14 | // notes were not valid YAML/JSON, oh well... 15 | } 16 | meta = meta || {} 17 | 18 | let index = 1 19 | for (const key of keyboard.keys) { 20 | const id = `key${index++}` 21 | const colid = `${id}col` 22 | const rowid = `${id}row` 23 | // we try to look at the first non-empty label 24 | const label = key.labels.filter(e => !!e)[0] || '' 25 | 26 | // PCB nets can be specified through key labels 27 | let row_net = id 28 | let col_net = 'GND' 29 | if (label.match(/^\d+_\d+$/)) { 30 | const parts = label.split('_') 31 | row_net = `row_${parts[0]}` 32 | col_net = `col_${parts[1]}` 33 | } 34 | 35 | // need to account for keycap sizes, as KLE anchors 36 | // at the corners, while we consider the centers 37 | const x = key.x + (key.width - 1) / 2 38 | const y = key.y + (key.height - 1) / 2 39 | 40 | // KLE deals in absolute rotation origins so we calculate 41 | // a relative difference as an origin for the column rotation 42 | // again, considering corner vs. center with the extra half width/height 43 | const diff_x = key.rotation_x - (key.x + key.width / 2) 44 | const diff_y = key.rotation_y - (key.y + key.height / 2) 45 | 46 | // anchoring the per-key zone to the KLE-computed coords 47 | const converted = { 48 | anchor: { 49 | shift: [`${x} u`, `${-y} u`], 50 | }, 51 | columns: {} 52 | } 53 | 54 | // adding a column-level rotation with origin 55 | converted.columns[colid] = { 56 | rotate: -key.rotation_angle, 57 | origin: [`${diff_x} u`, `${-diff_y} u`], 58 | rows: {} 59 | } 60 | 61 | // passing along metadata to each key 62 | converted.columns[colid].rows[rowid] = u.deepcopy(meta) 63 | converted.columns[colid].rows[rowid].width = key.width 64 | converted.columns[colid].rows[rowid].height = key.height 65 | converted.columns[colid].rows[rowid].label = label 66 | converted.columns[colid].rows[rowid].column_net = col_net 67 | converted.columns[colid].rows[rowid].row_net = row_net 68 | 69 | result.points.zones[id] = converted 70 | } 71 | 72 | return result 73 | } -------------------------------------------------------------------------------- /src/footprints/rgb.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | nets: { 3 | din: undefined, 4 | dout: undefined, 5 | VCC: 'VCC', 6 | GND: 'GND' 7 | }, 8 | params: { 9 | class: 'LED', 10 | side: 'F' 11 | }, 12 | body: p => ` 13 | 14 | (module WS2812B (layer F.Cu) (tedit 53BEE615) 15 | 16 | ${p.at /* parametric position */} 17 | 18 | ${'' /* footprint reference */} 19 | (fp_text reference "${p.ref}" (at 0 0) (layer F.SilkS) ${p.ref_hide} (effects (font (size 1.27 1.27) (thickness 0.15)))) 20 | (fp_text value "" (at 0 0) (layer F.SilkS) hide (effects (font (size 1.27 1.27) (thickness 0.15)))) 21 | 22 | (fp_line (start -1.75 -1.75) (end -1.75 1.75) (layer ${p.param.side}.SilkS) (width 0.15)) 23 | (fp_line (start -1.75 1.75) (end 1.75 1.75) (layer ${p.param.side}.SilkS) (width 0.15)) 24 | (fp_line (start 1.75 1.75) (end 1.75 -1.75) (layer ${p.param.side}.SilkS) (width 0.15)) 25 | (fp_line (start 1.75 -1.75) (end -1.75 -1.75) (layer ${p.param.side}.SilkS) (width 0.15)) 26 | 27 | (fp_line (start -2.5 -2.5) (end -2.5 2.5) (layer ${p.param.side}.SilkS) (width 0.15)) 28 | (fp_line (start -2.5 2.5) (end 2.5 2.5) (layer ${p.param.side}.SilkS) (width 0.15)) 29 | (fp_line (start 2.5 2.5) (end 2.5 -2.5) (layer ${p.param.side}.SilkS) (width 0.15)) 30 | (fp_line (start 2.5 -2.5) (end -2.5 -2.5) (layer ${p.param.side}.SilkS) (width 0.15)) 31 | 32 | (fp_poly (pts (xy 4 2.2) (xy 4 0.375) (xy 5 1.2875)) (layer ${p.param.side}.SilkS) (width 0.1)) 33 | 34 | (pad 1 smd rect (at -2.2 -0.875 ${p.rot}) (size 2.6 1) (layers ${p.param.side}.Cu ${p.param.side}.Paste ${p.param.side}.Mask) ${p.net.VCC.str}) 35 | (pad 2 smd rect (at -2.2 0.875 ${p.rot}) (size 2.6 1) (layers ${p.param.side}.Cu ${p.param.side}.Paste ${p.param.side}.Mask) ${p.net.dout.str}) 36 | (pad 3 smd rect (at 2.2 0.875 ${p.rot}) (size 2.6 1) (layers ${p.param.side}.Cu ${p.param.side}.Paste ${p.param.side}.Mask) ${p.net.GND.str}) 37 | (pad 4 smd rect (at 2.2 -0.875 ${p.rot}) (size 2.6 1) (layers ${p.param.side}.Cu ${p.param.side}.Paste ${p.param.side}.Mask) ${p.net.din.str}) 38 | 39 | (pad 11 smd rect (at -2.5 -1.6 ${p.rot}) (size 2 1.2) (layers ${p.param.side}.Cu ${p.param.side}.Paste ${p.param.side}.Mask) ${p.net.VCC.str}) 40 | (pad 22 smd rect (at -2.5 1.6 ${p.rot}) (size 2 1.2) (layers ${p.param.side}.Cu ${p.param.side}.Paste ${p.param.side}.Mask) ${p.net.dout.str}) 41 | (pad 33 smd rect (at 2.5 1.6 ${p.rot}) (size 2 1.2) (layers ${p.param.side}.Cu ${p.param.side}.Paste ${p.param.side}.Mask) ${p.net.GND.str}) 42 | (pad 44 smd rect (at 2.5 -1.6 ${p.rot}) (size 2 1.2) (layers ${p.param.side}.Cu ${p.param.side}.Paste ${p.param.side}.Mask) ${p.net.din.str}) 43 | 44 | ) 45 | 46 | ` 47 | } -------------------------------------------------------------------------------- /test/cli/big/reference/pcbs/_export.kicad_pcb: -------------------------------------------------------------------------------- 1 | 2 | 3 | (kicad_pcb (version 20171130) (host pcbnew 5.1.6) 4 | 5 | (page A3) 6 | (title_block 7 | (title _export) 8 | (rev v1.0.0) 9 | (company Unknown) 10 | ) 11 | 12 | (general 13 | (thickness 1.6) 14 | ) 15 | 16 | (layers 17 | (0 F.Cu signal) 18 | (31 B.Cu signal) 19 | (32 B.Adhes user) 20 | (33 F.Adhes user) 21 | (34 B.Paste user) 22 | (35 F.Paste user) 23 | (36 B.SilkS user) 24 | (37 F.SilkS user) 25 | (38 B.Mask user) 26 | (39 F.Mask user) 27 | (40 Dwgs.User user) 28 | (41 Cmts.User user) 29 | (42 Eco1.User user) 30 | (43 Eco2.User user) 31 | (44 Edge.Cuts user) 32 | (45 Margin user) 33 | (46 B.CrtYd user) 34 | (47 F.CrtYd user) 35 | (48 B.Fab user) 36 | (49 F.Fab user) 37 | ) 38 | 39 | (setup 40 | (last_trace_width 0.25) 41 | (trace_clearance 0.2) 42 | (zone_clearance 0.508) 43 | (zone_45_only no) 44 | (trace_min 0.2) 45 | (via_size 0.8) 46 | (via_drill 0.4) 47 | (via_min_size 0.4) 48 | (via_min_drill 0.3) 49 | (uvia_size 0.3) 50 | (uvia_drill 0.1) 51 | (uvias_allowed no) 52 | (uvia_min_size 0.2) 53 | (uvia_min_drill 0.1) 54 | (edge_width 0.05) 55 | (segment_width 0.2) 56 | (pcb_text_width 0.3) 57 | (pcb_text_size 1.5 1.5) 58 | (mod_edge_width 0.12) 59 | (mod_text_size 1 1) 60 | (mod_text_width 0.15) 61 | (pad_size 1.524 1.524) 62 | (pad_drill 0.762) 63 | (pad_to_mask_clearance 0.05) 64 | (aux_axis_origin 0 0) 65 | (visible_elements FFFFFF7F) 66 | (pcbplotparams 67 | (layerselection 0x010fc_ffffffff) 68 | (usegerberextensions false) 69 | (usegerberattributes true) 70 | (usegerberadvancedattributes true) 71 | (creategerberjobfile true) 72 | (excludeedgelayer true) 73 | (linewidth 0.100000) 74 | (plotframeref false) 75 | (viasonmask false) 76 | (mode 1) 77 | (useauxorigin false) 78 | (hpglpennumber 1) 79 | (hpglpenspeed 20) 80 | (hpglpendiameter 15.000000) 81 | (psnegative false) 82 | (psa4output false) 83 | (plotreference true) 84 | (plotvalue true) 85 | (plotinvisibletext false) 86 | (padsonsilk false) 87 | (subtractmaskfromsilk false) 88 | (outputformat 1) 89 | (mirror false) 90 | (drillshape 1) 91 | (scaleselection 1) 92 | (outputdirectory "")) 93 | ) 94 | 95 | (net 0 "") 96 | 97 | (net_class Default "This is the default net class." 98 | (clearance 0.2) 99 | (trace_width 0.25) 100 | (via_dia 0.8) 101 | (via_drill 0.4) 102 | (uvia_dia 0.3) 103 | (uvia_drill 0.1) 104 | (add_net "") 105 | ) 106 | 107 | 108 | 109 | 110 | ) 111 | 112 | -------------------------------------------------------------------------------- /test/cli/big/reference/pcbs/export.kicad_pcb: -------------------------------------------------------------------------------- 1 | 2 | 3 | (kicad_pcb (version 20171130) (host pcbnew 5.1.6) 4 | 5 | (page A3) 6 | (title_block 7 | (title export) 8 | (rev v1.0.0) 9 | (company Unknown) 10 | ) 11 | 12 | (general 13 | (thickness 1.6) 14 | ) 15 | 16 | (layers 17 | (0 F.Cu signal) 18 | (31 B.Cu signal) 19 | (32 B.Adhes user) 20 | (33 F.Adhes user) 21 | (34 B.Paste user) 22 | (35 F.Paste user) 23 | (36 B.SilkS user) 24 | (37 F.SilkS user) 25 | (38 B.Mask user) 26 | (39 F.Mask user) 27 | (40 Dwgs.User user) 28 | (41 Cmts.User user) 29 | (42 Eco1.User user) 30 | (43 Eco2.User user) 31 | (44 Edge.Cuts user) 32 | (45 Margin user) 33 | (46 B.CrtYd user) 34 | (47 F.CrtYd user) 35 | (48 B.Fab user) 36 | (49 F.Fab user) 37 | ) 38 | 39 | (setup 40 | (last_trace_width 0.25) 41 | (trace_clearance 0.2) 42 | (zone_clearance 0.508) 43 | (zone_45_only no) 44 | (trace_min 0.2) 45 | (via_size 0.8) 46 | (via_drill 0.4) 47 | (via_min_size 0.4) 48 | (via_min_drill 0.3) 49 | (uvia_size 0.3) 50 | (uvia_drill 0.1) 51 | (uvias_allowed no) 52 | (uvia_min_size 0.2) 53 | (uvia_min_drill 0.1) 54 | (edge_width 0.05) 55 | (segment_width 0.2) 56 | (pcb_text_width 0.3) 57 | (pcb_text_size 1.5 1.5) 58 | (mod_edge_width 0.12) 59 | (mod_text_size 1 1) 60 | (mod_text_width 0.15) 61 | (pad_size 1.524 1.524) 62 | (pad_drill 0.762) 63 | (pad_to_mask_clearance 0.05) 64 | (aux_axis_origin 0 0) 65 | (visible_elements FFFFFF7F) 66 | (pcbplotparams 67 | (layerselection 0x010fc_ffffffff) 68 | (usegerberextensions false) 69 | (usegerberattributes true) 70 | (usegerberadvancedattributes true) 71 | (creategerberjobfile true) 72 | (excludeedgelayer true) 73 | (linewidth 0.100000) 74 | (plotframeref false) 75 | (viasonmask false) 76 | (mode 1) 77 | (useauxorigin false) 78 | (hpglpennumber 1) 79 | (hpglpenspeed 20) 80 | (hpglpendiameter 15.000000) 81 | (psnegative false) 82 | (psa4output false) 83 | (plotreference true) 84 | (plotvalue true) 85 | (plotinvisibletext false) 86 | (padsonsilk false) 87 | (subtractmaskfromsilk false) 88 | (outputformat 1) 89 | (mirror false) 90 | (drillshape 1) 91 | (scaleselection 1) 92 | (outputdirectory "")) 93 | ) 94 | 95 | (net 0 "") 96 | 97 | (net_class Default "This is the default net class." 98 | (clearance 0.2) 99 | (trace_width 0.25) 100 | (via_dia 0.8) 101 | (via_drill 0.4) 102 | (uvia_dia 0.3) 103 | (uvia_drill 0.1) 104 | (add_net "") 105 | ) 106 | 107 | 108 | 109 | 110 | ) 111 | 112 | -------------------------------------------------------------------------------- /test/cli/medium/reference/pcbs/export.kicad_pcb: -------------------------------------------------------------------------------- 1 | 2 | 3 | (kicad_pcb (version 20171130) (host pcbnew 5.1.6) 4 | 5 | (page A3) 6 | (title_block 7 | (title export) 8 | (rev v1.0.0) 9 | (company Unknown) 10 | ) 11 | 12 | (general 13 | (thickness 1.6) 14 | ) 15 | 16 | (layers 17 | (0 F.Cu signal) 18 | (31 B.Cu signal) 19 | (32 B.Adhes user) 20 | (33 F.Adhes user) 21 | (34 B.Paste user) 22 | (35 F.Paste user) 23 | (36 B.SilkS user) 24 | (37 F.SilkS user) 25 | (38 B.Mask user) 26 | (39 F.Mask user) 27 | (40 Dwgs.User user) 28 | (41 Cmts.User user) 29 | (42 Eco1.User user) 30 | (43 Eco2.User user) 31 | (44 Edge.Cuts user) 32 | (45 Margin user) 33 | (46 B.CrtYd user) 34 | (47 F.CrtYd user) 35 | (48 B.Fab user) 36 | (49 F.Fab user) 37 | ) 38 | 39 | (setup 40 | (last_trace_width 0.25) 41 | (trace_clearance 0.2) 42 | (zone_clearance 0.508) 43 | (zone_45_only no) 44 | (trace_min 0.2) 45 | (via_size 0.8) 46 | (via_drill 0.4) 47 | (via_min_size 0.4) 48 | (via_min_drill 0.3) 49 | (uvia_size 0.3) 50 | (uvia_drill 0.1) 51 | (uvias_allowed no) 52 | (uvia_min_size 0.2) 53 | (uvia_min_drill 0.1) 54 | (edge_width 0.05) 55 | (segment_width 0.2) 56 | (pcb_text_width 0.3) 57 | (pcb_text_size 1.5 1.5) 58 | (mod_edge_width 0.12) 59 | (mod_text_size 1 1) 60 | (mod_text_width 0.15) 61 | (pad_size 1.524 1.524) 62 | (pad_drill 0.762) 63 | (pad_to_mask_clearance 0.05) 64 | (aux_axis_origin 0 0) 65 | (visible_elements FFFFFF7F) 66 | (pcbplotparams 67 | (layerselection 0x010fc_ffffffff) 68 | (usegerberextensions false) 69 | (usegerberattributes true) 70 | (usegerberadvancedattributes true) 71 | (creategerberjobfile true) 72 | (excludeedgelayer true) 73 | (linewidth 0.100000) 74 | (plotframeref false) 75 | (viasonmask false) 76 | (mode 1) 77 | (useauxorigin false) 78 | (hpglpennumber 1) 79 | (hpglpenspeed 20) 80 | (hpglpendiameter 15.000000) 81 | (psnegative false) 82 | (psa4output false) 83 | (plotreference true) 84 | (plotvalue true) 85 | (plotinvisibletext false) 86 | (padsonsilk false) 87 | (subtractmaskfromsilk false) 88 | (outputformat 1) 89 | (mirror false) 90 | (drillshape 1) 91 | (scaleselection 1) 92 | (outputdirectory "")) 93 | ) 94 | 95 | (net 0 "") 96 | 97 | (net_class Default "This is the default net class." 98 | (clearance 0.2) 99 | (trace_width 0.25) 100 | (via_dia 0.8) 101 | (via_drill 0.4) 102 | (uvia_dia 0.3) 103 | (uvia_drill 0.1) 104 | (add_net "") 105 | ) 106 | 107 | 108 | 109 | 110 | ) 111 | 112 | -------------------------------------------------------------------------------- /src/footprints/slider.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | nets: { 3 | from: undefined, 4 | to: undefined 5 | }, 6 | params: { 7 | class: 'T', // for Toggle 8 | side: 'F' 9 | }, 10 | body: p => { 11 | 12 | const left = p.param.side == 'F' ? '-' : '' 13 | const right = p.param.side == 'F' ? '' : '-' 14 | 15 | return ` 16 | 17 | (module E73:SPDT_C128955 (layer F.Cu) (tstamp 5BF2CC3C) 18 | 19 | ${p.at /* parametric position */} 20 | 21 | ${'' /* footprint reference */} 22 | (fp_text reference "${p.ref}" (at 0 0) (layer F.SilkS) ${p.ref_hide} (effects (font (size 1.27 1.27) (thickness 0.15)))) 23 | (fp_text value "" (at 0 0) (layer F.SilkS) hide (effects (font (size 1.27 1.27) (thickness 0.15)))) 24 | 25 | ${'' /* outline */} 26 | (fp_line (start 1.95 -1.35) (end -1.95 -1.35) (layer ${p.param.side}.SilkS) (width 0.15)) 27 | (fp_line (start 0 -1.35) (end -3.3 -1.35) (layer ${p.param.side}.SilkS) (width 0.15)) 28 | (fp_line (start -3.3 -1.35) (end -3.3 1.5) (layer ${p.param.side}.SilkS) (width 0.15)) 29 | (fp_line (start -3.3 1.5) (end 3.3 1.5) (layer ${p.param.side}.SilkS) (width 0.15)) 30 | (fp_line (start 3.3 1.5) (end 3.3 -1.35) (layer ${p.param.side}.SilkS) (width 0.15)) 31 | (fp_line (start 0 -1.35) (end 3.3 -1.35) (layer ${p.param.side}.SilkS) (width 0.15)) 32 | 33 | ${'' /* extra indicator for the slider */} 34 | (fp_line (start -1.95 -3.85) (end 1.95 -3.85) (layer Dwgs.User) (width 0.15)) 35 | (fp_line (start 1.95 -3.85) (end 1.95 -1.35) (layer Dwgs.User) (width 0.15)) 36 | (fp_line (start -1.95 -1.35) (end -1.95 -3.85) (layer Dwgs.User) (width 0.15)) 37 | 38 | ${'' /* bottom cutouts */} 39 | (pad "" np_thru_hole circle (at 1.5 0) (size 1 1) (drill 0.9) (layers *.Cu *.Mask)) 40 | (pad "" np_thru_hole circle (at -1.5 0) (size 1 1) (drill 0.9) (layers *.Cu *.Mask)) 41 | 42 | ${'' /* pins */} 43 | (pad 1 smd rect (at ${right}2.25 2.075 ${p.rot}) (size 0.9 1.25) (layers ${p.param.side}.Cu ${p.param.side}.Paste ${p.param.side}.Mask) ${p.net.from.str}) 44 | (pad 2 smd rect (at ${left}0.75 2.075 ${p.rot}) (size 0.9 1.25) (layers ${p.param.side}.Cu ${p.param.side}.Paste ${p.param.side}.Mask) ${p.net.to.str}) 45 | (pad 3 smd rect (at ${left}2.25 2.075 ${p.rot}) (size 0.9 1.25) (layers ${p.param.side}.Cu ${p.param.side}.Paste ${p.param.side}.Mask)) 46 | 47 | ${'' /* side mounts */} 48 | (pad "" smd rect (at 3.7 -1.1 ${p.rot}) (size 0.9 0.9) (layers ${p.param.side}.Cu ${p.param.side}.Paste ${p.param.side}.Mask)) 49 | (pad "" smd rect (at 3.7 1.1 ${p.rot}) (size 0.9 0.9) (layers ${p.param.side}.Cu ${p.param.side}.Paste ${p.param.side}.Mask)) 50 | (pad "" smd rect (at -3.7 1.1 ${p.rot}) (size 0.9 0.9) (layers ${p.param.side}.Cu ${p.param.side}.Paste ${p.param.side}.Mask)) 51 | (pad "" smd rect (at -3.7 -1.1 ${p.rot}) (size 0.9 0.9) (layers ${p.param.side}.Cu ${p.param.side}.Paste ${p.param.side}.Mask)) 52 | ) 53 | 54 | ` 55 | } 56 | } -------------------------------------------------------------------------------- /src/footprints/trrs.js: -------------------------------------------------------------------------------- 1 | // TRRS-PJ-320A-dual 2 | // _________________ 3 | // | (1) (3) (4)| 4 | // | | 5 | // |___(2)__________| 6 | // 7 | // Nets 8 | // A: corresponds to pin 1 9 | // B: corresponds to pin 2 10 | // C: corresponds to pin 3 11 | // D: corresponds to pin 4 12 | // Params 13 | // reverse: default is false 14 | // if true, will flip the footprint such that the pcb can be reversible 15 | // symmetric: default is false 16 | // if true, will only work if reverse is also true 17 | // this will cause the footprint to be symmetrical on each half 18 | // pins 1 and 2 must be identical if symmetric is true, as they will overlap 19 | 20 | module.exports = { 21 | nets: { 22 | A: undefined, 23 | B: undefined, 24 | C: undefined, 25 | D: undefined 26 | }, 27 | params: { 28 | class: 'TRRS', 29 | reverse: false, 30 | symmetric: false 31 | }, 32 | body: p => { 33 | const standard = ` 34 | (module TRRS-PJ-320A-dual (layer F.Cu) (tedit 5970F8E5) 35 | 36 | ${p.at /* parametric position */} 37 | 38 | ${'' /* footprint reference */} 39 | (fp_text reference REF** (at 0 14.2) (layer Dwgs.User) (effects (font (size 1 1) (thickness 0.15)))) 40 | (fp_text value TRRS-PJ-320A-dual (at 0 -5.6) (layer F.Fab) (effects (font (size 1 1) (thickness 0.15)))) 41 | 42 | ${''/* corner marks */} 43 | (fp_line (start 0.5 -2) (end -5.1 -2) (layer Dwgs.User) (width 0.15)) 44 | (fp_line (start -5.1 0) (end -5.1 -2) (layer Dwgs.User) (width 0.15)) 45 | (fp_line (start 0.5 0) (end 0.5 -2) (layer Dwgs.User) (width 0.15)) 46 | (fp_line (start -5.35 0) (end -5.35 12.1) (layer Dwgs.User) (width 0.15)) 47 | (fp_line (start 0.75 0) (end 0.75 12.1) (layer Dwgs.User) (width 0.15)) 48 | (fp_line (start 0.75 12.1) (end -5.35 12.1) (layer Dwgs.User) (width 0.15)) 49 | (fp_line (start 0.75 0) (end -5.35 0) (layer Dwgs.User) (width 0.15)) 50 | 51 | ${''/* stabilizers */} 52 | (pad "" np_thru_hole circle (at -2.3 8.6) (size 1.5 1.5) (drill 1.5) (layers *.Cu *.Mask)) 53 | (pad "" np_thru_hole circle (at -2.3 1.6) (size 1.5 1.5) (drill 1.5) (layers *.Cu *.Mask)) 54 | ` 55 | function pins(def_neg, def_pos) { 56 | return ` 57 | (pad 1 thru_hole oval (at ${def_neg} 11.3 ${p.rot}) (size 1.6 2.2) (drill oval 0.9 1.5) (layers *.Cu *.Mask) ${p.net.A.str}) 58 | (pad 2 thru_hole oval (at ${def_pos} 10.2 ${p.rot}) (size 1.6 2.2) (drill oval 0.9 1.5) (layers *.Cu *.Mask) ${p.net.B.str}) 59 | (pad 3 thru_hole oval (at ${def_pos} 6.2 ${p.rot}) (size 1.6 2.2) (drill oval 0.9 1.5) (layers *.Cu *.Mask) ${p.net.C.str}) 60 | (pad 4 thru_hole oval (at ${def_pos} 3.2 ${p.rot}) (size 1.6 2.2) (drill oval 0.9 1.5) (layers *.Cu *.Mask) ${p.net.D.str}) 61 | ` 62 | } 63 | if(p.param.reverse & p.param.symmetric) { 64 | return ` 65 | ${standard} 66 | ${pins('0', '-4.6')} 67 | ${pins('-4.6', '0')}) 68 | ` 69 | } else if(p.param.reverse) { 70 | return ` 71 | ${standard} 72 | ${pins('-4.6', '0')} 73 | ${pins('4.6', '0')}) 74 | ` 75 | } else { 76 | return ` 77 | ${standard} 78 | ${pins('-4.6', '0')}) 79 | ` 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /test/unit/prepare.js: -------------------------------------------------------------------------------- 1 | const p = require('../../src/prepare') 2 | 3 | describe('Prepare', function() { 4 | it('unnest', function() { 5 | p.unnest({'a.b.c': 1}).should.deep.equal({a: {b: {c: 1}}}) 6 | p.unnest({'a.b.c': { 7 | d: 2, 8 | 'e.f': 3 9 | }}).should.deep.equal({a: {b: {c: {d: 2, e: {f: 3}}}}}) 10 | p.unnest({'root': [{ 11 | 'a.b': 1 12 | }]}).should.deep.equal({root: [{a: {b: 1}}]}) 13 | }) 14 | 15 | it('extend', function() { 16 | p.extend('something', undefined).should.equal('something') 17 | should.equal(p.extend('something', '$unset'), undefined) 18 | p.extend(undefined, 'something').should.equal('something') 19 | p.extend(28, 'something').should.equal('something') 20 | p.extend('something', 28).should.equal(28) 21 | p.extend(27, 28).should.equal(28) 22 | p.extend({a: 1, c: 1, d: 1}, {b: 2, c: 2, d: '$unset'}).should.deep.equal({a: 1, b: 2, c: 2}) 23 | p.extend([3, 2, 1], [null, 4, 5]).should.deep.equal([3, 4, 5]) 24 | }) 25 | 26 | it('inherit', function() { 27 | p.inherit({ 28 | a: { 29 | x: 1, 30 | y: 2 31 | }, 32 | b: { 33 | $extends: 'a', 34 | z: 3 35 | }, 36 | c: { 37 | $extends: ['b'], 38 | w: 4 39 | } 40 | }).c.should.deep.equal({ 41 | x: 1, 42 | y: 2, 43 | z: 3, 44 | w: 4 45 | }) 46 | }) 47 | 48 | it('parameterize', function() { 49 | p.parameterize(1).should.equal(1) 50 | 51 | p.parameterize({ 52 | unused: { 53 | $params: ['PAR'] 54 | }, 55 | skip: { 56 | $skip: true 57 | } 58 | }).should.deep.equal({}) 59 | 60 | p.parameterize({ 61 | decl: { 62 | a: 'PAR', 63 | $params: ['PAR'], 64 | $args: [1] 65 | } 66 | }).decl.should.deep.equal({ 67 | a: '1' 68 | }) 69 | 70 | p.parameterize({ 71 | decl: { 72 | normal_use: 'PAR1', 73 | sub: { 74 | nested_use: 'PAR2 * 2' 75 | }, 76 | $params: ['PAR1', 'PAR2'], 77 | $args: ['text', 14] 78 | } 79 | }).decl.should.deep.equal({ 80 | normal_use: 'text', 81 | sub: { 82 | nested_use: '14 * 2', 83 | } 84 | }) 85 | 86 | p.parameterize.bind(this, { 87 | decl: { 88 | $args: [1] 89 | } 90 | }).should.throw('missing') 91 | 92 | p.parameterize.bind(this, { 93 | decl: { 94 | $params: ['PAR1', 'PAR2'], 95 | $args: [1] 96 | } 97 | }).should.throw('match') 98 | 99 | p.parameterize.bind(this, { 100 | decl: { 101 | a: 'PAR', 102 | $params: ['PAR'], 103 | $args: ['in"jection'] 104 | } 105 | }).should.throw('valid') 106 | }) 107 | }) -------------------------------------------------------------------------------- /test/points/overrides___demo_dxf.dxf: -------------------------------------------------------------------------------- 1 | 0 2 | SECTION 3 | 2 4 | HEADER 5 | 9 6 | $INSUNITS 7 | 70 8 | 4 9 | 0 10 | ENDSEC 11 | 0 12 | SECTION 13 | 2 14 | TABLES 15 | 0 16 | TABLE 17 | 2 18 | LTYPE 19 | 0 20 | LTYPE 21 | 72 22 | 65 23 | 70 24 | 64 25 | 2 26 | CONTINUOUS 27 | 3 28 | ______ 29 | 73 30 | 0 31 | 40 32 | 0 33 | 0 34 | ENDTAB 35 | 0 36 | TABLE 37 | 2 38 | LAYER 39 | 0 40 | ENDTAB 41 | 0 42 | ENDSEC 43 | 0 44 | SECTION 45 | 2 46 | ENTITIES 47 | 0 48 | LINE 49 | 8 50 | 0 51 | 10 52 | -9 53 | 20 54 | 9 55 | 11 56 | 9 57 | 21 58 | 9 59 | 0 60 | LINE 61 | 8 62 | 0 63 | 10 64 | 9 65 | 20 66 | 9 67 | 11 68 | 9 69 | 21 70 | -9 71 | 0 72 | LINE 73 | 8 74 | 0 75 | 10 76 | 9 77 | 20 78 | -9 79 | 11 80 | -9 81 | 21 82 | -9 83 | 0 84 | LINE 85 | 8 86 | 0 87 | 10 88 | -9 89 | 20 90 | -9 91 | 11 92 | -9 93 | 21 94 | 9 95 | 0 96 | LINE 97 | 8 98 | 0 99 | 10 100 | -9 101 | 20 102 | 28 103 | 11 104 | 9 105 | 21 106 | 28 107 | 0 108 | LINE 109 | 8 110 | 0 111 | 10 112 | 9 113 | 20 114 | 28 115 | 11 116 | 9 117 | 21 118 | 10 119 | 0 120 | LINE 121 | 8 122 | 0 123 | 10 124 | 9 125 | 20 126 | 10 127 | 11 128 | -9 129 | 21 130 | 10 131 | 0 132 | LINE 133 | 8 134 | 0 135 | 10 136 | -9 137 | 20 138 | 10 139 | 11 140 | -9 141 | 21 142 | 28 143 | 0 144 | LINE 145 | 8 146 | 0 147 | 10 148 | 10 149 | 20 150 | 9 151 | 11 152 | 28 153 | 21 154 | 9 155 | 0 156 | LINE 157 | 8 158 | 0 159 | 10 160 | 28 161 | 20 162 | 9 163 | 11 164 | 28 165 | 21 166 | -9 167 | 0 168 | LINE 169 | 8 170 | 0 171 | 10 172 | 28 173 | 20 174 | -9 175 | 11 176 | 10 177 | 21 178 | -9 179 | 0 180 | LINE 181 | 8 182 | 0 183 | 10 184 | 10 185 | 20 186 | -9 187 | 11 188 | 10 189 | 21 190 | 9 191 | 0 192 | LINE 193 | 8 194 | 0 195 | 10 196 | 10 197 | 20 198 | 28 199 | 11 200 | 28 201 | 21 202 | 28 203 | 0 204 | LINE 205 | 8 206 | 0 207 | 10 208 | 28 209 | 20 210 | 28 211 | 11 212 | 28 213 | 21 214 | 10 215 | 0 216 | LINE 217 | 8 218 | 0 219 | 10 220 | 28 221 | 20 222 | 10 223 | 11 224 | 10 225 | 21 226 | 10 227 | 0 228 | LINE 229 | 8 230 | 0 231 | 10 232 | 10 233 | 20 234 | 10 235 | 11 236 | 10 237 | 21 238 | 28 239 | 0 240 | LINE 241 | 8 242 | 0 243 | 10 244 | 10 245 | 20 246 | 47 247 | 11 248 | 28 249 | 21 250 | 47 251 | 0 252 | LINE 253 | 8 254 | 0 255 | 10 256 | 28 257 | 20 258 | 47 259 | 11 260 | 28 261 | 21 262 | 29 263 | 0 264 | LINE 265 | 8 266 | 0 267 | 10 268 | 28 269 | 20 270 | 29 271 | 11 272 | 10 273 | 21 274 | 29 275 | 0 276 | LINE 277 | 8 278 | 0 279 | 10 280 | 10 281 | 20 282 | 29 283 | 11 284 | 10 285 | 21 286 | 47 287 | 0 288 | LINE 289 | 8 290 | 0 291 | 10 292 | 29 293 | 20 294 | 28 295 | 11 296 | 47 297 | 21 298 | 28 299 | 0 300 | LINE 301 | 8 302 | 0 303 | 10 304 | 47 305 | 20 306 | 28 307 | 11 308 | 47 309 | 21 310 | 10 311 | 0 312 | LINE 313 | 8 314 | 0 315 | 10 316 | 47 317 | 20 318 | 10 319 | 11 320 | 29 321 | 21 322 | 10 323 | 0 324 | LINE 325 | 8 326 | 0 327 | 10 328 | 29 329 | 20 330 | 10 331 | 11 332 | 29 333 | 21 334 | 28 335 | 0 336 | LINE 337 | 8 338 | 0 339 | 10 340 | 29 341 | 20 342 | 47 343 | 11 344 | 47 345 | 21 346 | 47 347 | 0 348 | LINE 349 | 8 350 | 0 351 | 10 352 | 47 353 | 20 354 | 47 355 | 11 356 | 47 357 | 21 358 | 29 359 | 0 360 | LINE 361 | 8 362 | 0 363 | 10 364 | 47 365 | 20 366 | 29 367 | 11 368 | 29 369 | 21 370 | 29 371 | 0 372 | LINE 373 | 8 374 | 0 375 | 10 376 | 29 377 | 20 378 | 29 379 | 11 380 | 29 381 | 21 382 | 47 383 | 0 384 | ENDSEC 385 | 0 386 | EOF -------------------------------------------------------------------------------- /src/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs-extra') 4 | const path = require('path') 5 | const yaml = require('js-yaml') 6 | const yargs = require('yargs') 7 | const ergogen = require('./ergogen') 8 | const pkg = require('../package.json') 9 | 10 | // command line args 11 | 12 | const args = yargs 13 | .option('output', { 14 | alias: 'o', 15 | default: path.resolve('output'), 16 | describe: 'Output folder', 17 | type: 'string' 18 | }) 19 | .option('debug', { 20 | alias: 'd', 21 | default: false, 22 | describe: 'Debug mode', 23 | type: 'boolean' 24 | }) 25 | .option('clean', { 26 | default: false, 27 | describe: 'Clean output dir before parsing', 28 | type: 'boolean' 29 | }) 30 | .argv 31 | 32 | // config reading 33 | 34 | const config_file = args._[0] 35 | if (!config_file) { 36 | console.error('Usage: ergogen [options]') 37 | process.exit(1) 38 | } 39 | 40 | let config_text 41 | try { 42 | config_text = fs.readFileSync(config_file).toString() 43 | } catch (err) { 44 | console.error(`Could not read config file "${config_file}": ${err}`) 45 | process.exit(2) 46 | } 47 | 48 | const title_suffix = args.debug ? ' (Debug Mode)' : '' 49 | console.log(`Ergogen v${pkg.version} CLI${title_suffix}`) 50 | console.log() 51 | 52 | ;(async () => { 53 | 54 | // processing 55 | 56 | let results 57 | try { 58 | results = await ergogen.process(config_text, args.debug, s => console.log(s)) 59 | } catch (err) { 60 | console.error(err) 61 | process.exit(3) 62 | } 63 | 64 | // helpers 65 | 66 | const single = (data, rel) => { 67 | if (!data) return 68 | const abs = path.join(args.o, rel) 69 | fs.mkdirpSync(path.dirname(abs)) 70 | if (abs.endsWith('.yaml')) { 71 | fs.writeFileSync(abs, yaml.dump(data, {indent: 4})) 72 | } else { 73 | fs.writeFileSync(abs, data) 74 | } 75 | } 76 | 77 | const composite = (data, rel) => { 78 | if (!data) return 79 | const abs = path.join(args.o, rel) 80 | if (data.yaml) { 81 | fs.mkdirpSync(path.dirname(abs)) 82 | fs.writeFileSync(abs + '.yaml', yaml.dump(data.yaml, {indent: 4})) 83 | } 84 | for (const format of ['svg', 'dxf', 'jscad']) { 85 | if (data[format]) { 86 | fs.mkdirpSync(path.dirname(abs)) 87 | fs.writeFileSync(abs + '.' + format, data[format]) 88 | } 89 | } 90 | } 91 | 92 | // output 93 | 94 | if (args.clean) { 95 | console.log('Cleaning output folder...') 96 | fs.removeSync(args.o) 97 | } 98 | 99 | console.log('Writing output to disk...') 100 | fs.mkdirpSync(args.o) 101 | 102 | single(results.raw, 'source/raw.txt') 103 | single(results.canonical, 'source/canonical.yaml') 104 | 105 | single(results.units, 'points/units.yaml') 106 | single(results.points, 'points/points.yaml') 107 | composite(results.demo, 'points/demo') 108 | 109 | for (const [name, outline] of Object.entries(results.outlines)) { 110 | composite(outline, `outlines/${name}`) 111 | } 112 | 113 | for (const [name, _case] of Object.entries(results.cases)) { 114 | composite(_case, `cases/${name}`) 115 | } 116 | 117 | for (const [name, pcb] of Object.entries(results.pcbs)) { 118 | single(pcb, `pcbs/${name}.kicad_pcb`) 119 | } 120 | 121 | // goodbye 122 | 123 | console.log('Done.') 124 | console.log() 125 | 126 | })() 127 | -------------------------------------------------------------------------------- /src/ergogen.js: -------------------------------------------------------------------------------- 1 | const u = require('./utils') 2 | const io = require('./io') 3 | const prepare = require('./prepare') 4 | const units_lib = require('./units') 5 | const points_lib = require('./points') 6 | const outlines_lib = require('./outlines') 7 | const cases_lib = require('./cases') 8 | const pcbs_lib = require('./pcbs') 9 | 10 | const version = require('../package.json').version 11 | 12 | const process = async (raw, debug=false, logger=()=>{}) => { 13 | 14 | const prefix = 'Interpreting format: ' 15 | let empty = true 16 | let [config, format] = io.interpret(raw, logger) 17 | let suffix = format 18 | if (format == 'KLE') { 19 | suffix = `${format} (Auto-debug)` 20 | debug = true 21 | } 22 | logger(prefix + suffix) 23 | 24 | logger('Preprocessing input...') 25 | config = prepare.unnest(config) 26 | config = prepare.inherit(config) 27 | config = prepare.parameterize(config) 28 | const results = {} 29 | if (debug) { 30 | results.raw = raw 31 | results.canonical = u.deepcopy(config) 32 | } 33 | 34 | if (config.meta && config.meta.engine) { 35 | logger('Checking compatibility...') 36 | const engine = u.semver(config.meta.engine, 'config.meta.engine') 37 | if (!u.satisfies(version, engine)) { 38 | throw new Error(`Current ergogen version (${version}) doesn\'t satisfy config's engine requirement (${config.meta.engine})!`) 39 | } 40 | } 41 | 42 | logger('Calculating variables...') 43 | const units = units_lib.parse(config) 44 | if (debug) { 45 | results.units = units 46 | } 47 | 48 | logger('Parsing points...') 49 | if (!config.points) { 50 | throw new Error('Input does not contain a points clause!') 51 | } 52 | const points = points_lib.parse(config.points, units) 53 | if (!Object.keys(points).length) { 54 | throw new Error('Input does not contain any points!') 55 | } 56 | if (debug) { 57 | results.points = points 58 | results.demo = io.twodee(points_lib.visualize(points, units), debug) 59 | } 60 | 61 | logger('Generating outlines...') 62 | const outlines = outlines_lib.parse(config.outlines || {}, points, units) 63 | results.outlines = {} 64 | for (const [name, outline] of Object.entries(outlines)) { 65 | if (!debug && name.startsWith('_')) continue 66 | results.outlines[name] = io.twodee(outline, debug) 67 | empty = false 68 | } 69 | 70 | logger('Modeling cases...') 71 | const cases = cases_lib.parse(config.cases || {}, outlines, units) 72 | results.cases = {} 73 | for (const [case_name, case_script] of Object.entries(cases)) { 74 | if (!debug && case_name.startsWith('_')) continue 75 | results.cases[case_name] = {jscad: case_script} 76 | empty = false 77 | } 78 | 79 | logger('Scaffolding PCBs...') 80 | const pcbs = pcbs_lib.parse(config, points, outlines, units) 81 | results.pcbs = {} 82 | for (const [pcb_name, pcb_text] of Object.entries(pcbs)) { 83 | if (!debug && pcb_name.startsWith('_')) continue 84 | results.pcbs[pcb_name] = pcb_text 85 | empty = false 86 | } 87 | 88 | if (!debug && empty) { 89 | logger('Output would be empty, rerunning in debug mode...') 90 | return process(raw, true, () => {}) 91 | } 92 | return results 93 | } 94 | 95 | module.exports = { 96 | version, 97 | process, 98 | inject_footprint: pcbs_lib.inject_footprint 99 | } -------------------------------------------------------------------------------- /test/unit/assert.js: -------------------------------------------------------------------------------- 1 | const a = require('../../src/assert') 2 | 3 | describe('Assert', function() { 4 | 5 | it('mathnum', function() { 6 | a.mathnum('1')().should.equal(1) 7 | a.mathnum('1 + 2')().should.equal(3) 8 | a.mathnum('nope').should.throw('symbol') 9 | }) 10 | 11 | it('assert', function() { 12 | a.assert.bind(this, false, 'msg').should.throw('msg') 13 | a.assert.bind(this, true, 'msg').should.not.throw('msg') 14 | }) 15 | 16 | it('type', function() { 17 | // a more complete `typeof` operator 18 | a.type(undefined)().should.equal('undefined') 19 | a.type(null)().should.equal('null') 20 | a.type(false)().should.equal('boolean') 21 | a.type(0)().should.equal('number') 22 | a.type(3.14)().should.equal('number') 23 | a.type('arst')().should.equal('string') 24 | a.type('1 + 2')().should.equal('number') // formulas are also numbers here! 25 | a.type('arst')({arst: 2}).should.equal('number') // and variables count, too! 26 | a.type([])().should.equal('array') 27 | a.type({})().should.equal('object') 28 | }) 29 | 30 | it('sane', function() { 31 | a.sane('arst', 'name', 'string')().should.equal('arst') 32 | a.sane('arst', 'name', 'number').should.throw('name') 33 | a.sane('arst', 'name', 'number')({arst: 2}).should.equal(2) 34 | }) 35 | 36 | it('unexpected', function() { 37 | const good_keys = ['good'] 38 | a.unexpected.bind(this, [], 'name', good_keys).should.throw('object') 39 | a.unexpected.bind(this, {}, 'name', good_keys).should.not.throw() 40 | a.unexpected.bind(this, {good: true}, 'name', good_keys).should.not.throw() 41 | a.unexpected.bind(this, {good: true, bad: true}, 'name', good_keys).should.throw('bad') 42 | }) 43 | 44 | it('in', function() { 45 | a.in('match', 'name', ['match']).should.equal('match') 46 | a.in.bind(this, 'not.a.match', 'name', ['match']).should.throw('name') 47 | }) 48 | 49 | it('arr', function() { 50 | // non-arrays are bad 51 | a.arr('val', 'name', 0, 'string', '').should.throw('array') 52 | // arrays are good 53 | a.arr(['val'], 'name', 0, 'string', '')().should.deep.equal(['val']) 54 | // length check works 55 | a.arr(['val'], 'name', 2, 'string', '').should.throw('length') 56 | // defaults get filled 57 | a.arr([undefined, 'val'], 'name', 2, 'string', 'def')().should.deep.equal(['def', 'val']) 58 | // mathnums are supported 59 | a.arr(['1 + 2'], 'name', 0, 'number', '')().should.deep.equal([3]) 60 | 61 | // arr derivatives 62 | a.strarr(['val'], 'name').should.deep.equal(a.arr(['val'], 'name', 0, 'string', '')()) 63 | a.numarr([1], 'name', 1)().should.deep.equal(a.arr([1], 'name', 1, 'number', 0)()) 64 | a.xy([1, 2], 'name')().should.deep.equal(a.arr([1, 2], 'name', 2, 'number', 0)()) 65 | 66 | // wh adds array expansion 67 | a.wh([1, 2], 'name')().should.deep.equal(a.arr([1, 2], 'name', 2, 'number', 0)()) 68 | a.wh(1, 'name')().should.deep.equal(a.arr([1, 1], 'name', 2, 'number', 0)()) 69 | 70 | // trbl adds 1->4 and 2->4 array expansions 71 | a.trbl([1, 2, 3, 4], 'name')().should.deep.equal(a.arr([1, 2, 3, 4], 'name', 4, 'number', 0)()) 72 | a.trbl(1, 'name')().should.deep.equal(a.arr([1, 1, 1, 1], 'name', 4, 'number', 0)()) 73 | // the 2->4 is "inverted", because 2 number usually mean x,y, so the first is the horizontal 74 | // while for 4, it start with top, which is vertical 75 | a.trbl([1, 2], 'name')().should.deep.equal(a.arr([2, 1, 2, 1], 'name', 4, 'number', 0)()) 76 | }) 77 | }) -------------------------------------------------------------------------------- /config/input.yaml: -------------------------------------------------------------------------------- 1 | units: 2 | ring_shift_x: 20.032 3 | ring_shift_y: 19.93 4 | middle_shift_x: ring_shift_x + 24.68 5 | middle_shift_y: ring_shift_y + 8.77 6 | inner_shift_x: middle_shift_x + 27.66 - 5 7 | inner_shift_y: middle_shift_y + -10.41 8 | global_bind: 10 9 | points: 10 | key: 11 | padding: cy 12 | footprints: 13 | choc_hotswap: 14 | type: choc 15 | nets: 16 | from: =column_net 17 | to: GND 18 | params: 19 | keycaps: true 20 | reverse: true 21 | hotswap: true 22 | choc: 23 | type: choc 24 | anchor: 25 | rotate: 180 26 | nets: 27 | from: =column_net 28 | to: GND 29 | params: 30 | keycaps: true 31 | reverse: true 32 | zones: 33 | pinky_zone: 34 | anchor: 35 | rotate: 0 36 | columns: 37 | pinky: 38 | rotate: 8.75 39 | rows: 40 | bottom: 41 | bind: [1, 5, 0, 0] 42 | column_net: P21 43 | home: 44 | bind: [1, 5, 1, 0] 45 | column_net: P20 46 | top: 47 | bind: [0, 5, 1, 0] 48 | column_net: P19 49 | ring_zone: 50 | anchor: 51 | shift: [ring_shift_x, ring_shift_y] 52 | columns: 53 | ring: 54 | rotate: 10 55 | rows: 56 | bottom: 57 | bind: [1, 5, 16.5, 5] 58 | column_net: P18 59 | middle: 60 | bind: [1, 5, 1, 5] 61 | column_net: P17 62 | top: 63 | bind: [0, 5, 1, 5] 64 | column_net: P16 65 | middle_zone: 66 | anchor: 67 | shift: [middle_shift_x, middle_shift_y] 68 | columns: 69 | middle: 70 | rotate: 10 71 | rows: 72 | bottom: 73 | bind: [1, 5, 21, 5] 74 | column_net: P15 75 | middle: 76 | bind: [1, 5, 1, 5] 77 | column_net: P14 78 | top: 79 | bind: [0, 5, 1, 5] 80 | column_net: P13 81 | inner_zone: 82 | anchor: 83 | shift: [inner_shift_x, inner_shift_y] 84 | rotate: -10 85 | columns: 86 | index: 87 | inner: 88 | spread: cx 89 | rows: 90 | bottom: 91 | bind: [1, 19, 5, 5] 92 | column_net: P12 93 | home: 94 | bind: [1, 19, 1, 5] 95 | column_net: P11 96 | top: 97 | bind: [0, 27, 5, 15] 98 | column_net: P10 99 | thumb: 100 | anchor: 101 | ref: inner_zone_inner_bottom 102 | orient: 10 103 | shift: [7.2 + 5, -22.24] 104 | rotate: -173 105 | columns: 106 | space: 107 | switch: 108 | spread: cx + 0.25 * cx 109 | rows: 110 | thumb: 111 | bind: [0, 5, 8, 5] 112 | column_net: P9 113 | rotate: 0 114 | mirror: 115 | ref: pinky_zone_pinky_home 116 | distance: 260 117 | outlines: 118 | exports: 119 | raw: 120 | - type: keys 121 | side: left 122 | size: [1cx , 1cx] 123 | cutout: 124 | - type: outline 125 | name: raw 126 | fillet: 3 127 | keycap_outlines: 128 | - type: keys 129 | side: left 130 | size: [1cx - 0.5, 1cy - 0.5] # Choc keycaps are 17.5 x 16.5 131 | bound: false 132 | pcbs: 133 | main: 134 | outlines: 135 | main: 136 | outline: cutout 137 | footprints: 138 | promicro: 139 | type: promicro 140 | anchor: 141 | ref: inner_zone_inner_bottom 142 | shift: [19, 25.5] 143 | rotate: -90 144 | params: 145 | orientation: down 146 | -------------------------------------------------------------------------------- /test/unit/point.js: -------------------------------------------------------------------------------- 1 | const m = require('makerjs') 2 | const Point = require('../../src/point') 3 | const {check} = require('../helpers/point') 4 | 5 | describe('Point', function() { 6 | 7 | it('construction', function() { 8 | // increasingly verbose parametrization 9 | check(new Point(), [0, 0, 0, {}]) 10 | check(new Point(1), [1, 0, 0, {}]) 11 | check(new Point(1, 2), [1, 2, 0, {}]) 12 | check(new Point(1, 2, 3), [1, 2, 3, {}]) 13 | check(new Point(1, 2, 3, {arst: 'neio'}), [1, 2, 3, {arst: 'neio'}]) 14 | // point-like instantiation 15 | check(new Point([1, 2]), [1, 2, 0, {}]) 16 | }) 17 | 18 | it('access', function() { 19 | // getting and setting through the virtual `p` field 20 | const p = new Point(1, 2) 21 | p.p.should.deep.equal([1, 2]) 22 | p.p = [3, 4] 23 | p.p.should.deep.equal([3, 4]) 24 | }) 25 | 26 | it('cloning', function() { 27 | const p = new Point(1, 2) 28 | const clone = p.clone() 29 | // make sure cloning works 30 | check(clone, [1, 2, 0, {}]) 31 | clone.p = [3, 4] 32 | // and make sure it's then independent of the original 33 | check(p, [1, 2, 0, {}]) 34 | check(clone, [3, 4, 0, {}]) 35 | }) 36 | 37 | it('shifting', function() { 38 | const p = new Point(0, 0, -90) // at origin, "looking right" 39 | // absolute shift up and left, should be up and left 40 | check(p.clone().shift([-1, 1], false), [-1, 1, -90, {}]) 41 | // relative shift up and left, should be up and right 42 | check(p.clone().shift([-1, 1]), [1, 1, -90, {}]) 43 | }) 44 | 45 | it('rotation', function() { 46 | const p = new Point(0, 1, 0) // above origin, "looking up" 47 | // around default origin 48 | check(p.clone().rotate(-90), [1, 0, -90, {}]) 49 | // around custom origin 50 | check(p.clone().rotate(-90, [1, 1]), [1, 2, -90, {}]) 51 | }) 52 | 53 | it('mirroring', function() { 54 | const p = new Point(0, 1, 0) 55 | // make sure mirroring inverts rotation, as well as positions correctly 56 | check((new Point(-1, 0, 10)).mirror(0), [1, 0, -10, {}]) 57 | }) 58 | 59 | it('positioning', function() { 60 | const p = new Point(1, 1, -90) // above origin, "looking right" 61 | const model = { 62 | paths: { 63 | line: new m.paths.Line([0, 0], [0, 1]), 64 | } 65 | } 66 | // it's both rotated and shifted to the correct position 67 | p.position(model).should.deep.equal({ 68 | paths: { 69 | line: { 70 | type: 'line', 71 | origin: [0, 0], 72 | end: [1, 0] 73 | } 74 | }, 75 | origin: [1, 1] 76 | }) 77 | // `rect` does the same, only with built-in rectangles 78 | const expected_rect = { 79 | origin: [-6, -6], 80 | paths: { 81 | bottom: { 82 | end: [0, 14], 83 | origin: [0, 0], 84 | type: 'line' 85 | }, 86 | left: { 87 | end: [14, 14], 88 | origin: [0, 14], 89 | type: 'line' 90 | }, 91 | right: { 92 | end: [0, 0], 93 | origin: [14, 0], 94 | type: 'line' 95 | }, 96 | top: { 97 | end: [14, 0], 98 | origin: [14, 14], 99 | type: 'line' 100 | } 101 | } 102 | } 103 | p.rect(14).should.deep.equal(expected_rect) 104 | // default size is 14 for keyboard hole reasons 105 | p.rect().should.deep.equal(expected_rect) 106 | }) 107 | }) -------------------------------------------------------------------------------- /test/outlines/affect_mirror___outlines_test_dxf.dxf: -------------------------------------------------------------------------------- 1 | 0 2 | SECTION 3 | 2 4 | HEADER 5 | 9 6 | $INSUNITS 7 | 70 8 | 4 9 | 0 10 | ENDSEC 11 | 0 12 | SECTION 13 | 2 14 | TABLES 15 | 0 16 | TABLE 17 | 2 18 | LTYPE 19 | 0 20 | LTYPE 21 | 72 22 | 65 23 | 70 24 | 64 25 | 2 26 | CONTINUOUS 27 | 3 28 | ______ 29 | 73 30 | 0 31 | 40 32 | 0 33 | 0 34 | ENDTAB 35 | 0 36 | TABLE 37 | 2 38 | LAYER 39 | 0 40 | ENDTAB 41 | 0 42 | ENDSEC 43 | 0 44 | SECTION 45 | 2 46 | ENTITIES 47 | 0 48 | LINE 49 | 8 50 | 0 51 | 10 52 | -7 53 | 20 54 | -7 55 | 11 56 | 7 57 | 21 58 | -7 59 | 0 60 | LINE 61 | 8 62 | 0 63 | 10 64 | 7 65 | 20 66 | -7 67 | 11 68 | 7 69 | 21 70 | 7 71 | 0 72 | LINE 73 | 8 74 | 0 75 | 10 76 | 7 77 | 20 78 | 7 79 | 11 80 | -7 81 | 21 82 | 7 83 | 0 84 | LINE 85 | 8 86 | 0 87 | 10 88 | -7 89 | 20 90 | 7 91 | 11 92 | -7 93 | 21 94 | -7 95 | 0 96 | LINE 97 | 8 98 | 0 99 | 10 100 | -7 101 | 20 102 | 12 103 | 11 104 | 7 105 | 21 106 | 12 107 | 0 108 | LINE 109 | 8 110 | 0 111 | 10 112 | 7 113 | 20 114 | 12 115 | 11 116 | 7 117 | 21 118 | 26 119 | 0 120 | LINE 121 | 8 122 | 0 123 | 10 124 | 7 125 | 20 126 | 26 127 | 11 128 | -7 129 | 21 130 | 26 131 | 0 132 | LINE 133 | 8 134 | 0 135 | 10 136 | -7 137 | 20 138 | 26 139 | 11 140 | -7 141 | 21 142 | 12 143 | 0 144 | LINE 145 | 8 146 | 0 147 | 10 148 | 23 149 | 20 150 | -7 151 | 11 152 | 37 153 | 21 154 | -7 155 | 0 156 | LINE 157 | 8 158 | 0 159 | 10 160 | 37 161 | 20 162 | -7 163 | 11 164 | 37 165 | 21 166 | 7 167 | 0 168 | LINE 169 | 8 170 | 0 171 | 10 172 | 37 173 | 20 174 | 7 175 | 11 176 | 23 177 | 21 178 | 7 179 | 0 180 | LINE 181 | 8 182 | 0 183 | 10 184 | 23 185 | 20 186 | 7 187 | 11 188 | 23 189 | 21 190 | -7 191 | 0 192 | LINE 193 | 8 194 | 0 195 | 10 196 | 23 197 | 20 198 | 12 199 | 11 200 | 37 201 | 21 202 | 12 203 | 0 204 | LINE 205 | 8 206 | 0 207 | 10 208 | 37 209 | 20 210 | 12 211 | 11 212 | 37 213 | 21 214 | 26 215 | 0 216 | LINE 217 | 8 218 | 0 219 | 10 220 | 37 221 | 20 222 | 26 223 | 11 224 | 23 225 | 21 226 | 26 227 | 0 228 | LINE 229 | 8 230 | 0 231 | 10 232 | 23 233 | 20 234 | 26 235 | 11 236 | 23 237 | 21 238 | 12 239 | 0 240 | LINE 241 | 8 242 | 0 243 | 10 244 | 27 245 | 20 246 | -5.1961524 247 | 11 248 | 32.1961524 249 | 21 250 | -8.1961524 251 | 0 252 | LINE 253 | 8 254 | 0 255 | 10 256 | 32.1961524 257 | 20 258 | -8.1961524 259 | 11 260 | 35.1961524 261 | 21 262 | -3 263 | 0 264 | LINE 265 | 8 266 | 0 267 | 10 268 | 35.1961524 269 | 20 270 | -3 271 | 11 272 | 30 273 | 21 274 | 0 275 | 0 276 | LINE 277 | 8 278 | 0 279 | 10 280 | 30 281 | 20 282 | 0 283 | 11 284 | 27 285 | 21 286 | -5.1961524 287 | 0 288 | LINE 289 | 8 290 | 0 291 | 10 292 | 27 293 | 20 294 | 13.8038476 295 | 11 296 | 32.1961524 297 | 21 298 | 10.8038476 299 | 0 300 | LINE 301 | 8 302 | 0 303 | 10 304 | 32.1961524 305 | 20 306 | 10.8038476 307 | 11 308 | 35.1961524 309 | 21 310 | 16 311 | 0 312 | LINE 313 | 8 314 | 0 315 | 10 316 | 35.1961524 317 | 20 318 | 16 319 | 11 320 | 30 321 | 21 322 | 19 323 | 0 324 | LINE 325 | 8 326 | 0 327 | 10 328 | 30 329 | 20 330 | 19 331 | 11 332 | 27 333 | 21 334 | 13.8038476 335 | 0 336 | LINE 337 | 8 338 | 0 339 | 10 340 | -2.1961524 341 | 20 342 | -8.1961524 343 | 11 344 | 3 345 | 21 346 | -5.1961524 347 | 0 348 | LINE 349 | 8 350 | 0 351 | 10 352 | 3 353 | 20 354 | -5.1961524 355 | 11 356 | 0 357 | 21 358 | 0 359 | 0 360 | LINE 361 | 8 362 | 0 363 | 10 364 | 0 365 | 20 366 | 0 367 | 11 368 | -5.1961524 369 | 21 370 | -3 371 | 0 372 | LINE 373 | 8 374 | 0 375 | 10 376 | -5.1961524 377 | 20 378 | -3 379 | 11 380 | -2.1961524 381 | 21 382 | -8.1961524 383 | 0 384 | LINE 385 | 8 386 | 0 387 | 10 388 | -2.1961524 389 | 20 390 | 10.8038476 391 | 11 392 | 3 393 | 21 394 | 13.8038476 395 | 0 396 | LINE 397 | 8 398 | 0 399 | 10 400 | 3 401 | 20 402 | 13.8038476 403 | 11 404 | 0 405 | 21 406 | 19 407 | 0 408 | LINE 409 | 8 410 | 0 411 | 10 412 | 0 413 | 20 414 | 19 415 | 11 416 | -5.1961524 417 | 21 418 | 16 419 | 0 420 | LINE 421 | 8 422 | 0 423 | 10 424 | -5.1961524 425 | 20 426 | 16 427 | 11 428 | -2.1961524 429 | 21 430 | 10.8038476 431 | 0 432 | ENDSEC 433 | 0 434 | EOF -------------------------------------------------------------------------------- /src/footprints/choc.js: -------------------------------------------------------------------------------- 1 | // Kailh Choc PG1350 2 | // Nets 3 | // from: corresponds to pin 1 4 | // to: corresponds to pin 2 5 | // Params 6 | // hotswap: default is false 7 | // if true, will include holes and pads for Kailh choc hotswap sockets 8 | // reverse: default is false 9 | // if true, will flip the footprint such that the pcb can be reversible 10 | // keycaps: default is false 11 | // if true, will add choc sized keycap box around the footprint 12 | // 13 | // note: hotswap and reverse can be used simultaneously 14 | 15 | module.exports = { 16 | nets: { 17 | from: undefined, 18 | to: undefined 19 | }, 20 | params: { 21 | class: 'S', 22 | hotswap: false, 23 | reverse: false, 24 | keycaps: false 25 | }, 26 | body: p => { 27 | const standard = ` 28 | (module PG1350 (layer F.Cu) (tedit 5DD50112) 29 | ${p.at /* parametric position */} 30 | 31 | ${'' /* footprint reference */} 32 | (fp_text reference "${p.ref}" (at 0 0) (layer F.SilkS) ${p.ref_hide} (effects (font (size 1.27 1.27) (thickness 0.15)))) 33 | (fp_text value "" (at 0 0) (layer F.SilkS) hide (effects (font (size 1.27 1.27) (thickness 0.15)))) 34 | 35 | ${''/* corner marks */} 36 | (fp_line (start -7 -6) (end -7 -7) (layer Dwgs.User) (width 0.15)) 37 | (fp_line (start -7 7) (end -6 7) (layer Dwgs.User) (width 0.15)) 38 | (fp_line (start -6 -7) (end -7 -7) (layer Dwgs.User) (width 0.15)) 39 | (fp_line (start -7 7) (end -7 6) (layer Dwgs.User) (width 0.15)) 40 | (fp_line (start 7 6) (end 7 7) (layer Dwgs.User) (width 0.15)) 41 | (fp_line (start 7 -7) (end 6 -7) (layer Dwgs.User) (width 0.15)) 42 | (fp_line (start 6 7) (end 7 7) (layer Dwgs.User) (width 0.15)) 43 | (fp_line (start 7 -7) (end 7 -6) (layer Dwgs.User) (width 0.15)) 44 | 45 | ${''/* middle shaft */} 46 | (pad "" np_thru_hole circle (at 0 0) (size 3.429 3.429) (drill 3.429) (layers *.Cu *.Mask)) 47 | 48 | ${''/* stabilizers */} 49 | (pad "" np_thru_hole circle (at 5.5 0) (size 1.7018 1.7018) (drill 1.7018) (layers *.Cu *.Mask)) 50 | (pad "" np_thru_hole circle (at -5.5 0) (size 1.7018 1.7018) (drill 1.7018) (layers *.Cu *.Mask)) 51 | ` 52 | const keycap = ` 53 | ${'' /* keycap marks */} 54 | (fp_line (start -9 -8.5) (end 9 -8.5) (layer Dwgs.User) (width 0.15)) 55 | (fp_line (start 9 -8.5) (end 9 8.5) (layer Dwgs.User) (width 0.15)) 56 | (fp_line (start 9 8.5) (end -9 8.5) (layer Dwgs.User) (width 0.15)) 57 | (fp_line (start -9 8.5) (end -9 -8.5) (layer Dwgs.User) (width 0.15)) 58 | ` 59 | function pins(def_neg, def_pos, def_side) { 60 | if(p.param.hotswap) { 61 | return ` 62 | ${'' /* holes */} 63 | (pad "" np_thru_hole circle (at ${def_pos}5 -3.75) (size 3 3) (drill 3) (layers *.Cu *.Mask)) 64 | (pad "" np_thru_hole circle (at 0 -5.95) (size 3 3) (drill 3) (layers *.Cu *.Mask)) 65 | 66 | ${'' /* net pads */} 67 | (pad 1 smd rect (at ${def_neg}3.275 -5.95 ${p.rot}) (size 2.6 2.6) (layers ${def_side}.Cu ${def_side}.Paste ${def_side}.Mask) ${p.net.from.str}) 68 | (pad 2 smd rect (at ${def_pos}8.275 -3.75 ${p.rot}) (size 2.6 2.6) (layers ${def_side}.Cu ${def_side}.Paste ${def_side}.Mask) ${p.net.to.str}) 69 | ` 70 | } else { 71 | return ` 72 | ${''/* pins */} 73 | (pad 1 thru_hole circle (at ${def_pos}5 -3.8) (size 2.032 2.032) (drill 1.27) (layers *.Cu *.Mask) ${p.net.from.str}) 74 | (pad 2 thru_hole circle (at ${def_pos}0 -5.9) (size 2.032 2.032) (drill 1.27) (layers *.Cu *.Mask) ${p.net.to.str}) 75 | ` 76 | } 77 | } 78 | if(p.param.reverse) { 79 | return ` 80 | ${standard} 81 | ${p.param.keycaps ? keycap : ''} 82 | ${pins('-', '', 'B')} 83 | ${pins('', '-', 'F')}) 84 | ` 85 | } else { 86 | return ` 87 | ${standard} 88 | ${p.param.keycaps ? keycap : ''} 89 | ${pins('-', '', 'B')}) 90 | ` 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /src/footprints/mx.js: -------------------------------------------------------------------------------- 1 | // Any MX switch 2 | // Nets 3 | // from: corresponds to pin 1 4 | // to: corresponds to pin 2 5 | // Params 6 | // hotswap: default is false 7 | // if true, will include holes and pads for Kailh MX hotswap sockets 8 | // reverse: default is false 9 | // if true, will flip the footprint such that the pcb can be reversible 10 | // keycaps: default is false 11 | // if true, will add choc sized keycap box around the footprint 12 | // 13 | // note: hotswap and reverse can be used simultaneously 14 | 15 | module.exports = { 16 | nets: { 17 | from: undefined, 18 | to: undefined 19 | }, 20 | params: { 21 | class: 'S', 22 | hotswap: false, 23 | reverse: false, 24 | keycaps: false 25 | }, 26 | body: p => { 27 | const standard = ` 28 | (module MX (layer F.Cu) (tedit 5DD4F656) 29 | ${p.at /* parametric position */} 30 | 31 | ${'' /* footprint reference */} 32 | (fp_text reference "${p.ref}" (at 0 0) (layer F.SilkS) ${p.ref_hide} (effects (font (size 1.27 1.27) (thickness 0.15)))) 33 | (fp_text value "" (at 0 0) (layer F.SilkS) hide (effects (font (size 1.27 1.27) (thickness 0.15)))) 34 | 35 | ${''/* corner marks */} 36 | (fp_line (start -7 -6) (end -7 -7) (layer Dwgs.User) (width 0.15)) 37 | (fp_line (start -7 7) (end -6 7) (layer Dwgs.User) (width 0.15)) 38 | (fp_line (start -6 -7) (end -7 -7) (layer Dwgs.User) (width 0.15)) 39 | (fp_line (start -7 7) (end -7 6) (layer Dwgs.User) (width 0.15)) 40 | (fp_line (start 7 6) (end 7 7) (layer Dwgs.User) (width 0.15)) 41 | (fp_line (start 7 -7) (end 6 -7) (layer Dwgs.User) (width 0.15)) 42 | (fp_line (start 6 7) (end 7 7) (layer Dwgs.User) (width 0.15)) 43 | (fp_line (start 7 -7) (end 7 -6) (layer Dwgs.User) (width 0.15)) 44 | 45 | ${''/* middle shaft */} 46 | (pad "" np_thru_hole circle (at 0 0) (size 3.9878 3.9878) (drill 3.9878) (layers *.Cu *.Mask)) 47 | 48 | ${''/* stabilizers */} 49 | (pad "" np_thru_hole circle (at 5.08 0) (size 1.7018 1.7018) (drill 1.7018) (layers *.Cu *.Mask)) 50 | (pad "" np_thru_hole circle (at -5.08 0) (size 1.7018 1.7018) (drill 1.7018) (layers *.Cu *.Mask)) 51 | ` 52 | const keycap = ` 53 | ${'' /* keycap marks */} 54 | (fp_line (start -9.5 -9.5) (end 9.5 -9.5) (layer Dwgs.User) (width 0.15)) 55 | (fp_line (start 9.5 -9.5) (end 9.5 9.5) (layer Dwgs.User) (width 0.15)) 56 | (fp_line (start 9.5 9.5) (end -9.5 9.5) (layer Dwgs.User) (width 0.15)) 57 | (fp_line (start -9.5 9.5) (end -9.5 -9.5) (layer Dwgs.User) (width 0.15)) 58 | ` 59 | function pins(def_neg, def_pos, def_side) { 60 | if(p.param.hotswap) { 61 | return ` 62 | ${'' /* holes */} 63 | (pad "" np_thru_hole circle (at ${def_pos}2.54 -5.08) (size 3 3) (drill 3) (layers *.Cu *.Mask)) 64 | (pad "" np_thru_hole circle (at ${def_neg}3.81 -2.54) (size 3 3) (drill 3) (layers *.Cu *.Mask)) 65 | 66 | ${'' /* net pads */} 67 | (pad 1 smd rect (at ${def_neg}7.085 -2.54 180) (size 2.55 2.5) (layers ${def_side}.Cu ${def_side}.Paste ${def_side}.Mask) ${p.net.from.str}) 68 | (pad 2 smd rect (at ${def_pos}5.842 -5.08 180) (size 2.55 2.5) (layers ${def_side}.Cu ${def_side}.Paste ${def_side}.Mask) ${p.net.to.str}) 69 | ` 70 | } else { 71 | return ` 72 | ${''/* pins */} 73 | (pad 1 thru_hole circle (at ${def_pos}2.54 -5.08) (size 2.286 2.286) (drill 1.4986) (layers *.Cu *.Mask) ${p.net.from.str}) 74 | (pad 2 thru_hole circle (at ${def_neg}3.81 -2.54) (size 2.286 2.286) (drill 1.4986) (layers *.Cu *.Mask) ${p.net.to.str}) 75 | ` 76 | } 77 | } 78 | if(p.param.reverse){ 79 | return ` 80 | ${standard} 81 | ${p.param.keycaps ? keycap : ''} 82 | ${pins('-', '', 'B')} 83 | ${pins('', '-', 'F')}) 84 | ` 85 | } else { 86 | return ` 87 | ${standard} 88 | ${p.param.keycaps ? keycap : ''} 89 | ${pins('-', '', 'B')}) 90 | ` 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const m = require('makerjs') 2 | 3 | exports.deepcopy = value => { 4 | if (value === undefined) return undefined 5 | return JSON.parse(JSON.stringify(value)) 6 | } 7 | 8 | const deep = exports.deep = (obj, key, val) => { 9 | const levels = key.split('.') 10 | const last = levels.pop() 11 | let step = obj 12 | for (const level of levels) { 13 | step[level] = step[level] || {} 14 | step = step[level] 15 | } 16 | if (val === undefined) return step[last] 17 | step[last] = val 18 | return obj 19 | } 20 | 21 | exports.template = (str, vals={}) => { 22 | const regex = /\{\{([^}]*)\}\}/g 23 | let res = str 24 | let shift = 0 25 | for (const match of str.matchAll(regex)) { 26 | const replacement = deep(vals, match[1]) || '' 27 | res = res.substring(0, match.index + shift) 28 | + replacement 29 | + res.substring(match.index + shift + match[0].length) 30 | shift += replacement.length - match[0].length 31 | } 32 | return res 33 | } 34 | 35 | const eq = exports.eq = (a=[], b=[]) => { 36 | return a[0] === b[0] && a[1] === b[1] 37 | } 38 | 39 | const line = exports.line = (a, b) => { 40 | return new m.paths.Line(a, b) 41 | } 42 | 43 | exports.circle = (p, r) => { 44 | return {paths: {circle: new m.paths.Circle(p, r)}} 45 | } 46 | 47 | exports.rect = (w, h, o=[0, 0]) => { 48 | const res = { 49 | top: line([0, h], [w, h]), 50 | right: line([w, h], [w, 0]), 51 | bottom: line([w, 0], [0, 0]), 52 | left: line([0, 0], [0, h]) 53 | } 54 | return m.model.move({paths: res}, o) 55 | } 56 | 57 | exports.poly = (arr) => { 58 | let counter = 0 59 | let prev = arr[arr.length - 1] 60 | const res = { 61 | paths: {} 62 | } 63 | for (const p of arr) { 64 | if (eq(prev, p)) continue 65 | res.paths['p' + (++counter)] = line(prev, p) 66 | prev = p 67 | } 68 | return res 69 | } 70 | 71 | exports.bbox = (arr) => { 72 | let minx = Infinity 73 | let miny = Infinity 74 | let maxx = -Infinity 75 | let maxy = -Infinity 76 | for (const p of arr) { 77 | minx = Math.min(minx, p[0]) 78 | miny = Math.min(miny, p[1]) 79 | maxx = Math.max(maxx, p[0]) 80 | maxy = Math.max(maxy, p[1]) 81 | } 82 | return {low: [minx, miny], high: [maxx, maxy]} 83 | } 84 | 85 | const farPoint = exports.farPoint = [1234.1234, 2143.56789] 86 | 87 | exports.union = exports.add = (a, b) => { 88 | return m.model.combine(a, b, false, true, false, true, { 89 | farPoint 90 | }) 91 | } 92 | 93 | exports.subtract = (a, b) => { 94 | return m.model.combine(a, b, false, true, true, false, { 95 | farPoint 96 | }) 97 | } 98 | 99 | exports.intersect = (a, b) => { 100 | return m.model.combine(a, b, true, false, true, false, { 101 | farPoint 102 | }) 103 | } 104 | 105 | exports.stack = (a, b) => { 106 | return { 107 | models: { 108 | a, b 109 | } 110 | } 111 | } 112 | 113 | const semver = exports.semver = (str, name='') => { 114 | let main = str.split('-')[0] 115 | if (main.startsWith('v')) { 116 | main = main.substring(1) 117 | } 118 | while (main.split('.').length < 3) { 119 | main += '.0' 120 | } 121 | if (/^\d+\.\d+\.\d+$/.test(main)) { 122 | const parts = main.split('.').map(part => parseInt(part, 10)) 123 | return {major: parts[0], minor: parts[1], patch: parts[2]} 124 | } else throw new Error(`Invalid semver "${str}" at ${name}!`) 125 | } 126 | 127 | const satisfies = exports.satisfies = (current, expected) => { 128 | if (current.major === undefined) current = semver(current) 129 | if (expected.major === undefined) expected = semver(expected) 130 | return current.major === expected.major && ( 131 | current.minor > expected.minor || ( 132 | current.minor === expected.minor && 133 | current.patch >= expected.patch 134 | ) 135 | ) 136 | } -------------------------------------------------------------------------------- /test/unit/interface.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const yaml = require('js-yaml') 4 | const ergogen = require('../../src/ergogen') 5 | const version = require('../../package.json').version 6 | 7 | // fixtures 8 | const load = name => yaml.safeLoad(fs.readFileSync( 9 | path.join(__dirname, `../fixtures/${name}`) 10 | ).toString()) 11 | const minimal = load('minimal.yaml') 12 | const big = load('big.yaml') 13 | const kle = load('atreus_kle.json') 14 | 15 | describe('Interface', function() { 16 | 17 | this.timeout(120000) 18 | this.slow(120000) 19 | 20 | it('debug', async function() { 21 | // to check whether the output has "private" exports 22 | const underscore = obj => { 23 | for (const val of Object.values(obj)) { 24 | for (const key of Object.keys(val)) { 25 | if (key.startsWith('_')) return true 26 | } 27 | } 28 | return false 29 | } 30 | underscore(await ergogen.process(minimal)).should.be.false 31 | underscore(await ergogen.process(big, false)).should.be.false 32 | underscore(await ergogen.process(big, true)).should.be.true 33 | }) 34 | 35 | it('formats', async function() { 36 | const logger = msg => { 37 | if (msg.startsWith('Interpreting format:')) { 38 | throw msg.split(':')[1].trim() 39 | } 40 | } 41 | return Promise.all([ 42 | ergogen.process(minimal, true, logger).should.be.rejectedWith('OBJ'), 43 | ergogen.process(yaml.dump(minimal), true, logger).should.be.rejectedWith('YAML'), 44 | ergogen.process(` 45 | //: 46 | return {points: {}} 47 | `, true, logger).should.be.rejectedWith('JS'), 48 | ergogen.process(` 49 | //: 50 | return 'not an object'; 51 | `, true, logger).should.be.rejectedWith('not valid'), 52 | ergogen.process(kle, true, logger).should.be.rejectedWith('KLE'), 53 | ergogen.process('not an object', true, logger).should.be.rejectedWith('object'), 54 | ergogen.process({}, true, logger).should.be.rejectedWith('empty'), 55 | ergogen.process({not_points: {}}, true, () => {}).should.be.rejectedWith('points clause'), 56 | ergogen.process({points: {zones: {}}}, true, () => {}).should.be.rejectedWith('any points') 57 | ]) 58 | }) 59 | 60 | it('preprocessor', async function() { 61 | return Promise.all([ 62 | // unnesting 63 | ergogen.process({'points.zones.matrix': {}}).should.eventually.have.deep.property('canonical', { 64 | points: {zones: {matrix: {}}} 65 | }), 66 | // inheritance 67 | ergogen.process({ 68 | 'points.zones.parent.key.a': 1, 69 | 'points.zones.child': { 70 | '$extends': 'points.zones.parent', 71 | 'key.b': 2 72 | } 73 | }).should.eventually.have.deep.nested.property('canonical.points.zones.child.key', { 74 | a: 1, 75 | b: 2 76 | }), 77 | // parameterization 78 | ergogen.process({ 79 | 'points.zones.matrix.key': { 80 | a: 'PAR', 81 | $params: ['PAR'], 82 | $args: [1] 83 | } 84 | }).should.eventually.have.deep.nested.property('canonical.points.zones.matrix.key', { 85 | a: '1' 86 | }) 87 | ]) 88 | }) 89 | 90 | it('engine', async function() { 91 | return Promise.all([ 92 | ergogen.process({'meta.engine': 'invalid'}).should.be.rejectedWith('Invalid'), 93 | ergogen.process({'meta.engine': '0.1.2'}).should.be.rejectedWith('satisfy'), 94 | // no "points clause" means we're over the engine check, so it "succeeded" 95 | ergogen.process({'meta.engine': `${version}`}).should.be.rejectedWith('points clause') 96 | ]) 97 | }) 98 | 99 | }) -------------------------------------------------------------------------------- /src/footprints/scrollwheel.js: -------------------------------------------------------------------------------- 1 | // Panasonic EVQWGD001 horizontal rotary encoder 2 | // 3 | // __________________ 4 | // (f) (t) | | 5 | // | (1) | | 6 | // | (2) | | 7 | // | (3) | | 8 | // | (4) | | 9 | // |_( )___________|_| 10 | // 11 | // Nets 12 | // from: corresponds to switch pin 1 (for button presses) 13 | // to: corresponds to switch pin 2 (for button presses) 14 | // A: corresponds to pin 1 (for rotary) 15 | // B: corresponds to pin 2 (for rotary, should be GND) 16 | // C: corresponds to pin 3 (for rotary) 17 | // D: corresponds to pin 4 (for rotary, unused) 18 | // Params 19 | // reverse: default is false 20 | // if true, will flip the footprint such that the pcb can be reversible 21 | 22 | 23 | module.exports = { 24 | nets: { 25 | from: undefined, 26 | to: undefined, 27 | A: undefined, 28 | B: undefined, 29 | C: undefined, 30 | D: undefined 31 | }, 32 | params: { 33 | class: 'S', 34 | reverse: false 35 | }, 36 | body: p => { 37 | const standard = ` 38 | (module RollerEncoder_Panasonic_EVQWGD001 (layer F.Cu) (tedit 6040A10C) 39 | ${p.at /* parametric position */} 40 | (fp_text reference REF** (at 0 0 ${p.rot}) (layer F.Fab) (effects (font (size 1 1) (thickness 0.15)))) 41 | (fp_text value RollerEncoder_Panasonic_EVQWGD001 (at -0.1 9 ${p.rot}) (layer F.Fab) (effects (font (size 1 1) (thickness 0.15)))) 42 | 43 | ${'' /* corner marks */} 44 | (fp_line (start -8.4 -6.4) (end 8.4 -6.4) (layer Dwgs.User) (width 0.12)) 45 | (fp_line (start 8.4 -6.4) (end 8.4 7.4) (layer Dwgs.User) (width 0.12)) 46 | (fp_line (start 8.4 7.4) (end -8.4 7.4) (layer Dwgs.User) (width 0.12)) 47 | (fp_line (start -8.4 7.4) (end -8.4 -6.4) (layer Dwgs.User) (width 0.12)) 48 | ` 49 | function pins(def_neg, def_pos) { 50 | return ` 51 | ${'' /* edge cuts */} 52 | (fp_line (start ${def_pos}9.8 7.3) (end ${def_pos}9.8 -6.3) (layer Edge.Cuts) (width 0.15)) 53 | (fp_line (start ${def_pos}7.4 -6.3) (end ${def_pos}7.4 7.3) (layer Edge.Cuts) (width 0.15)) 54 | (fp_line (start ${def_pos}9.5 -6.6) (end ${def_pos}7.7 -6.6) (layer Edge.Cuts) (width 0.15)) 55 | (fp_line (start ${def_pos}7.7 7.6) (end ${def_pos}9.5 7.6) (layer Edge.Cuts) (width 0.15)) 56 | (fp_arc (start ${def_pos}7.7 7.3) (end ${def_pos}7.4 7.3) (angle ${def_neg}90) (layer Edge.Cuts) (width 0.15)) 57 | (fp_arc (start ${def_pos}9.5 7.3) (end ${def_pos}9.5 7.6) (angle ${def_neg}90) (layer Edge.Cuts) (width 0.15)) 58 | (fp_arc (start ${def_pos}7.7 -6.3) (end ${def_pos}7.7 -6.6) (angle ${def_neg}90) (layer Edge.Cuts) (width 0.15)) 59 | (fp_arc (start ${def_pos}9.5 -6.3) (end ${def_pos}9.8 -6.3) (angle ${def_neg}90) (layer Edge.Cuts) (width 0.15)) 60 | 61 | ${'' /* pins */} 62 | (pad S1 thru_hole circle (at ${def_neg}6.85 -6.2 ${p.rot}) (size 1.6 1.6) (drill 0.9) (layers *.Cu *.Mask) ${p.net.from.str}) 63 | (pad S2 thru_hole circle (at ${def_neg}5 -6.2 ${p.rot}) (size 1.6 1.6) (drill 0.9) (layers *.Cu *.Mask) ${p.net.to.str}) 64 | (pad A thru_hole circle (at ${def_neg}5.625 -3.81 ${p.rot}) (size 1.6 1.6) (drill 0.9) (layers *.Cu *.Mask) ${p.net.A.str}) 65 | (pad B thru_hole circle (at ${def_neg}5.625 -1.27 ${p.rot}) (size 1.6 1.6) (drill 0.9) (layers *.Cu *.Mask) ${p.net.B.str}) 66 | (pad C thru_hole circle (at ${def_neg}5.625 1.27 ${p.rot}) (size 1.6 1.6) (drill 0.9) (layers *.Cu *.Mask) ${p.net.C.str}) 67 | (pad D thru_hole circle (at ${def_neg}5.625 3.81 ${p.rot}) (size 1.6 1.6) (drill 0.9) (layers *.Cu *.Mask) ${p.net.D.str}) 68 | 69 | ${'' /* stabilizer */} 70 | (pad "" np_thru_hole circle (at ${def_neg}5.625 6.3 ${p.rot}) (size 1.5 1.5) (drill 1.5) (layers *.Cu *.Mask)) 71 | ` 72 | } 73 | if(p.param.reverse) { 74 | return ` 75 | ${standard} 76 | ${pins('-', '')} 77 | ${pins('', '-')}) 78 | ` 79 | } else { 80 | return ` 81 | ${standard} 82 | ${pins('-', '')}) 83 | ` 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /test/helpers/mock_footprints.js: -------------------------------------------------------------------------------- 1 | exports.inject = (ergogen) => { 2 | ergogen.inject_footprint('trace_test', { 3 | nets: { 4 | P1: 'P1' 5 | }, 6 | params: { 7 | class: 'T', 8 | side: 'F' 9 | }, 10 | body: p => { 11 | return ` 12 | 13 | (module trace_test (layer F.Cu) (tedit 5CF31DEF) 14 | 15 | ${p.at /* parametric position */} 16 | 17 | (pad 1 smd rect (at 0 0 ${p.rot}) (size 1 1) (layers F.Cu F.Paste F.Mask) 18 | ${p.net.P1.str} (solder_mask_margin 0.2)) 19 | 20 | (pad 2 smd rect (at 5 5 ${p.rot}) (size 1 1) (layers F.Cu F.Paste F.Mask) 21 | ${p.net.P1.str} (solder_mask_margin 0.2)) 22 | 23 | ) 24 | 25 | (segment (start ${p.xy(0, 0)}) (end ${p.xy(5, 5)}) (width 0.25) (layer F.Cu) (net ${p.net.P1.index})) 26 | 27 | ` 28 | } 29 | }) 30 | 31 | ergogen.inject_footprint('zone_test', { 32 | nets: { 33 | P1: 'P1' 34 | }, 35 | params: { 36 | class: 'T', 37 | side: 'F' 38 | }, 39 | body: p => { 40 | return ` 41 | 42 | (module zone_test (layer F.Cu) (tedit 5CF31DEF) 43 | 44 | ${p.at /* parametric position */} 45 | 46 | (pad 1 smd rect (at 0 0 ${p.rot}) (size 1 1) (layers F.Cu F.Paste F.Mask) 47 | ${p.net.P1.str} (solder_mask_margin 0.2)) 48 | 49 | (pad 2 smd rect (at 5 5 ${p.rot}) (size 1 1) (layers F.Cu F.Paste F.Mask) 50 | ${p.net.P1.str} (solder_mask_margin 0.2)) 51 | 52 | ) 53 | 54 | (zone (net ${p.net.P1.index}) (net_name ${p.net.P1.name}) (layer ${p.param.side}.Cu) (tstamp 0) (hatch full 0.508) 55 | (connect_pads (clearance 0.508)) 56 | (min_thickness 0.254) 57 | (fill yes (arc_segments 32) (thermal_gap 0.508) (thermal_bridge_width 0.508)) 58 | (polygon (pts (xy ${p.xy(5, 5)}) (xy ${p.xy(5, -5)}) (xy ${p.xy(-5, -5)}) (xy ${p.xy(-5, 5)}))) 59 | ) 60 | 61 | ` 62 | } 63 | }) 64 | 65 | ergogen.inject_footprint('dynamic_net_test', { 66 | nets: {}, 67 | params: { 68 | class: 'T', 69 | side: 'F' 70 | }, 71 | body: p => { 72 | return ` 73 | 74 | (module dynamic_net_test (layer F.Cu) (tedit 5CF31DEF) 75 | 76 | ${p.at /* parametric position */} 77 | 78 | (pad 1 smd rect (at 0 0 ${p.rot}) (size 1 1) (layers F.Cu F.Paste F.Mask) 79 | ${p.local_net('1').str} (solder_mask_margin 0.2)) 80 | 81 | (pad 1 smd rect (at 0 0 ${p.rot}) (size 1 1) (layers F.Cu F.Paste F.Mask) 82 | ${p.local_net('2').str} (solder_mask_margin 0.2)) 83 | 84 | (pad 1 smd rect (at 0 0 ${p.rot}) (size 1 1) (layers F.Cu F.Paste F.Mask) 85 | ${p.local_net('3').str} (solder_mask_margin 0.2)) 86 | 87 | ) 88 | 89 | ` 90 | } 91 | }) 92 | 93 | ergogen.inject_footprint('anchor_test', { 94 | nets: {}, 95 | params: { 96 | class: 'T', 97 | side: 'F' 98 | }, 99 | anchors: { 100 | end: undefined 101 | }, 102 | body: p => { 103 | return ` 104 | 105 | (module anchor_test (layer F.Cu) (tedit 5CF31DEF) 106 | 107 | ${p.at /* parametric position */} 108 | 109 | (fp_line (start 0 0) (end ${p.anchors.end.x} ${p.anchors.end.y}) (layer Dwgs.User) (width 0.05)) 110 | 111 | ) 112 | 113 | ` 114 | } 115 | }) 116 | 117 | ergogen.inject_footprint('references_test', { 118 | nets: {}, 119 | params: {}, 120 | body: p => { 121 | return `references ${p.ref_hide ? 'hidden' : 'shown'}` 122 | } 123 | }) 124 | } -------------------------------------------------------------------------------- /test/fixtures/atreus_kle.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | { 4 | "r": 10, 5 | "rx": 1, 6 | "y": -0.09999999999999998, 7 | "x": 2 8 | }, 9 | "E" 10 | ], 11 | [ 12 | { 13 | "y": -0.65, 14 | "x": 1 15 | }, 16 | "W", 17 | { 18 | "x": 1 19 | }, 20 | "R" 21 | ], 22 | [ 23 | { 24 | "y": -0.75 25 | }, 26 | "Q" 27 | ], 28 | [ 29 | { 30 | "y": -0.9, 31 | "x": 4 32 | }, 33 | "T" 34 | ], 35 | [ 36 | { 37 | "y": -0.7000000000000001, 38 | "x": 2 39 | }, 40 | "D" 41 | ], 42 | [ 43 | { 44 | "y": -0.6499999999999999, 45 | "x": 1 46 | }, 47 | "S", 48 | { 49 | "x": 1 50 | }, 51 | "F" 52 | ], 53 | [ 54 | { 55 | "y": -0.75 56 | }, 57 | "A" 58 | ], 59 | [ 60 | { 61 | "y": -0.8999999999999999, 62 | "x": 4 63 | }, 64 | "G" 65 | ], 66 | [ 67 | { 68 | "y": -0.7000000000000002, 69 | "x": 2 70 | }, 71 | "C" 72 | ], 73 | [ 74 | { 75 | "y": -0.6499999999999999, 76 | "x": 1 77 | }, 78 | "X", 79 | { 80 | "x": 1 81 | }, 82 | "V" 83 | ], 84 | [ 85 | { 86 | "y": -0.75 87 | }, 88 | "Z" 89 | ], 90 | [ 91 | { 92 | "y": -0.8999999999999999, 93 | "x": 4 94 | }, 95 | "B" 96 | ], 97 | [ 98 | { 99 | "y": -0.75, 100 | "x": 5, 101 | "h": 1.5 102 | }, 103 | "Ctrl" 104 | ], 105 | [ 106 | { 107 | "y": -0.9500000000000002, 108 | "x": 2 109 | }, 110 | "super" 111 | ], 112 | [ 113 | { 114 | "y": -0.6499999999999999, 115 | "x": 1 116 | }, 117 | "Tab", 118 | { 119 | "x": 1 120 | }, 121 | "Shift" 122 | ], 123 | [ 124 | { 125 | "y": -0.75 126 | }, 127 | "Esc" 128 | ], 129 | [ 130 | { 131 | "y": -0.8999999999999999, 132 | "x": 4 133 | }, 134 | "Bksp" 135 | ], 136 | [ 137 | { 138 | "r": -10, 139 | "rx": 7, 140 | "ry": 0.965, 141 | "y": -0.20000000000000018, 142 | "x": 2 143 | }, 144 | "I" 145 | ], 146 | [ 147 | { 148 | "y": -0.6499999999999999, 149 | "x": 1 150 | }, 151 | "U", 152 | { 153 | "x": 1 154 | }, 155 | "O" 156 | ], 157 | [ 158 | { 159 | "y": -0.75, 160 | "x": 4 161 | }, 162 | "P" 163 | ], 164 | [ 165 | { 166 | "y": -0.8999999999999999 167 | }, 168 | "Y" 169 | ], 170 | [ 171 | { 172 | "y": -0.7000000000000002, 173 | "x": 2 174 | }, 175 | "K" 176 | ], 177 | [ 178 | { 179 | "y": -0.6499999999999999, 180 | "x": 1 181 | }, 182 | "J", 183 | { 184 | "x": 1 185 | }, 186 | "L" 187 | ], 188 | [ 189 | { 190 | "y": -0.75, 191 | "x": 4 192 | }, 193 | ":\n;" 194 | ], 195 | [ 196 | { 197 | "y": -0.8999999999999999 198 | }, 199 | "H" 200 | ], 201 | [ 202 | { 203 | "y": -0.7000000000000002, 204 | "x": 2 205 | }, 206 | "<\n," 207 | ], 208 | [ 209 | { 210 | "y": -0.6499999999999999, 211 | "x": 1 212 | }, 213 | "M", 214 | { 215 | "x": 1 216 | }, 217 | ">\n." 218 | ], 219 | [ 220 | { 221 | "y": -0.7500000000000004, 222 | "x": 4 223 | }, 224 | "?\n/" 225 | ], 226 | [ 227 | { 228 | "y": -0.9000000000000004 229 | }, 230 | "N" 231 | ], 232 | [ 233 | { 234 | "y": -0.7499999999999996, 235 | "x": -1, 236 | "h": 1.5 237 | }, 238 | "Alt" 239 | ], 240 | [ 241 | { 242 | "y": -0.9499999999999997, 243 | "x": 2 244 | }, 245 | "_\n-" 246 | ], 247 | [ 248 | { 249 | "y": -0.6500000000000004, 250 | "x": 1 251 | }, 252 | "fn", 253 | { 254 | "x": 1 255 | }, 256 | "\"\n'" 257 | ], 258 | [ 259 | { 260 | "y": -0.75, 261 | "x": 4 262 | }, 263 | "Enter" 264 | ], 265 | [ 266 | { 267 | "y": -0.9000000000000004 268 | }, 269 | "Space" 270 | ] 271 | ] -------------------------------------------------------------------------------- /src/footprints/rotary.js: -------------------------------------------------------------------------------- 1 | // EC11 rotary encoder 2 | // 3 | // Nets 4 | // from: corresponds to switch pin 1 (for button presses) 5 | // to: corresponds to switch pin 2 (for button presses) 6 | // A: corresponds to pin 1 (for rotary) 7 | // B: corresponds to pin 2 (for rotary, should be GND) 8 | // C: corresponds to pin 3 (for rotary) 9 | 10 | module.exports = { 11 | nets: { 12 | from: undefined, 13 | to: undefined, 14 | A: undefined, 15 | B: undefined, 16 | C: undefined 17 | }, 18 | params: { 19 | class: 'ROT' 20 | }, 21 | body: p => ` 22 | (module rotary_encoder (layer F.Cu) (tedit 603326DE) 23 | 24 | ${p.at /* parametric position */} 25 | 26 | ${'' /* footprint reference */} 27 | (fp_text reference "${p.ref}" (at 0 0.5) (layer F.SilkS) 28 | ${p.ref_hide} (effects (font (size 1 1) (thickness 0.15)))) 29 | (fp_text value "" (at 0 8.89) (layer F.Fab) 30 | (effects (font (size 1 1) (thickness 0.15)))) 31 | 32 | ${''/* component outline */} 33 | (fp_line (start -0.62 -0.04) (end 0.38 -0.04) (layer F.SilkS) (width 0.12)) 34 | (fp_line (start -0.12 -0.54) (end -0.12 0.46) (layer F.SilkS) (width 0.12)) 35 | (fp_line (start 5.98 3.26) (end 5.98 5.86) (layer F.SilkS) (width 0.12)) 36 | (fp_line (start 5.98 -1.34) (end 5.98 1.26) (layer F.SilkS) (width 0.12)) 37 | (fp_line (start 5.98 -5.94) (end 5.98 -3.34) (layer F.SilkS) (width 0.12)) 38 | (fp_line (start -3.12 -0.04) (end 2.88 -0.04) (layer F.Fab) (width 0.12)) 39 | (fp_line (start -0.12 -3.04) (end -0.12 2.96) (layer F.Fab) (width 0.12)) 40 | (fp_line (start -7.32 -4.14) (end -7.62 -3.84) (layer F.SilkS) (width 0.12)) 41 | (fp_line (start -7.92 -4.14) (end -7.32 -4.14) (layer F.SilkS) (width 0.12)) 42 | (fp_line (start -7.62 -3.84) (end -7.92 -4.14) (layer F.SilkS) (width 0.12)) 43 | (fp_line (start -6.22 -5.84) (end -6.22 5.86) (layer F.SilkS) (width 0.12)) 44 | (fp_line (start -2.12 -5.84) (end -6.22 -5.84) (layer F.SilkS) (width 0.12)) 45 | (fp_line (start -2.12 5.86) (end -6.22 5.86) (layer F.SilkS) (width 0.12)) 46 | (fp_line (start 5.98 5.86) (end 1.88 5.86) (layer F.SilkS) (width 0.12)) 47 | (fp_line (start 1.88 -5.94) (end 5.98 -5.94) (layer F.SilkS) (width 0.12)) 48 | (fp_line (start -6.12 -4.74) (end -5.12 -5.84) (layer F.Fab) (width 0.12)) 49 | (fp_line (start -6.12 5.76) (end -6.12 -4.74) (layer F.Fab) (width 0.12)) 50 | (fp_line (start 5.88 5.76) (end -6.12 5.76) (layer F.Fab) (width 0.12)) 51 | (fp_line (start 5.88 -5.84) (end 5.88 5.76) (layer F.Fab) (width 0.12)) 52 | (fp_line (start -5.12 -5.84) (end 5.88 -5.84) (layer F.Fab) (width 0.12)) 53 | (fp_line (start -8.87 -6.89) (end 7.88 -6.89) (layer F.CrtYd) (width 0.05)) 54 | (fp_line (start -8.87 -6.89) (end -8.87 6.81) (layer F.CrtYd) (width 0.05)) 55 | (fp_line (start 7.88 6.81) (end 7.88 -6.89) (layer F.CrtYd) (width 0.05)) 56 | (fp_line (start 7.88 6.81) (end -8.87 6.81) (layer F.CrtYd) (width 0.05)) 57 | (fp_circle (center -0.12 -0.04) (end 2.88 -0.04) (layer F.SilkS) (width 0.12)) 58 | (fp_circle (center -0.12 -0.04) (end 2.88 -0.04) (layer F.Fab) (width 0.12)) 59 | 60 | ${''/* pin names */} 61 | (pad A thru_hole rect (at -7.62 -2.54 ${p.rot}) (size 2 2) (drill 1) (layers *.Cu *.Mask) ${p.net.A.str}) 62 | (pad C thru_hole circle (at -7.62 -0.04) (size 2 2) (drill 1) (layers *.Cu *.Mask) ${p.net.C.str}) 63 | (pad B thru_hole circle (at -7.62 2.46) (size 2 2) (drill 1) (layers *.Cu *.Mask) ${p.net.B.str}) 64 | (pad 1 thru_hole circle (at 6.88 -2.54) (size 1.5 1.5) (drill 1) (layers *.Cu *.Mask) ${p.net.from.str}) 65 | (pad 2 thru_hole circle (at 6.88 2.46) (size 1.5 1.5) (drill 1) (layers *.Cu *.Mask) ${p.net.to.str}) 66 | 67 | ${''/* Legs */} 68 | (pad "" thru_hole rect (at -0.12 -5.64 ${p.rot}) (size 3.2 2) (drill oval 2.8 1.5) (layers *.Cu *.Mask)) 69 | (pad "" thru_hole rect (at -0.12 5.56 ${p.rot}) (size 3.2 2) (drill oval 2.8 1.5) (layers *.Cu *.Mask)) 70 | ) 71 | ` 72 | } 73 | -------------------------------------------------------------------------------- /src/footprints/chocmini.js: -------------------------------------------------------------------------------- 1 | // Kailh Choc PG1232 2 | // Nets 3 | // from: corresponds to pin 1 4 | // to: corresponds to pin 2 5 | // Params 6 | // reverse: default is false 7 | // if true, will flip the footprint such that the pcb can be reversible 8 | // keycaps: default is false 9 | // if true, will add choc sized keycap box around the footprint 10 | 11 | module.exports = { 12 | nets: { 13 | from: undefined, 14 | to: undefined 15 | }, 16 | params: { 17 | class: 'S', 18 | side: 'F', 19 | reverse: false, 20 | keycaps: false 21 | }, 22 | body: p => { 23 | const standard = ` 24 | (module lib:Kailh_PG1232 (layer F.Cu) (tedit 5E1ADAC2) 25 | ${p.at /* parametric position */} 26 | 27 | ${'' /* footprint reference */} 28 | (fp_text reference "${p.ref}" (at 0 0) (layer F.SilkS) ${p.ref_hide} (effects (font (size 1.27 1.27) (thickness 0.15)))) 29 | (fp_text value Kailh_PG1232 (at 0 -7.3) (layer F.Fab) (effects (font (size 1 1) (thickness 0.15)))) 30 | 31 | ${'' /* corner marks */} 32 | (fp_line (start -7.25 -6.75) (end -6.25 -6.75) (layer Dwgs.User) (width 0.15)) 33 | (fp_line (start -7.25 -6.75) (end -7.25 -5.75) (layer Dwgs.User) (width 0.15)) 34 | 35 | (fp_line (start -7.25 6.75) (end -6.25 6.75) (layer Dwgs.User) (width 0.15)) 36 | (fp_line (start -7.25 6.75) (end -7.25 5.75) (layer Dwgs.User) (width 0.15)) 37 | 38 | (fp_line (start 7.25 -6.75) (end 6.25 -6.75) (layer Dwgs.User) (width 0.15)) 39 | (fp_line (start 7.25 -6.75) (end 7.25 -5.75) (layer Dwgs.User) (width 0.15)) 40 | 41 | (fp_line (start 7.25 6.75) (end 6.25 6.75) (layer Dwgs.User) (width 0.15)) 42 | (fp_line (start 7.25 6.75) (end 7.25 5.75) (layer Dwgs.User) (width 0.15)) 43 | 44 | 45 | (fp_line (start 2.8 -5.35) (end -2.8 -5.35) (layer Dwgs.User) (width 0.15)) 46 | (fp_line (start -2.8 -3.2) (end 2.8 -3.2) (layer Dwgs.User) (width 0.15)) 47 | (fp_line (start 2.8 -3.2) (end 2.8 -5.35) (layer Dwgs.User) (width 0.15)) 48 | (fp_line (start -2.8 -3.2) (end -2.8 -5.35) (layer Dwgs.User) (width 0.15)) 49 | 50 | ${''/* middle shaft */} 51 | (fp_line (start 2.25 2.6) (end 5.8 2.6) (layer Edge.Cuts) (width 0.12)) 52 | (fp_line (start -2.25 2.6) (end -5.8 2.6) (layer Edge.Cuts) (width 0.12)) 53 | (fp_line (start 2.25 3.6) (end 2.25 2.6) (layer Edge.Cuts) (width 0.12)) 54 | (fp_line (start -2.25 3.6) (end 2.25 3.6) (layer Edge.Cuts) (width 0.12)) 55 | (fp_line (start -2.25 2.6) (end -2.25 3.6) (layer Edge.Cuts) (width 0.12)) 56 | (fp_line (start -5.8 2.6) (end -5.8 -2.95) (layer Edge.Cuts) (width 0.12)) 57 | (fp_line (start 5.8 -2.95) (end 5.8 2.6) (layer Edge.Cuts) (width 0.12)) 58 | (fp_line (start -5.8 -2.95) (end 5.8 -2.95) (layer Edge.Cuts) (width 0.12)) 59 | 60 | ${''/* stabilizers */} 61 | (pad 3 thru_hole circle (at 5.3 -4.75) (size 1.6 1.6) (drill 1.1) (layers *.Cu *.Mask) (clearance 0.2)) 62 | (pad 4 thru_hole circle (at -5.3 -4.75) (size 1.6 1.6) (drill 1.1) (layers *.Cu *.Mask) (clearance 0.2)) 63 | ` 64 | const keycap = ` 65 | ${'' /* keycap marks */} 66 | (fp_line (start -9 -8.5) (end 9 -8.5) (layer Dwgs.User) (width 0.15)) 67 | (fp_line (start 9 -8.5) (end 9 8.5) (layer Dwgs.User) (width 0.15)) 68 | (fp_line (start 9 8.5) (end -9 8.5) (layer Dwgs.User) (width 0.15)) 69 | (fp_line (start -9 8.5) (end -9 -8.5) (layer Dwgs.User) (width 0.15)) 70 | ` 71 | function pins(def_neg, def_pos) { 72 | return ` 73 | ${''/* pins */} 74 | (pad 1 thru_hole circle (at ${def_neg}4.58 5.1) (size 1.6 1.6) (drill 1.1) (layers *.Cu *.Mask) ${p.net.from.str} (clearance 0.2)) 75 | (pad 2 thru_hole circle (at ${def_pos}2 5.4) (size 1.6 1.6) (drill 1.1) (layers *.Cu *.Mask) ${p.net.to.str} (clearance 0.2)) 76 | ` 77 | } 78 | if(p.param.reverse){ 79 | return ` 80 | ${standard} 81 | ${p.param.keycaps ? keycap : ''} 82 | ${pins('-', '')} 83 | ${pins('', '-')}) 84 | 85 | ` 86 | } else { 87 | return ` 88 | ${standard} 89 | ${p.param.keycaps ? keycap : ''} 90 | ${pins('-', '')}) 91 | ` 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /config/adux.yaml: -------------------------------------------------------------------------------- 1 | points: 2 | zones: 3 | matrix: 4 | columns: 5 | pinky: 6 | spread: 18 7 | rotate: 15 8 | origin: [0, -17] 9 | rows: 10 | bottom: 11 | bind: [5, 0, 0, 0] 12 | column_net: P7 13 | home: 14 | bind: [0, 12, 0, 0] 15 | column_net: P6 16 | top: 17 | bind: [0, 8, 5, 0] 18 | column_net: P5 19 | ring: 20 | spread: 18 21 | stagger: 17 22 | rotate: -10 23 | origin: [0, -17] 24 | rows: 25 | bottom: 26 | bind: [0, 0, 2, 10] 27 | column_net: P4 28 | home: 29 | bind: [5, 0, 5, 0] 30 | column_net: P3 31 | top: 32 | bind: [0, 5, 0, 0] 33 | column_net: P0 34 | middle: 35 | spread: 18 36 | stagger: 17/3 37 | rotate: -5 38 | origin: [0, -17] 39 | rows: 40 | bottom: 41 | bind: [0, 10, 0, 5] 42 | column_net: P1 43 | home: 44 | bind: 5 45 | column_net: P19 46 | top: 47 | bind: [0, 0, 0, 0] 48 | column_net: P18 49 | key: 50 | shift: [0.2, 0] 51 | index: 52 | spread: 18 53 | stagger: -17/3 54 | rotate: -5 55 | origin: [0, -17] 56 | rows: 57 | bottom: 58 | bind: [0, 5, 0, 0] 59 | column_net: P15 60 | home: 61 | bind: [5, 0, 5, 0] 62 | column_net: P14 63 | top: 64 | bind: [0, 0, 0, 6] 65 | column_net: P16 66 | inner: 67 | spread: 18 68 | stagger: -17/6 69 | origin: [0, -17] 70 | rows: 71 | bottom: 72 | bind: [5, 19, 20, 2] 73 | column_net: P10 74 | home: 75 | bind: [0, 27, 0, 5] 76 | column_net: P20 77 | top: 78 | bind: [0, 0, 5, 5] 79 | column_net: P21 80 | key: 81 | footprints: 82 | choc_hotswap: 83 | type: choc 84 | nets: 85 | from: =column_net 86 | to: GND 87 | params: 88 | keycaps: true 89 | reverse: true 90 | hotswap: true 91 | choc: 92 | type: choc 93 | anchor: 94 | rotate: 180 95 | nets: 96 | from: =column_net 97 | to: GND 98 | params: 99 | keycaps: true 100 | reverse: true 101 | rows: 102 | bottom: 103 | padding: 17 104 | home: 105 | padding: 17 106 | top: 107 | thumb: 108 | anchor: 109 | ref: matrix_inner_bottom 110 | shift: [0,-24] 111 | columns: 112 | first: 113 | rotate: -15 114 | rows: 115 | only: 116 | column_net: P8 117 | bind: [10, 1, 0, 70] 118 | second: 119 | spread: 18 120 | rotate: -10 121 | origin: [-9, -9.5] 122 | rows: 123 | only: 124 | column_net: P9 125 | bind: [0, 0, 0, 5] 126 | rows: 127 | only: 128 | padding: 17 129 | key: 130 | footprints: 131 | choc_hotswap: 132 | type: choc 133 | nets: 134 | from: =column_net 135 | to: GND 136 | params: 137 | keycaps: true 138 | reverse: true 139 | hotswap: true 140 | choc: 141 | type: choc 142 | anchor: 143 | rotate: 180 144 | nets: 145 | from: =column_net 146 | to: GND 147 | params: 148 | keycaps: true 149 | reverse: true 150 | outlines: 151 | exports: 152 | raw: 153 | - type: keys 154 | side: left 155 | size: [18,17] 156 | corner: 1 157 | first: 158 | - type: outline 159 | name: raw 160 | fillet: 3 161 | second: 162 | - type: outline 163 | name: first 164 | fillet: 2 165 | third: 166 | - type: outline 167 | name: second 168 | fillet: 1 169 | panel: 170 | - type: outline 171 | name: third 172 | fillet: 0.5 -------------------------------------------------------------------------------- /src/prepare.js: -------------------------------------------------------------------------------- 1 | const u = require('./utils') 2 | const a = require('./assert') 3 | 4 | const _extend = exports._extend = (to, from) => { 5 | const to_type = a.type(to)() 6 | const from_type = a.type(from)() 7 | if (from === undefined || from === null) return to 8 | if (from === '$unset') return undefined 9 | if (to_type != from_type) return from 10 | if (from_type == 'object') { 11 | const res = u.deepcopy(to) 12 | for (const key of Object.keys(from)) { 13 | res[key] = _extend(to[key], from[key]) 14 | if (res[key] === undefined) delete res[key] 15 | } 16 | return res 17 | } else if (from_type == 'array') { 18 | const res = u.deepcopy(to) 19 | for (const [i, val] of from.entries()) { 20 | res[i] = _extend(res[i], val) 21 | } 22 | return res 23 | } else return from 24 | } 25 | 26 | const extend = exports.extend = (...args) => { 27 | let res = args[0] 28 | for (const arg of args) { 29 | if (res == arg) continue 30 | res = _extend(res, arg) 31 | } 32 | return res 33 | } 34 | 35 | const traverse = exports.traverse = (config, root, breadcrumbs, op) => { 36 | if (a.type(config)() == 'object') { 37 | const result = {} 38 | for (const [key, val] of Object.entries(config)) { 39 | breadcrumbs.push(key) 40 | op(result, key, traverse(val, root, breadcrumbs, op), root, breadcrumbs) 41 | breadcrumbs.pop() 42 | } 43 | return result 44 | } else if (a.type(config)() == 'array') { 45 | const result = [] 46 | let index = 0 47 | for (const val of config) { 48 | breadcrumbs.push(`[${index}]`) 49 | result[index] = traverse(val, root, breadcrumbs, op) 50 | breadcrumbs.pop() 51 | index++ 52 | } 53 | return result 54 | } 55 | return config 56 | } 57 | 58 | exports.unnest = config => traverse(config, config, [], (target, key, val) => { 59 | u.deep(target, key, val) 60 | }) 61 | 62 | exports.inherit = config => traverse(config, config, [], (target, key, val, root, breadcrumbs) => { 63 | if (val && val.$extends !== undefined) { 64 | let candidates = u.deepcopy(val.$extends) 65 | if (a.type(candidates)() !== 'array') candidates = [candidates] 66 | const list = [val] 67 | while (candidates.length) { 68 | const path = candidates.shift() 69 | const other = u.deepcopy(u.deep(root, path)) 70 | a.assert(other, `"${path}" (reached from "${breadcrumbs.join('.')}.$extends") does not name a valid inheritance target!`) 71 | let parents = other.$extends || [] 72 | if (a.type(parents)() !== 'array') parents = [parents] 73 | candidates = candidates.concat(parents) 74 | list.unshift(other) 75 | } 76 | val = extend.apply(null, list) 77 | delete val.$extends 78 | } 79 | target[key] = val 80 | }) 81 | 82 | exports.parameterize = config => traverse(config, config, [], (target, key, val, root, breadcrumbs) => { 83 | 84 | // we only care about objects 85 | if (a.type(val)() !== 'object') { 86 | target[key] = val 87 | return 88 | } 89 | 90 | let params = val.$params 91 | let args = val.$args 92 | 93 | // explicitly skipped (probably intermediate) template, remove (by not setting it) 94 | if (val.$skip) return 95 | 96 | // nothing to do here, just pass the original value through 97 | if (!params && !args) { 98 | target[key] = val 99 | return 100 | } 101 | 102 | // unused template, remove (by not setting it) 103 | if (params && !args) return 104 | 105 | if (!params && args) { 106 | throw new Error(`Trying to parameterize through "${breadcrumbs}.$args", but the corresponding "$params" field is missing!`) 107 | } 108 | 109 | params = a.strarr(params, `${breadcrumbs}.$params`) 110 | args = a.sane(args, `${breadcrumbs}.$args`, 'array')() 111 | if (params.length !== args.length) { 112 | throw new Error(`The number of "$params" and "$args" don't match for "${breadcrumbs}"!`) 113 | } 114 | 115 | let str = JSON.stringify(val) 116 | const zip = rows => rows[0].map((_, i) => rows.map(row => row[i])) 117 | for (const [par, arg] of zip([params, args])) { 118 | str = str.replace(new RegExp(`${par}`, 'g'), arg) 119 | } 120 | try { 121 | val = JSON.parse(str) 122 | } catch (ex) { 123 | throw new Error(`Replacements didn't lead to a valid JSON object at "${breadcrumbs}"! ` + ex) 124 | } 125 | 126 | delete val.$params 127 | delete val.$args 128 | target[key] = val 129 | }) -------------------------------------------------------------------------------- /test/pcbs/references___pcbs.json: -------------------------------------------------------------------------------- 1 | { 2 | "shown": "\n \n(kicad_pcb (version 20171130) (host pcbnew 5.1.6)\n\n (page A3)\n (title_block\n (title shown)\n (rev v1.0.0)\n (company Unknown)\n )\n\n (general\n (thickness 1.6)\n )\n\n (layers\n (0 F.Cu signal)\n (31 B.Cu signal)\n (32 B.Adhes user)\n (33 F.Adhes user)\n (34 B.Paste user)\n (35 F.Paste user)\n (36 B.SilkS user)\n (37 F.SilkS user)\n (38 B.Mask user)\n (39 F.Mask user)\n (40 Dwgs.User user)\n (41 Cmts.User user)\n (42 Eco1.User user)\n (43 Eco2.User user)\n (44 Edge.Cuts user)\n (45 Margin user)\n (46 B.CrtYd user)\n (47 F.CrtYd user)\n (48 B.Fab user)\n (49 F.Fab user)\n )\n\n (setup\n (last_trace_width 0.25)\n (trace_clearance 0.2)\n (zone_clearance 0.508)\n (zone_45_only no)\n (trace_min 0.2)\n (via_size 0.8)\n (via_drill 0.4)\n (via_min_size 0.4)\n (via_min_drill 0.3)\n (uvia_size 0.3)\n (uvia_drill 0.1)\n (uvias_allowed no)\n (uvia_min_size 0.2)\n (uvia_min_drill 0.1)\n (edge_width 0.05)\n (segment_width 0.2)\n (pcb_text_width 0.3)\n (pcb_text_size 1.5 1.5)\n (mod_edge_width 0.12)\n (mod_text_size 1 1)\n (mod_text_width 0.15)\n (pad_size 1.524 1.524)\n (pad_drill 0.762)\n (pad_to_mask_clearance 0.05)\n (aux_axis_origin 0 0)\n (visible_elements FFFFFF7F)\n (pcbplotparams\n (layerselection 0x010fc_ffffffff)\n (usegerberextensions false)\n (usegerberattributes true)\n (usegerberadvancedattributes true)\n (creategerberjobfile true)\n (excludeedgelayer true)\n (linewidth 0.100000)\n (plotframeref false)\n (viasonmask false)\n (mode 1)\n (useauxorigin false)\n (hpglpennumber 1)\n (hpglpenspeed 20)\n (hpglpendiameter 15.000000)\n (psnegative false)\n (psa4output false)\n (plotreference true)\n (plotvalue true)\n (plotinvisibletext false)\n (padsonsilk false)\n (subtractmaskfromsilk false)\n (outputformat 1)\n (mirror false)\n (drillshape 1)\n (scaleselection 1)\n (outputdirectory \"\"))\n )\n\n (net 0 \"\")\n \n (net_class Default \"This is the default net class.\"\n (clearance 0.2)\n (trace_width 0.25)\n (via_dia 0.8)\n (via_drill 0.4)\n (uvia_dia 0.3)\n (uvia_drill 0.1)\n (add_net \"\")\n )\n\n references shown\n \n \n)\n\n ", 3 | "hidden": "\n \n(kicad_pcb (version 20171130) (host pcbnew 5.1.6)\n\n (page A3)\n (title_block\n (title hidden)\n (rev v1.0.0)\n (company Unknown)\n )\n\n (general\n (thickness 1.6)\n )\n\n (layers\n (0 F.Cu signal)\n (31 B.Cu signal)\n (32 B.Adhes user)\n (33 F.Adhes user)\n (34 B.Paste user)\n (35 F.Paste user)\n (36 B.SilkS user)\n (37 F.SilkS user)\n (38 B.Mask user)\n (39 F.Mask user)\n (40 Dwgs.User user)\n (41 Cmts.User user)\n (42 Eco1.User user)\n (43 Eco2.User user)\n (44 Edge.Cuts user)\n (45 Margin user)\n (46 B.CrtYd user)\n (47 F.CrtYd user)\n (48 B.Fab user)\n (49 F.Fab user)\n )\n\n (setup\n (last_trace_width 0.25)\n (trace_clearance 0.2)\n (zone_clearance 0.508)\n (zone_45_only no)\n (trace_min 0.2)\n (via_size 0.8)\n (via_drill 0.4)\n (via_min_size 0.4)\n (via_min_drill 0.3)\n (uvia_size 0.3)\n (uvia_drill 0.1)\n (uvias_allowed no)\n (uvia_min_size 0.2)\n (uvia_min_drill 0.1)\n (edge_width 0.05)\n (segment_width 0.2)\n (pcb_text_width 0.3)\n (pcb_text_size 1.5 1.5)\n (mod_edge_width 0.12)\n (mod_text_size 1 1)\n (mod_text_width 0.15)\n (pad_size 1.524 1.524)\n (pad_drill 0.762)\n (pad_to_mask_clearance 0.05)\n (aux_axis_origin 0 0)\n (visible_elements FFFFFF7F)\n (pcbplotparams\n (layerselection 0x010fc_ffffffff)\n (usegerberextensions false)\n (usegerberattributes true)\n (usegerberadvancedattributes true)\n (creategerberjobfile true)\n (excludeedgelayer true)\n (linewidth 0.100000)\n (plotframeref false)\n (viasonmask false)\n (mode 1)\n (useauxorigin false)\n (hpglpennumber 1)\n (hpglpenspeed 20)\n (hpglpendiameter 15.000000)\n (psnegative false)\n (psa4output false)\n (plotreference true)\n (plotvalue true)\n (plotinvisibletext false)\n (padsonsilk false)\n (subtractmaskfromsilk false)\n (outputformat 1)\n (mirror false)\n (drillshape 1)\n (scaleselection 1)\n (outputdirectory \"\"))\n )\n\n (net 0 \"\")\n \n (net_class Default \"This is the default net class.\"\n (clearance 0.2)\n (trace_width 0.25)\n (via_dia 0.8)\n (via_drill 0.4)\n (uvia_dia 0.3)\n (uvia_drill 0.1)\n (add_net \"\")\n )\n\n references hidden\n \n \n)\n\n " 4 | } 5 | -------------------------------------------------------------------------------- /test/unit/anchor.js: -------------------------------------------------------------------------------- 1 | const {parse} = require('../../src/anchor') 2 | const Point = require('../../src/point') 3 | const {check} = require('../helpers/point') 4 | 5 | describe('Anchor', function() { 6 | 7 | const points = { 8 | o: new Point(0, 0, 0, {label: 'o'}), 9 | ten: new Point(10, 10, -90, {label: 'ten'}), 10 | mirror_ten: new Point(-10, 10, 90, {mirrored: true}) 11 | } 12 | 13 | it('params', function() { 14 | // an empty anchor definition leads to the default point 15 | check( 16 | parse({}, 'name')(), 17 | [0, 0, 0, {}] 18 | ) 19 | // single reference 20 | check( 21 | parse({ref: 'o'}, 'name', points)(), 22 | [0, 0, 0, {label: 'o'}] 23 | ) 24 | // default point can be overridden 25 | check( 26 | parse({}, 'name', {}, new Point(1, 1))(), 27 | [1, 1, 0, {}] 28 | ) 29 | // mirrored references can be forced 30 | check( 31 | parse({ref: 'ten'}, 'name', points, undefined, true)(), 32 | [-10, 10, 90, {mirrored: true}] 33 | ) 34 | }) 35 | 36 | it('recursive', function() { 37 | // recursive references are supported (keeping metadata) 38 | check( 39 | parse({ 40 | ref: { 41 | ref: 'o', 42 | shift: [2, 2] 43 | } 44 | }, 'name', points)(), 45 | [2, 2, 0, {label: 'o'}] 46 | ) 47 | }) 48 | 49 | it('aggregate', function() { 50 | // average of multiple references (metadata gets ignored) 51 | check( 52 | parse({ 53 | aggregate: { 54 | parts: ['o', 'ten'] 55 | } 56 | }, 'name', points)(), 57 | [5, 5, -45, {}] 58 | ) 59 | }) 60 | 61 | it('shift', function() { 62 | // normal shift 63 | check( 64 | parse({shift: [1, 1]}, 'name')(), 65 | [1, 1, 0, {}] 66 | ) 67 | // shift should respect mirrored points (and invert along the x axis) 68 | check( 69 | parse({ref: 'mirror_ten', shift: [1, 1]}, 'name', points)(), 70 | [-11, 9, 90, {mirrored: true}] 71 | ) 72 | }) 73 | 74 | it('orient', function() { 75 | // an orient by itself is equal to rotation 76 | check( 77 | parse({orient: 10}, 'name')(), 78 | [0, 0, 10, {}] 79 | ) 80 | // orient acts before shifting 81 | // so when we orient to the right, an upward shift goes to the right 82 | check( 83 | parse({orient: -90, shift: [0, 1]}, 'name')(), 84 | [1, 0, -90, {}] 85 | ) 86 | // orient towards another point (and then move a diagonal to get to [1, 1]) 87 | check( 88 | parse({orient: 'ten', shift: [0, Math.SQRT2]}, 'name', points)(), 89 | [1, 1, -45, {}] 90 | ) 91 | }) 92 | 93 | it('rotate', function() { 94 | // basic rotation 95 | check( 96 | parse({rotate: 10}, 'name')(), 97 | [0, 0, 10, {}] 98 | ) 99 | // rotate acts *after* shifting 100 | // so even tho we rotate to the right, an upward shift does go upward 101 | check( 102 | parse({shift: [0, 1], rotate: -90}, 'name')(), 103 | [0, 1, -90, {}] 104 | ) 105 | // rotate towards another point 106 | check( 107 | parse({rotate: {shift: [-1, -1]}}, 'name')(), 108 | [0, 0, 135, {}] 109 | ) 110 | }) 111 | 112 | it('affect', function() { 113 | // affect can restrict which point fields (x, y, r) are affected by the transformations 114 | check( 115 | parse({orient: -90, shift: [0, 1], rotate: 10, affect: 'r'}, 'name')(), 116 | [0, 0, -80, {}] 117 | ) 118 | check( 119 | parse({orient: -90, shift: [0, 1], rotate: 10, affect: 'xy'}, 'name')(), 120 | [1, 0, 0, {}] 121 | ) 122 | // affects can also be arrays (example same as above) 123 | check( 124 | parse({orient: -90, shift: [0, 1], rotate: 10, affect: ['x', 'y']}, 'name')(), 125 | [1, 0, 0, {}] 126 | ) 127 | }) 128 | 129 | it('string', function() { 130 | // basic string form 131 | check( 132 | parse('ten', 'name', points)(), 133 | [10, 10, -90, {label: 'ten'}] 134 | ) 135 | }) 136 | 137 | it('array', function() { 138 | // basic multi-anchor 139 | check( 140 | parse([ 141 | {shift: [1, 1]}, 142 | {rotate: 10} 143 | ], 'name')(), 144 | [1, 1, 10, {}] 145 | ) 146 | }) 147 | }) -------------------------------------------------------------------------------- /src/filter.js: -------------------------------------------------------------------------------- 1 | const u = require('./utils') 2 | const a = require('./assert') 3 | const anchor_lib = require('./anchor') 4 | const Point = require('./point') 5 | const anchor = anchor_lib.parse 6 | 7 | const _true = () => true 8 | const _false = () => false 9 | const _and = arr => p => arr.map(e => e(p)).reduce((a, b) => a && b) 10 | const _or = arr => p => arr.map(e => e(p)).reduce((a, b) => a || b) 11 | 12 | const similar = (key, reference, name, units) => { 13 | let neg = false 14 | 15 | if (reference.startsWith('-')) { 16 | neg = true 17 | reference = reference.slice(1) 18 | } 19 | 20 | // support both string or regex as reference 21 | let internal_tester = val => (''+val) == reference 22 | if (reference.startsWith('/')) { 23 | const regex_parts = reference.split('/') 24 | regex_parts.shift() // remove starting slash 25 | const flags = regex_parts.pop() 26 | const regex = new RegExp(regex_parts.join('/'), flags) 27 | internal_tester = val => regex.test(''+val) 28 | } 29 | 30 | // support strings, arrays, or objects as key 31 | const external_tester = point => { 32 | const value = u.deep(point, key) 33 | if (a.type(value)() == 'array') { 34 | return value.some(subkey => internal_tester(subkey)) 35 | } else if (a.type(value)() == 'object') { 36 | return Object.keys(value).some(subkey => internal_tester(subkey)) 37 | } else { 38 | return internal_tester(value) 39 | } 40 | } 41 | 42 | // negation happens at the end 43 | if (neg) { 44 | return point => !external_tester(point) 45 | } 46 | return external_tester 47 | } 48 | 49 | const comparators = { 50 | '~': similar 51 | // TODO: extension point for other operators... 52 | } 53 | const symbols = Object.keys(comparators) 54 | 55 | const simple = (exp, name, units) => { 56 | 57 | let keys = ['meta.name', 'meta.tags'] 58 | let op = '~' 59 | let value 60 | const parts = exp.split(/\s+/g) 61 | 62 | // full case 63 | if (symbols.includes(parts[1])) { 64 | keys = parts[0].split(',') 65 | op = parts[1] 66 | value = parts.slice(2).join(' ') 67 | 68 | // middle case, just an operator spec, default "keys" 69 | } else if (symbols.includes(parts[0])) { 70 | op = parts[0] 71 | value = parts.slice(1).join(' ') 72 | 73 | // basic case, only "value" 74 | } else { 75 | value = exp 76 | } 77 | 78 | return point => keys.some(key => comparators[op](key, value, name, units)(point)) 79 | } 80 | 81 | const complex = (config, name, units, aggregator=_or) => { 82 | 83 | // we branch by type 84 | const type = a.type(config)(units) 85 | switch(type) { 86 | 87 | // boolean --> either all or nothing 88 | case 'boolean': 89 | return config ? _true : _false 90 | 91 | // string --> base case, meaning a simple/single filter 92 | case 'string': 93 | return simple(config, name, units) 94 | 95 | // array --> aggregated simple filters with alternating and/or conditions 96 | case 'array': 97 | const alternate = aggregator == _and ? _or : _and 98 | return aggregator(config.map(elem => complex(elem, name, units, alternate))) 99 | 100 | default: 101 | throw new Error(`Unexpected type "${type}" found at filter "${name}"!`) 102 | } 103 | } 104 | 105 | const contains_object = (val) => { 106 | if (a.type(val)() == 'object') return true 107 | if (a.type(val)() == 'array') return val.some(el => contains_object(el)) 108 | return false 109 | } 110 | 111 | exports.parse = (config, name, points={}, units={}, include_mirrors=false) => { 112 | 113 | let result = [] 114 | 115 | // if a filter decl is undefined, it's just the default point at [0, 0] 116 | if (config === undefined) { 117 | result.push(new Point()) 118 | 119 | // if a filter decl is an object, or an array that contains an object at any depth, it is an anchor 120 | } else if (contains_object(config)) { 121 | result.push(anchor(config, name, points)(units)) 122 | if (include_mirrors) { 123 | // this is strict: if the ref of the anchor doesn't have a mirror pair, it will error out 124 | result.push(anchor(config, name, points, undefined, true)(units)) 125 | } 126 | 127 | // otherwise, it is treated as a condition to filter all available points 128 | } else { 129 | result = Object.values(points).filter(complex(config, name, units)) 130 | if (include_mirrors) { 131 | // this is permissive: we only include mirrored versions if they exist, and don't fuss if they don't 132 | result = result.concat(result.map(p => points[anchor_lib.mirror(p.meta.name)]).filter(p => !!p)) 133 | } 134 | } 135 | 136 | return result 137 | } -------------------------------------------------------------------------------- /src/anchor.js: -------------------------------------------------------------------------------- 1 | const u = require('./utils') 2 | const a = require('./assert') 3 | const Point = require('./point') 4 | 5 | const mirror_ref = exports.mirror = (ref, mirror=true) => { 6 | if (mirror) { 7 | if (ref.startsWith('mirror_')) { 8 | return ref.substring(7) 9 | } 10 | return 'mirror_' + ref 11 | } 12 | return ref 13 | } 14 | 15 | const aggregator_common = ['parts', 'method'] 16 | 17 | const aggregators = { 18 | average: (config, name, parts) => { 19 | a.unexpected(config, name, aggregator_common) 20 | let x = 0, y = 0, r = 0 21 | const len = parts.length 22 | for (const part of parts) { 23 | x += part.x 24 | y += part.y 25 | r += part.r 26 | } 27 | return new Point(x / len, y / len, r / len) 28 | } 29 | } 30 | 31 | const anchor = exports.parse = (raw, name, points={}, default_point=new Point(), mirror=false) => units => { 32 | 33 | // 34 | // Anchor type handling 35 | // 36 | 37 | if (a.type(raw)() == 'string') { 38 | raw = {ref: raw} 39 | } 40 | 41 | else if (a.type(raw)() == 'array') { 42 | // recursive call with incremental default_point mods, according to `affect`s 43 | let current = default_point.clone() 44 | let index = 1 45 | for (const step of raw) { 46 | current = anchor(step, `${name}[${index++}]`, points, current, mirror)(units) 47 | } 48 | return current 49 | } 50 | 51 | a.unexpected(raw, name, ['ref', 'aggregate', 'orient', 'shift', 'rotate', 'affect']) 52 | 53 | // 54 | // Reference or aggregate handling 55 | // 56 | 57 | let point = default_point.clone() 58 | if (raw.ref !== undefined && raw.aggregate !== undefined) { 59 | throw new Error(`Fields "ref" and "aggregate" cannot appear together in anchor "${name}"!`) 60 | } 61 | 62 | if (raw.ref !== undefined) { 63 | // base case, resolve directly 64 | if (a.type(raw.ref)() == 'string') { 65 | const parsed_ref = mirror_ref(raw.ref, mirror) 66 | a.assert(points[parsed_ref], `Unknown point reference "${parsed_ref}" in anchor "${name}"!`) 67 | point = points[parsed_ref].clone() 68 | // recursive case 69 | } else { 70 | point = anchor(raw.ref, `${name}.ref`, points, default_point, mirror)(units) 71 | } 72 | 73 | } 74 | 75 | if (raw.aggregate !== undefined) { 76 | raw.aggregate = a.sane(raw.aggregate, `${name}.aggregate`, 'object')() 77 | raw.aggregate.method = a.sane(raw.aggregate.method || 'average', `${name}.aggregate.method`, 'string')() 78 | a.assert(aggregators[raw.aggregate.method], `Unknown aggregator method "${raw.aggregate.method}" in anchor "${name}"!`) 79 | raw.aggregate.parts = a.sane(raw.aggregate.parts || [], `${name}.aggregate.parts`, 'array')() 80 | 81 | const parts = [] 82 | let index = 1 83 | for (const part of raw.aggregate.parts) { 84 | parts.push(anchor(part, `${name}.aggregate.parts[${index++}]`, points, default_point, mirror)(units)) 85 | } 86 | 87 | point = aggregators[raw.aggregate.method](raw.aggregate, `${name}.aggregate`, parts) 88 | } 89 | 90 | // 91 | // Actual orient/shift/rotate/affect handling 92 | // 93 | 94 | const rotator = (config, name, point) => { 95 | // simple case: number gets added to point rotation 96 | if (a.type(config)(units) == 'number') { 97 | let angle = a.sane(config, name, 'number')(units) 98 | if (point.meta.mirrored) { 99 | angle = -angle 100 | } 101 | point.r += angle 102 | // recursive case: points turns "towards" target anchor 103 | } else { 104 | const target = anchor(config, name, points, default_point, mirror)(units) 105 | point.r = point.angle(target) 106 | } 107 | } 108 | 109 | if (raw.orient !== undefined) { 110 | rotator(raw.orient, `${name}.orient`, point) 111 | } 112 | if (raw.shift !== undefined) { 113 | let xyval = a.wh(raw.shift, `${name}.shift`)(units) 114 | if (point.meta.mirrored) { 115 | xyval[0] = -xyval[0] 116 | } 117 | point.shift(xyval, true) 118 | } 119 | if (raw.rotate !== undefined) { 120 | rotator(raw.rotate, `${name}.rotate`, point) 121 | } 122 | if (raw.affect !== undefined) { 123 | const candidate = point.clone() 124 | point = default_point.clone() 125 | point.meta = candidate.meta 126 | let affect = raw.affect 127 | if (a.type(affect)() == 'string') affect = affect.split('') 128 | affect = a.strarr(affect, `${name}.affect`) 129 | let i = 0 130 | for (const aff of affect) { 131 | a.in(aff, `${name}.affect[${++i}]`, ['x', 'y', 'r']) 132 | point[aff] = candidate[aff] 133 | } 134 | } 135 | 136 | return point 137 | } -------------------------------------------------------------------------------- /test/pcbs/mock_footprints___pcbs.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "\n \n(kicad_pcb (version 20171130) (host pcbnew 5.1.6)\n\n (page A3)\n (title_block\n (title main)\n (rev v1.0.0)\n (company Unknown)\n )\n\n (general\n (thickness 1.6)\n )\n\n (layers\n (0 F.Cu signal)\n (31 B.Cu signal)\n (32 B.Adhes user)\n (33 F.Adhes user)\n (34 B.Paste user)\n (35 F.Paste user)\n (36 B.SilkS user)\n (37 F.SilkS user)\n (38 B.Mask user)\n (39 F.Mask user)\n (40 Dwgs.User user)\n (41 Cmts.User user)\n (42 Eco1.User user)\n (43 Eco2.User user)\n (44 Edge.Cuts user)\n (45 Margin user)\n (46 B.CrtYd user)\n (47 F.CrtYd user)\n (48 B.Fab user)\n (49 F.Fab user)\n )\n\n (setup\n (last_trace_width 0.25)\n (trace_clearance 0.2)\n (zone_clearance 0.508)\n (zone_45_only no)\n (trace_min 0.2)\n (via_size 0.8)\n (via_drill 0.4)\n (via_min_size 0.4)\n (via_min_drill 0.3)\n (uvia_size 0.3)\n (uvia_drill 0.1)\n (uvias_allowed no)\n (uvia_min_size 0.2)\n (uvia_min_drill 0.1)\n (edge_width 0.05)\n (segment_width 0.2)\n (pcb_text_width 0.3)\n (pcb_text_size 1.5 1.5)\n (mod_edge_width 0.12)\n (mod_text_size 1 1)\n (mod_text_width 0.15)\n (pad_size 1.524 1.524)\n (pad_drill 0.762)\n (pad_to_mask_clearance 0.05)\n (aux_axis_origin 0 0)\n (visible_elements FFFFFF7F)\n (pcbplotparams\n (layerselection 0x010fc_ffffffff)\n (usegerberextensions false)\n (usegerberattributes true)\n (usegerberadvancedattributes true)\n (creategerberjobfile true)\n (excludeedgelayer true)\n (linewidth 0.100000)\n (plotframeref false)\n (viasonmask false)\n (mode 1)\n (useauxorigin false)\n (hpglpennumber 1)\n (hpglpenspeed 20)\n (hpglpendiameter 15.000000)\n (psnegative false)\n (psa4output false)\n (plotreference true)\n (plotvalue true)\n (plotinvisibletext false)\n (padsonsilk false)\n (subtractmaskfromsilk false)\n (outputformat 1)\n (mirror false)\n (drillshape 1)\n (scaleselection 1)\n (outputdirectory \"\"))\n )\n\n (net 0 \"\")\n(net 1 \"P1\")\n(net 2 \"T3_1\")\n(net 3 \"T3_2\")\n(net 4 \"T3_3\")\n \n (net_class Default \"This is the default net class.\"\n (clearance 0.2)\n (trace_width 0.25)\n (via_dia 0.8)\n (via_drill 0.4)\n (uvia_dia 0.3)\n (uvia_drill 0.1)\n (add_net \"\")\n(add_net \"P1\")\n(add_net \"T3_1\")\n(add_net \"T3_2\")\n(add_net \"T3_3\")\n )\n\n \n\n (module trace_test (layer F.Cu) (tedit 5CF31DEF)\n\n (at 1 -1 30)\n\n (pad 1 smd rect (at 0 0 30) (size 1 1) (layers F.Cu F.Paste F.Mask)\n (net 1 \"P1\") (solder_mask_margin 0.2))\n\n (pad 2 smd rect (at 5 5 30) (size 1 1) (layers F.Cu F.Paste F.Mask)\n (net 1 \"P1\") (solder_mask_margin 0.2))\n\n )\n\n (segment (start 1 -1) (end 7.830127 0.8301270000000001) (width 0.25) (layer F.Cu) (net 1))\n\n \n\n\n (module zone_test (layer F.Cu) (tedit 5CF31DEF)\n\n (at 1 -1 30)\n\n (pad 1 smd rect (at 0 0 30) (size 1 1) (layers F.Cu F.Paste F.Mask)\n (net 1 \"P1\") (solder_mask_margin 0.2))\n\n (pad 2 smd rect (at 5 5 30) (size 1 1) (layers F.Cu F.Paste F.Mask)\n (net 1 \"P1\") (solder_mask_margin 0.2))\n\n )\n\n (zone (net 1) (net_name P1) (layer F.Cu) (tstamp 0) (hatch full 0.508)\n (connect_pads (clearance 0.508))\n (min_thickness 0.254)\n (fill yes (arc_segments 32) (thermal_gap 0.508) (thermal_bridge_width 0.508))\n (polygon (pts (xy 7.830127 0.8301270000000001) (xy 2.830127 -7.830127) (xy -5.830127 -2.830127) (xy -0.8301270000000001 5.830127)))\n )\n\n \n \n\n (module dynamic_net_test (layer F.Cu) (tedit 5CF31DEF)\n\n (at 0 0 0)\n\n (pad 1 smd rect (at 0 0 0) (size 1 1) (layers F.Cu F.Paste F.Mask)\n (net 2 \"T3_1\") (solder_mask_margin 0.2))\n\n (pad 1 smd rect (at 0 0 0) (size 1 1) (layers F.Cu F.Paste F.Mask)\n (net 3 \"T3_2\") (solder_mask_margin 0.2))\n\n (pad 1 smd rect (at 0 0 0) (size 1 1) (layers F.Cu F.Paste F.Mask)\n (net 4 \"T3_3\") (solder_mask_margin 0.2))\n\n )\n\n \n \n\n (module anchor_test (layer F.Cu) (tedit 5CF31DEF)\n\n (at 0 0 0)\n\n (fp_line (start 0 0) (end 10 -10) (layer Dwgs.User) (width 0.05))\n\n )\n\n \n (gr_line (start -9.5 9.5) (end 9.5 9.5) (angle 90) (layer Edge.Cuts) (width 0.15))\n(gr_line (start 9.5 9.5) (end 9.5 -9.5) (angle 90) (layer Edge.Cuts) (width 0.15))\n(gr_line (start 9.5 -9.5) (end -9.5 -9.5) (angle 90) (layer Edge.Cuts) (width 0.15))\n(gr_line (start -9.5 -9.5) (end -9.5 9.5) (angle 90) (layer Edge.Cuts) (width 0.15))\n \n)\n\n " 3 | } -------------------------------------------------------------------------------- /roadmap.md: -------------------------------------------------------------------------------- 1 | # Ergogen TODO 2 | 3 | 4 | 5 | ## CLI 6 | 7 | ### Major 8 | 9 | - Key-level access to full anchors 10 | - this could provide extra variables `padding`, `spread`, `splay` for custom layout purposes 11 | - make row anchors cumulative, too (like columns), so fingers arcs and other edits can happen 12 | - Restructure pcb point/footprint filtering 13 | - Use the same `what`/`where` infrastructure as outlines 14 | - Collapse params/nets/anchors into a single hierarchy from the user's POV 15 | - Add per-footprint mirror support 16 | - Add some way for footprints to be able to "resist" the mirroring-related special treatment of negative X shift, rotation, etc. 17 | - Merge, generalize, uniform-ize and externalize footprints! 18 | - onnx-like incremental opset versioning 19 | - Template for creating them, built-in variables they can use, documentation, external links, etc. 20 | - Add access to whole set of points + filtering logic, so they can implement their own connection logic as well maybe (see daisy chaining) 21 | - Also considering how (or, on which layer) they define their silks, universal mirroring behaviour, etc. 22 | - Rename class to designator in this context (https://en.wikipedia.org/wiki/Reference_designator#Designators) 23 | 24 | ### Minor 25 | 26 | - Include raw kicad footprint integrations 27 | - pull torik's script to be able to convert raw kicad footprints into positionable ergogen ones 28 | - have a `dummy` footprint which can just be updated from schematic 29 | - Allow footprints to publish outlines 30 | - Make these usable in the `outlines` section through a new `what` 31 | - 3D orient for cases 32 | - Post-process anchor for global (post-mirror!) orient/shift/rotate for everything 33 | - Even more extreme anchor stuff 34 | - Checkpoints, intersects, distances, weighted combinations? 35 | - Allow both object (as well as arrays) in multiple anchor refs 36 | - SVG input (for individual outlines, or even combinations parsed by line color, etc.) 37 | - And once that's done, possibly even STL or other input for cases or pcb renders 38 | - Support text silk output to PCBs (in configurable fonts, through SVG?) 39 | - Maybe a partial markdown preprocess to support bold and italic? 40 | - Look into gr_curve to possibly add beziers to the kicad conversion 41 | - Support curves (arcs as well as Béziers) in polygons 42 | - Add snappable line footprint 43 | - Figure out a manual, but still reasonably comfortable routing method directly from the config 44 | - Add filleting syntax with `@`? 45 | - Eeschema support for pcbs 46 | - Generate ZMK shield from config 47 | - Export **to** KLE? 48 | - Include 3D models paths in kicad output for visualization 49 | - Also, provide 3D models for built-in footprints 50 | - Look into kicad 5 vs. 6 output format 51 | - Update json schema and add syntax highlight to editors 52 | - Support different netclasses 53 | - `round`, `pointy` and `beveled` symbolic constants for expand joint types 54 | - also, string shorthands like `3)`, `5>` and `10]` 55 | 56 | 57 | ### Patch 58 | 59 | - YAML lib v4 update - breaking changes in how undefined is handled! 60 | - Prevent double mirroring (see discord "mirror_mirror_") 61 | - Check unexpected keys at top level, too 62 | - Better error handling for the fillet option? 63 | - Integration and end2end tests to get coverage to 100% 64 | - Add custom fillet implementation that considers line-line connections only? 65 | - Add nicer filleting error messages when makerjs dies for some reason 66 | - Empty nets should be allowed (to mean unconnected) 67 | 68 | 69 | ## WEBUI 70 | 71 | ### Major 72 | 73 | - Change over to Cache's live preview implementation 74 | - Add missing KLE functionality 75 | - Create browserified version of semver lib 76 | - Or at least a shim with a console warning 77 | - Visualizing multiple outlines at once, with different colors 78 | 79 | ### Minor 80 | 81 | - Propagate log output from the ergogen module 82 | - Attempt to auto-compile (if inactive for n secs, or whatever) 83 | - Support saving to gists 84 | - Add kicad_pcb visualization as well 85 | - Get dropdown examples from a separate repo 86 | - Expand the config dropdown with opensource stuff: corne, lily, ergodox, atreus... 87 | 88 | ### Patch 89 | 90 | - Streamline (and document) an update pipeline 91 | - Add puppeteer tests 92 | 93 | 94 | 95 | ## DOCS 96 | 97 | - n00b tutorials 98 | - With a progression of increasingly complex steps 99 | - And lots of illustrations! 100 | - Complete reference 101 | - Some known deficiencies: 102 | - Units separated to their own block at the front 103 | - Key-level `width` and `height` are supported during visualization 104 | - This key-level example should probably be added from discord: https://discord.com/channels/714176584269168732/759825860617437204/773104093546676244 105 | - Change outline fields to have their full anchor support documented 106 | - Mention the ability to opt out of gluing! 107 | - Key-level defaults are based around u's, not 19! 108 | - Contribution guidelines 109 | - Include test commands (npm test, npm run coverage, --what switch, --dump switch) 110 | - Changelog, Roadmap 111 | - A public catalog of real-life ergogen configs 112 | - Probably could be the same as the separate examples repo for the dropdown 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | --------------------------------------------------------------------------------