From 8d6be0ea00fd79a787077a40ebff4b876bd3262a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A1n=20D=C3=A9nes?= Date: Sat, 27 Jun 2020 20:13:06 +0200 Subject: [PATCH] Sanitization + better error reporting --- README.md | 11 +-- src/assert.js | 51 ++++++++++ src/cli.js | 2 +- src/outline.js | 152 +++------------------------- src/points.js | 196 +++++++++++++++++++++++++++---------- src/utils.js | 12 --- test/fixtures/absolem.yaml | 8 +- 7 files changed, 221 insertions(+), 211 deletions(-) create mode 100644 src/assert.js diff --git a/README.md b/README.md index def4c63..9aa3dd5 100644 --- a/README.md +++ b/README.md @@ -101,8 +101,8 @@ points: my_zone_name: anchor: ref: - rotate: num # default = 0 shift: [x, y] # default = [0, 0] + rotate: num # default = 0 columns: column_name: ... @@ -114,7 +114,7 @@ points: ``` `anchors` are used to, well, anchor the zone to something. -It's the `[0, 0]` origin with a 0 degree orientation by default, but it can be changed to any other pre-existing point.(Consequently, the first done can't use a ref, because there isn't any yet.) +It's the `[0, 0]` origin with a 0 degree orientation by default, but it can be changed to any other pre-existing point.(Consequently, the first zone can't use a ref, because there isn't any yet.) This initial position can then be changed with the `rotate` and `shift` options, adding extra rotation and translation, respectively. Once we know _where_ to start, we can describe the `columns` of our layout. @@ -125,7 +125,7 @@ columns: stagger: num # default = 0 spread: num # default = 19 rotate: num # default = 0 - origin: [x, y] # default = center of column's first key + origin: [x, y] # relative to center of column's first key, default = [0, 0] rows: row_name: ... @@ -180,7 +180,6 @@ Now for the "official" key-level attributes: name: name_override # default = a concatenation of column and row shift: [x, y] # default = [0, 0] rotate: num # default = 0 -origin: [x, y] # default = the center of the key padding: num # default = 19 skip: boolean # default = false asym: left | right | both # default = both @@ -188,7 +187,7 @@ asym: left | right | both # default = both `name` is the unique identifier of this specific key. It defaults to a `_` format, but can be overridden if necessary. -`shift` and `rotate`/`origin` declare an extra, key-level translation or rotation, respectively. +`shift` and `rotate` declare an extra, key-level translation or rotation, respectively. Then we leave `padding` amount of vertical space before moving on to the next key in the column. `skip` signals that the point is just a "helper" and should not be included in the output. This can happen when a _real_ point is more easily calculable through a "stepping stone", but then we don't actually want the stepping stone to be a key itself. @@ -308,7 +307,7 @@ We use two, key-level declarations for this: ```yaml neighbors: [dir_x, dir_y] -bind: num | [num_x, num_y] | [num_t, num_r, num_b, num_l] +bind: num | [num_x, num_y] | [num_t, num_r, num_b, num_l] # default = 10 ``` The former declares the directions we want to bind in, where `dir_x` can be one of `left`, `right`, or `both`; and `dir_y` can be one of `up`, `down`, or `both`. diff --git a/src/assert.js b/src/assert.js new file mode 100644 index 0000000..d25a008 --- /dev/null +++ b/src/assert.js @@ -0,0 +1,51 @@ +const Point = require('./point') + +const assert = exports.assert = (exp, msg) => { + if (!exp) { + throw new Error(msg) + } +} + +const type = exports.type = (val) => { + if (Array.isArray(val)) return 'array' + if (val === null) return 'null' + return typeof val +} + +const sane = exports.sane = (val, name, _type) => { + assert(type(val) == _type, `Field "${name}" should be of type ${_type}!`) + return val +} + +const detect_unexpected = exports.detect_unexpected = (obj, name, expected) => { + 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}"!`) + } +} + +const xy = exports.xy = (raw, name) => { + assert(type(raw) == 'array' && raw.length == 2, `Field "${name}" should be an array of length 2!`) + const x = raw[0] || 0 + const y = raw[1] || 0 + assert(type(x) == 'number' && type(y) == 'number', `Field "${name}" should contain numbers!`) + return {x, y} +} + +exports.anchor = (raw, name, points={}) => { + detect_unexpected(raw, name, ['ref', 'shift', 'rotate']) + let a = new Point() + if (raw.ref !== undefined) { + assert(points[raw.ref], `Unknown point reference "${raw.ref}" in anchor "${name}"!`) + a = points[raw.ref].clone() + } + if (raw.shift !== undefined) { + const xyval = xy(raw.shift, name + '.shift') + a.x += xyval.x + a.y += xyval.y + } + if (raw.rotate !== undefined) { + a.r += sane(raw.rotate || 0, name + '.rotate', 'number') + } + return a +} \ No newline at end of file diff --git a/src/cli.js b/src/cli.js index cce3553..50dba27 100644 --- a/src/cli.js +++ b/src/cli.js @@ -8,7 +8,7 @@ const yargs = require('yargs') const u = require('./utils') const points_lib = require('./points') -const outline_lib = require('./outline') +// const outline_lib = require('./outline') const dump_model = (model, file='model') => { const assembly = m.model.originate({ diff --git a/src/outline.js b/src/outline.js index c91678d..a547eb9 100644 --- a/src/outline.js +++ b/src/outline.js @@ -11,11 +11,12 @@ const layout = exports.layout = (points, shape) => { const outline = exports._outline = (points, config={}) => params => { + + let size = params.size || [18, 18] if (!Array.isArray(size)) size = [size, size] const corner = params.corner || 0 - const global_bind = config.bind || 5 let glue = {paths: {}} @@ -81,8 +82,6 @@ const outline = exports._outline = (points, config={}) => params => { } - // TODO - jsu = require('util') let i = 0 const keys = {} @@ -97,7 +96,16 @@ const outline = exports._outline = (points, config={}) => params => { let from_x = -footprint / 2, to_x = footprint / 2 let from_y = -footprint / 2, to_y = footprint / 2 - const bind = p.meta.row.bind || p.meta.col.bind || global_bind + + let bind = p.meta.bind || 10 + if (!Array.isArray(bind)) { + u.assert(u.type(bind) == 'number', `Incorrect "bind" field for point "${p.meta.name}"!`) + bind = {top: bind, right: bind, bottom: bind, left: bind} + } else { + u.assert([2, 4].includes(bind.length), `The "bind" field for point "${p.meta.name}" should contain 2 or 4 elements!`) + bind.map(val => u.assert(u.type(val) == 'number', `The "bind" field for point "${p.meta.name}" should contain numbers!`)) + } + const mirrored = p.meta.mirrored const bind_x = p.meta.row.bind_x || p.meta.col.bind_x @@ -119,33 +127,11 @@ const outline = exports._outline = (points, config={}) => params => { let key = new m.models.RoundRectangle(to_x - from_x, to_y - from_y, corner) key = m.model.moveRelative(key, [from_x, from_y]) key = p.position(key) - // console.log(i+1, pname, jsu.inspect(key, true, null, true)) - // if (i == 7) throw 7 - keys[pname] = u.deepcopy(p.position(u.rect(14, 14, [-7, -7]))) if (mirrored) { - // TODO running into the problem again where the right side doesn't combine properly - // have to debug this at a lower level, it might be a bug in the makerjs source :/ - // first step is to export these inspections and create a minimal reproduction - // if that fails as well, I have to dive into the combineUnion code... - - // if (pname === 'mirror_inner_top') { - // u.dump_model({a: right_keys, b: key}, `debug_bad`, true) - // } - - - right_keys = m.model.combineUnion(key, right_keys) - u.dump_model({a: glue, c: right_keys}, `right_${++i}`) + right_keys = m.model.combineUnion(right_keys, key) } else { - - // if (pname === 'inner_top') { - // u.dump_model({a: left_keys, b: key}, `debug_good`, true) - // } - - left_keys = m.model.combineUnion(key, left_keys) - u.dump_model({a: glue, b: left_keys}, `left_${++i}`) + left_keys = m.model.combineUnion(left_keys, key) } - - // u.dump_model({a: glue}, `glue_${i++}`) } } } @@ -156,113 +142,3 @@ const outline = exports._outline = (points, config={}) => params => { u.dump_model({a: glue, b: left_keys, c: {models: right_keys}}, `all_after_left`) glue = m.model.combineUnion(glue, right_keys) u.dump_model({a: glue, b: {models: keys}}, `fullll`) - - - // glue = m.model.outline(glue, expansion) - // const keys = {} - // // let i = 1 - // for (const zone of Object.values(config.zones)) { - // // interate cols in reverse order so they can - // // always overlap with the growing middle patch - // for (const col of zone.columns.slice().reverse()) { - // for (const [pname, p] of Object.entries(points)) { - // if (p.meta.col.name != col.name) continue - - // // let key = new m.models.RoundRectangle(footprint, footprint, corner) - // let key = u.rect(footprint, footprint) - // key = m.model.moveRelative(key, [-footprint/2, -footprint/2]) - // key = p.position(key) - // keys[pname] = u.deepcopy(key) - // key = m.model.outline(key, expansion) - // glue = m.model.combineUnion(glue, key) - - // // u.dump_model(keys, `keys_${i}`) - // // u.dump_model({a: glue}, `glue_${i++}`) - // } - // } - // } - - // u.dump_model({a: glue, b: {models: keys}}, `all`) - // glue = m.model.outline(glue, expansion, 1, true) - // u.dump_model({a: glue}, `glue_post`) - - - - - - // u.dump_model({ - // a: { - // models: {a: glue}, - // // paths: { - // // tll: tll, - // // trl: trl, - // // tip: u.circle(tip, 1), - // // tlp: u.circle(tlp, 1), - // // trp: u.circle(trp, 1), - // // bll: bll, - // // brl: brl, - // // bip: u.circle(bip, 1), - // // blp: u.circle(blp, 1), - // // brp: u.circle(brp, 1), - // // } - // }, - // b: { models: keys } - // }, 'valami', true) - - // throw 3 - - - - // let tl = m.model.moveRelative(m.model.rotate(u.rect(a_lot, size, [-size/2, -size/2]), tlp.r), tlp.p) - // let tr = m.model.moveRelative(m.model.rotate(u.rect(a_lot, size, [-a_lot+size/2, -size/2]), trp.r), trp.p) - // tl = m.model.originate(tl) - // tr = m.model.originate(tr) - // let top_intersect = m.path.intersection(tl.paths.top, tr.paths.top).intersectionPoints - // if (!top_intersect) { - // throw new Error('Top intersect') - // } - // console.log(tlp.p, tl, tl.paths.top, ',,,', trp.p, tr, tr.paths.top, top_intersect) - - - - // // create the two bottoms - // assert.ok(config.bottom.left) - // assert.ok(config.bottom.right) - // const blp = points[config.bottom.left] - // const brp = points[config.bottom.right] - - // // create middle "patch" - // const tll = new m.paths.Line(tlp.p, tlp.add([a_lot, 0]).rotate(tlp.r, tlp.p).p) - // const trl = new m.paths.Line(trp.p, trp.add([a_lot, 0]).rotate(trp.r, trp.p).p) - - - // const bll = new m.paths.Line(blp.p, blp.add([a_lot, 0]).rotate(blp.r, blp.p).p) - // const brl = new m.paths.Line(brp.p, brp.add([a_lot, 0]).rotate(brp.r, brp.p).p) - // const bottom_intersect = m.path.intersection(bll, brl).intersectionPoints[0] - - - // console.log(tll, trl, top_intersect) - // throw 2 - - // for (const p of Object.values(points)) { - // const r = new m.models.RoundRectangle(size, size, config.corner || 0) - // } -} - -exports.draw = (points, config) => { - // const lefts = {} - // const rights = {} - // for (const [k, p] of Object.entries(points)) { - // if (p.meta.mirrored) { - // rights[k] = p - // } else { - // lefts[k] = p - // } - // } - - - // TODO this is just a test - outline(points, config) - - -} \ No newline at end of file diff --git a/src/points.js b/src/points.js index 2d53187..8a78a26 100644 --- a/src/points.js +++ b/src/points.js @@ -1,11 +1,11 @@ const m = require('makerjs') const u = require('./utils') -const Point = require('./point') +const a = require('./assert') const extend_pair = exports._extend_pair = (to, from) => { - const to_type = u.type(to) - const from_type = u.type(from) - if (!from && ['array', 'object'].includes(to_type)) return to + const to_type = a.type(to) + const from_type = a.type(from) + if (from === undefined || from === null) return to if (to_type != from_type) return from if (from_type == 'object') { const res = {} @@ -42,109 +42,203 @@ const push_rotation = exports._push_rotation = (list, angle, origin) => { }) } -const render_zone = exports._render_zone = (cols, rows, zone_wide_key, anchor) => { +const render_zone = exports._render_zone = (zone_name, zone, anchor) => { + + // zone-wide sanitization + + a.detect_unexpected(zone, `points.zones.${zone_name}`, ['anchor', '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 || {'default': {}}, `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') + } + const zone_wide_key = a.sane(zone.key || {}, `points.zones.${zone_name}.key`, 'object') + + // algorithm prep const points = {} const rotations = [] - // transferring the anchor rotation to "real" rotations rotations.push({ angle: anchor.r, origin: anchor.p }) - for (const [colname, col] of Object.entries(cols)) { - + // column layout + + for (const [col_name, col] of Object.entries(cols)) { + + // column-level sanitization + + a.detect_unexpected( + col, + `points.zones.${zone_name}.columns.${col_name}`, + ['stagger', 'spread', 'rotate', 'origin', 'rows', 'key'] + ) + col.stagger = a.sane( + col.stagger || 0, + `points.zones.${zone_name}.columns.${col_name}.stagger`, + 'number' + ) + col.spread = a.sane( + col.spread || 19, + `points.zones.${zone_name}.columns.${col_name}.spread`, + 'number' + ) + col.rotate = a.sane( + col.rotate || 0, + `points.zones.${zone_name}.columns.${col_name}.rotate`, + 'number' + ) + col.origin = a.xy( + col.origin || [0, 0], + `points.zones.${zone_name}.columns.${col_name}.origin`, + ) + col.rows = a.sane( + col.rows || {}, + `points.zones.${zone_name}.columns.${col_name}.rows`, + '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' + ) + + // column-level prep + + // propagate object key to name field + col.name = col_name + // combine row data from zone-wide defs and col-specific defs + const actual_rows = Object.keys(col.rows).length ? col.rows : zone_wide_rows + + // setting up column-level anchor + anchor.y += col.stagger || 0 const col_anchor = anchor.clone() // clear potential rotations, as they will get re-applied anyway // and we don't want to apply them twice... col_anchor.r = 0 - // combine row data from zone-wide defs and col-specific defs - const col_specific_rows = col.rows || {} - const zone_wide_rows = rows || {} - const actual_rows = col_specific_rows || zone_wide_rows - - // get key config through the 4-level extension + // getting key config through the 4-level extension + const keys = [] + const default_key = { + shift: [0, 0], + rotate: 0, + padding: 19, + skip: false, + asym: 'both' + } for (const row of Object.keys(actual_rows)) { - const key = extend(zone_wide_key, col.key || {}, zone_wide_rows[row] || {}, col_specific_rows[row] || {}) + const key = extend( + default_key, + zone_wide_key, + col.key, + zone_wide_rows[row] || {}, + col.rows[row] || {} + ) + + require('fs-extra').writeJSONSync('arst.json', { + default_key, + zone_wide_key, + col_key: col.key, + zone_wide_rows: zone_wide_rows[row] || {}, + col_rows: col.rows[row] || {}, + result: key + }, {spaces: 4}) + throw 28 + + key.name = key.name || `${col_name}_${row}` + key.shift = a.xy(key.shift, `${key.name}.shift`) + key.rotate = a.sane(key.rotate, `${key.name}.rotate`, 'number') + key.padding = a.sane(key.padding, `${key.name}.padding`, 'number') + key.skip = a.sane(key.skip, `${key.name}.skip`, 'boolean') + a.assert( + ['left', 'right', 'both'].includes(key.asym), + `${key.name}.asym should be one of "left", "right", or "both"!` + ) key.col = col key.row = row - key.name = `${colname}_${row}` keys.push(key) } - // lay out keys + // actually laying out keys + for (const key of keys) { let point = col_anchor.clone() for (const r of rotations) { point.rotate(r.angle, r.origin) } if (key.rotate) { - point.rotate(key.rotate, point.add(key.origin || [0, 0]).p) + point.r += key.rotate } point.meta = key points[key.name] = point - - col_anchor.y += key.padding || 19 + col_anchor.y += key.padding } - // apply col-level rotation for the next columns + // applying col-level rotation for the next columns + if (col.rotate) { push_rotation( rotations, col.rotate, - anchor.add(col.origin || [0, 0]).p + anchor.add(col.origin).p ) } - anchor.x += col.spread || 19 + // moving over and starting the next column + + anchor.x += col.spread } return points } -const anchor = exports._anchor = (raw, points={}) => { - let a = new Point() - if (raw) { - if (raw.ref && points[raw.ref]) { - a = points[raw.ref].clone() - } - if (raw.shift) { - a.x += raw.shift[0] || 0 - a.y += raw.shift[1] || 0 - } - a.r += raw.rotate || 0 - } - return a -} + exports.parse = (config) => { let points = {} - for (const zone of Object.values(config.zones)) { - points = Object.assign(points, render_zone( - zone.columns || [], - zone.rows || [{name: 'default'}], - zone.key || {}, - anchor(zone.anchor, points) - )) + // getting original points + + const zones = a.sane(config.zones || {}, 'points.zones', 'object') + for (const [zone_name, zone] of Object.entries(zones)) { + const anchor = a.anchor(zone.anchor || new Point(), `points.zones.${zone_name}.anchor`, points) + points = Object.assign(points, render_zone(zone_name, zone, anchor)) } - if (config.rotate) { + // applying global rotation + + if (config.rotate !== undefined) { + const r = a.sane(config.rotate || 0, 'points.rotate', 'number') for (const p of Object.values(points)) { p.rotate(config.rotate) } } - if (config.mirror) { - let axis = config.mirror.axis - if (!axis) { - axis = anchor(config.mirror, points).x - axis += (config.mirror.distance || 0) / 2 + // mirroring + + if (config.mirror !== undefined) { + const mirror = a.sane(config.mirror || {}, 'points.mirror', 'object') + let axis = mirror.axis + if (axis === undefined) { + const distance = a.sane(mirror.distance || 0, 'points.mirror.distance', 'number') + delete mirror.distance + axis = a.anchor(mirror, 'points.mirror', points).x + axis += distance / 2 + } else { + axis = a.sane(axis || 0, 'points.mirror.axis', 'number') } const mirrored_points = {} for (const [name, p] of Object.entries(points)) { @@ -159,6 +253,8 @@ exports.parse = (config) => { } Object.assign(points, mirrored_points) } + + // removing temporary points const filtered = {} for (const [k, p] of Object.entries(points)) { diff --git a/src/utils.js b/src/utils.js index 3d11f2e..513de21 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,19 +1,7 @@ const m = require('makerjs') -exports.assert = (exp, msg) => { - if (!exp) { - throw new Error(msg) - } -} - exports.deepcopy = (value) => JSON.parse(JSON.stringify(value)) -exports.type = (val) => { - if (Array.isArray(val)) return 'array' - if (val === null) return 'null' - return typeof val -} - const eq = exports.eq = (a=[], b=[]) => { return a[0] === b[0] && a[1] === b[1] } diff --git a/test/fixtures/absolem.yaml b/test/fixtures/absolem.yaml index 7cbb477..5eacb78 100644 --- a/test/fixtures/absolem.yaml +++ b/test/fixtures/absolem.yaml @@ -89,18 +89,18 @@ outline: top: left: ref: inner_top - shift: [,7] + shift: [,.5] right: ref: mirror_inner_top - shift: [,7] + shift: [,.5] bottom: left: ref: far_thumb - shift: [7] + shift: [.5] rotate: 90 right: ref: mirror_far_thumb - shift: [-7] + shift: [-.5] rotate: 90 waypoints: - percent: 50