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
- 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
- Use the same `what`/`where` infrastructure as outlines
- Collapse params/nets/anchors into a single hierarchy from the user's POV
@ -30,12 +27,8 @@
- Allow footprints to publish outlines
- Make these usable in the `outlines` section through a new `what`
- 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
- 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.)
- 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?)
@ -44,7 +37,6 @@
- Support curves (arcs as well as Béziers) in polygons
- Add snappable line footprint
- Figure out a manual, but still reasonably comfortable routing method directly from the config
- Add filleting syntax with `@`?
- Eeschema support for pcbs
- Generate ZMK shield from config
- Export **to** KLE?
@ -53,11 +45,7 @@
- Look into kicad 5 vs. 6 output format
- Update json schema and add syntax highlight to editors
- 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.)
- 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

View file

@ -69,7 +69,6 @@ const anchor = exports.parse = (raw, name, points={}, default_point=new Point(),
} else {
point = anchor(raw.ref, `${name}.ref`, points, default_point, mirror)(units)
}
}
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
if (a.type(config)(units) == 'number') {
let angle = a.sane(config, name, 'number')(units)
if (point.meta.mirrored) {
angle = -angle
}
point.r += angle
point.rotate(angle, false)
// recursive case: points turns "towards" target anchor
} else {
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) {
let xyval = a.wh(raw.shift, `${name}.shift`)(units)
if (point.meta.mirrored) {
xyval[0] = -xyval[0]
}
point.shift(xyval, true)
point.shift(xyval)
}
if (raw.rotate !== undefined) {
rotator(raw.rotate, `${name}.rotate`, point)

View file

@ -1,10 +1,16 @@
const op_prefix = exports.op_prefix = str => {
const prefix = str[0]
const suffix = str.slice(1)
if (str.startsWith('+')) return {name: suffix, operation: 'add'}
if (str.startsWith('-')) return {name: suffix, operation: 'subtract'}
if (str.startsWith('~')) return {name: suffix, operation: 'intersect'}
if (str.startsWith('^')) return {name: suffix, operation: 'stack'}
return {name: str, operation: 'add'}
const result = {name: suffix, operation: 'add'}
if (prefix == '+') ; // noop
else if (prefix == '-') result.operation = 'subtract'
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)) => {

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)
// 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 [w, h] = size
@ -66,13 +66,9 @@ const rectangle = (config, name, points, outlines, units) => {
}
if (corner > 0) rect = m.model.outline(rect, corner, 0)
rect = m.model.moveRelative(rect, [-cw/2, -ch/2])
if (bound) {
const bbox = {high: [w/2, h/2], low: [-w/2, -h/2]}
rect = binding(rect, bbox, point, rec_units)
}
rect = point.position(rect)
const bbox = {high: [w/2, h/2], low: [-w/2, -h/2]}
return rect
return [rect, bbox]
}, rec_units]
}
@ -86,14 +82,10 @@ const circle = (config, name, points, outlines, units) => {
}, units)
// return shape function and its units
return [(point, bound) => {
return [() => {
let circle = u.circle([0, 0], radius)
if (bound) {
const bbox = {high: [radius, radius], low: [-radius, -radius]}
circle = binding(circle, bbox, point, circ_units)
}
circle = point.position(circle)
return circle
const bbox = {high: [radius, radius], low: [-radius, -radius]}
return [circle, bbox]
}, circ_units]
}
@ -104,10 +96,10 @@ const polygon = (config, name, points, outlines, units) => {
const poly_points = a.sane(config.points, `${name}.points`, 'array')()
// return shape function and its units
return [(point, bound) => {
return [point => {
const parsed_points = []
// the point starts at [0, 0] as it will be positioned later
// but we keep the metadata for potential mirroring purposes
// the poly starts at [0, 0] as it will be positioned later
// but we keep the point metadata for potential mirroring purposes
let last_anchor = new Point(0, 0, 0, point.meta)
let poly_index = -1
for (const poly_point of poly_points) {
@ -116,52 +108,24 @@ const polygon = (config, name, points, outlines, units) => {
parsed_points.push(last_anchor.p)
}
let poly = u.poly(parsed_points)
if (bound) {
const bbox = u.bbox(parsed_points)
poly = binding(poly, bbox, point, units)
}
poly = point.position(poly)
return poly
const bbox = u.bbox(parsed_points)
return [poly, bbox]
}, units]
}
const outline = (config, name, points, outlines, units) => {
// 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!`)
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 scale = a.sane(config.scale || 1, `${name}.scale`, 'number')(units)
// return shape function and its units
return [(point, bound) => {
return [() => {
let o = u.deepcopy(outlines[config.name])
o = origin.unposition(o)
if (scale !== 1) {
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
const bbox = m.measure.modelExtents(o)
return [o, bbox]
}, units]
}
@ -172,6 +136,29 @@ const whats = {
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 = {}) => {
// 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 bound = !!part.bound
const mirror = a.sane(part.mirror || false, `${name}.mirror`, 'boolean')()
// `where` is delayed until we have all, potentially what-dependent units
// 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 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
delete part.operation
delete part.what
delete part.bound
delete part.mirror
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
const [shape_maker, shape_units] = whats[what](part, name, points, outlines, units)
// and then the shape is repeated for all where positions
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)
}
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

View file

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