Outlining improvements

This commit is contained in:
Bán Dénes 2022-05-29 20:25:52 +02:00
parent 5a25c1c423
commit 5e68bdb630
5 changed files with 92 additions and 80 deletions

View file

@ -6,9 +6,6 @@
### Major ### Major
- Key-level access to full anchors
- this could provide extra variables `padding`, `spread`, `splay` for custom layout purposes
- make row anchors cumulative, too (like columns), so fingers arcs and other edits can happen
- Restructure pcb point/footprint filtering - Restructure pcb point/footprint filtering
- Use the same `what`/`where` infrastructure as outlines - Use the same `what`/`where` infrastructure as outlines
- Collapse params/nets/anchors into a single hierarchy from the user's POV - Collapse params/nets/anchors into a single hierarchy from the user's POV
@ -30,12 +27,8 @@
- Allow footprints to publish outlines - Allow footprints to publish outlines
- Make these usable in the `outlines` section through a new `what` - Make these usable in the `outlines` section through a new `what`
- 3D orient for cases - 3D orient for cases
- Allow a generic `adjust` field for outlines that accepts an anchor
- This could swallow `origin` from `outline`
- Post-process anchor for global (post-mirror!) orient/shift/rotate for everything
- Even more extreme anchor stuff - Even more extreme anchor stuff
- Checkpoints, intersects, distances, weighted combinations? - Checkpoints, intersects, distances, weighted combinations?
- Allow both object (as well as arrays) in multiple anchor refs
- SVG input (for individual outlines, or even combinations parsed by line color, etc.) - SVG input (for individual outlines, or even combinations parsed by line color, etc.)
- And once that's done, possibly even STL or other input for cases or pcb renders - And once that's done, possibly even STL or other input for cases or pcb renders
- Support text silk output to PCBs (in configurable fonts, through SVG?) - Support text silk output to PCBs (in configurable fonts, through SVG?)
@ -44,7 +37,6 @@
- Support curves (arcs as well as Béziers) in polygons - Support curves (arcs as well as Béziers) in polygons
- Add snappable line footprint - Add snappable line footprint
- Figure out a manual, but still reasonably comfortable routing method directly from the config - Figure out a manual, but still reasonably comfortable routing method directly from the config
- Add filleting syntax with `@`?
- Eeschema support for pcbs - Eeschema support for pcbs
- Generate ZMK shield from config - Generate ZMK shield from config
- Export **to** KLE? - Export **to** KLE?
@ -53,11 +45,7 @@
- Look into kicad 5 vs. 6 output format - Look into kicad 5 vs. 6 output format
- Update json schema and add syntax highlight to editors - Update json schema and add syntax highlight to editors
- Support different netclasses - Support different netclasses
- `round`, `pointy` and `beveled` symbolic constants for expand joint types
- Also, string shorthands like `3)`, `5>` and `10]`
- Allow a potential filter for filleting (only on angles =90°, <45°, left turn vs. right turn when going clockwise, etc.) - Allow a potential filter for filleting (only on angles =90°, <45°, left turn vs. right turn when going clockwise, etc.)
- Support cumulative handling of outline parts (i.e., add `fillet` as an generic option that applies to all the parts up to that point)
- Similar with adjust
### Patch ### Patch

View file

