Outlines rewrite, part 2

This commit is contained in:
Bán Dénes 2022-01-10 13:44:57 +01:00
parent 6504b2b952
commit df7b76c610
3 changed files with 127 additions and 266 deletions

View file

@ -1,6 +1,7 @@
const u = require('./utils') const u = require('./utils')
const a = require('./assert') const a = require('./assert')
const anchor = require('./anchor').parse const anchor_lib = require('./anchor')
const anchor = anchor_lib.parse
const _true = () => true const _true = () => true
const _and = arr => p => arr.map(e => e(p)).reduce((a, b) => a && b) const _and = arr => p => arr.map(e => e(p)).reduce((a, b) => a && b)
@ -100,13 +101,26 @@ const complex = (config, name, units, aggregator=_or) => {
} }
} }
exports.parse = (config, name, points={}, units={}) => { exports.parse = (config, name, points={}, units={}, include_mirrors=false) => {
let result = []
// if a filter decl is an object, it is an anchor // if a filter decl is an object, it is an anchor
if (a.type(config)() == 'object') { if (a.type(config)() == 'object') {
return [anchor(config, name, points)(units)] result.push(anchor(config, name, points)(units))
if (include_mirrors) {
// this is strict: if the ref of the anchor doesn't have a mirror pair, it will error out
result.push(anchor(config, name, points, true, undefined, true)(units))
}
// otherwise, it is treated as a condition to filter all available points
} else {
result = Object.values(points).filter(complex(config, name, units))
if (include_mirrors) {
// this is permissive: we only include mirrored versions if they exist, and don't fuss if they don't
result = result.concat(result.map(p => points[anchor_lib.mirror(p.meta.name)]).filter(p => !!p))
}
} }
// otherwise, it is treated as a condition to filter all available points return result
return Object.values(points).filter(complex(config, name, units))
} }

View file

