From 6504b2b9528c6072badb769d9fcdd28536b9850e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A1n=20D=C3=A9nes?= Date: Sun, 9 Jan 2022 22:56:05 +0100 Subject: [PATCH] Outlines rewrite in progress --- src/anchor.js | 5 +- src/outlines.js | 381 +++++++++++++++++++++++++++--------------------- src/utils.js | 2 +- 3 files changed, 216 insertions(+), 172 deletions(-) diff --git a/src/anchor.js b/src/anchor.js index 5f08925..92e52b6 100644 --- a/src/anchor.js +++ b/src/anchor.js @@ -2,13 +2,12 @@ const u = require('./utils') const a = require('./assert') const Point = require('./point') -const mirror_ref = exports.mirror = (ref, mirror) => { +const mirror_ref = exports.mirror = (ref, mirror=true) => { if (mirror) { if (ref.startsWith('mirror_')) { return ref.substring(7) - } else { - return 'mirror_' + ref } + return 'mirror_' + ref } return ref } diff --git a/src/outlines.js b/src/outlines.js index 66f4308..28a47ed 100644 --- a/src/outlines.js +++ b/src/outlines.js @@ -5,82 +5,80 @@ const o = require('./operation') const Point = require('./point') const prep = require('./prepare') const anchor_lib = require('./anchor') +const filter = require('./filter').parse -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)!` - const mod = 2 * (corner + bevel) - const cw = w - mod - a.assert(cw >= 0, error('wide', w)) - const ch = h - mod - a.assert(ch >= 0, error('tall', h)) +const binding = (base, w, h, point, units) => { - let res = new m.models.Rectangle(cw, ch) - if (bevel) { - res = u.poly([ - [-bevel, 0], - [-bevel, ch], - [0, ch + bevel], - [cw, ch + bevel], - [cw + bevel, ch], - [cw + bevel, 0], - [cw, -bevel], - [0, -bevel] - ]) + let bind = a.trbl(point.meta.bind || 0, `${point.meta.name}.bind`)(units) + // if it's a mirrored key, we swap the left and right bind values + if (point.meta.mirrored) { + bind = [bind[0], bind[3], bind[2], bind[1]] } - if (corner > 0) res = m.model.outline(res, corner, 0) - return m.model.moveRelative(res, [corner + bevel, corner + bevel]) + + const bt = h/2 + Math.max(bind[0], 0) + const br = w/2 + Math.max(bind[1], 0) + const bd = -h/2 - Math.max(bind[2], 0) + const bl = -w/2 - Math.max(bind[3], 0) + + if (bind[0] || bind[1]) base = u.union(base, u.rect(br, bt)) + if (bind[1] || bind[2]) base = u.union(base, u.rect(br, -bd, [0, bd])) + if (bind[2] || bind[3]) base = u.union(base, u.rect(-bl, -bd, [bl, bd])) + if (bind[3] || bind[0]) base = u.union(base, u.rect(-bl, bt, [bl, 0])) + + return base } -const layout = exports._layout = (config = {}, points = {}, units = {}) => { +const rectangle = (config, name, points, units) => { - // Glue config sanitization + // prepare params + a.unexpected(config, `${name}`, ['size', 'corner', 'bevel']) + const size = a.wh(params.size, `${export_name}.size`)(units) + const rec_units = prep.extend({ + sx: size[0], + sy: size[1] + }, units) + const corner = a.sane(params.corner || 0, `${export_name}.corner`, 'number')(rec_units) + const bevel = a.sane(params.bevel || 0, `${export_name}.bevel`, 'number')(rec_units) - const parsed_glue = u.deepcopy(a.sane(config, 'outlines.glue', 'object')()) - for (let [gkey, gval] of Object.entries(parsed_glue)) { - a.unexpected(gval, `outlines.glue.${gkey}`, ['top', 'bottom', 'waypoints', 'extra']) - - for (const y of ['top', 'bottom']) { - a.unexpected(gval[y], `outlines.glue.${gkey}.${y}`, ['left', 'right']) - gval[y].left = anchor_lib.parse(gval[y].left, `outlines.glue.${gkey}.${y}.left`, points) - if (a.type(gval[y].right)(units) != 'number') { - gval[y].right = anchor_lib.parse(gval[y].right, `outlines.glue.${gkey}.${y}.right`, points) - } + // return shape function + return (point, bound, mirror) => { + + const error = (dim, val) => `Rectangle for "${name}" isn't ${dim} enough for its corner and bevel (${val} - 2 * ${corner} - 2 * ${bevel} <= 0)!` + const [w, h] = size + const mod = 2 * (corner + bevel) + const cw = w - mod + a.assert(cw >= 0, error('wide', w)) + const ch = h - mod + a.assert(ch >= 0, error('tall', h)) + + let rect = new m.models.Rectangle(cw, ch) + if (bevel) { + rect = u.poly([ + [-bevel, 0], + [-bevel, ch], + [0, ch + bevel], + [cw, ch + bevel], + [cw + bevel, ch], + [cw + bevel, 0], + [cw, -bevel], + [0, -bevel] + ]) } - - 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.unexpected(w, name, ['percent', 'width']) - w.percent = a.sane(w.percent, name + '.percent', 'number')(units) - w.width = a.wh(w.width, name + '.width')(units) - return w - }) + if (corner > 0) rect = m.model.outline(rect, corner, 0) + rect = m.model.moveRelative(res, [corner + bevel, corner + bevel]) - parsed_glue[gkey] = gval + let normal = u.deepcopy(rect) + if (bound) normal = binding(normal, w, h, point, rec_units) + normal = point.position(normal) + + let mirrored + if (mirror) { + mirrored_name = anchor_lib.mirror(point.name) + } } +} - // TODO: handle glue.extra (or revoke it from the docs) - - return (params, export_name, expected) => { - - // Layout params sanitization - - a.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 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 @@ -220,125 +218,172 @@ const layout = exports._layout = (config = {}, points = {}, units = {}) => { } } -exports.parse = (config = {}, points = {}, units = {}) => { - a.unexpected(config, 'outline', ['glue', 'exports']) - const layout_fn = layout(config.glue, points, units) +const whats = { + rectangle, +} + + + + + +exports.parse = (config = {}, points = {}, units = {}) => { + + // output outlines will be collected here const outlines = {} - const ex = a.sane(config.exports || {}, 'outlines.exports', 'object')() - for (let [key, parts] of Object.entries(ex)) { + // the config must be an actual object so that the exports have names + config = a.sane(config, 'outlines', 'object')() + for (let [outline_name, parts] of Object.entries(config)) { + + // placeholder for the current outline + outlines[outline_name] = {models: {}} + + // each export can consist of multiple parts + // either sub-objects or arrays are fine... if (a.type(parts)() == 'array') { parts = {...parts} } - parts = a.sane(parts, `outlines.exports.${key}`, 'object')() - let result = {models: {}} + parts = a.sane(parts, `outlines.${key}`, 'object')() + for (let [part_name, part] of Object.entries(parts)) { - const name = `outlines.exports.${key}.${part_name}` + + const name = `outlines.${key}.${part_name}` + + // string part-shortcuts are expanded first 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']) - part.operation = a.in(part.operation || 'add', `${name}.operation`, ['add', 'subtract', 'intersect', 'stack']) - 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 + // process keys that are common to all part declarations + const what = a.in(part.what || 'outline', `${name}.what`, ['rectangle', 'circle', 'polygon', 'outline']) + // where is delayed until we have all, potentially what-dependent units + const where = units => filter(part.where, `${name}.where`, points, units) + const operation = u[a.in(part.operation || 'add', `${name}.operation`, ['add', 'subtract', 'intersect', 'stack'])] + const bound = a.sane(part.bound === undefined ? true : part.bound, `${name}.bound`, 'boolean')() + const mirror = a.sane(part.mirror || false, `${name}.mirror`, 'boolean')() - let arg - let anchor - const anchor_def = part.anchor || {} - switch (part.type) { - case 'keys': - arg = layout_fn(part, name, expected) - break - case 'rectangle': - a.unexpected(part, name, expected.concat(['anchor', 'size', 'corner', 'bevel', 'mirror'])) - const size = a.wh(part.size, `${name}.size`)(units) - const rec_units = prep.extend({ - sx: size[0], - sy: size[1] - }, units) - anchor = anchor_lib.parse(anchor_def, `${name}.anchor`, points)(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 = m.model.moveRelative(rectangle(size[0], size[1], corner, bevel, name), [-size[0]/2, -size[1]/2]) - arg = anchor.position(u.deepcopy(rect)) - if (rect_mirror) { - const mirror_anchor = u.deepcopy(anchor_def) - a.assert(mirror_anchor.ref, `Field "${name}.anchor.ref" must be speficied if mirroring is required!`) - anchor = anchor_lib.parse(mirror_anchor, `${name}.anchor --> mirror`, points, undefined, undefined, true)(rec_units) - arg = u.union(arg, anchor.position(u.deepcopy(rect))) - } - break - case 'circle': - a.unexpected(part, name, expected.concat(['anchor', 'radius', 'mirror'])) - const radius = a.sane(part.radius, `${name}.radius`, 'number')(units) - const circle_units = prep.extend({ - r: radius - }, units) - anchor = anchor_lib.parse(anchor_def, `${name}.anchor`, points)(circle_units) - const circle_mirror = a.sane(part.mirror || false, `${name}.mirror`, 'boolean')() - arg = u.circle(anchor.p, radius) - if (circle_mirror) { - const mirror_anchor = u.deepcopy(anchor_def) - a.assert(mirror_anchor.ref, `Field "${name}.anchor.ref" must be speficied if mirroring is required!`) - anchor = anchor_lib.parse(mirror_anchor, `${name}.anchor --> mirror`, points, undefined, undefined, true)(circle_units) - arg = u.union(arg, u.circle(anchor.p, radius)) - } - break - case 'polygon': - a.unexpected(part, name, expected.concat(['points', 'mirror'])) - const poly_points = a.sane(part.points, `${name}.points`, 'array')() - const poly_mirror = a.sane(part.mirror || false, `${name.mirror}`, 'boolean')() - const parsed_points = [] - const mirror_points = [] - let poly_mirror_x = 0 - let last_anchor = new Point() - let poly_index = 0 - for (const poly_point of poly_points) { - const poly_name = `${name}.points[${++poly_index}]` - if (poly_index == 1 && poly_mirror) { - a.assert(poly_point.ref, `Field "${poly_name}.ref" must be speficied if mirroring is required!`) - const mirrored_ref = anchor_lib.mirror(poly_point.ref, poly_mirror) - a.assert(points[poly_point.ref], `Field "${poly_name}.ref" does not name an existing point!`) - a.assert(points[mirrored_ref], `The mirror of field "${poly_name}.ref" ("${mirrored_ref}") does not name an existing point!`) - poly_mirror_x = (points[poly_point.ref].x + points[mirrored_ref].x) / 2 - } - last_anchor = anchor_lib.parse(poly_point, poly_name, points, true, last_anchor)(units) - parsed_points.push(last_anchor.p) - mirror_points.push(last_anchor.clone().mirror(poly_mirror_x).p) - } - arg = u.poly(parsed_points) - if (poly_mirror) { - arg = u.union(arg, u.poly(mirror_points)) - } - break - case 'outline': - a.unexpected(part, name, expected.concat(['name', 'fillet'])) - a.assert(outlines[part.name], `Field "${name}.name" does not name an existing outline!`) - const fillet = a.sane(part.fillet || 0, `${name}.fillet`, 'number')(units) - arg = u.deepcopy(outlines[part.name]) - if (fillet) { - for (const [index, chain] of m.model.findChains(arg).entries()) { - arg.models[`fillet_${index}`] = m.chain.fillet(chain, fillet) - } - } - break - default: - throw new Error(`Field "${name}.type" (${part.type}) does not name a valid outline part type!`) + // which are then removed, so ops can check their own unexpected keys + delete part.what + delete part.where + delete part.operation + delete part.bound + delete part.mirror + + // a prototype "shape" maker (and its units) are computed + const [shape, shape_units] = whats[what](part, name, points, units) + + // and then repeated for all where positions + for (const w of where(shape_units)) { + const [normal, mirrored] = shape(w, bound, mirror) + outlines[outline_name] = operation(outlines[outline_name], normal) + // and even their mirrors, if applicable + if (mirror) { + outlines[outline_name] = operation(outlines[outline_name], mirrored) + } } - - result = op(result, arg) } - m.model.originate(result) - m.model.simplify(result) - outlines[key] = result + m.model.originate(outlines[outline_name]) + m.model.simplify(outlines[outline_name]) + } return outlines -} \ No newline at end of file +} + +// let arg +// let anchor +// const anchor_def = part.anchor || {} +// switch (part.type) { +// case 'keys': +// arg = layout_fn(part, name, expected) +// break +// case 'rectangle': +// a.unexpected(part, name, expected.concat(['anchor', 'size', 'corner', 'bevel', 'mirror'])) +// const size = a.wh(part.size, `${name}.size`)(units) +// const rec_units = prep.extend({ +// sx: size[0], +// sy: size[1] +// }, units) +// anchor = anchor_lib.parse(anchor_def, `${name}.anchor`, points)(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 = m.model.moveRelative(rectangle(size[0], size[1], corner, bevel, name), [-size[0]/2, -size[1]/2]) +// arg = anchor.position(u.deepcopy(rect)) +// if (rect_mirror) { +// const mirror_anchor = u.deepcopy(anchor_def) +// a.assert(mirror_anchor.ref, `Field "${name}.anchor.ref" must be speficied if mirroring is required!`) +// anchor = anchor_lib.parse(mirror_anchor, `${name}.anchor --> mirror`, points, undefined, undefined, true)(rec_units) +// arg = u.union(arg, anchor.position(u.deepcopy(rect))) +// } +// break +// case 'circle': +// a.unexpected(part, name, expected.concat(['anchor', 'radius', 'mirror'])) +// const radius = a.sane(part.radius, `${name}.radius`, 'number')(units) +// const circle_units = prep.extend({ +// r: radius +// }, units) +// anchor = anchor_lib.parse(anchor_def, `${name}.anchor`, points)(circle_units) +// const circle_mirror = a.sane(part.mirror || false, `${name}.mirror`, 'boolean')() +// arg = u.circle(anchor.p, radius) +// if (circle_mirror) { +// const mirror_anchor = u.deepcopy(anchor_def) +// a.assert(mirror_anchor.ref, `Field "${name}.anchor.ref" must be speficied if mirroring is required!`) +// anchor = anchor_lib.parse(mirror_anchor, `${name}.anchor --> mirror`, points, undefined, undefined, true)(circle_units) +// arg = u.union(arg, u.circle(anchor.p, radius)) +// } +// break +// case 'polygon': +// a.unexpected(part, name, expected.concat(['points', 'mirror'])) +// const poly_points = a.sane(part.points, `${name}.points`, 'array')() +// const poly_mirror = a.sane(part.mirror || false, `${name.mirror}`, 'boolean')() +// const parsed_points = [] +// const mirror_points = [] +// let poly_mirror_x = 0 +// let last_anchor = new Point() +// let poly_index = 0 +// for (const poly_point of poly_points) { +// const poly_name = `${name}.points[${++poly_index}]` +// if (poly_index == 1 && poly_mirror) { +// a.assert(poly_point.ref, `Field "${poly_name}.ref" must be speficied if mirroring is required!`) +// const mirrored_ref = anchor_lib.mirror(poly_point.ref, poly_mirror) +// a.assert(points[poly_point.ref], `Field "${poly_name}.ref" does not name an existing point!`) +// a.assert(points[mirrored_ref], `The mirror of field "${poly_name}.ref" ("${mirrored_ref}") does not name an existing point!`) +// poly_mirror_x = (points[poly_point.ref].x + points[mirrored_ref].x) / 2 +// } +// last_anchor = anchor_lib.parse(poly_point, poly_name, points, true, last_anchor)(units) +// parsed_points.push(last_anchor.p) +// mirror_points.push(last_anchor.clone().mirror(poly_mirror_x).p) +// } +// arg = u.poly(parsed_points) +// if (poly_mirror) { +// arg = u.union(arg, u.poly(mirror_points)) +// } +// break +// case 'outline': +// a.unexpected(part, name, expected.concat(['name', 'fillet'])) +// a.assert(outlines[part.name], `Field "${name}.name" does not name an existing outline!`) +// const fillet = a.sane(part.fillet || 0, `${name}.fillet`, 'number')(units) +// arg = u.deepcopy(outlines[part.name]) +// if (fillet) { +// for (const [index, chain] of m.model.findChains(arg).entries()) { +// arg.models[`fillet_${index}`] = m.chain.fillet(chain, fillet) +// } +// } +// break +// default: +// throw new Error(`Field "${name}.type" (${part.type}) does not name a valid outline part type!`) +// } + +// result = op(result, arg) +// } + +// m.model.originate(result) +// m.model.simplify(result) +// outlines[key] = result +// } + +// return outlines +// } \ No newline at end of file diff --git a/src/utils.js b/src/utils.js index 4955cf8..cfdb1ea 100644 --- a/src/utils.js +++ b/src/utils.js @@ -56,7 +56,7 @@ exports.poly = (arr) => { const farPoint = [1234.1234, 2143.56789] -exports.union = (a, b) => { +exports.union = exports.add = (a, b) => { return m.model.combine(a, b, false, true, false, true, { farPoint })