@ -69,7 +69,6 @@ const anchor = exports.parse = (raw, name, points={}, default_point=new Point(),
} else { } else {
point = anchor(raw.ref, `${name}.ref`, points, default_point, mirror)(units) point = anchor(raw.ref, `${name}.ref`, points, default_point, mirror)(units)
} }
} }
if (raw.aggregate !== undefined) { if (raw.aggregate !== undefined) {
@ -95,10 +94,7 @@ const anchor = exports.parse = (raw, name, points={}, default_point=new Point(),
// simple case: number gets added to point rotation // simple case: number gets added to point rotation
if (a.type(config)(units) == 'number') { if (a.type(config)(units) == 'number') {
let angle = a.sane(config, name, 'number')(units) let angle = a.sane(config, name, 'number')(units)
if (point.meta.mirrored) { point.rotate(angle, false)
angle = -angle
}
point.r += angle
// recursive case: points turns "towards" target anchor // recursive case: points turns "towards" target anchor
} else { } else {
const target = anchor(config, name, points, default_point, mirror)(units) const target = anchor(config, name, points, default_point, mirror)(units)
@ -111,10 +107,7 @@ const anchor = exports.parse = (raw, name, points={}, default_point=new Point(),
} }
if (raw.shift !== undefined) { if (raw.shift !== undefined) {
let xyval = a.wh(raw.shift, `${name}.shift`)(units) let xyval = a.wh(raw.shift, `${name}.shift`)(units)
if (point.meta.mirrored) { point.shift(xyval)
xyval[0] = -xyval[0]
}
point.shift(xyval, true)
} }
if (raw.rotate !== undefined) { if (raw.rotate !== undefined) {
rotator(raw.rotate, `${name}.rotate`, point) rotator(raw.rotate, `${name}.rotate`, point)

View file

@ -1,10 +1,16 @@
const op_prefix = exports.op_prefix = str => { const op_prefix = exports.op_prefix = str => {
const prefix = str[0]
const suffix = str.slice(1) const suffix = str.slice(1)
if (str.startsWith('+')) return {name: suffix, operation: 'add'} const result = {name: suffix, operation: 'add'}
if (str.startsWith('-')) return {name: suffix, operation: 'subtract'}
if (str.startsWith('~')) return {name: suffix, operation: 'intersect'} if (prefix == '+') ; // noop
if (str.startsWith('^')) return {name: suffix, operation: 'stack'} else if (prefix == '-') result.operation = 'subtract'
return {name: str, operation: 'add'} else if (prefix == '~') result.operation = 'intersect'
else if (prefix == '^') result.operation = 'stack'
else result.name = str // no prefix, so the name was the whole string
return result
} }
exports.operation = (str, choices={}, order=Object.keys(choices)) => { exports.operation = (str, choices={}, order=Object.keys(choices)) => {

View file

@ -41,7 +41,7 @@ const rectangle = (config, name, points, outlines, units) => {
const bevel = a.sane(config.bevel || 0, `${name}.bevel`, 'number')(rec_units) const bevel = a.sane(config.bevel || 0, `${name}.bevel`, 'number')(rec_units)
// return shape function and its units // return shape function and its units
return [(point, bound) => { return [() => {
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,13 +66,9 @@ const rectangle = (config, name, points, outlines, 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(rect, [-cw/2, -ch/2]) rect = m.model.moveRelative(rect, [-cw/2, -ch/2])
if (bound) { const bbox = {high: [w/2, h/2], low: [-w/2, -h/2]}
const bbox = {high: [w/2, h/2], low: [-w/2, -h/2]}
rect = binding(rect, bbox, point, rec_units)
}
rect = point.position(rect)
return rect return [rect, bbox]
}, rec_units] }, rec_units]
} }
@ -86,14 +82,10 @@ const circle = (config, name, points, outlines, units) => {
}, units) }, units)
// return shape function and its units // return shape function and its units
return [(point, bound) => { return [() => {
let circle = u.circle([0, 0], radius) let circle = u.circle([0, 0], radius)
if (bound) { const bbox = {high: [radius, radius], low: [-radius, -radius]}
const bbox = {high: [radius, radius], low: [-radius, -radius]} return [circle, bbox]
circle = binding(circle, bbox, point, circ_units)
}
circle = point.position(circle)
return circle
}, circ_units] }, circ_units]
} }
@ -104,10 +96,10 @@ const polygon = (config, name, points, outlines, units) => {
const poly_points = a.sane(config.points, `${name}.points`, 'array')() const poly_points = a.sane(config.points, `${name}.points`, 'array')()
// return shape function and its units // return shape function and its units
return [(point, bound) => { return [point => {
const parsed_points = [] const parsed_points = []
// the point starts at [0, 0] as it will be positioned later // the poly starts at [0, 0] as it will be positioned later
// but we keep the metadata for potential mirroring purposes // but we keep the point metadata for potential mirroring purposes
let last_anchor = new Point(0, 0, 0, point.meta) let last_anchor = new Point(0, 0, 0, point.meta)
let poly_index = -1 let poly_index = -1
for (const poly_point of poly_points) { for (const poly_point of poly_points) {
@ -116,52 +108,24 @@ const polygon = (config, name, points, outlines, units) => {
parsed_points.push(last_anchor.p) parsed_points.push(last_anchor.p)
} }
let poly = u.poly(parsed_points) let poly = u.poly(parsed_points)
if (bound) { const bbox = u.bbox(parsed_points)
const bbox = u.bbox(parsed_points) return [poly, bbox]
poly = binding(poly, bbox, point, units)
}
poly = point.position(poly)
return poly
}, units] }, units]
} }
const outline = (config, name, points, outlines, units) => { const outline = (config, name, points, outlines, units) => {
// prepare params // prepare params
a.unexpected(config, `${name}`, ['name', 'fillet', 'expand', 'origin', 'scale']) a.unexpected(config, `${name}`, ['name', 'origin'])
a.assert(outlines[config.name], `Field "${name}.name" does not name an existing outline!`) 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 joints = a.in(a.sane(config.joints || 0, `${name}.joints`, 'number')(units), `${name}.joints`, [0, 1, 2])
const origin = anchor(config.origin || {}, `${name}.origin`, points)(units) const origin = anchor(config.origin || {}, `${name}.origin`, points)(units)
const scale = a.sane(config.scale || 1, `${name}.scale`, 'number')(units)
// return shape function and its units // return shape function and its units
return [(point, bound) => { return [() => {
let o = u.deepcopy(outlines[config.name]) let o = u.deepcopy(outlines[config.name])
o = origin.unposition(o) o = origin.unposition(o)
const bbox = m.measure.modelExtents(o)
if (scale !== 1) { return [o, bbox]
o = m.model.scale(o, scale)
}
if (fillet) {
for (const [index, chain] of m.model.findChains(o).entries()) {
o.models[`fillet_${index}`] = m.chain.fillet(chain, fillet)
}
}
if (expand) {
o = m.model.outline(o, Math.abs(expand), joints, (expand < 0), {farPoint: u.farPoint})
}
if (bound) {
const bbox = m.measure.modelExtents(o)
o = binding(o, bbox, point, units)
}
o = point.position(o)
return o
}, units] }, units]
} }
@ -172,6 +136,29 @@ const whats = {
outline outline
} }
const expand_shorthand = (config, units) => {
if (a.type(config.expand)(units) == 'string') {
const prefix = config.expand.slice(0, -1)
const suffix = config.expand.slice(-1)
let expand = suffix
let joints = 0
if (suffix == ')') ; // noop
else if (suffix == '>') joints = 1
else if (suffix == ']') joints = 2
else expand = config.expand
config.expand = parseFloat(expand)
config.joints = config.joints || joints
}
if (a.type(config.joints)(units) == 'string') {
if (config.joints == 'round') config.joints = 0
if (config.joints == 'pointy') config.joints = 1
if (config.joints == 'beveled') config.joints = 2
}
}
exports.parse = (config = {}, points = {}, units = {}) => { exports.parse = (config = {}, points = {}, units = {}) => {
// output outlines will be collected here // output outlines will be collected here
@ -205,26 +192,60 @@ exports.parse = (config = {}, points = {}, units = {}) => {
const what = a.in(part.what || 'outline', `${name}.what`, ['rectangle', 'circle', 'polygon', 'outline']) const what = a.in(part.what || 'outline', `${name}.what`, ['rectangle', 'circle', 'polygon', 'outline'])
const bound = !!part.bound const bound = !!part.bound
const mirror = a.sane(part.mirror || false, `${name}.mirror`, 'boolean')() const mirror = a.sane(part.mirror || false, `${name}.mirror`, 'boolean')()
// `where` is delayed until we have all, potentially what-dependent units // `where` is delayed until we have all, potentially what-dependent units
// default where is [0, 0], as per filter parsing // default where is [0, 0], as per filter parsing
const original_where = part.where // need to save, so the delete's don't get rid of it below const original_where = part.where // need to save, so the delete's don't get rid of it below
const where = units => filter(original_where, `${name}.where`, points, units, mirror) const where = units => filter(original_where, `${name}.where`, points, units, mirror)
const adjust = anchor(part.adjust || {}, `${name}.adjust`, points)(units)
const fillet = a.sane(part.fillet || 0, `${name}.fillet`, 'number')(units)
expand_shorthand(part, units)
const expand = a.sane(part.expand || 0, `${name}.expand`, 'number')(units)
const joints = a.in(a.sane(part.joints || 0, `${name}.joints`, 'number')(units), `${name}.joints`, [0, 1, 2])
const scale = a.sane(part.scale || 1, `${name}.scale`, 'number')(units)
// these keys are then removed, so ops can check their own unexpected keys without interference // these keys are then removed, so ops can check their own unexpected keys without interference
delete part.operation delete part.operation
delete part.what delete part.what
delete part.bound delete part.bound
delete part.mirror delete part.mirror
delete part.where delete part.where
delete part.adjust
delete part.fillet
delete part.expand
delete part.joints
delete part.scale
// a prototype "shape" maker (and its units) are computed // a prototype "shape" maker (and its units) are computed
const [shape_maker, shape_units] = whats[what](part, name, points, outlines, units) const [shape_maker, shape_units] = whats[what](part, name, points, outlines, units)
// and then the shape is 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 shape = shape_maker(w, bound) const point = w.clone().shift(adjust.p).rotate(adjust.r, false)
let [shape, bbox] = shape_maker(point) // point is passed for mirroring metadata only...
if (bound) {
shape = binding(shape, bbox, point, shape_units)
}
shape = point.position(shape) // ...actual positioning happens here
outlines[outline_name] = operation(outlines[outline_name], shape) outlines[outline_name] = operation(outlines[outline_name], shape)
} }
if (scale !== 1) {
outlines[outline_name] = m.model.scale(outlines[outline_name], scale)
}
if (expand) {
outlines[outline_name] = m.model.outline(
outlines[outline_name], Math.abs(expand), joints, (expand < 0), {farPoint: u.farPoint}
)
}
if (fillet) {
for (const [index, chain] of m.model.findChains(outlines[outline_name]).entries()) {
outlines[outline_name].models[`fillet_${part_name}_${index}`] = m.chain.fillet(chain, fillet)
}
}
} }
// final adjustments // final adjustments

View file

@ -25,6 +25,7 @@ module.exports = class Point {
} }
shift(s, relative=true) { shift(s, relative=true) {
s[0] *= this.meta.mirrored ? -1 : 1
if (relative) { if (relative) {
s = m.point.rotate(s, this.r) s = m.point.rotate(s, this.r)
} }
@ -34,7 +35,10 @@ module.exports = class Point {
} }
rotate(angle, origin=[0, 0]) { rotate(angle, origin=[0, 0]) {
this.p = m.point.rotate(this.p, angle, origin) angle *= this.meta.mirrored ? -1 : 1
if (origin) {
this.p = m.point.rotate(this.p, angle, origin)
}
this.r += angle this.r += angle
return this return this
} }