@ -4,10 +4,10 @@ const a = require('./assert')
const o = require('./operation') const o = require('./operation')
const Point = require('./point') const Point = require('./point')
const prep = require('./prepare') const prep = require('./prepare')
const anchor_lib = require('./anchor') const anchor = require('./anchor').parse
const filter = require('./filter').parse const filter = require('./filter').parse
const binding = (base, w, h, point, units) => { const binding = (base, point, units, origin=[0, 0]) => {
let bind = a.trbl(point.meta.bind || 0, `${point.meta.name}.bind`)(units) 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 it's a mirrored key, we swap the left and right bind values
@ -15,6 +15,10 @@ const binding = (base, w, h, point, units) => {
bind = [bind[0], bind[3], bind[2], bind[1]] bind = [bind[0], bind[3], bind[2], bind[1]]
} }
const bbox = m.measure.modelExtents(base)
// TODO transition to bbox + origin computation
const bt = h/2 + Math.max(bind[0], 0) const bt = h/2 + Math.max(bind[0], 0)
const br = w/2 + Math.max(bind[1], 0) const br = w/2 + Math.max(bind[1], 0)
const bd = -h/2 - Math.max(bind[2], 0) const bd = -h/2 - Math.max(bind[2], 0)
@ -28,7 +32,7 @@ const binding = (base, w, h, point, units) => {
return base return base
} }
const rectangle = (config, name, points, units) => { const rectangle = (config, name, points, outlines, units) => {
// prepare params // prepare params
a.unexpected(config, `${name}`, ['size', 'corner', 'bevel']) a.unexpected(config, `${name}`, ['size', 'corner', 'bevel'])
@ -41,7 +45,7 @@ const rectangle = (config, name, points, units) => {
const bevel = a.sane(params.bevel || 0, `${export_name}.bevel`, 'number')(rec_units) const bevel = a.sane(params.bevel || 0, `${export_name}.bevel`, 'number')(rec_units)
// return shape function // return shape function
return (point, bound, mirror) => { return (point, bound) => {
const error = (dim, val) => `Rectangle for "${name}" isn't ${dim} enough for its corner and bevel (${val} - 2 * ${corner} - 2 * ${bevel} <= 0)!` 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 [w, h] = size
@ -66,167 +70,94 @@ const rectangle = (config, name, points, units) => {
} }
if (corner > 0) rect = m.model.outline(rect, corner, 0) if (corner > 0) rect = m.model.outline(rect, corner, 0)
rect = m.model.moveRelative(res, [corner + bevel, corner + bevel]) rect = m.model.moveRelative(res, [corner + bevel, corner + bevel])
if (bound) rect = binding(rect, w, h, point, rec_units)
rect = point.position(rect)
let normal = u.deepcopy(rect) return 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)
}
} }
} }
const circle = (config, name, points, outlines, units) => {
// prepare params
a.unexpected(config, `${name}`, ['radius'])
const radius = a.sane(config.radius, `${name}.radius`, 'number')(units)
const circ_units = prep.extend({
r: radius
}, units)
// Actual layout // return shape function
return (point, bound) => {
let left = {models: {}} let circle = u.circle([0, 0], radius)
let right = {models: {}} if (bound) circle = binding(circle, radius, radius, point, circ_units)
if (['left', 'right', 'middle', 'both'].includes(side)) { circle = point.position(circle)
for (const [pname, p] of Object.entries(points)) { return circle
// filter by tags, if necessary
if (tags.length) {
const source = p.meta.tags || {}
const point_tags = Object.keys(source).filter(t => !!source[t])
const relevant = point_tags.some(pt => tags.includes(pt))
if (!relevant) continue
}
let from_x = -size[0] / 2, to_x = size[0] / 2
let from_y = -size[1] / 2, to_y = size[1] / 2
// the original position
let rect = rectangle(to_x - from_x, to_y - from_y, corner, bevel, `${export_name}.size`)
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`)(relative_units)
// 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 = u.union(right, rect)
} else {
left = u.union(left, rect)
}
}
}
if (side == 'left') return left
if (side == 'right') return right
// allow opting out of gluing, when
// A) there are no glue definitions, or
// B) glue is explicitly set to false
const glue_opt_out = (!Object.keys(parsed_glue).length || params.glue === false)
let glue = {models: {}}
if (bound && ['middle', 'both', 'glue'].includes(side) && !glue_opt_out) {
const default_glue_name = Object.keys(parsed_glue)[0]
const computed_glue_name = a.sane(params.glue || default_glue_name, `${export_name}.glue`, 'string')()
const glue_def = parsed_glue[computed_glue_name]
a.assert(glue_def, `Field "${export_name}.glue" does not name a valid glue!`)
const get_line = (anchor) => {
if (a.type(anchor)(relative_units) == 'number') {
return u.line([anchor, -1000], [anchor, 1000])
}
// if it wasn't a number, then it's a (possibly relative) anchor
const from = anchor(relative_units).clone()
const to = from.clone().shift([from.meta.mirrored ? -1 : 1, 0])
return u.line(from.p, to.p)
}
const tll = get_line(glue_def.top.left)
const trl = get_line(glue_def.top.right)
const tip = m.path.converge(tll, trl)
if (!tip) {
throw new Error(`Top lines don't intersect in glue "${computed_glue_name}"!`)
}
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(glue_def.bottom.left)
const brl = get_line(glue_def.bottom.right)
const bip = m.path.converge(bll, brl)
if (!bip) {
throw new Error(`Bottom lines don't intersect in glue "${computed_glue_name}"!`)
}
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 glue_def.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.width[0]
const right_x = center_x + w.width[1]
left_waypoints.push([left_x, center_y])
right_waypoints.unshift([right_x, center_y])
}
let waypoints
const is_split = a.type(glue_def.top.right)(relative_units) == '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)
}
if (side == 'glue') return glue
if (side == 'middle') {
let middle = u.subtract(glue, left)
middle = u.subtract(middle, right)
return middle
}
let both = u.union(u.deepcopy(left), glue)
both = u.union(both, u.deepcopy(right))
return both
} }
} }
const polygon = (config, name, points, outlines, units) => {
// prepare params
a.unexpected(config, `${name}`, ['points'])
const poly_points = a.sane(config.points, `${name}.points`, 'array')()
// return shape function
return (point, bound) => {
const parsed_points = []
let last_anchor = new Point()
let poly_index = -1
for (const poly_point of poly_points) {
const poly_name = `${name}.points[${++poly_index}]`
last_anchor = anchor(poly_point, poly_name, points, true, last_anchor)(units)
parsed_points.push(last_anchor.p)
}
let poly = u.poly(parsed_points)
const bbox = u.bbox(parsed_points)
if (bound) poly = binding(poly, bbox.high[0] - bbox.low[0], bbox.high[1] - bbox.low[1], point, units)
poly = point.position(poly)
return poly
}
}
const outline = (config, name, points, outlines, units) => {
// prepare params
a.unexpected(config, `${name}`, ['name', 'fillet', 'expand', 'origin'])
a.assert(outlines[config.name], `Field "${name}.name" does not name an existing outline!`)
const fillet = a.sane(config.fillet || 0, `${name}.fillet`, 'number')(units)
const expand = a.sane(config.expand || 0, `${name}.expand`, 'number')(units)
const origin = a.xy(config.origin, `${name}.origin`)(units)
// return shape function
return (point, bound) => {
let o = u.deepcopy(outlines[config.name])
o = m.model.moveRelative(o, [-origin[0], -origin[1]])
if (fillet) {
for (const [index, chain] of m.model.findChains(o).entries()) {
o.models[`fillet_${index}`] = m.chain.fillet(chain, fillet)
}
}
if (extend) {
// TODO
}
const bbox = m.measure.modelExtents(o)
if (bound) o = binding(o, bbox.high[0] - bbox.low[0], bbox.high[1] - bbox.low[1], point, units)
o = point.position(o)
return o
}
}
const whats = { const whats = {
rectangle, rectangle,
circle,
polygon,
outline
} }
exports.parse = (config = {}, points = {}, units = {}) => { exports.parse = (config = {}, points = {}, units = {}) => {
// output outlines will be collected here // output outlines will be collected here
@ -256,34 +187,32 @@ exports.parse = (config = {}, points = {}, units = {}) => {
} }
// process keys that are common to all part declarations // 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 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 bound = a.sane(part.bound === undefined ? true : part.bound, `${name}.bound`, 'boolean')()
const mirror = a.sane(part.mirror || false, `${name}.mirror`, 'boolean')() const mirror = a.sane(part.mirror || false, `${name}.mirror`, 'boolean')()
const what = a.in(part.what || 'outline', `${name}.what`, ['rectangle', 'circle', 'polygon', 'outline'])
// `where` is delayed until we have all, potentially what-dependent units
// default where is the single default anchor (at [0,0])
const where = units => filter(part.where || {}, `${name}.where`, points, units, mirror)
// which are then removed, so ops can check their own unexpected keys // these keys are then removed, so ops can check their own unexpected keys without interference
delete part.what
delete part.where
delete part.operation delete part.operation
delete part.bound delete part.bound
delete part.mirror delete part.mirror
delete part.what
delete part.where
// a prototype "shape" maker (and its units) are computed // a prototype "shape" maker (and its units) are computed
const [shape, shape_units] = whats[what](part, name, points, units) const [shape_maker, shape_units] = whats[what](part, name, points, outlines, units)
// and then repeated for all where positions // and then the shape is repeated for all where positions
for (const w of where(shape_units)) { for (const w of where(shape_units)) {
const [normal, mirrored] = shape(w, bound, mirror) const shape = shape_maker(w, bound)
outlines[outline_name] = operation(outlines[outline_name], normal) outlines[outline_name] = operation(outlines[outline_name], shape)
// and even their mirrors, if applicable
if (mirror) {
outlines[outline_name] = operation(outlines[outline_name], mirrored)
}
} }
} }
// final adjustments
m.model.originate(outlines[outline_name]) m.model.originate(outlines[outline_name])
m.model.simplify(outlines[outline_name]) m.model.simplify(outlines[outline_name])
@ -291,99 +220,3 @@ exports.parse = (config = {}, points = {}, units = {}) => {
return outlines return outlines
} }
// 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
// }

View file

@ -54,6 +54,20 @@ exports.poly = (arr) => {
return res return res
} }
exports.bbox = (arr) => {
let minx = Infinity
let miny = Infinity
let maxx = -Infinity
let maxy = -Infinity
for (const p of arr) {
minx = Math.min(minx, p[0])
miny = Math.min(miny, p[1])
maxx = Math.max(maxx, p[0])
maxy = Math.max(maxy, p[1])
}
return {low: [minx, miny], high: [maxx, maxy]}
}
const farPoint = [1234.1234, 2143.56789] const farPoint = [1234.1234, 2143.56789]
exports.union = exports.add = (a, b) => { exports.union = exports.add = (a, b) => {