diff --git a/src/filter.js b/src/filter.js index ca1ef01..397dfd3 100644 --- a/src/filter.js +++ b/src/filter.js @@ -123,7 +123,11 @@ exports.parse = (config, name, points={}, units={}, asym='source') => { } if (['clone', 'both'].includes(asym)) { // 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, undefined, true)(units)) + // also, we check for duplicates as ref-less anchors mirror to themselves + const clone = anchor(config, name, points, undefined, true)(units) + if (result.every(p => !p.equals(clone))) { + result.push(clone) + } } // otherwise, it is treated as a condition to filter all available points @@ -132,9 +136,15 @@ exports.parse = (config, name, points={}, units={}, asym='source') => { if (['source', 'both'].includes(asym)) { result = result.concat(source) } - if (['source', 'both'].includes(asym)) { + if (['clone', 'both'].includes(asym)) { // this is permissive: we only include mirrored versions if they exist, and don't fuss if they don't - result = result.concat(source.map(p => points[anchor_lib.mirror(p.meta.name)]).filter(p => !!p)) + // also, we check for duplicates as clones can potentially refer back to their sources, too + const pool = result.map(p => p.meta.name) + result = result.concat( + source.map(p => points[anchor_lib.mirror(p.meta.name)]) + .filter(p => !!p) + .filter(p => !pool.includes(p.meta.name)) + ) } } diff --git a/src/point.js b/src/point.js index 160e1fa..27e538a 100644 --- a/src/point.js +++ b/src/point.js @@ -76,4 +76,11 @@ module.exports = class Point { const dy = other.y - this.y return -Math.atan2(dx, dy) * (180 / Math.PI) } + + equals(other) { + return this.x === other.x + && this.y === other.y + && this.r === other.r + && JSON.stringify(this.meta) === JSON.stringify(other.meta) + } } diff --git a/test/pcbs/mock_footprints___pcbs_main.kicad_pcb b/test/pcbs/mock_footprints___pcbs_main.kicad_pcb index 7ce5985..6cde5ae 100644 --- a/test/pcbs/mock_footprints___pcbs_main.kicad_pcb +++ b/test/pcbs/mock_footprints___pcbs_main.kicad_pcb @@ -94,9 +94,9 @@ (net 0 "") (net 1 "P1") -(net 2 "T6_1") -(net 3 "T6_2") -(net 4 "T6_3") +(net 2 "T4_1") +(net 3 "T4_2") +(net 4 "T4_3") (net_class Default "This is the default net class." (clearance 0.2) @@ -107,9 +107,9 @@ (uvia_drill 0.1) (add_net "") (add_net "P1") -(add_net "T6_1") -(add_net "T6_2") -(add_net "T6_3") +(add_net "T4_1") +(add_net "T4_2") +(add_net "T4_3") ) @@ -148,40 +148,6 @@ - (module trace_test (layer B.Cu) (tedit 5CF31DEF) - - (at 19 -1 -30) - - (pad 1 smd rect (at 0 0 -30) (size 1 1) (layers B.Cu B.Paste B.Mask) - (net 1 "P1") (solder_mask_margin 0.2)) - - (pad 2 smd rect (at -5 5 -30) (size 1 1) (layers B.Cu B.Paste B.Mask) - (net 1 "P1") (solder_mask_margin 0.2)) - - ) - - (segment (start 19 -1) (end 12.169872999999999 0.8301270000000001) (width 0.475) (layer B.Cu) (net 1)) - - - - - (module trace_test (layer F.Cu) (tedit 5CF31DEF) - - (at 1 -1 30) - - (pad 1 smd rect (at 0 0 30) (size 1 1) (layers F.Cu F.Paste F.Mask) - (net 1 "P1") (solder_mask_margin 0.2)) - - (pad 2 smd rect (at 5 5 30) (size 1 1) (layers F.Cu F.Paste F.Mask) - (net 1 "P1") (solder_mask_margin 0.2)) - - ) - - (segment (start 1 -1) (end 7.830127 0.8301270000000001) (width 0.475) (layer F.Cu) (net 1)) - - - - (module zone_test (layer F.Cu) (tedit 5CF31DEF) (at 1 -1 30) @@ -209,13 +175,13 @@ (at 0 0 0) (pad 1 smd rect (at 0 0 0) (size 1 1) (layers F.Cu F.Paste F.Mask) - (net 2 "T6_1") (solder_mask_margin 0.2)) + (net 2 "T4_1") (solder_mask_margin 0.2)) (pad 1 smd rect (at 0 0 0) (size 1 1) (layers F.Cu F.Paste F.Mask) - (net 3 "T6_2") (solder_mask_margin 0.2)) + (net 3 "T4_2") (solder_mask_margin 0.2)) (pad 1 smd rect (at 0 0 0) (size 1 1) (layers F.Cu F.Paste F.Mask) - (net 4 "T6_3") (solder_mask_margin 0.2)) + (net 4 "T4_3") (solder_mask_margin 0.2)) ) diff --git a/test/unit/filter.js b/test/unit/filter.js index 6097150..c040687 100644 --- a/test/unit/filter.js +++ b/test/unit/filter.js @@ -4,48 +4,56 @@ const Point = require('../../src/point') describe('Filter', function() { - it('empty', function() { - filter(undefined, '').should.deep.equal([new Point()]) - filter(true, '').should.deep.equal([]) - filter(false, '').should.deep.equal([]) + it('without points', function() { + filter(undefined, '').should.deep.equal([new Point()]) + filter(true, '').should.deep.equal([]) + filter(false, '').should.deep.equal([]) + filter({}, '').should.deep.equal([anchor({}, '', points)()]) }) const points = { one: new Point(0, 1, 0, {name: 'one', tags: ['odd']}), two: new Point(0, 2, 0, {name: 'two', tags: ['even', 'prime']}), - three: new Point(0, 3, 0, {name: 'three', tags: {odd: 'yes', prime: 'yupp'}}) + three: new Point(0, 3, 0, {name: 'three', tags: {odd: 'yes', prime: 'yupp'}}), + mirror_one: new Point(0, 1, 0, {name: 'mirror_one', tags: ['odd'], mirrored: true}) } - const names = points => points.map(p => p.meta.name) - - it('similar', function() { + it('empty filter', function() { // an undefined config leads to a default point filter(undefined, '', points).should.deep.equal([new Point()]) // true shouldn't filter anything, while false should filter everything filter(true, '', points).should.deep.equal(Object.values(points)) filter(false, '', points).should.deep.equal([]) // points should only be returned on their respective halves + // - so `source` is every match filter(true, '', points, undefined, 'source').should.deep.equal(Object.values(points)) - filter(true, '', points, undefined, 'clone').should.deep.equal([]) + // - `clone` is the mirror image of every match, which maps one to mirror_one, mirror_one to one, and two/three to nothing (as they don't have mirror parts) + filter(true, '', points, undefined, 'clone').should.deep.equal([points.mirror_one, points.one]) + // - and `both` is every match plus its mirror image as well filter(true, '', points, undefined, 'both').should.deep.equal(Object.values(points)) // objects just propagate to anchor (and then wrap in array for consistency) filter({}, '', points).should.deep.equal([anchor({}, '', points)()]) filter({}, '', points, undefined, 'source').should.deep.equal([anchor({}, '', points)()]) filter({}, '', points, undefined, 'clone').should.deep.equal([anchor({}, '', points)()]) - filter({}, '', points, undefined, 'both').should.deep.equal([anchor({}, '', points)(), anchor({}, '', points)()]) + filter({}, '', points, undefined, 'both').should.deep.equal([anchor({}, '', points)()]) + }) + + const names = points => points.map(p => p.meta.name) + + it('similar', function() { // simple name string names(filter('one', '', points)).should.deep.equal(['one']) // simple name regex names(filter('/^t/', '', points)).should.deep.equal(['two', 'three']) - // tags should count, too (one for the name, three for the odd tag) - names(filter('/^o/', '', points)).should.deep.equal(['one', 'three']) + // tags should count, too (one and mirror_one for the name, three for the odd tag) + names(filter('/^o/', '', points)).should.deep.equal(['one', 'three', 'mirror_one']) // middle spec, should be the same as above, only explicit - names(filter('~ /^o/', '', points)).should.deep.equal(['one', 'three']) + names(filter('~ /^o/', '', points)).should.deep.equal(['one', 'three', 'mirror_one']) // full spec (n would normally match both one and even, but on the tags level, it's just even) names(filter('meta.tags ~ /n/', '', points)).should.deep.equal(['two']) // negation - names(filter('meta.tags ~ -/n/', '', points)).should.deep.equal(['one', 'three']) - // arrays OR by default at odd levels levels (including top level)... + names(filter('meta.tags ~ -/n/', '', points)).should.deep.equal(['one', 'three', 'mirror_one']) + // arrays OR by default at odd levels (including top level)... names(filter(['one', 'two'], '', points)).should.deep.equal(['one', 'two']) // ...and AND at even levels names(filter([['even', 'prime']], '', points)).should.deep.equal(['two'])