diff --git a/README.md b/README.md index 742a3e8..2b83e95 100644 --- a/README.md +++ b/README.md @@ -369,21 +369,15 @@ 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: -- `all` : the combined outline that we've just created. Its parameters include: +- `keys` : the combined outline that we've just created. Its parameters include: + - `side: left | right | both | glue | raw` : the part we want to use + - `left` and `right` are just the appropriate side of the laid out keys, without the glue. + - `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` - `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 rectangle - - `bevel: num # default = 0)` : corner bevel of the rectangle, can be combined with rounding -- `keys` : only one side of the laid out keys, without the glue. Parameters: - - everything we could specify for `all` - - `side: left | right` : the side we want -- `glue` : just the glue, but the "ideal" version of it. This means that instead of the `glue` we defined above, we get `all` - `left` - `right`, so the _exact_ middle piece we would have needed to glue everything together. Parameters: - - everything we could specify for `all` (since those are needed for the calculation) - - `raw: boolean # default = false)` : optionally, we could choose to get the "raw" (i.e., the non-idealized) glue as well -- `ref` : a previously defined outline, see below. - - `name: outline_name` : the name of the referenced outline - -Additionally, we can use primitive shapes: - + - `corner: num # default = 0)` : corner radius of the rectangles + - `bevel: num # default = 0)` : corner bevel of the rectangles, can be combined with rounding - `rectangle` : an independent rectangle primitive. Parameters: - `ref: ` : what position and rotation to consider as the origin - `rotate: num` : extra rotation @@ -395,6 +389,8 @@ Additionally, we can use primitive shapes: - `polygon` : an independent polygon primitive. Parameters: - `ref`, `rotate`, and `shift` are the same as above - `points: [[x, y], ...]` : the points of the polygon +- `ref` : a previously defined outline, see below. + - `name: outline_name` : the name of the referenced outline Using these, we define exports as follows: @@ -408,7 +404,7 @@ exports: ``` Operations are performed in order, and the resulting shape is exported as an output. -Additionally, it is going to be available to further export declarations (using the `ref` type) under the name specified (`my_name`, in this case). +Additionally, it is going to be available for further export declarations to use (through the `ref` type) under the name specified (`my_name`, in this case). If we only want to use it as a building block for further exports, we can start the name with an underscore (e.g., `_my_name`) to prevent it from being actually exported. diff --git a/src/assert.js b/src/assert.js index 5464b8d..0e908f1 100644 --- a/src/assert.js +++ b/src/assert.js @@ -24,6 +24,11 @@ const detect_unexpected = exports.detect_unexpected = (obj, name, expected) => { } } +exports.in = (raw, name, arr) => { + assert(arr.includes(raw), `Field "${name}" should be one of [${arr.join(', ')}]!`) + return raw +} + const numarr = exports.numarr = (raw, name, length) => { assert(type(raw) == 'array' && raw.length == length, `Field "${name}" should be an array of length ${length}!`) raw = raw.map(val => val || 0) diff --git a/src/outline.js b/src/outline.js index 09aa38d..9aca4fb 100644 --- a/src/outline.js +++ b/src/outline.js @@ -158,17 +158,103 @@ const parse_glue = exports._parse_glue = (config = {}, points = {}) => { } } + config.waypoints = a.sane(config.waypoints || [], 'outline.glue.waypoints', 'array') + let wi = 0 + config.waypoints = config.waypoints.map(w => { + const name = `outline.glue.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') + return w + }) + + // TODO: handle glue.extra (or revoke it from the docs) + + 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') + + let glue + if (['both', 'glue', 'raw'].includes(params.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 - config.bottom = a.sane(config.bottom, 'outline.glue.bottom', 'object') - config.bottom.left = a.anchor(config.bottom.left, 'outline.glue.bottom.left', points) - if (a.type(config.bottom.right) != 'number') { - config.bottom.right = a.anchor(config.bottom.right, 'outline.glue.bottom.right', points) + return u.line(from, to) + } + + const tll = get_line(config.top.left) + const trl = get_line(config.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 + + const bll = get_line(config.bottom.left) + const brl = get_line(config.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.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]) + } + + let waypoints + const is_split = a.type(config.top.right) == 'number' + if (is_split) { + waypoints = [tip, tlp] + .concat(left_waypoints) + .concat([blp, bip]) + } else { + waypoints = [trp, tip, tlp] + .concat(left_waypoints) + .concat([blp, bip, brp]) + .concat(right_waypoints) + } + + glue = u.poly(waypoints) + } + } +} + + +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']) + } } exports.parse = (config = {}, points = {}) => { a.detect_unexpected(config, 'outline', ['glue', 'exports']) 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']) + } } \ No newline at end of file