From 6dc6b5d8e9edf6777bd1d9b91706319132d5b101 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A1n=20D=C3=A9nes?= Date: Sun, 27 Feb 2022 11:11:45 +0100 Subject: [PATCH] Anchor recursivization --- src/anchor.js | 85 +++++++++++++++++++++++++++-------- src/filter.js | 2 +- src/outlines.js | 2 +- src/pcbs.js | 6 +-- test/outlines/circles.yaml | 2 +- test/outlines/polygons.yaml | 2 +- test/outlines/rectangles.yaml | 2 +- test/unit/anchor.js | 63 ++++++++++++++++++-------- 8 files changed, 119 insertions(+), 45 deletions(-) diff --git a/src/anchor.js b/src/anchor.js index 92e52b6..031b50b 100644 --- a/src/anchor.js +++ b/src/anchor.js @@ -12,37 +12,85 @@ const mirror_ref = exports.mirror = (ref, mirror=true) => { return ref } -const anchor = exports.parse = (raw, name, points={}, check_unexpected=true, default_point=new Point(), mirror=false) => units => { - if (a.type(raw)() == 'array') { +const aggregator_common = ['parts', 'method'] + +const aggregators = { + average: (config, name, parts) => { + a.unexpected(config, name, aggregator_common) + let x = 0, y = 0, r = 0 + const len = parts.length + for (const part of parts) { + x += part.x + y += part.y + r += part.r + } + return new Point(x / len, y / len, r / len) + } +} + +const anchor = exports.parse = (raw, name, points={}, default_point=new Point(), mirror=false) => units => { + + // + // Anchor type handling + // + + if (a.type(raw)() == 'string') { + raw = {ref: raw} + } + + else if (a.type(raw)() == 'array') { // recursive call with incremental default_point mods, according to `affect`s let current = default_point.clone() + let index = 1 for (const step of raw) { - current = anchor(step, name, points, check_unexpected, current, mirror)(units) + current = anchor(step, `${name}[${index++}]`, points, current, mirror)(units) } return current } - if (check_unexpected) a.unexpected(raw, name, ['ref', 'orient', 'shift', 'rotate', 'affect']) + + a.unexpected(raw, name, ['ref', 'aggregate', 'orient', 'shift', 'rotate', 'affect']) + + // + // Reference or aggregate handling + // + let point = default_point.clone() + if (raw.ref !== undefined && raw.aggregate !== undefined) { + throw new Error(`Fields "ref" and "aggregate" cannot appear together in anchor "${name}"!`) + } + if (raw.ref !== undefined) { - if (a.type(raw.ref)() == 'array') { - // averaging multiple anchors - let x = 0, y = 0, r = 0 - const len = raw.ref.length - for (const ref of raw.ref) { - const parsed_ref = mirror_ref(ref, mirror) - a.assert(points[parsed_ref], `Unknown point reference "${parsed_ref}" in anchor "${name}"!`) - const resolved = points[parsed_ref] - x += resolved.x - y += resolved.y - r += resolved.r - } - point = new Point(x / len, y / len, r / len) - } else { + // base case, resolve directly + if (a.type(raw.ref)() == 'string') { const parsed_ref = mirror_ref(raw.ref, mirror) a.assert(points[parsed_ref], `Unknown point reference "${parsed_ref}" in anchor "${name}"!`) point = points[parsed_ref].clone() + // recursive case + } else { + point = anchor(raw.ref, `${name}.ref`, points, default_point, mirror)(units) } + } + + if (raw.aggregate !== undefined) { + raw.aggregate = a.sane(raw.aggregate, `${name}.aggregate`, 'object')() + raw.aggregate.method = a.sane(raw.aggregate.method || 'average', `${name}.aggregate.method`, 'string')() + a.assert(aggregators[raw.aggregate.method], `Unknown aggregator method "${raw.aggregate.method}" in anchor "${name}"!`) + raw.aggregate.parts = a.sane(raw.aggregate.parts || [], `${name}.aggregate.parts`, 'array')() + + const parts = [] + let index = 1 + for (const part of raw.aggregate.parts) { + parts.push(anchor(part, `${name}.aggregate.parts[${index++}]`, points, default_point, mirror)(units)) + } + + point = aggregators[raw.aggregate.method](raw.aggregate, `${name}.aggregate`, parts) + } + + // + // Actual orient/shift/rotate/affect handling + // + if (raw.orient !== undefined) { let angle = a.sane(raw.orient, `${name}.orient`, 'number')(units) if (point.meta.mirrored) { @@ -77,5 +125,6 @@ const anchor = exports.parse = (raw, name, points={}, check_unexpected=true, def point[aff] = candidate[aff] } } + return point } \ No newline at end of file diff --git a/src/filter.js b/src/filter.js index 2d36df9..cbece43 100644 --- a/src/filter.js +++ b/src/filter.js @@ -121,7 +121,7 @@ exports.parse = (config, name, points={}, units={}, include_mirrors=false) => { 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)) + result.push(anchor(config, name, points, undefined, true)(units)) } // otherwise, it is treated as a condition to filter all available points diff --git a/src/outlines.js b/src/outlines.js index b49288b..66e0849 100644 --- a/src/outlines.js +++ b/src/outlines.js @@ -112,7 +112,7 @@ const polygon = (config, name, points, outlines, units) => { 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) + last_anchor = anchor(poly_point, poly_name, points, last_anchor)(units) parsed_points.push(last_anchor.p) } let poly = u.poly(parsed_points) diff --git a/src/pcbs.js b/src/pcbs.js index 90c0a82..5eb0714 100644 --- a/src/pcbs.js +++ b/src/pcbs.js @@ -156,7 +156,7 @@ const footprint = exports._footprint = (config, name, points, point, net_indexer // config sanitization a.unexpected(config, name, ['type', 'anchor', 'nets', 'anchors', 'params']) const type = a.in(config.type, `${name}.type`, Object.keys(footprint_types)) - let anchor = anchor_lib.parse(config.anchor || {}, `${name}.anchor`, points, true, point)(units) + let anchor = anchor_lib.parse(config.anchor || {}, `${name}.anchor`, points, point)(units) const nets = a.sane(config.nets || {}, `${name}.nets`, 'object')() const anchors = a.sane(config.anchors || {}, `${name}.anchors`, 'object')() const params = a.sane(config.params || {}, `${name}.params`, 'object')() @@ -189,7 +189,7 @@ const footprint = exports._footprint = (config, name, points, point, net_indexer parsed_params.xy = (x, y) => { const new_anchor = anchor_lib.parse({ shift: [x, -y] - }, '_internal_footprint_xy', points, true, anchor)(units) + }, '_internal_footprint_xy', points, anchor)(units) return `${new_anchor.x} ${-new_anchor.y}` } @@ -224,7 +224,7 @@ const footprint = exports._footprint = (config, name, points, point, net_indexer // parsing anchor-type parameters parsed_params.anchors = {} for (const [anchor_name, anchor_config] of Object.entries(prep.extend(fp.anchors || {}, anchors))) { - let parsed_anchor = anchor_lib.parse(anchor_config || {}, `${name}.anchors.${anchor_name}`, points, true, anchor)(units) + let parsed_anchor = anchor_lib.parse(anchor_config || {}, `${name}.anchors.${anchor_name}`, points, anchor)(units) parsed_anchor.y = -parsed_anchor.y parsed_params.anchors[anchor_name] = parsed_anchor } diff --git a/test/outlines/circles.yaml b/test/outlines/circles.yaml index aa7af33..f1aaa51 100644 --- a/test/outlines/circles.yaml +++ b/test/outlines/circles.yaml @@ -12,7 +12,7 @@ outlines: middle_circle: what: circle where: - ref: + aggregate.parts: - matrix - mirror_matrix radius: 15 diff --git a/test/outlines/polygons.yaml b/test/outlines/polygons.yaml index cc094b8..09bf345 100644 --- a/test/outlines/polygons.yaml +++ b/test/outlines/polygons.yaml @@ -11,7 +11,7 @@ outlines: bound: false middle_poly: what: polygon - where.ref: + where.aggregate.parts: - matrix - mirror_matrix points: diff --git a/test/outlines/rectangles.yaml b/test/outlines/rectangles.yaml index 86e2e95..df2cab0 100644 --- a/test/outlines/rectangles.yaml +++ b/test/outlines/rectangles.yaml @@ -12,7 +12,7 @@ outlines: middle_rect: what: rectangle where: - ref: + aggregate.parts: - matrix - mirror_matrix shift: [0, sy/2] diff --git a/test/unit/anchor.js b/test/unit/anchor.js index b335840..0cde3ee 100644 --- a/test/unit/anchor.js +++ b/test/unit/anchor.js @@ -6,8 +6,8 @@ describe('Anchor', function() { const points = { o: new Point(0, 0, 0, {label: 'o'}), - ten: new Point(10, 10, 10, {label: 'ten'}), - mirror: new Point(20, 0, 0, {mirrored: true}) + ten: new Point(10, 10, -90, {label: 'ten'}), + mirror_ten: new Point(-10, 10, 90, {mirrored: true}) } it('params', function() { @@ -16,28 +16,45 @@ describe('Anchor', function() { parse({}, 'name')(), [0, 0, 0, {}] ) - // unexpected check can be disabled - check( - parse({unexpected_key: true}, 'name', {}, false)(), - [0, 0, 0, {}] - ) - // default point can be overridden - check( - parse({}, 'name', {}, true, new Point(1, 1))(), - [1, 1, 0, {}] - ) - }) - - it('ref', function() { // single reference check( parse({ref: 'o'}, 'name', points)(), [0, 0, 0, {label: 'o'}] ) + // default point can be overridden + check( + parse({}, 'name', {}, new Point(1, 1))(), + [1, 1, 0, {}] + ) + // mirrored references can be forced + check( + parse({ref: 'ten'}, 'name', points, undefined, true)(), + [-10, 10, 90, {mirrored: true}] + ) + }) + + it('recursive', function() { + // recursive references are supported (keeping metadata) + check( + parse({ + ref: { + ref: 'o', + shift: [2, 2] + } + }, 'name', points)(), + [2, 2, 0, {label: 'o'}] + ) + }) + + it('aggregate', function() { // average of multiple references (metadata gets ignored) check( - parse({ref: ['o', 'ten']}, 'name', points)(), - [5, 5, 5, {}] + parse({ + aggregate: { + parts: ['o', 'ten'] + } + }, 'name', points)(), + [5, 5, -45, {}] ) }) @@ -49,8 +66,8 @@ describe('Anchor', function() { ) // shift should respect mirrored points (and invert along the x axis) check( - parse({ref: 'mirror', shift: [1, 1]}, 'name', points)(), - [19, 1, 0, {mirrored: true}] + parse({ref: 'mirror_ten', shift: [1, 1]}, 'name', points)(), + [-11, 9, 90, {mirrored: true}] ) }) @@ -99,6 +116,14 @@ describe('Anchor', function() { ) }) + it('string', function() { + // basic string form + check( + parse('ten', 'name', points)(), + [10, 10, -90, {label: 'ten'}] + ) + }) + it('array', function() { // basic multi-anchor check(