diff --git a/README.md b/README.md index d9a7bf8..ebb2e60 100644 --- a/README.md +++ b/README.md @@ -334,17 +334,19 @@ glue: - ... ``` -...where an `` is the same as it was for points: +...where an `` is (mostly) the same as it was for points: ```yaml ref: shift: [x, y] # default = [0, 0] rotate: num # default = 0 +relative: boolean # default = true ``` The section's `top` and `bottom` are both formatted the same, and describe the center line's top and bottom intersections, respectively. In a one-piece case, this means that we project a line from a left-side reference point (optionally rotated and translated), another from the right, and converge them to where they meet. Split designs can specify `right` as a single number to mean the x coordinate where the side should be "cut off". +The `relative` flag means that the `shift` is interpreted in layout size units instead of mms (see below). This leads to a gluing middle patch that can be used to meld the left and right sides together, given by the counter-clockwise polygon: @@ -374,9 +376,10 @@ Now we can configure what we want to "export" as outlines from this phase, given - `middle` means an "ideal" version of the glue (meaning that instead of the `outline.glue` we defined above, we get `both` - `left` - `right`, so the _exact_ middle piece we would have needed to glue everything together - `both` means both sides, held together by the glue - `glue` is just the raw glue shape we defined above under `outline.glue` - - `size: num | [num_x, num_y]` : the width/height of the rectangles to lay onto the points + - `size: num | [num_x, num_y]` : the width/height of the rectangles to lay onto the points. Note that the `relative` flag for the glue declaration above meant this size as the basis of the shift. So during a `keys` layout with a size of 18, for example, a relative shift of `[.5, .5]` actually means `[9, 9]` in mms. - `corner: num # default = 0)` : corner radius of the rectangles - `bevel: num # default = 0)` : corner bevel of the rectangles, can be combined with rounding + - `bound: boolean # default = true` : whether to use the binding declared previously - `rectangle` : an independent rectangle primitive. Parameters: - `ref: ` : what position and rotation to consider as the origin - `rotate: num` : extra rotation @@ -395,7 +398,7 @@ Using these, we define exports as follows: ```yaml exports: my_name: - - operation: add | subtract | intersect # default = add + - operation: add | subtract | intersect | stack # default = add type: - ... diff --git a/src/assert.js b/src/assert.js index b9d888d..cf064c6 100644 --- a/src/assert.js +++ b/src/assert.js @@ -1,3 +1,4 @@ +const m = require('makerjs') const Point = require('./point') const assert = exports.assert = (exp, msg) => { @@ -57,9 +58,8 @@ exports.anchor = (raw, name, points={}, check_unexpected=true, default_point=new a = points[raw.ref].clone() } if (raw.shift !== undefined) { - const xyval = wh(raw.shift || [0, 0], name + '.shift') - a.x += xyval[0] - a.y += xyval[1] + let xyval = wh(raw.shift || [0, 0], name + '.shift') + a.shift(xyval, true) } if (raw.rotate !== undefined) { a.r += sane(raw.rotate || 0, name + '.rotate', 'number') diff --git a/src/io.js b/src/io.js index 5de648d..fa1c105 100644 --- a/src/io.js +++ b/src/io.js @@ -6,7 +6,9 @@ const u = require('./utils') exports.dump_model = (model, file='model', debug=false) => { const assembly = m.model.originate({ - models: u.deepcopy(model), + models: { + export: u.deepcopy(model) + }, units: 'mm' }) diff --git a/src/outline.js b/src/outline.js index 2747545..f47fb36 100644 --- a/src/outline.js +++ b/src/outline.js @@ -4,27 +4,46 @@ const a = require('./assert') const Point = require('./point') const rectangle = (w, h, corner, bevel, name='') => { - const error = (dim, val) => `Rectangle for "${name}" isn't ${dim} enough for its corner and bevel (${val} - ${corner} - ${bevel} <= 0)!` - const cw = w - corner - bevel + const error = (dim, val) => `Rectangle for "${name}" isn't ${dim} enough for its corner and bevel (${val} - 2 * ${corner} - 2 * ${bevel} <= 0)!` + const mod = 2 * (corner + bevel) + const cw = w - mod a.assert(cw >= 0, error('wide', w)) - ch = h - corner - bevel + const ch = h - mod a.assert(ch >= 0, error('tall', h)) - let res = m.models.Rectangle(w, h) - res = m.model.outline(res, bevel, 2) - res = m.model.outline(res, corner, 0) + let res = new m.models.Rectangle(cw, ch) + if (bevel > 0) res = m.model.outline(res, bevel, 2) + if (corner > 0) res = m.model.outline(res, corner, 0) return m.model.moveRelative(res, [corner + bevel, corner + bevel]) } -const parse_glue = exports._parse_glue = (config = {}, points = {}) => { +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 = {}) => { + + // Glue config sanitization a.detect_unexpected(config, 'outline.glue', ['top', 'bottom', 'waypoints', 'extra']) + + for (const y of ['top', 'bottom']) { a.detect_unexpected(config[y], `outline.glue.${y}`, ['left', 'right']) - config[y].left = a.anchor(config[y].left, `outline.glue.${y}.left`, points) + config[y].left = relative_anchor(config[y].left, `outline.glue.${y}.left`, points) if (a.type(config[y].right) != 'number') { - config[y].right = a.anchor(config[y].right, `outline.glue.${y}.right`, points) + config[y].right = relative_anchor(config[y].right, `outline.glue.${y}.right`, points) } } @@ -42,11 +61,16 @@ const parse_glue = exports._parse_glue = (config = {}, points = {}) => { return (params, export_name, expected) => { - a.detect_unexpected(params, `${export_name}`, expected.concat(['side', 'size', 'corner', 'bevel'])) + // Layout params sanitization + + a.detect_unexpected(params, `${export_name}`, expected.concat(['side', 'size', 'corner', 'bevel', 'bound'])) const side = a.in(params.side, `${export_name}.side`, ['left', 'right', 'middle', 'both', 'glue']) const size = a.wh(params.size, `${export_name}.size`) const corner = a.sane(params.corner || 0, `${export_name}.corner`, 'number') const bevel = a.sane(params.bevel || 0, `${export_name}.bevel`, 'number') + const bound = a.sane(params.bound === undefined ? true : params.bound, `${export_name}.bound`, 'boolean') + + // Actual layout let left = {models: {}} let right = {models: {}} @@ -56,25 +80,35 @@ const parse_glue = exports._parse_glue = (config = {}, points = {}) => { let from_x = -size[0] / 2, to_x = size[0] / 2 let from_y = -size[1] / 2, to_y = size[1] / 2 - let bind = a.trbl(p.meta.bind || 0, `${pname}.bind`) - // 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]] - } - - from_x -= bind[3] - to_x += bind[1] - - from_y -= bind[2] - to_y += bind[0] - + // the original position let rect = rectangle(to_x - from_x, to_y - from_y, corner, bevel, `${export_name}.size`) - rect = m.model.move(rect, [from_x, from_y]) + rect = m.model.moveRelative(rect, [from_x, from_y]) + + // extra binding "material", if necessary + if (bound) { + let bind = a.trbl(p.meta.bind || 0, `${pname}.bind`) + // 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]] + } + + const bt = to_y + Math.max(bind[0], 0) + const br = to_x + Math.max(bind[1], 0) + const bd = from_y - Math.max(bind[2], 0) + const bl = from_x - Math.max(bind[3], 0) + + if (bind[0] || bind[1]) rect = u.union(rect, u.rect(br, bt)) + if (bind[1] || bind[2]) rect = u.union(rect, u.rect(br, -bd, [0, bd])) + if (bind[2] || bind[3]) rect = u.union(rect, u.rect(-bl, -bd, [bl, bd])) + if (bind[3] || bind[0]) rect = u.union(rect, u.rect(-bl, bt, [bl, 0])) + } + + // positioning and unioning the resulting shape rect = p.position(rect) if (p.meta.mirrored) { - right = m.model.combineUnion(right, rect) + right = u.union(right, rect) } else { - left = m.model.combineUnion(left, rect) + left = u.union(left, rect) } } } @@ -82,18 +116,19 @@ const parse_glue = exports._parse_glue = (config = {}, points = {}) => { if (side == 'right') return right let glue = {models: {}} - if (['middle', 'both', 'glue'].includes(side)) { + if (bound && ['middle', 'both', 'glue'].includes(side)) { const get_line = (anchor) => { if (a.type(anchor) == 'number') { return u.line([anchor, -1000], [anchor, 1000]) } - - let from = anchor.clone() - let to = anchor.add([anchor.meta.mirrored ? -1 : 1, 0]) - to = to.rotate(anchor.r, anchor.p).p - return u.line(from, to) + // 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() + const to = from.clone().shift([from.meta.mirrored ? -1 : 1, 0]) + + return u.line(from.p, to.p) } const tll = get_line(config.top.left) @@ -110,13 +145,13 @@ const parse_glue = exports._parse_glue = (config = {}, points = {}) => { const left_waypoints = [] const right_waypoints = [] - + for (const w of config.waypoints) { const percent = w.percent / 100 const center_x = tip[0] + percent * (bip[0] - tip[0]) const center_y = tip[1] + percent * (bip[1] - tip[1]) - const left_x = center_x - (w.left || w.width / 2) - const right_x = center_x + (w.right || w.width / 2) + const left_x = center_x - w.width[0] + const right_x = center_x + w.width[1] left_waypoints.push([left_x, center_y]) right_waypoints.unshift([right_x, center_y]) } @@ -133,24 +168,26 @@ const parse_glue = exports._parse_glue = (config = {}, points = {}) => { .concat([blp, bip, brp]) .concat(right_waypoints) } - + glue = u.poly(waypoints) } if (side == 'glue') return glue - let both = m.model.combineUnion(u.deepcopy(left), glue) - both = m.model.combineUnion(both, u.deepcopy(right)) - if (side == 'both') return both + if (side == 'middle') { + let middle = u.subtract(glue, left) + middle = u.subtract(middle, right) + return middle + } - let middle = m.model.combineSubtraction(both, left) - middle = m.model.combineSubtraction(both, right) - return middle + let both = u.union(u.deepcopy(left), glue) + both = u.union(both, u.deepcopy(right)) + return both } } exports.parse = (config = {}, points = {}) => { a.detect_unexpected(config, 'outline', ['glue', 'exports']) - const glue = parse_glue(config.glue, points) + const layout_fn = layout(config.glue, points) const outlines = {} @@ -162,17 +199,18 @@ exports.parse = (config = {}, points = {}) => { const name = `outline.exports.${key}[${++index}]` const expected = ['type', 'operation'] part.type = a.in(part.type, `${name}.type`, ['keys', 'rectangle', 'circle', 'polygon', 'ref']) - part.operation = a.in(part.operation || 'add', `${name}.operation`, ['add', 'subtract', 'intersect']) + part.operation = a.in(part.operation || 'add', `${name}.operation`, ['add', 'subtract', 'intersect', 'stack']) - let op = m.model.combineUnion - if (part.operation == 'subtract') op = m.model.combineSubtraction - else if (part.operation == 'intersect') op = m.model.combineIntersection + let op = u.union + if (part.operation == 'subtract') op = u.subtract + else if (part.operation == 'intersect') op = u.intersect + else if (part.operation == 'stack') op = u.stack let arg let anchor switch (part.type) { case 'keys': - arg = glue(part, name, expected) + arg = layout_fn(part, name, expected) break case 'rectangle': a.detect_unexpected(part, name, expected.concat(['ref', 'shift', 'rotate', 'size', 'corner', 'bevel'])) @@ -181,7 +219,6 @@ exports.parse = (config = {}, points = {}) => { const corner = a.sane(part.corner || 0, `${name}.corner`, 'number') const bevel = a.sane(part.bevel || 0, `${name}.bevel`, 'number') arg = rectangle(size[0], size[1], corner, bevel, name) - arg = m.model.move(arg, [-size[0]/2, -size[1]/2]) // center arg = anchor.position(arg) break case 'circle': @@ -198,14 +235,14 @@ exports.parse = (config = {}, points = {}) => { let poly_index = 0 for (const poly_point of poly_points) { const poly_name = `${name}.points[${++poly_index}]` - const anchor = a.anchor(point, point_name, points, true, last_anchor) + const anchor = a.anchor(poly_point, poly_name, points, true, last_anchor) parsed_points.push(anchor.p) } arg = u.poly(parsed_points) break case 'ref': a.assert(outlines[part.name], `Field "${name}.name" does not name an existing outline!`) - arg = outlines[part.name] + arg = u.deepcopy(outlines[part.name]) break } diff --git a/src/point.js b/src/point.js index 9df127a..41d2c85 100644 --- a/src/point.js +++ b/src/point.js @@ -24,14 +24,10 @@ module.exports = class Point { [this.x, this.y] = val } - add(a) { - const res = this.clone() - res.x += a[0] - res.y += a[1] - return res - } - - shift(s) { + shift(s, relative=true) { + if (relative) { + s = m.point.rotate(s, this.r) + } this.x += s[0] this.y += s[1] return this diff --git a/src/points.js b/src/points.js index d454ad2..ff0b180 100644 --- a/src/points.js +++ b/src/points.js @@ -183,7 +183,7 @@ const render_zone = exports._render_zone = (zone_name, zone, anchor, global_key) push_rotation( rotations, col.rotate, - anchor.add(col.origin).p + anchor.clone().shift(col.origin, false).p ) } @@ -264,5 +264,5 @@ exports.position = (points, shape) => { for (const [pname, p] of Object.entries(points)) { shapes[pname] = p.position(u.deepcopy(shape)) } - return {layout: {models: shapes}} + return {models: shapes} } \ No newline at end of file diff --git a/src/utils.js b/src/utils.js index 513de21..6ffa26d 100644 --- a/src/utils.js +++ b/src/utils.js @@ -14,18 +14,13 @@ exports.circle = (p, r) => { return new m.paths.Circle(p, r) } -exports.rect = (w, h, o=[0, 0], mirrored=false) => { +exports.rect = (w, h, o=[0, 0]) => { const res = { top: line([0, h], [w, h]), right: line([w, h], [w, 0]), bottom: line([w, 0], [0, 0]), left: line([0, 0], [0, h]) } - if (mirrored) { - for (const segment of Object.values(res)) { - [segment.origin, segment.end] = [segment.end, segment.origin] - } - } return m.model.move({paths: res}, o) } @@ -42,3 +37,31 @@ exports.poly = (arr) => { } return res } + +const farPoint = [1234.1234, 2143.56789] + +exports.union = (a, b) => { + return m.model.combine(a, b, false, true, false, true, { + farPoint + }) +} + +exports.subtract = (a, b) => { + return m.model.combine(a, b, false, true, true, false, { + farPoint + }) +} + +exports.intersect = (a, b) => { + return m.model.combine(a, b, true, false, true, false, { + farPoint + }) +} + +exports.stack = (a, b) => { + return { + models: { + a, b + } + } +} \ No newline at end of file diff --git a/test/fixtures/absolem.yaml b/test/fixtures/absolem.yaml index 147f8f5..c65e75d 100644 --- a/test/fixtures/absolem.yaml +++ b/test/fixtures/absolem.yaml @@ -43,6 +43,7 @@ points: stagger: -2 rows: bottom: + bind: [,,10,] home: bind: [,,,10] top: @@ -64,21 +65,18 @@ points: origin: [9.5, -9] rows: thumb: - bind: [,10] + bind: [10,1,,] home: spread: 21.25 rotate: -28 origin: [11.75, -9] rows: thumb: - bind: [,10,,10] + bind: [,10,,15] far: rows: thumb: - bind: [,,,10] - rows: - thumb: - bind: [10] + bind: [-1,,,5] key: bind: [0, 0, 0, 0] rotate: -20 @@ -105,12 +103,51 @@ outline: rotate: 90 waypoints: - percent: 50 - width: 100 - - percent: 90 width: 50 + - percent: 90 + width: 25 exports: - basic: + outline: - type: keys side: both size: 18 - corner: .5 \ No newline at end of file + corner: .5 + holes: + - type: keys + operation: stack + side: both + size: 14 + bound: false + middle: + - type: keys + operation: stack + side: middle + size: 24 + - type: rectangle + size: [25, 5] + ref: home_thumb + shift: [0, 12] + - type: rectangle + size: [25, 5] + ref: far_thumb + shift: [-25, 12] + - type: rectangle + size: [25, 5] + ref: mirror_home_thumb + shift: [-25, 12] + - type: rectangle + size: [25, 5] + ref: mirror_far_thumb + shift: [0, 12] + - type: ref + name: outline + operation: intersect + complex: + - type: ref + name: outline + - type: ref + name: holes + operation: stack + - type: ref + name: middle + operation: stack \ No newline at end of file