1
0

Refactor, units, tests

This commit is contained in:
Bán Dénes 2021-01-01 00:50:04 +01:00
parent 519c34bc60
commit cd90705ba1
24 changed files with 1221 additions and 1222 deletions

55
package-lock.json generated
View File

@ -604,6 +604,11 @@
"integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
"dev": true
},
"complex.js": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.0.11.tgz",
"integrity": "sha512-6IArJLApNtdg1P1dFtn3dnyzoZBEF0MwMnrfF1exSBRpZYoy4yieMkpZhQDC0uwctw48vii0CFVyHfpgZ/DfGw=="
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -644,6 +649,11 @@
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
},
"decimal.js": {
"version": "10.2.1",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.2.1.tgz",
"integrity": "sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw=="
},
"deep-eql": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz",
@ -739,6 +749,11 @@
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
"dev": true
},
"escape-latex": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz",
"integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw=="
},
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
@ -804,6 +819,11 @@
"signal-exit": "^3.0.2"
}
},
"fraction.js": {
"version": "4.0.13",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.0.13.tgz",
"integrity": "sha512-E1fz2Xs9ltlUp+qbiyx9wmt2n9dRzPsS11Jtdb8D2o+cC7wr9xkkKsVKJuBX0ST+LVS+LhLO+SbLJNtfWcJvXA=="
},
"fromentries": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.2.0.tgz",
@ -1226,6 +1246,11 @@
"iterate-iterator": "^1.0.1"
}
},
"javascript-natural-sort": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
"integrity": "sha1-+eIwPUUH9tdDVac2ZNFED7Wg71k="
},
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -1344,6 +1369,21 @@
"kdbush": "^2.0.1"
}
},
"mathjs": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/mathjs/-/mathjs-8.1.1.tgz",
"integrity": "sha512-b3TX3EgiZObujjwb8lZnTDLUuivC2jar4ZBjmGJ4stFYCDXx/DNwx5yry5t/z65p9mvejyZel1qoeR05KtChcQ==",
"requires": {
"complex.js": "^2.0.11",
"decimal.js": "^10.2.1",
"escape-latex": "^1.2.0",
"fraction.js": "^4.0.13",
"javascript-natural-sort": "^0.7.1",
"seedrandom": "^3.0.5",
"tiny-emitter": "^2.1.0",
"typed-function": "^2.0.0"
}
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
@ -1867,6 +1907,11 @@
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true
},
"seedrandom": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz",
"integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg=="
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
@ -2017,6 +2062,11 @@
"minimatch": "^3.0.4"
}
},
"tiny-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
},
"to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
@ -2044,6 +2094,11 @@
"integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
"dev": true
},
"typed-function": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/typed-function/-/typed-function-2.0.0.tgz",
"integrity": "sha512-Hhy1Iwo/e4AtLZNK10ewVVcP2UEs408DS35ubP825w/YgSBK1KVLwALvvIG4yX75QJrxjCpcWkzkVRB0BwwYlA=="
},
"typedarray-to-buffer": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",

View File

@ -11,18 +11,20 @@
"bin": "./src/cli.js",
"scripts": {
"build": "rollup -c",
"test": "nyc --reporter=html --reporter=text mocha -r chai/register-should"
"test": "nyc --reporter=html --reporter=text mocha -r chai/register-should test/unit/*.js test/complex/index.js"
},
"dependencies": {
"fs-extra": "^9.0.1",
"js-yaml": "^3.14.0",
"makerjs": "github:mrzealot/maker.js-dist",
"mathjs": "^8.1.1",
"yargs": "^15.4.1"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^13.0.2",
"@rollup/plugin-replace": "^2.3.3",
"chai": "^4.2.0",
"glob": "^7.1.6",
"mocha": "^8.1.3",
"nyc": "^15.1.0",
"rollup": "^2.22.1"
@ -33,7 +35,8 @@
"src/*.js"
],
"exclude": [
"src/cli.js"
"src/cli.js",
"src/io.js"
]
}
}

61
src/anchor.js Normal file
View File

@ -0,0 +1,61 @@
const u = require('./utils')
const a = require('./assert')
const Point = require('./point')
const anchor = module.exports = (raw, name, points={}, check_unexpected=true, default_point=new Point()) => units => {
if (a.type(raw) == 'array') {
// recursive call with incremental default_point mods, according to `affect`s
let current = () => default_point.clone()
for (const step of raw) {
current = anchor(step, name, points, check_unexpected, current(units))
}
return current
}
if (check_unexpected) a.detect_unexpected(raw, name, ['ref', 'orient', 'shift', 'rotate', 'affect'])
let point = default_point.clone()
if (raw.ref !== undefined) {
if (a.type(raw.ref) == 'array') {
// averaging multiple anchors
let x = 0, y = 0, r = 0
const len = raw.ref.length
for (const ref of raw.ref) {
a.assert(points[ref], `Unknown point reference "${ref}" in anchor "${name}"!`)
const resolved = points[ref]
x += resolved.x
y += resolved.y
r += resolved.r
}
point = new Point(x / len, y / len, r / len)
} else {
a.assert(points[raw.ref], `Unknown point reference "${raw.ref}" in anchor "${name}"!`)
point = points[raw.ref].clone()
}
}
if (raw.orient !== undefined) {
point.r += a.sane(raw.orient || 0, `${name}.orient`, 'number')(units)
}
if (raw.shift !== undefined) {
let xyval = a.wh(raw.shift || [0, 0], `${name}.shift`)(units)
if (point.meta.mirrored) {
xyval[0] = -xyval[0]
}
point.shift(xyval, true)
}
if (raw.rotate !== undefined) {
point.r += a.sane(raw.rotate || 0, `${name}.rotate`, 'number')(units)
}
if (raw.affect !== undefined) {
const candidate = point
point = default_point.clone()
const valid_affects = ['x', 'y', 'r']
let affect = raw.affect || valid_affects
if (a.type(affect) == 'string') affect = affect.split('')
affect = a.strarr(affect, `${name}.affect`)
let i = 0
for (const a of affect) {
a._in(a, `${name}.affect[${++i}]`, valid_affects)
point[a] = candidate[a]
}
}
return point
}

View File

