From 999a5dfad8019854110ad85a3e1cddaaccf6a220 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A1n=20D=C3=A9nes?= Date: Fri, 3 Jul 2020 22:29:39 +0200 Subject: [PATCH] Further outline progress --- README.md | 17 ++-- src/outline.js | 226 ++++++++++++++----------------------------------- 2 files changed, 72 insertions(+), 171 deletions(-) diff --git a/README.md b/README.md index 2b83e95..6805488 100644 --- a/README.md +++ b/README.md @@ -303,17 +303,14 @@ TODO: Absolem points here, with pics Once the raw points are available, we want to turn them into solid, continuous outlines. The points are enough to create properly positioned and rotated rectangles (with parametric side lengths), but they won't combine since there won't be any overlap. So the first part of the outline generation is "binding", where we make the individual holes _bind_ to each other. -We use two, key-level declarations for this: +We use a 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] # default = 10 +bind: num | [num_x, num_y] | [num_t, num_r, num_b, num_l] # default = 0 ``` Again, key-level declaration means that both of these should be specified in the `points` section, benefiting from the same extension process every key-level setting does. -The former declares the directions we want to bind in, where `dir_x` can be one of `left`, `right`, `both` or `neither`; and `dir_y` can be one of `up`, `down`, `both` or `neither`. -(If left empty, `neither` will be assumed.) -The latter declares how much we want to bind, i.e., the amount of overlap we want in that direction to make sure that we can reach the neighbor (`num` applies to all directions, `num_x` horizontally, `num_y` vertically, and the t/r/b/l versions to top/right/bottom/left, respectively). +This field declares how much we want to bind in each direction, i.e., the amount of overlap we want to make sure that we can reach the neighbor (`num` applies to all directions, `num_x` horizontally, `num_y` vertically, and the t/r/b/l versions to top/right/bottom/left, respectively). If it's a one-piece design, we also need to "glue" the halves together (or we might want to leave some extra space for the controller on the inner side for splits). This is where the following section comes into play: @@ -370,11 +367,11 @@ Note that this outline is still parametric, so that we can specify different wid Now we can configure what we want to "export" as outlines from this phase, given by the combination/subtraction of the following primitives: - `keys` : the combined outline that we've just created. Its parameters include: - - `side: left | right | both | glue | raw` : the part we want to use + - `side: left | right | middle | both | glue` : the part we want to use - `left` and `right` are just the appropriate side of the laid out keys, without the glue. + - `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` 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 - - `raw` is the raw glue shape we defined above under `outline.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 - `corner: num # default = 0)` : corner radius of the rectangles - `bevel: num # default = 0)` : corner bevel of the rectangles, can be combined with rounding @@ -382,7 +379,7 @@ Now we can configure what we want to "export" as outlines from this phase, given - `ref: ` : what position and rotation to consider as the origin - `rotate: num` : extra rotation - `shift: [x, y]` : extra translation - - `size: num | [width, height]` : the size of the rectangle + - `size`, `corner` and `bevel`, just like for `keys` - `circle` : an independent circle primitive. Parameters: - `ref`, `rotate`, and `shift` are the same as above - `radius: num` : the radius of the circle diff --git a/src/outline.js b/src/outline.js index 9aca4fb..2d8b738 100644 --- a/src/outline.js +++ b/src/outline.js @@ -2,150 +2,19 @@ const m = require('makerjs') const u = require('./utils') const a = require('./assert') -const outline = exports._outline = (points, config={}) => params => { +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 + a.assert(cw >= 0, error('wide', w)) + ch = h - corner - bevel + a.assert(ch >= 0, error('tall', h)) - - - const size = a.wh(params.size || [18, 18], '') - if (!Array.isArray(size)) size = [size, size] - size = a.xy(size, `outline.exports.${params.name}.size`) - const corner = params.corner || 0 - - - let glue = {paths: {}} - - if (config.glue) { - - const internal_part = (line) => { - // taking the middle part only, so that we don't interfere with corner rounding - return u.line(m.point.middle(line, 0.4), m.point.middle(line, 0.6)) - } - - const get_line = (def={}) => { - const ref = points[def.ref] - if (!ref) throw new Error(`Point ${def.ref} not found...`) - - let from = [0, 0] - let to = [ref.meta.mirrored ? -1 : 1, 0] - - // todo: position according to point to get the lines... - - let point = ref.clone().shift(def.shift || [0, 0]) - point.rotate(def.rotate || 0, point.add(def.origin || [0, 0])) - - - const rect = m.model.originate(point.rect(footprint)) - line = rect.paths[def.line || 'top'] - return internal_part(line) - } - - assert.ok(config.glue.top) - const tll = get_line(config.glue.top.left) - const trl = get_line(config.glue.top.right) - const tip = m.path.converge(tll, trl) - const tlp = u.eq(tll.origin, tip) ? tll.end : tll.origin - const trp = u.eq(trl.origin, tip) ? trl.end : trl.origin - - assert.ok(config.glue.bottom) - const bll = get_line(config.glue.bottom.left) - const brl = get_line(config.glue.bottom.right) - const bip = m.path.converge(bll, brl) - const blp = u.eq(bll.origin, bip) ? bll.end : bll.origin - const brp = u.eq(brl.origin, bip) ? brl.end : brl.origin - - const left_waypoints = [] - const right_waypoints = [] - - for (const w of config.glue.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) - left_waypoints.push([left_x, center_y]) - right_waypoints.unshift([right_x, center_y]) - } - - const waypoints = - [trp, tip, tlp] - .concat(left_waypoints) - .concat([blp, bip, brp]) - .concat(right_waypoints) - - glue = u.poly(waypoints) - - } - - - let i = 0 - const keys = {} - let left_keys = {} - let right_keys = {} - 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 from_x = -footprint / 2, to_x = footprint / 2 - let from_y = -footprint / 2, to_y = footprint / 2 - - 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 - if ((bind_x == 'left' && !mirrored) || (bind_x == 'right' && mirrored) || bind_x == 'both') { - from_x -= bind - } - if ((bind_x == 'right' && !mirrored) || (bind_x == 'left' && mirrored) || bind_x == 'both') { - to_x += bind - } - - const bind_y = p.meta.row.bind_y || p.meta.col.bind_y - if (bind_y == 'down' || bind_y == 'both') { - from_y -= bind - } - if (bind_y == 'up' || bind_y == 'both') { - to_y += bind - } - - 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) - if (mirrored) { - right_keys = m.model.combineUnion(right_keys, key) - } else { - left_keys = m.model.combineUnion(left_keys, key) - } - } - } - } - - - u.dump_model({a: glue, b: left_keys, c: {models: right_keys}}, `all_before`) - glue = m.model.combineUnion(glue, left_keys) - 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`) + let res = m.models.Rectangle(w, h) + res = m.model.outline(res, bevel, 2) + res = m.model.outline(res, corner, 0) + return m.model.moveRelative(res, [corner + bevel, corner + bevel]) } - - - - - - - - const parse_glue = exports._parse_glue = (config = {}, points = {}) => { a.detect_unexpected(config, 'outline.glue', ['top', 'bottom', 'waypoints', 'extra']) @@ -172,14 +41,47 @@ const parse_glue = exports._parse_glue = (config = {}, points = {}) => { return (export_name, params) => { - a.detect_unexpected(params, `outline.exports.${export_name}`, ['side', 'size', 'corner', 'bevel']) - params.side = a.in(params.side, `outline.exports.${export_name}.side`, ['left', 'right', 'both', 'glue', 'raw']) - params.size = a.wh(params.size, `outline.exports.${export_name}.size`) - params.corner = a.sane(params.corner || 0, `outline.exports.${export_name}.corner`, 'number') - params.bevel = a.sane(params.bevel || 0, `outline.exports.${export_name}.bevel`, 'number') + a.detect_unexpected(params, `${export_name}`, ['side', 'size', 'corner', 'bevel']) + 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') - let glue - if (['both', 'glue', 'raw'].includes(params.side)) { + let left = {paths: {}} + let right = {paths: {}} + if (['left', 'right', 'middle', 'both'].includes(params.side)) { + for (const [pname, p] of Object.entries(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] + + 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 = p.position(rect) + if (p.meta.mirrored) { + right = m.model.combineUnion(right, rect) + } else { + left = m.model.combineUnion(left, rect) + } + } + } + if (params.side == 'left') return left + if (params.side == 'right') return right + + let glue = {paths: {}} + if (['middle', 'both', 'glue'].includes(params.side)) { const get_line = (anchor) => { if (a.type(anchor) == 'number') { @@ -233,18 +135,15 @@ const parse_glue = exports._parse_glue = (config = {}, points = {}) => { glue = u.poly(waypoints) } - - } -} + if (params.side == 'glue') return glue + let both = m.model.combineUnion(u.deepcopy(left), glue) + both = m.model.combineUnion(both, u.deepcopy(right)) + if (params.side == 'both') return both - -const parse_exports = exports._parse_exports = (config = {}, points = {}) => { - - config = a.sane(config, 'outline.exports', 'object') - for (const [key, val] of Object.entries(config)) { - params.op = a.in(params.op || 'add', `outline.exports.${key}.op`, ['add', 'sub', 'diff']) - params.type = a.in(params.type, `outline.exports.${key}.type`, ['add', 'sub', 'diff']) + let middle = m.model.combineSubtraction(both, left) + middle = m.model.combineSubtraction(both, right) + return middle } } @@ -253,8 +152,13 @@ exports.parse = (config = {}, points = {}) => { const glue = parse_glue(config.glue, points) config = a.sane(config, 'outline.exports', 'object') - for (const [key, val] of Object.entries(config)) { - params.op = a.in(params.op || 'add', `outline.exports.${key}.op`, ['add', 'sub', 'diff']) - params.type = a.in(params.type, `outline.exports.${key}.type`, ['add', 'sub', 'diff']) + for (const [key, parts] of Object.entries(config)) { + let index = 0 + for (const part of parts) { + const name = `outline.exports.${key}[${++index}]` + part.op = a.in(part.op || 'add', `${name}.op`, ['add', 'sub', 'diff']) + part.type = a.in(part.type, `${name}.type`, ['keys', 'rectangle', 'circle', 'polygon', 'ref']) + + } } } \ No newline at end of file