@ -1,6 +1,11 @@
const m = require('makerjs')
const u = require('./utils')
const Point = require('./point')
const mathjs = require('mathjs')
const mathnum = exports.mathnum = raw => units => {
return mathjs.evaluate(`${raw}`, units || {})
}
const assert = exports.assert = (exp, msg) => {
if (!exp) {
@ -8,19 +13,24 @@ const assert = exports.assert = (exp, msg) => {
}
}
const type = exports.type = (val) => {
const type = exports.type = val => units => {
if (Array.isArray(val)) return 'array'
if (val === null) return 'null'
try {
const num = mathnum(val)(units)
if (typeof num === 'number') return 'number'
} catch (err) {}
return typeof val
}
const sane = exports.sane = (val, name, _type) => {
assert(type(val) == _type, `Field "${name}" should be of type ${_type}!`)
const sane = exports.sane = (val, name, _type) => units => {
assert(type(val)(units) == _type, `Field "${name}" should be of type ${_type}!`)
if (_type == 'number') return mathnum(val)(units)
return val
}
const detect_unexpected = exports.detect_unexpected = (obj, name, expected) => {
const sane_obj = sane(obj, name, 'object')
const sane_obj = sane(obj, name, 'object')()
for (const key of Object.keys(sane_obj)) {
assert(expected.includes(key), `Unexpected key "${key}" within field "${name}"!`)
}
@ -31,155 +41,29 @@ const _in = exports.in = (raw, name, arr) => {
return raw
}
const arr = exports.arr = (raw, name, length, _type, _default) => {
assert(type(raw) == 'array', `Field "${name}" should be an array!`)
const arr = exports.arr = (raw, name, length, _type, _default) => units => {
assert(type(raw)(units) == 'array', `Field "${name}" should be an array!`)
assert(length == 0 || raw.length == length, `Field "${name}" should be an array of length ${length}!`)
raw = raw.map(val => val || _default)
raw.map(val => assert(type(val) == _type, `Field "${name}" should contain ${_type}s!`))
raw.map(val => assert(type(val)(units) == _type, `Field "${name}" should contain ${_type}s!`))
if (_type == 'number') {
raw = raw.map(val => mathnum(val)(units))
}
return raw
}
const numarr = exports.numarr = (raw, name, length) => arr(raw, name, length, 'number', 0)
const strarr = exports.strarr = (raw, name) => arr(raw, name, 0, 'string', '')
const numarr = exports.numarr = (raw, name, length) => units => arr(raw, name, length, 'number', 0)(units)
const strarr = exports.strarr = (raw, name) => arr(raw, name, 0, 'string', '')()
const xy = exports.xy = (raw, name) => numarr(raw, name, 2)
const xy = exports.xy = (raw, name) => units => numarr(raw, name, 2)(units)
const wh = exports.wh = (raw, name) => {
const wh = exports.wh = (raw, name) => units => {
if (!Array.isArray(raw)) raw = [raw, raw]
return xy(raw, name)
return xy(raw, name)(units)
}
exports.trbl = (raw, name) => {
exports.trbl = (raw, name) => units => {
if (!Array.isArray(raw)) raw = [raw, raw, raw, raw]
if (raw.length == 2) raw = [raw[1], raw[0], raw[1], raw[0]]
return numarr(raw, name, 4)
}
const anchor = exports.anchor = (raw, name, points={}, check_unexpected=true, default_point=new Point()) => {
if (type(raw) == 'array') {
// recursive call with incremental default_point mods, according to `affect`s
let current = default_point.clone()
for (const step of raw) {
current = anchor(step, name, points, check_unexpected, current)
}
return current
}
if (check_unexpected) detect_unexpected(raw, name, ['ref', 'orient', 'shift', 'rotate', 'affect'])
let point = default_point.clone()
if (raw.ref !== undefined) {
if (type(raw.ref) == 'array') {
// averaging multiple anchors
let x = 0, y = 0, r = 0
const len = raw.ref.length
for (const ref of raw.ref) {
assert(points[ref], `Unknown point reference "${ref}" in anchor "${name}"!`)
const resolved = points[ref]
x += resolved.x
y += resolved.y
r += resolved.r
}
point = new Point(x / len, y / len, r / len)
} else {
assert(points[raw.ref], `Unknown point reference "${raw.ref}" in anchor "${name}"!`)
point = points[raw.ref].clone()
}
}
if (raw.orient !== undefined) {
point.r += sane(raw.orient || 0, `${name}.orient`, 'number')
}
if (raw.shift !== undefined) {
let xyval = wh(raw.shift || [0, 0], `${name}.shift`)
if (point.meta.mirrored) {
xyval[0] = -xyval[0]
}
point.shift(xyval, true)
}
if (raw.rotate !== undefined) {
point.r += sane(raw.rotate || 0, `${name}.rotate`, 'number')
}
if (raw.affect !== undefined) {
const candidate = point
point = default_point.clone()
const valid_affects = ['x', 'y', 'r']
let affect = raw.affect || valid_affects
if (type(affect) == 'string') affect = affect.split('')
affect = strarr(affect, `${name}.affect`)
let i = 0
for (const a of affect) {
_in(a, `${name}.affect[${++i}]`, valid_affects)
point[a] = candidate[a]
}
}
return point
}
const extend_pair = exports.extend_pair = (to, from) => {
const to_type = type(to)
const from_type = type(from)
if (from === undefined || from === null) return to
if (from === '!!unset') return undefined
if (to_type != from_type) return from
if (from_type == 'object') {
const res = u.deepcopy(to)
for (const key of Object.keys(from)) {
res[key] = extend_pair(to[key], from[key])
if (res[key] === undefined) delete res[key]
}
return res
} else if (from_type == 'array') {
const res = u.deepcopy(to)
for (const [i, val] of from.entries()) {
res[i] = extend_pair(res[i], val)
}
return res
} else return from
}
const extend = exports.extend = (...args) => {
let res = args[0]
for (const arg of args) {
if (res == arg) continue
res = extend_pair(res, arg)
}
return res
}
const inherit = exports.inherit = (name_prefix, name, set) => {
let result = u.deepcopy(set[name])
if (result.extends !== undefined) {
let candidates = [name]
const list = []
while (candidates.length) {
const item = candidates.shift()
const other = u.deepcopy(set[item])
assert(other, `"${item}" (reached from "${name_prefix}.${name}.extends") does not name a valid target!`)
let parents = other.extends || []
if (type(parents) !== 'array') parents = [parents]
candidates = candidates.concat(parents)
delete other.extends
list.unshift(other)
}
result = extend.apply(this, list)
}
return result
}
const op_prefix = exports.op_prefix = str => {
const suffix = str.slice(1)
if (str.startsWith('+')) return {name: suffix, operation: 'add'}
if (str.startsWith('-')) return {name: suffix, operation: 'subtract'}
if (str.startsWith('~')) return {name: suffix, operation: 'intersect'}
if (str.startsWith('^')) return {name: suffix, operation: 'stack'}
return {name: str, operation: 'add'}
}
exports.op_str = (str, choices={}, order=Object.keys(choices)) => {
let res = op_prefix(str)
for (const key of order) {
if (choices[key].includes(res.name)) {
res.type = key
break
}
}
return res
return numarr(raw, name, 4, 'number', 0)(units)
}

View File

@ -9,12 +9,8 @@ const yargs = require('yargs')
// internals
const u = require('./utils')
const io = require('./io')
const points_lib = require('./points')
const outlines_lib = require('./outlines')
const pcbs_lib = require('./pcbs')
const cases_lib = require('./cases')
const ergogen = require('./ergogen')
// command line args
@ -47,6 +43,8 @@ const args = yargs
if (args.clean) fs.removeSync(args.o)
fs.mkdirpSync(args.o)
// config parsing
let config_text
try {
config_text = fs.readFileSync(args.c).toString()
@ -62,45 +60,38 @@ try {
} catch (err) {
throw new Error(`Malformed input within "${args.c}": ${err}`)
}
config = u.expand_nested_keys(config)
// points
// processing
const results = ergogen.process(config, args.debug, s => console.log(s))
// output
console.log('Writing output to disk...')
console.log('Parsing points...')
const points = points_lib.parse(config.points)
if (args.debug) {
const points_demo = points_lib.visualize(points)
io.dump_model(points_demo, path.join(args.o, 'points/points_demo'), args.debug)
fs.writeJSONSync(path.join(args.o, 'points/points.json'), points, {spaces: 4})
io.dump_model(results.points.demo, path.join(args.o, 'points/demo'), args.debug)
fs.writeJSONSync(path.join(args.o, 'points/data.json'), results.points.data, {spaces: 4})
}
// outlines
console.log('Generating outlines...')
const outlines = outlines_lib.parse(config.outlines || {}, points)
for (const [name, outline] of Object.entries(outlines)) {
if (!args.debug && name.startsWith('_')) continue
for (const [name, outline] of Object.entries(results.outlines)) {
io.dump_model(outline, path.join(args.o, `outlines/${name}`), args.debug)
}
// pcbs
// for (const [name, pcb] of Object.entries(results.pcbs)) {
// const file = path.join(args.o, `pcbs/${name}.kicad_pcb`)
// fs.mkdirpSync(path.dirname(file))
// fs.writeFileSync(file, pcb)
// }
console.log('Scaffolding PCBs...')
const pcbs = pcbs_lib.parse(config.pcbs || {}, points, outlines)
for (const [pcb_name, pcb_text] of Object.entries(pcbs)) {
const pcb_file = path.join(args.o, `pcbs/${pcb_name}.kicad_pcb`)
fs.mkdirpSync(path.dirname(pcb_file))
fs.writeFileSync(pcb_file, pcb_text)
}
// for (const [name, _case] of Object.entries(results.cases)) {
// const file = path.join(args.o, `cases/${name}.jscad`)
// fs.mkdirpSync(path.dirname(file))
// fs.writeFileSync(file, _case)
// }
// cases
console.log('Extruding cases...')
const cases = cases_lib.parse(config.cases || {}, outlines)
for (const [case_name, case_text] of Object.entries(cases)) {
const case_file = path.join(args.o, `cases/${case_name}.jscad`)
fs.mkdirpSync(path.dirname(case_file))
fs.writeFileSync(case_file, case_text)
if (args.debug) {
fs.writeJSONSync(path.join(args.o, 'results.json'), results, {spaces: 4})
}
// goodbye

View File

@ -1,8 +1,54 @@
const prepare = require('./prepare')
const points_lib = require('./points')
const outlines_lib = require('./outlines')
const cases_lib = require('./cases')
const pcbs_lib = require('./pcbs')
const noop = () => {}
module.exports = {
utils: require('./utils'),
points: require('./points'),
outlines: require('./outlines'),
cases: require('./cases'),
pcbs: require('./pcbs'),
version: '__ergogen_version'
version: '__ergogen_version',
process: (config, debug=false, logger=noop) => {
logger('Preparing input...')
config = prepare.unnest(config)
config = prepare.inherit(config)
const results = {}
logger('Parsing points...')
const [points, units] = points_lib.parse(config.points)
if (debug) {
results.points = {
demo: points_lib.visualize(points),
data: points,
units: units
}
}
logger('Generating outlines...')
const outlines = outlines_lib.parse(config.outlines || {}, points, units)
results.outlines = {}
for (const [name, outline] of Object.entries(outlines)) {
if (!debug && name.startsWith('_')) continue
results.outlines[name] = outline
}
// logger('Extruding cases...')
// const cases = cases_lib.parse(config.cases || {}, outlines, units)
// results.cases = {}
// for (const [case_name, case_text] of Object.entries(cases)) {
// if (!debug && case_name.startsWith('_')) continue
// results.cases[case_name] = case_text
// }
// logger('Scaffolding PCBs...')
// const pcbs = pcbs_lib.parse(config.pcbs || {}, points, outlines, units)
// results.pcbs = {}
// for (const [pcb_name, pcb_text] of Object.entries(pcbs)) {
// if (!debug && pcb_name.startsWith('_')) continue
// results.cases[pcb_name] = pcb_text
// }
return results
}
}

19
src/operation.js Normal file
View File

@ -0,0 +1,19 @@
const op_prefix = exports.op_prefix = str => {
const suffix = str.slice(1)
if (str.startsWith('+')) return {name: suffix, operation: 'add'}
if (str.startsWith('-')) return {name: suffix, operation: 'subtract'}
if (str.startsWith('~')) return {name: suffix, operation: 'intersect'}
if (str.startsWith('^')) return {name: suffix, operation: 'stack'}
return {name: str, operation: 'add'}
}
exports.operation = (str, choices={}, order=Object.keys(choices)) => {
let res = op_prefix(str)
for (const key of order) {
if (choices[key].includes(res.name)) {
res.type = key
break
}
}
return res
}

View File

@ -1,7 +1,10 @@
const m = require('makerjs')
const u = require('./utils')
const a = require('./assert')
const o = require('./operation')
const Point = require('./point')
const prep = require('./prepare')
const make_anchor = require('./anchor')
const rectangle = (w, h, corner, bevel, name='') => {
const error = (dim, val) => `Rectangle for "${name}" isn't ${dim} enough for its corner and bevel (${val} - 2 * ${corner} - 2 * ${bevel} <= 0)!`
@ -17,44 +20,29 @@ const rectangle = (w, h, corner, bevel, name='') => {
return m.model.moveRelative(res, [corner + bevel, corner + bevel])
}
const relative_anchor = (decl, name, points={}, check_unexpected=true, default_point=new Point()) => {
decl.shift = a.wh(decl.shift || [0, 0], name + '.shift')
const relative = a.sane(decl.relative === undefined ? true : decl.relative, `${name}.relative`, 'boolean')
delete decl.relative
if (relative) {
return size => {
const copy = u.deepcopy(decl)
copy.shift = [copy.shift[0] * size[0], copy.shift[1] * size[1]]
return a.anchor(copy, name, points, check_unexpected, default_point)
}
}
return () => a.anchor(decl, name, points, check_unexpected, default_point)
}
const layout = exports._layout = (config = {}, points = {}) => {
const layout = exports._layout = (config = {}, points = {}, units = {}) => {
// Glue config sanitization
const parsed_glue = u.deepcopy(a.sane(config, 'outlines.glue', 'object'))
const parsed_glue = u.deepcopy(a.sane(config, 'outlines.glue', 'object')())
for (let [gkey, gval] of Object.entries(parsed_glue)) {
gval = a.inherit('outlines.glue', gkey, config)
a.detect_unexpected(gval, `outlines.glue.${gkey}`, ['top', 'bottom', 'waypoints', 'extra'])
for (const y of ['top', 'bottom']) {
a.detect_unexpected(gval[y], `outlines.glue.${gkey}.${y}`, ['left', 'right'])
gval[y].left = relative_anchor(gval[y].left, `outlines.glue.${gkey}.${y}.left`, points)
if (a.type(gval[y].right) != 'number') {
gval[y].right = relative_anchor(gval[y].right, `outlines.glue.${gkey}.${y}.right`, points)
gval[y].left = make_anchor(gval[y].left, `outlines.glue.${gkey}.${y}.left`, points)
if (a.type(gval[y].right)(units) != 'number') {
gval[y].right = make_anchor(gval[y].right, `outlines.glue.${gkey}.${y}.right`, points)
}
}
gval.waypoints = a.sane(gval.waypoints || [], `outlines.glue.${gkey}.waypoints`, 'array')
gval.waypoints = a.sane(gval.waypoints || [], `outlines.glue.${gkey}.waypoints`, 'array')(units)
let wi = 0
gval.waypoints = gval.waypoints.map(w => {
const name = `outlines.glue.${gkey}.waypoints[${++wi}]`
a.detect_unexpected(w, name, ['percent', 'width'])
w.percent = a.sane(w.percent, name + '.percent', 'number')
w.width = a.wh(w.width, name + '.width')
w.percent = a.sane(w.percent, name + '.percent', 'number')(units)
w.width = a.wh(w.width, name + '.width')(units)
return w
})
@ -69,12 +57,19 @@ const layout = exports._layout = (config = {}, points = {}) => {
// Layout params sanitization
a.detect_unexpected(params, `${export_name}`, expected.concat(['side', 'tags', 'glue', 'size', 'corner', 'bevel', 'bound']))
const size = a.wh(params.size, `${export_name}.size`)(units)
const relative_units = prep.extend({
sx: size[0],
sy: size[1]
}, units)
const side = a.in(params.side, `${export_name}.side`, ['left', 'right', 'middle', 'both', 'glue'])
const tags = a.sane(params.tags || [], `${export_name}.tags`, 'array')
const size = a.wh(params.size, `${export_name}.size`)
const corner = a.sane(params.corner || 0, `${export_name}.corner`, 'number')
const bevel = a.sane(params.bevel || 0, `${export_name}.bevel`, 'number')
const bound = a.sane(params.bound === undefined ? true : params.bound, `${export_name}.bound`, 'boolean')
const tags = a.sane(params.tags || [], `${export_name}.tags`, 'array')()
const corner = a.sane(params.corner || 0, `${export_name}.corner`, 'number')(relative_units)
const bevel = a.sane(params.bevel || 0, `${export_name}.bevel`, 'number')(relative_units)
const bound = a.sane(params.bound === undefined ? true : params.bound, `${export_name}.bound`, 'boolean')()
// Actual layout
@ -100,7 +95,7 @@ const layout = exports._layout = (config = {}, points = {}) => {
// extra binding "material", if necessary
if (bound) {
let bind = a.trbl(p.meta.bind || 0, `${pname}.bind`)
let bind = a.trbl(p.meta.bind || 0, `${pname}.bind`)(relative_units)
// if it's a mirrored key, we swap the left and right bind values
if (p.meta.mirrored) {
bind = [bind[0], bind[3], bind[2], bind[1]]
@ -133,18 +128,17 @@ const layout = exports._layout = (config = {}, points = {}) => {
if (bound && ['middle', 'both', 'glue'].includes(side)) {
const default_glue_name = Object.keys(parsed_glue)[0]
const computed_glue_name = a.sane(params.glue || default_glue_name, `${export_name}.glue`, 'string')
const computed_glue_name = a.sane(params.glue || default_glue_name, `${export_name}.glue`, 'string')()
const glue_def = parsed_glue[computed_glue_name]
a.assert(glue_def, `Field "${export_name}.glue" does not name a valid glue!`)
const get_line = (anchor) => {
if (a.type(anchor) == 'number') {
if (a.type(anchor)(relative_units) == 'number') {
return u.line([anchor, -1000], [anchor, 1000])
}
// if it wasn't a number, then it's a function returning an achor
// have to feed it `size` first in case it's relative
const from = anchor(size).clone()
// if it wasn't a number, then it's a (possibly relative) achor
const from = anchor(relative_units).clone()
const to = from.clone().shift([from.meta.mirrored ? -1 : 1, 0])
return u.line(from.p, to.p)
@ -182,7 +176,7 @@ const layout = exports._layout = (config = {}, points = {}) => {
}
let waypoints
const is_split = a.type(glue_def.top.right) == 'number'
const is_split = a.type(glue_def.top.right)(relative_units) == 'number'
if (is_split) {
waypoints = [tip, tlp]
.concat(left_waypoints)
@ -210,24 +204,23 @@ const layout = exports._layout = (config = {}, points = {}) => {
}
}
exports.parse = (config = {}, points = {}) => {
exports.parse = (config = {}, points = {}, units = {}) => {
a.detect_unexpected(config, 'outline', ['glue', 'exports'])
const layout_fn = layout(config.glue, points)
const layout_fn = layout(config.glue, points, units)
const outlines = {}
const ex = a.sane(config.exports || {}, 'outlines.exports', 'object')
const ex = a.sane(config.exports || {}, 'outlines.exports', 'object')()
for (let [key, parts] of Object.entries(ex)) {
parts = a.inherit('outlines.exports', key, ex)
if (a.type(parts) == 'array') {
if (a.type(parts)() == 'array') {
parts = {...parts}
}
parts = a.sane(parts, `outlines.exports.${key}`, 'object')
parts = a.sane(parts, `outlines.exports.${key}`, 'object')()
let result = {models: {}}
for (let [part_name, part] of Object.entries(parts)) {
const name = `outlines.exports.${key}.${part_name}`
if (a.type(part) == 'string') {
part = a.op_str(part, {outline: Object.keys(outlines)})
if (a.type(part)() == 'string') {
part = o.operation(part, {outline: Object.keys(outlines)})
}
const expected = ['type', 'operation']
part.type = a.in(part.type || 'outline', `${name}.type`, ['keys', 'rectangle', 'circle', 'polygon', 'outline'])
@ -246,45 +239,49 @@ exports.parse = (config = {}, points = {}) => {
break
case 'rectangle':
a.detect_unexpected(part, name, expected.concat(['ref', 'shift', 'rotate', 'size', 'corner', 'bevel', 'mirror']))
anchor = a.anchor(part, name, points, false)
const size = a.wh(part.size, `${name}.size`)
const corner = a.sane(part.corner || 0, `${name}.corner`, 'number')
const bevel = a.sane(part.bevel || 0, `${name}.bevel`, 'number')
const rect_mirror = a.sane(part.mirror || false, `${name}.mirror`, 'boolean')
const size = a.wh(part.size, `${name}.size`)(units)
const rec_units = prep.extend({
sx: size[0],
sy: size[1]
}, units)
anchor = a.anchor(part, name, points, false)(rec_units)
const corner = a.sane(part.corner || 0, `${name}.corner`, 'number')(rec_units)
const bevel = a.sane(part.bevel || 0, `${name}.bevel`, 'number')(rec_units)
const rect_mirror = a.sane(part.mirror || false, `${name}.mirror`, 'boolean')()
const rect = rectangle(size[0], size[1], corner, bevel, name)
arg = anchor.position(u.deepcopy(rect))
if (rect_mirror) {
const mirror_part = u.deepcopy(part)
a.assert(mirror_part.ref, `Field "${name}.ref" must be speficied if mirroring is required!`)
mirror_part.ref = `mirror_${mirror_part.ref}`
anchor = a.anchor(mirror_part, name, points, false)
anchor = make_anchor(mirror_part, name, points, false)(rec_units)
const mirror_rect = m.model.moveRelative(u.deepcopy(rect), [-size[0], 0])
arg = u.union(arg, anchor.position(mirror_rect))
}
break
case 'circle':
a.detect_unexpected(part, name, expected.concat(['ref', 'shift', 'rotate', 'radius', 'mirror']))
anchor = a.anchor(part, name, points, false)
const radius = a.sane(part.radius, `${name}.radius`, 'number')
const circle_mirror = a.sane(part.mirror || false, `${name}.mirror`, 'boolean')
anchor = make_anchor(part, name, points, false)(units)
const radius = a.sane(part.radius, `${name}.radius`, 'number')(units)
const circle_mirror = a.sane(part.mirror || false, `${name}.mirror`, 'boolean')()
arg = u.circle(anchor.p, radius)
if (circle_mirror) {
const mirror_part = u.deepcopy(part)
a.assert(mirror_part.ref, `Field "${name}.ref" must be speficied if mirroring is required!`)
mirror_part.ref = `mirror_${mirror_part.ref}`
anchor = a.anchor(mirror_part, name, points, false)
anchor = make_anchor(mirror_part, name, points, false)(units)
arg = u.union(arg, u.circle(anchor.p, radius))
}
break
case 'polygon':
a.detect_unexpected(part, name, expected.concat(['points']))
const poly_points = a.sane(part.points, `${name}.points`, 'array')
const poly_points = a.sane(part.points, `${name}.points`, 'array')()
const parsed_points = []
let last_anchor = new Point()
let poly_index = 0
for (const poly_point of poly_points) {
const poly_name = `${name}.points[${++poly_index}]`
const anchor = a.anchor(poly_point, poly_name, points, true, last_anchor)
const anchor = make_anchor(poly_point, poly_name, points, true, last_anchor)(units)
parsed_points.push(anchor.p)
}
arg = u.poly(parsed_points)

View File

@ -1,6 +1,8 @@
const m = require('makerjs')
const u = require('./utils')
const a = require('./assert')
const prep = require('./prepare')
const make_anchor = require('./anchor')
const push_rotation = exports._push_rotation = (list, angle, origin) => {
let candidate = origin
@ -13,18 +15,18 @@ const push_rotation = exports._push_rotation = (list, angle, origin) => {
})
}
const render_zone = exports._render_zone = (zone_name, zone, anchor, global_key) => {
const render_zone = exports._render_zone = (zone_name, zone, anchor, global_key, units) => {
// zone-wide sanitization
a.detect_unexpected(zone, `points.zones.${zone_name}`, ['columns', 'rows', 'key'])
// the anchor comes from "above", because it needs other zones too (for references)
const cols = a.sane(zone.columns || {}, `points.zones.${zone_name}.columns`, 'object')
const zone_wide_rows = a.sane(zone.rows || {}, `points.zones.${zone_name}.rows`, 'object')
const cols = a.sane(zone.columns || {}, `points.zones.${zone_name}.columns`, 'object')()
const zone_wide_rows = a.sane(zone.rows || {}, `points.zones.${zone_name}.rows`, 'object')()
for (const [key, val] of Object.entries(zone_wide_rows)) {
zone_wide_rows[key] = a.sane(val || {}, `points.zones.${zone_name}.rows.${key}`, 'object')
zone_wide_rows[key] = a.sane(val || {}, `points.zones.${zone_name}.rows.${key}`, 'object')()
}
const zone_wide_key = a.sane(zone.key || {}, `points.zones.${zone_name}.key`, 'object')
const zone_wide_key = a.sane(zone.key || {}, `points.zones.${zone_name}.key`, 'object')()
// algorithm prep
@ -54,47 +56,47 @@ const render_zone = exports._render_zone = (zone_name, zone, anchor, global_key)
col.stagger || 0,
`points.zones.${zone_name}.columns.${col_name}.stagger`,
'number'
)
)(units)
col.spread = a.sane(
col.spread || (first_col ? 0 : 19),
col.spread || (first_col ? 0 : 'u'),
`points.zones.${zone_name}.columns.${col_name}.spread`,
'number'
)
)(units)
col.rotate = a.sane(
col.rotate || 0,
`points.zones.${zone_name}.columns.${col_name}.rotate`,
'number'
)
)(units)
col.origin = a.xy(
col.origin || [0, 0],
`points.zones.${zone_name}.columns.${col_name}.origin`,
)
`points.zones.${zone_name}.columns.${col_name}.origin`
)(units)
let override = false
col.rows = a.sane(
col.rows || {},
`points.zones.${zone_name}.columns.${col_name}.rows`,
'object'
)
)()
if (col.row_overrides) {
override = true
col.rows = a.sane(
col.row_overrides,
`points.zones.${zone_name}.columns.${col_name}.row_overrides`,
'object'
)
)()
}
for (const [key, val] of Object.entries(col.rows)) {
col.rows[key] = a.sane(
val || {},
`points.zones.${zone_name}.columns.${col_name}.rows.${key}`,
'object'
)
)()
}
col.key = a.sane(
col.key || {},
`points.zones.${zone_name}.columns.${col_name}.key`,
'object'
)
)()
// propagating object key to name field
@ -104,7 +106,7 @@ const render_zone = exports._render_zone = (zone_name, zone, anchor, global_key)
// (while also handling potential overrides)
const actual_rows = override ? Object.keys(col.rows)
: Object.keys(a.extend(zone_wide_rows, col.rows))
: Object.keys(prep.extend(zone_wide_rows, col.rows))
if (!actual_rows.length) {
actual_rows.push('default')
}
@ -134,14 +136,14 @@ const render_zone = exports._render_zone = (zone_name, zone, anchor, global_key)
const default_key = {
shift: [0, 0],
rotate: 0,
padding: 19,
padding: 'u',
width: 1,
height: 1,
skip: false,
asym: 'both'
}
for (const row of actual_rows) {
const key = a.extend(
const key = prep.extend(
default_key,
global_key,
zone_wide_key,
@ -152,12 +154,12 @@ const render_zone = exports._render_zone = (zone_name, zone, anchor, global_key)
key.name = key.name || `${zone_name}_${col_name}_${row}`
key.colrow = `${col_name}_${row}`
key.shift = a.xy(key.shift, `${key.name}.shift`)
key.rotate = a.sane(key.rotate, `${key.name}.rotate`, 'number')
key.width = a.sane(key.width, `${key.name}.width`, 'number')
key.height = a.sane(key.height, `${key.name}.height`, 'number')
key.padding = a.sane(key.padding, `${key.name}.padding`, 'number')
key.skip = a.sane(key.skip, `${key.name}.skip`, 'boolean')
key.shift = a.xy(key.shift, `${key.name}.shift`)(units)
key.rotate = a.sane(key.rotate, `${key.name}.rotate`, 'number')(units)
key.width = a.sane(key.width, `${key.name}.width`, 'number')(units)
key.height = a.sane(key.height, `${key.name}.height`, 'number')(units)
key.padding = a.sane(key.padding, `${key.name}.padding`, 'number')(units)
key.skip = a.sane(key.skip, `${key.name}.skip`, 'boolean')()
key.asym = a.in(key.asym, `${key.name}.asym`, ['left', 'right', 'both'])
key.col = col
key.row = row
@ -184,12 +186,12 @@ const render_zone = exports._render_zone = (zone_name, zone, anchor, global_key)
return points
}
const parse_axis = exports._parse_axis = (config, name, points) => {
if (!['number', 'undefined'].includes(a.type(config))) {
const mirror_obj = a.sane(config || {}, name, 'object')
const distance = a.sane(mirror_obj.distance || 0, `${name}.distance`, 'number')
const parse_axis = exports._parse_axis = (config, name, points, units) => {
if (!['number', 'undefined'].includes(a.type(config)(units))) {
const mirror_obj = a.sane(config || {}, name, 'object')()
const distance = a.sane(mirror_obj.distance || 0, `${name}.distance`, 'number')(units)
delete mirror_obj.distance
let axis = a.anchor(mirror_obj, name, points).x
let axis = make_anchor(mirror_obj, name, points)(units).x
axis += distance / 2
return axis
} else return config
@ -201,7 +203,7 @@ const perform_mirror = exports._perform_mirror = (point, axis) => {
if (point.meta.asym == 'left') return ['', null]
const mp = point.clone().mirror(axis)
const mirrored_name = `mirror_${point.meta.name}`
mp.meta = a.extend(mp.meta, mp.meta.mirror || {})
mp.meta = prep.extend(mp.meta, mp.meta.mirror || {})
mp.meta.name = mirrored_name
mp.meta.colrow = `mirror_${mp.meta.colrow}`
mp.meta.mirrored = true
@ -215,32 +217,41 @@ const perform_mirror = exports._perform_mirror = (point, axis) => {
exports.parse = (config = {}) => {
// parsing units
const raw_units = prep.extend({
u: 19,
cx: 18,
cy: 17
}, a.sane(config.units || {}, 'points.units', 'object')())
const units = {}
for (const [key, val] of Object.entries(raw_units)) {
units[key] = a.mathnum(val)(units)
}
// config sanitization
a.detect_unexpected(config, 'points', ['zones', 'key', 'rotate', 'mirror'])
const zones = a.sane(config.zones || {}, 'points.zones', 'object')
const global_key = a.sane(config.key || {}, 'points.key', 'object')
const global_rotate = a.sane(config.rotate || 0, 'points.rotate', 'number')
a.detect_unexpected(config, 'points', ['units', 'zones', 'key', 'rotate', 'mirror'])
const zones = a.sane(config.zones || {}, 'points.zones', 'object')()
const global_key = a.sane(config.key || {}, 'points.key', 'object')()
const global_rotate = a.sane(config.rotate || 0, 'points.rotate', 'number')(units)
const global_mirror = config.mirror
let points = {}
let mirrored_points = {}
let all_points = {}
// rendering zones
for (let [zone_name, zone] of Object.entries(zones)) {
// handle zone-level `extends` clauses
zone = a.inherit('points.zones', zone_name, zones)
// extracting keys that are handled here, not at the zone render level
const anchor = a.anchor(zone.anchor || {}, `points.zones.${zone_name}.anchor`, all_points)
const rotate = a.sane(zone.rotate || 0, `points.zones.${zone_name}.rotate`, 'number')
const anchor = make_anchor(zone.anchor || {}, `points.zones.${zone_name}.anchor`, all_points)(units)
const rotate = a.sane(zone.rotate || 0, `points.zones.${zone_name}.rotate`, 'number')(units)
const mirror = zone.mirror
delete zone.anchor
delete zone.rotate
delete zone.mirror
// creating new points
const new_points = render_zone(zone_name, zone, anchor, global_key)
const new_points = render_zone(zone_name, zone, anchor, global_key, units)
// adjusting new points
for (const [new_name, new_point] of Object.entries(new_points)) {
@ -261,7 +272,7 @@ exports.parse = (config = {}) => {
all_points = Object.assign(all_points, points)
// per-zone mirroring for the new keys
const axis = parse_axis(mirror, `points.zones.${zone_name}.mirror`, all_points)
const axis = parse_axis(mirror, `points.zones.${zone_name}.mirror`, all_points, units)
if (axis) {
for (const new_point of Object.values(new_points)) {
const [mname, mp] = perform_mirror(new_point, axis)
@ -284,7 +295,7 @@ exports.parse = (config = {}) => {
}
// global mirroring for points that haven't been mirrored yet
const global_axis = parse_axis(global_mirror, `points.mirror`, points)
const global_axis = parse_axis(global_mirror, `points.mirror`, points, units)
const global_mirrored_points = {}
for (const point of Object.values(points)) {
if (global_axis && point.mirrored === undefined) {
@ -306,7 +317,7 @@ exports.parse = (config = {}) => {
}
// done
return filtered
return [filtered, units]
}
exports.visualize = (points) => {

71
src/prepare.js Normal file
View File

@ -0,0 +1,71 @@
const u = require('./utils')
const a = require('./assert')
const unnest = exports.unnest = (config) => {
if (a.type(config)() !== 'object') return config
const result = {}
for (const [key, val] of Object.entries(config)) {
u.deep(result, key, unnest(val))
}
return result
}
const _extend = exports._extend = (to, from) => {
const to_type = a.type(to)()
const from_type = a.type(from)()
if (from === undefined || from === null) return to
if (from === '!!unset') return undefined
if (to_type != from_type) return from
if (from_type == 'object') {
const res = u.deepcopy(to)
for (const key of Object.keys(from)) {
res[key] = _extend(to[key], from[key])
if (res[key] === undefined) delete res[key]
}
return res
} else if (from_type == 'array') {
const res = u.deepcopy(to)
for (const [i, val] of from.entries()) {
res[i] = _extend(res[i], val)
}
return res
} else return from
}
const extend = exports.extend = (...args) => {
let res = args[0]
for (const arg of args) {
if (res == arg) continue
res = _extend(res, arg)
}
return res
}
const _inherit = exports._inherit = (config, root, breadcrumbs) => {
if (a.type(config)() !== 'object') return config
const result = {}
for (const [key, val] of Object.entries(config)) {
breadcrumbs.push(key)
let newval = _inherit(val, root, breadcrumbs)
if (newval && newval.extends !== undefined) {
let candidates = [newval.extends]
const list = [newval]
while (candidates.length) {
const path = candidates.shift()
const other = u.deepcopy(u.deep(root, path))
a.assert(other, `"${path}" (reached from "${breadcrumbs.join('.')}.${key}.extends") does not name a valid target!`)
let parents = other.extends || []
if (a.type(parents)() !== 'array') parents = [parents]
candidates = candidates.concat(parents)
list.unshift(other)
}
newval = extend.apply(this, list)
delete newval.extends
}
result[key] = newval
breadcrumbs.pop()
}
return result
}
const inherit = exports.inherit = (config) => _inherit(config, config, [])

View File

@ -2,7 +2,7 @@ const m = require('makerjs')
exports.deepcopy = (value) => JSON.parse(JSON.stringify(value))
const deep_assign = exports.deep_assign = (obj, key, val) => {
const deep = exports.deep = (obj, key, val) => {
const levels = key.split('.')
const last = levels.pop()
let step = obj
@ -10,21 +10,11 @@ const deep_assign = exports.deep_assign = (obj, key, val) => {
step[level] = step[level] || {}
step = step[level]
}
if (val === undefined) return step[last]
step[last] = val
return obj
}
const expand_nested_keys = exports.expand_nested_keys = (config) => {
if (typeof config == 'object') {
const result = {}
for (const [key, val] of Object.entries(config)) {
deep_assign(result, key, expand_nested_keys(val))
}
return result
}
return config
}
const eq = exports.eq = (a=[], b=[]) => {
return a[0] === b[0] && a[1] === b[1]
}

View File

@ -1,23 +0,0 @@
const fs = require('fs-extra')
const path = require('path')
const yaml = require('js-yaml')
const points_lib = require('../src/points')
const fixtures = path.join(__dirname, 'fixtures')
const absolem_config = yaml.load(fs.readFileSync(path.join(fixtures, 'absolem.yaml')).toString())
describe('Absolem', function() {
it('#points', function() {
const expected = fs.readJSONSync(path.join(fixtures, 'absolem_points.json'))
const actual = points_lib.parse(absolem_config.points)
// remove metadata, so that it only checks the points
Object.values(actual).map(val => delete val.meta)
// only check points in the "main" zones
for (const key of Object.keys(actual)) {
if (!expected[key]) {
delete actual[key]
}
}
actual.should.deep.equal(expected)
})
})

28
test/complex/index.js Normal file
View File

@ -0,0 +1,28 @@
const fs = require('fs-extra')
const path = require('path')
const yaml = require('js-yaml')
const glob = require('glob')
const u = require('../../src/utils')
const ergogen = require('../../src/ergogen')
const cap = s => s.charAt(0).toUpperCase() + s.slice(1)
for (const part of ['points', 'outlines', 'cases', 'pcbs']) {
describe(cap(part), function() {
const dir = path.join(__dirname, part)
for (const input_path of glob.sync(path.join(dir, '*.yaml'))) {
const basename = path.basename(input_path, '.yaml')
const title = basename.split('_').join(' ')
it(title, function() {
const dirname = path.dirname(input_path)
const input = yaml.load(fs.readFileSync(input_path).toString())
const actual = ergogen.process(input, true)
for (const expected_path of glob.sync(path.join(dirname, basename + '___*'))) {
const expected = JSON.parse(fs.readFileSync(expected_path).toString())
const comp_path = expected_path.split('___')[1].split('.')[0].split('_').join('.')
u.deep(actual, comp_path).should.deep.equal(expected)
}
})
}
})
}

View File

@ -0,0 +1,24 @@
points:
zones:
matrix:
columns:
left.key.bind: [,10,,]
right.key.bind: [,,,10]
rows:
bottom.key.bind: [10,,,]
top.key.bind: [,,10,]
key:
bind: [0, 0, 0, 0]
outlines:
exports:
outline:
main:
type: keys
side: left
size: 20
min:
type: keys
side: left
bound: false
size: 14
operation: subtract

View File

@ -0,0 +1,506 @@
{
"outline": {
"models": {
"a": {
"models": {
"a": {
"models": {},
"origin": [
0,
0
]
},
"b": {
"models": {
"a": {
"models": {
"a": {
"models": {
"a": {
"models": {
"a": {
"models": {},
"origin": [
0,
0
]
},
"b": {
"models": {
"a": {
"models": {
"a": {
"paths": {
"ShapeLine1": {
"type": "line",
"origin": [
-10,
-10
],
"end": [
29,
-10
]
},
"ShapeLine4": {
"type": "line",
"origin": [
-10,
-10
],
"end": [
-10,
29
]
}
},
"origin": [
0,
0
]
},
"b": {
"paths": {},
"origin": [
0,
0
]
}
},
"origin": [
0,
0
]
},
"b": {
"paths": {},
"origin": [
0,
0
]
}
},
"origin": [
0,
0
]
}
},
"origin": [
0,
0
]
},
"b": {
"models": {
"a": {
"models": {
"a": {
"paths": {
"ShapeLine3": {
"type": "line",
"origin": [
-10,
29
],
"end": [
29,
29
]
}
},
"origin": [
0,
0
]
},
"b": {
"paths": {},
"origin": [
0,
0
]
}
},
"origin": [
0,
0
]
},
"b": {
"paths": {},
"origin": [
0,
0
]
}
},
"origin": [
0,
0
]
}
},
"origin": [
0,
0
]
},
"b": {
"models": {
"a": {
"models": {
"a": {
"paths": {
"ShapeLine2": {
"type": "line",
"origin": [
29,
-10
],
"end": [
29,
29
]
}
},
"origin": [
0,
0
]
},
"b": {
"paths": {},
"origin": [
0,
0
]
}
},
"origin": [
0,
0
]
},
"b": {
"paths": {},
"origin": [
0,
0
]
}
},
"origin": [
0,
0
]
}
},
"origin": [
0,
0
]
},
"b": {
"models": {
"a": {
"models": {
"a": {
"paths": {},
"origin": [
0,
0
]
},
"b": {
"paths": {},
"origin": [
0,
0
]
}
},
"origin": [
0,
0
]
},
"b": {
"paths": {},
"origin": [
0,
0
]
}
},
"origin": [
0,
0
]
}
},
"origin": [
0,
0
]
}
},
"origin": [
0,
0
]
},
"b": {
"models": {
"a": {
"models": {
"a": {
"models": {
"a": {
"models": {
"a": {
"models": {},
"origin": [
0,
0
]
},
"b": {
"paths": {
"ShapeLine1": {
"type": "line",
"origin": [
-7,
-7
],
"end": [
7,
-7
]
},
"ShapeLine2": {
"type": "line",
"origin": [
7,
-7
],
"end": [
7,
7
]
},
"ShapeLine3": {
"type": "line",
"origin": [
7,
7
],
"end": [
-7,
7
]
},
"ShapeLine4": {
"type": "line",
"origin": [
-7,
7
],
"end": [
-7,
-7
]
}
},
"origin": [
0,
0
]
}
},
"origin": [
0,
0
]
},
"b": {
"paths": {
"ShapeLine1": {
"type": "line",
"origin": [
-7,
12
],
"end": [
7,
12
]
},
"ShapeLine2": {
"type": "line",
"origin": [
7,
12
],
"end": [
7,
26
]
},
"ShapeLine3": {
"type": "line",
"origin": [
7,
26
],
"end": [
-7,
26
]
},
"ShapeLine4": {
"type": "line",
"origin": [
-7,
26
],
"end": [
-7,
12
]
}
},
"origin": [
0,
0
]
}
},
"origin": [
0,
0
]
},
"b": {
"paths": {
"ShapeLine1": {
"type": "line",
"origin": [
12,
-7
],
"end": [
26,
-7
]
},
"ShapeLine2": {
"type": "line",
"origin": [
26,
-7
],
"end": [
26,
7
]
},
"ShapeLine3": {
"type": "line",
"origin": [
26,
7
],
"end": [
12,
7
]
},
"ShapeLine4": {
"type": "line",
"origin": [
12,
7
],
"end": [
12,
-7
]
}
},
"origin": [
0,
0
]
}
},
"origin": [
0,
0
]
},
"b": {
"paths": {
"ShapeLine1": {
"type": "line",
"origin": [
12,
12
],
"end": [
26,
12
]
},
"ShapeLine2": {
"type": "line",
"origin": [
26,
12
],
"end": [
26,
26
]
},
"ShapeLine3": {
"type": "line",
"origin": [
26,
26
],
"end": [
12,
26
]
},
"ShapeLine4": {
"type": "line",
"origin": [
12,
26
],
"end": [
12,
12
]
}
},
"origin": [
0,
0
]
}
},
"origin": [
0,
0
]
}
},
"origin": [
0,
0
]
}
}

View File

@ -0,0 +1,44 @@
points:
zones:
matrix:
columns:
left.key.bind: [,10,,]
right.key.bind: [,,,10]
rows:
bottom.key.bind: [10,,,]
top.key.bind: [,,10,]
key:
bind: [0, 0, 0, 0]
rotate: -20
mirror:
ref: matrix_right_top
distance: 30
outlines:
glue:
default:
top:
left:
ref: matrix_right_top
shift: [,sy / 2]
right:
ref: mirror_matrix_right_top
shift: [,sy / 2]
bottom:
left:
ref: matrix_right_bottom
shift: [,sy / -2]
right:
ref: mirror_matrix_right_bottom
shift: [,sy / -2]
exports:
outline:
main:
type: keys
side: both
size: 20
min:
type: keys
side: both
bound: false
size: 14
operation: subtract

View File

@ -0,0 +1,9 @@
points:
zones:
matrix:
columns:
left:
right:
rows:
bottom:
top:

View File

@ -0,0 +1,130 @@
{
"matrix_left_bottom": {
"x": 0,
"y": 0,
"r": 0,
"meta": {
"shift": [
0,
0
],
"rotate": 0,
"padding": 19,
"width": 1,
"height": 1,
"skip": false,
"asym": "both",
"name": "matrix_left_bottom",
"colrow": "left_bottom",
"col": {
"stagger": 0,
"spread": 0,
"rotate": 0,
"origin": [
0,
0
],
"rows": {},
"key": {},
"name": "left"
},
"row": "bottom"
}
},
"matrix_left_top": {
"x": 0,
"y": 19,
"r": 0,
"meta": {
"shift": [
0,
0
],
"rotate": 0,
"padding": 19,
"width": 1,
"height": 1,
"skip": false,
"asym": "both",
"name": "matrix_left_top",
"colrow": "left_top",
"col": {
"stagger": 0,
"spread": 0,
"rotate": 0,
"origin": [
0,
0
],
"rows": {},
"key": {},
"name": "left"
},
"row": "top"
}
},
"matrix_right_bottom": {
"x": 19,
"y": 0,
"r": 0,
"meta": {
"shift": [
0,
0
],
"rotate": 0,
"padding": 19,
"width": 1,
"height": 1,
"skip": false,
"asym": "both",
"name": "matrix_right_bottom",
"colrow": "right_bottom",
"col": {
"stagger": 0,
"spread": 19,
"rotate": 0,
"origin": [
0,
0
],
"rows": {},
"key": {},
"name": "right"
},
"row": "bottom"
}
},
"matrix_right_top": {
"x": 19,
"y": 19,
"r": 0,
"meta": {
"shift": [
0,
0
],
"rotate": 0,
"padding": 19,
"width": 1,
"height": 1,
"skip": false,
"asym": "both",
"name": "matrix_right_top",
"colrow": "right_top",
"col": {
"stagger": 0,
"spread": 19,
"rotate": 0,
"origin": [
0,
0
],
"rows": {},
"key": {},
"name": "right"
},
"row": "top"
}
}
}

View File

@ -0,0 +1,13 @@
points:
zones:
matrix:
columns:
left:
right:
stagger: 5
spread: 25
rotate: 5
origin: [-9, -9]
rows:
bottom:
top:

View File

@ -1,707 +0,0 @@
points:
zones:
matrix:
anchor:
rotate: 5
columns:
pinky:
rotate: -5
origin: [7, -7]
rows:
bottom:
home:
bind: [,15,-1]
top:
bind: [,15,-1]
key:
column_net: P1
column_mark: P
ring:
stagger: 12
rows:
bottom:
bind: [,,,10]
home:
bind: [,10]
top:
bind: [,10]
key:
column_net: P0
column_mark: R
middle:
stagger: 5
rows:
bottom:
bind: [,10,,10]
home:
bind: [,10,,10]
top:
key:
column_net: P2
column_mark: M
index:
stagger: -6
rows:
bottom:
bind: [,10]
home:
bind: [,,,10]
top:
bind: [,,,10]
key:
column_net: P3
column_mark: X
inner:
stagger: -2
rows:
bottom:
bind: [,,10,]
home:
bind: [,,,10]
top:
bind: [,,,10]
key:
column_net: P4
column_mark: I
rows:
bottom:
bind: [10]
row_net: P16
row_mark: LB
mirror:
row_net: P7
row_mark: RB
home:
bind: [10]
row_net: P14
row_mark: LH
mirror:
row_net: P6
row_mark: RH
top:
row_net: P15
row_mark: LT
mirror:
row_net: P5
row_mark: RT
s19:
extends: matrix
columns:
pinky:
rows:
bottom:
footprints:
diode:
anchor:
shift: [,12]
home:
footprints:
diode:
anchor:
shift: [,6]
top:
footprints:
row_ext:
anchor:
shift: [, -10]
extra_column:
type: pad
anchor:
shift: [-4, 4]
nets:
net: 'P19'
params:
width: 2
height: 2
front: false
text: 'E'
align: right
key:
footprints:
row_ext:
type: pad
anchor:
shift: [-4, -4]
nets:
net: '!row_net'
params:
width: 2
height: 2
front: false
text: '!row_mark'
align: right
ring:
rows:
bottom:
footprints:
diode:
anchor:
shift: [,5]
home:
footprints:
diode:
anchor:
shift: [,2.5]
middle:
rows:
top:
footprints:
diode:
anchor:
shift: [,-6]
home:
footprints:
diode:
anchor:
shift: [,-3]
index:
rows:
bottom:
footprints:
diode:
anchor:
shift: [-8.25,6]
home:
footprints:
diode:
anchor:
shift: [-8.25,3]
top:
footprints:
diode:
anchor:
shift: [-8.25,]
rows:
top:
footprints:
mx:
anchor:
rotate: 180
alps:
anchor:
rotate: 180
choc:
anchor:
rotate: 180
col_ext:
type: pad
anchor:
shift: [4, 4]
nets:
net: '!column_net'
params:
width: 2
height: 2
front: false
text: '!column_mark'
bottom:
footprints:
diode:
anchor:
rotate: 270
col_ext:
type: pad
anchor:
shift: [4, -4]
nets:
net: '!column_net'
params:
width: 2
height: 2
front: false
text: '!column_mark'
key:
tags:
s19: true
footprints: &quad
mx:
type: mx
nets:
from: '!colrow'
to: '!column_net'
alps:
type: alps
nets:
from: '!colrow'
to: '!column_net'
<<: &choc
choc:
type: choc
nets:
from: '!colrow'
to: '!column_net'
diode:
type: diode
anchor:
rotate: 90
shift: [8.25, 0]
nets:
from: '!colrow'
to: '!row_net'
mirror:
footprints: &quad_mirror
mx:
nets:
from: '!column_net'
to: '!colrow'
alps:
nets:
from: '!column_net'
to: '!colrow'
<<: &choc_mirror
choc:
nets:
from: '!column_net'
to: '!colrow'
s18:
extends: matrix
columns:
pinky:
stagger: 1
origin: [7, -8]
rows:
top:
footprints:
choc:
anchor:
rotate: 180
key:
padding: 18
tags:
s18: true
footprints: *choc
mirror:
footprints: *choc_mirror
thumbfan:
anchor:
ref: matrix_inner_bottom
shift: [-7, -19]
columns:
near:
spread: 21.25
rotate: -28
origin: [9.5, -9]
rows:
thumb:
bind: [10,5,,]
column_net: P2
tags:
classic: true
footprints:
choc:
anchor:
rotate: 180
diode:
anchor:
shift: [8,]
row_ext:
type: pad
anchor:
shift: [-3, 9]
nets:
net: '!row_net'
params:
width: 2
height: 2
front: false
text: '!row_mark'
align: right
home:
spread: 21.25
rotate: -28
origin: [11.75, -9]
rows:
thumb:
bind: [,10,,15]
column_net: P3
tags:
classic: true
uniform: true
footprints:
diode:
anchor:
shift: [8,]
far:
rows:
thumb:
bind: [-1,,,5]
column_net: P4
tags:
classic: true
footprints:
choc:
anchor:
rotate: 180
diode:
anchor:
shift: [-4,]
rotate: 0
mirror:
footprints:
diode:
anchor:
rotate: 180
rows:
thumb:
row_net: P10
row_mark: LF
footprints:
diode:
anchor:
shift: [0, 9]
rotate: 180
mirror:
row_net: P8
row_mark: RF
footprints:
diode:
anchor:
rotate: 0
key:
footprints: *quad
mirror: *quad_mirror
unifar:
anchor:
ref: thumbfan_home_thumb
columns:
home_again:
rotate: -28
origin: [9.5, -9]
key:
skip: true
far:
rows:
thumb:
bind: [-1,,,5]
key:
column_net: P4
footprints:
choc:
anchor:
rotate: 180
diode: '!!unset'
mirror:
footprints:
diode: '!!unset'
tags:
uniform: true
rows:
thumb:
row_net: P10
mirror:
row_net: P8
key:
footprints: *quad
mirror: *quad_mirror
uninear:
anchor:
ref: thumbfan_home_thumb
columns:
home_again:
spread: -19
rotate: 28
origin: [-9.5, -9]
key:
skip: true
near:
rows:
thumb:
bind: [10,5,,]
key:
column_net: P2
footprints:
choc:
anchor:
rotate: 180
diode: '!!unset'
mirror:
footprints:
diode: '!!unset'
tags:
uniform: true
rows:
thumb:
row_net: P10
mirror:
row_net: P8
key:
footprints: *quad
mirror: *quad_mirror
key:
bind: [0,0,0,0]
rotate: -20
mirror:
ref: matrix_pinky_home
distance: 223.7529778
outlines:
glue:
classic_s19:
top:
left:
ref: matrix_inner_top
shift: [, 0.5]
right:
ref: mirror_matrix_inner_top
shift: [, 0.5]
bottom:
left:
ref: thumbfan_far_thumb
shift: [0.5, 0]
rotate: 90
right:
ref: mirror_thumbfan_far_thumb
shift: [0.5, 0]
rotate: 90
waypoints:
- percent: 50
width: 50
- percent: 90
width: 25
uniform_s19:
extends: classic_s19
bottom:
left:
ref: unifar_far_thumb
right:
ref: mirror_unifar_far_thumb
classic_s18:
extends: classic_s19
top:
left:
ref: s18_inner_top
right:
ref: mirror_s18_inner_top
uniform_s18:
extends:
- uniform_s19
- classic_s18
exports:
classic_s19_outline:
main:
type: keys
side: both
tags:
- s19
- classic
glue: classic_s19
size: 13.5
corner: .5
uniform_s19_outline:
extends: classic_s19_outline
main:
tags:
- s19
- uniform
glue: uniform_s19
uniform_s18_outline:
extends: uniform_s19_outline
main:
tags:
- s18
- uniform
glue: uniform_s18
mounting_holes:
ring_top:
type: circle
ref: matrix_ring_home
shift: [-10, 5]
radius: 2.25
mirror: true
ring_bottom:
type: circle
ref: matrix_ring_home
shift: [-9, -9]
radius: 2.25
mirror: true
operation: stack
index_top:
type: circle
ref: matrix_index_home
shift: [9.5, 9.5]
radius: 2.25
mirror: true
operation: stack
index_bottom:
type: circle
ref: matrix_index_home
shift: [9.5, -9.5]
radius: 2.25
mirror: true
operation: stack
thumb_near:
type: circle
ref: thumbfan_home_thumb
shift: [-12, 2]
radius: 2.25
mirror: true
operation: stack
thumb_far:
type: circle
ref: thumbfan_home_thumb
shift: [12, 2]
radius: 2.25
mirror: true
operation: stack
middle:
type: circle
ref: thumbfan_home_thumb
shift: [-8, 19]
radius: 2.25
mirror: true
operation: stack
intersected_outline:
one:
type: outline
name: classic_s19_outline
two:
type: outline
name: uniform_s18_outline
operation: intersect
controller_cutout:
type: rectangle
ref:
- s18_inner_top
- mirror_s18_inner_top
shift: [-10, -5]
size: [20, 10]
operation: subtract
mounting_holes:
type: outline
name: mounting_holes
operation: subtract
classic_s19_switches:
main:
type: keys
side: both
tags:
- classic
glue: classic_s19
size: 14
bound: false
uniform_s19_switches:
main:
type: keys
side: both
tags:
- uniform
glue: uniform_s19
size: 14
bound: false
pcb_middle:
raw:
type: keys
side: middle
tags:
- s19
- classic
glue: classic_s19
size: 24
helper1:
type: rectangle
size: [25, 5]
ref: thumbfan_home_thumb
shift: [0, 12]
mirror: true
helper2:
type: rectangle
size: [25, 5]
ref: thumbfan_far_thumb
shift: [-25, 12]
mirror: true
outer_bounds:
type: outline
name: classic_s19_outline
operation: intersect
pcbs:
main:
outlines:
edge:
outline: intersected_outline
layer: Edge.Cuts
middle:
outline: pcb_middle
layer: F.SilkS
footprints:
mcu:
type: promicro
anchor:
ref:
- s18_inner_top
- mirror_s18_inner_top
shift: [0, -23]
rotate: 270
slider:
type: slider
anchor:
ref:
- s18_inner_top
- mirror_s18_inner_top
shift: [0, -7.5]
nets:
from: RAWER
to: RAW
params:
side: B
reset:
type: reset
anchor:
ref:
- s18_inner_top
- mirror_s18_inner_top
shift: [0, -17]
rotate: 90
nets:
from: RST
to: GND
params:
side: B
extra_row_left:
type: pad
anchor:
ref:
- s18_inner_top
- mirror_s18_inner_top
shift: [-15, -10]
nets:
net: 'P20'
params:
width: 3
height: 3
front: false
text: 'LN'
align: right
extra_row_right:
type: pad
anchor:
ref:
- s18_inner_top
- mirror_s18_inner_top
shift: [15, -10]
nets:
net: 'P21'
params:
width: 3
height: 3
front: false
text: 'RN'
battery:
type: jstph
anchor:
ref:
- matrix_inner_bottom
- mirror_matrix_inner_bottom
shift: [0, -43]
rotate: 180
nets:
pos: RAWER
neg: GND
led:
type: rgb
anchor:
ref:
- matrix_inner_bottom
- mirror_matrix_inner_bottom
shift: [0, -48]
nets:
din: 'P9'
dout: ''

View File

@ -1,182 +0,0 @@
{
"matrix_pinky_bottom": {
"x": 0,
"y": 0,
"r": -15
},
"matrix_pinky_home": {
"x": 4.9175619,
"y": 18.3525907,
"r": -15
},
"matrix_pinky_top": {
"x": 9.8351237,
"y": 36.7051814,
"r": -15
},
"matrix_ring_bottom": {
"x": 22.7244416,
"y": 5.176704,
"r": -20
},
"matrix_ring_home": {
"x": 29.2228244,
"y": 23.0308638,
"r": -20
},
"matrix_ring_top": {
"x": 35.7212071,
"y": 40.8850235,
"r": -20
},
"matrix_middle_bottom": {
"x": 42.2887022,
"y": 3.3767843,
"r": -20
},
"matrix_middle_home": {
"x": 48.7870849,
"y": 21.2309442,
"r": -20
},
"matrix_middle_top": {
"x": 55.2854676,
"y": 39.0851039,
"r": -20
},
"matrix_index_bottom": {
"x": 58.0907411,
"y": -8.7597541,
"r": -20
},
"matrix_index_home": {
"x": 64.5891238,
"y": 9.0944057,
"r": -20
},
"matrix_index_top": {
"x": 71.0875065,
"y": 26.9485655,
"r": -20
},
"matrix_inner_bottom": {
"x": 75.2608606,
"y": -17.1375221,
"r": -20
},
"matrix_inner_home": {
"x": 81.7592433,
"y": 0.7166377,
"r": -20
},
"matrix_inner_top": {
"x": 88.257626,
"y": 18.5707976,
"r": -20
},
"thumbfan_near_thumb": {
"x": 62.1846295,
"y": -32.5975409,
"r": -20
},
"thumbfan_home_thumb": {
"x": 82.5841162,
"y": -47.013742,
"r": -48
},
"thumbfan_far_thumb": {
"x": 94.7890169,
"y": -68.8083815,
"r": -76
},
"mirror_matrix_pinky_bottom": {
"x": 233.5881016,
"y": 0,
"r": 15
},
"mirror_matrix_pinky_home": {
"x": 228.67053969999998,
"y": 18.3525907,
"r": 15
},
"mirror_matrix_pinky_top": {
"x": 223.7529779,
"y": 36.7051814,
"r": 15
},
"mirror_matrix_ring_bottom": {
"x": 210.86365999999998,
"y": 5.176704,
"r": 20
},
"mirror_matrix_ring_home": {
"x": 204.36527719999998,
"y": 23.0308638,
"r": 20
},
"mirror_matrix_ring_top": {
"x": 197.8668945,
"y": 40.8850235,
"r": 20
},
"mirror_matrix_middle_bottom": {
"x": 191.29939939999997,
"y": 3.3767843,
"r": 20
},
"mirror_matrix_middle_home": {
"x": 184.8010167,
"y": 21.2309442,
"r": 20
},
"mirror_matrix_middle_top": {
"x": 178.30263399999998,
"y": 39.0851039,
"r": 20
},
"mirror_matrix_index_bottom": {
"x": 175.49736049999998,
"y": -8.7597541,
"r": 20
},
"mirror_matrix_index_home": {
"x": 168.99897779999998,
"y": 9.0944057,
"r": 20
},
"mirror_matrix_index_top": {
"x": 162.5005951,
"y": 26.9485655,
"r": 20
},
"mirror_matrix_inner_bottom": {
"x": 158.327241,
"y": -17.1375221,
"r": 20
},
"mirror_matrix_inner_home": {
"x": 151.82885829999998,
"y": 0.7166377,
"r": 20
},
"mirror_matrix_inner_top": {
"x": 145.3304756,
"y": 18.5707976,
"r": 20
},
"mirror_thumbfan_near_thumb": {
"x": 171.4034721,
"y": -32.5975409,
"r": 20
},
"mirror_thumbfan_home_thumb": {
"x": 151.00398539999998,
"y": -47.013742,
"r": 48
},
"mirror_thumbfan_far_thumb": {
"x": 138.79908469999998,
"y": -68.8083815,
"r": 76
}
}

View File

@ -1,8 +0,0 @@
const Point = require('../src/point')
describe('Point', function() {
it('#constructor', function() {
const point = new Point(1, 2, 45)
point.p.should.deep.equal([1, 2])
})
})

44
test/unit/prepare.js Normal file
View File

@ -0,0 +1,44 @@
const p = require('../../src/prepare')
describe('Prepare', function() {
it('unnest', function() {
p.unnest({'a.b.c': 1}).should.deep.equal({a: {b: {c: 1}}})
p.unnest({'a.b.c': {
d: 2,
'e.f': 3
}}).should.deep.equal({a: {b: {c: {d: 2, e: {f: 3}}}}})
})
it('extend', function() {
p.extend('something', undefined).should.equal('something')
should.equal(p.extend('something', '!!unset'), undefined)
p.extend(undefined, 'something').should.equal('something')
p.extend(28, 'something').should.equal('something')
p.extend('something', 28).should.equal(28)
p.extend(27, 28).should.equal(28)
p.extend({a: 1, c: 1, d: 1}, {b: 2, c: 2, d: '!!unset'}).should.deep.equal({a: 1, b: 2, c: 2})
p.extend([3, 2, 1], [null, 4, 5]).should.deep.equal([3, 4, 5])
})
it('inherit', function() {
p.inherit({
a: {
x: 1,
y: 2
},
b: {
extends: 'a',
z: 3
},
c: {
extends: 'b',
w: 4
}
}).c.should.deep.equal({
x: 1,
y: 2,
z: 3,
w: 4
})
})
})

View File

@ -1,7 +0,0 @@
const u = require('../src/utils')
describe('Utils', function() {
it('deep_assign', function() {
u.deep_assign({}, 'a.b.c', 1).should.deep.equal({a: {b: {c: 1}}})
})
})