kisch 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,152 @@
1
+ import {Point} from "../src/cartesian-math.js";
2
+ import {findGridPath, segmentBlocked} from "../src/manhattan-router.js";
3
+
4
+ describe("manhattan-router", () => {
5
+ const P = (x, y) => new Point([x, y]);
6
+ const R = (x, y, w, h) => ({
7
+ corner: P(x, y),
8
+ size: P(w, h)
9
+ });
10
+
11
+ it("connects two points directly", () => {
12
+ const path = findGridPath({
13
+ from: P(0, 0),
14
+ to: P(10, 0),
15
+ gridSize: 5,
16
+ avoidRects: []
17
+ });
18
+
19
+ expect(path.map(p => p.join(",")))
20
+ .toEqual(["0,0", "10,0"]);
21
+ });
22
+
23
+ it("creates an L-shaped path", () => {
24
+ const path = findGridPath({
25
+ from: P(0, 0),
26
+ to: P(10, 10),
27
+ gridSize: 5,
28
+ avoidRects: []
29
+ });
30
+
31
+ expect(path.length).toBe(3);
32
+ expect(path[0].equals(P(0, 0))).toBeTrue();
33
+ expect(path[2].equals(P(10, 10))).toBeTrue();
34
+ });
35
+
36
+ it("routes around a small obstacle", () => {
37
+ const path = findGridPath({
38
+ from: P(0, 0),
39
+ to: P(10, 0),
40
+ gridSize: 5,
41
+ avoidRects: [
42
+ R(4, -1, 2, 2) // blocks (5,0)
43
+ ]
44
+ });
45
+
46
+ const keys = path.map(p => p.join(","));
47
+ expect(keys).not.toContain("5,0");
48
+ expect(keys[0]).toBe("0,0");
49
+ expect(keys[keys.length - 1]).toBe("10,0");
50
+ });
51
+
52
+ it("avoids a rectangular keep-out area", () => {
53
+ const path = findGridPath({
54
+ from: P(0, 0),
55
+ to: P(20, 0),
56
+ gridSize: 5,
57
+ avoidRects: [
58
+ R(5, -5, 10, 10)
59
+ ]
60
+ });
61
+
62
+ for (let i = 0; i < path.length - 1; i++) {
63
+ expect(
64
+ segmentBlocked(path[i], path[i + 1], [
65
+ R(5, -5, 10, 10)
66
+ ])
67
+ ).toBeFalse();
68
+ }
69
+ });
70
+
71
+ it("is deterministic", () => {
72
+ const opts = {
73
+ from: P(0, 0),
74
+ to: P(10, 10),
75
+ gridSize: 5,
76
+ avoidRects: [R(5, 0, 2, 2)]
77
+ };
78
+
79
+ const a = findGridPath(opts).map(p => p.join(","));
80
+ const b = findGridPath(opts).map(p => p.join(","));
81
+
82
+ expect(a).toEqual(b);
83
+ });
84
+ });
85
+
86
+ describe("findGridPath – avoidLines", () => {
87
+
88
+ it("avoids drawing collinear overlapping horizontal lines", () => {
89
+ const path = findGridPath({
90
+ from: new Point([0, 0]),
91
+ to: new Point([10, 0]),
92
+ gridSize: 5,
93
+ avoidRects: [],
94
+ avoidLines: [
95
+ { a: new Point([0, 0]), b: new Point([10, 0]) }
96
+ ]
97
+ });
98
+
99
+ // must detour
100
+ expect(path.length).toBeGreaterThan(2);
101
+
102
+ // should not contain straight horizontal segment at y=0
103
+ for (let i = 0; i < path.length - 1; i++) {
104
+ const a = path[i];
105
+ const b = path[i + 1];
106
+ if (a[1] === 0 && b[1] === 0) {
107
+ fail("Path contains forbidden collinear segment");
108
+ }
109
+ }
110
+ });
111
+
112
+ it("allows orthogonal crossing of avoidLines", () => {
113
+ const path = findGridPath({
114
+ from: new Point([5, -5]),
115
+ to: new Point([5, 5]),
116
+ gridSize: 5,
117
+ avoidRects: [],
118
+ avoidLines: [
119
+ { a: new Point([0, 0]), b: new Point([10, 0]) }
120
+ ]
121
+ });
122
+
123
+ // straight vertical path is allowed
124
+ expect(path).toEqual([
125
+ new Point([5, -5]),
126
+ new Point([5, 5])
127
+ ]);
128
+ });
129
+
130
+ it("avoids vertical collinear overlap as well", () => {
131
+ const path = findGridPath({
132
+ from: new Point([0, 0]),
133
+ to: new Point([0, 10]),
134
+ gridSize: 5,
135
+ avoidRects: [],
136
+ avoidLines: [
137
+ { a: new Point([0, 0]), b: new Point([0, 10]) }
138
+ ]
139
+ });
140
+
141
+ expect(path.length).toBeGreaterThan(2);
142
+
143
+ for (let i = 0; i < path.length - 1; i++) {
144
+ const a = path[i];
145
+ const b = path[i + 1];
146
+ if (a[0] === 0 && b[0] === 0) {
147
+ fail("Vertical collinear overlap detected");
148
+ }
149
+ }
150
+ });
151
+
152
+ });
@@ -0,0 +1,14 @@
1
+ import {sexpParse, sexpStringify}from "../src/sexp.js";
2
+
3
+ describe("sexpr",()=>{
4
+ it("can parse and stringify s-expressions",()=>{
5
+ let input=`(symbol "R1" (value "10k") (footprint "Resistor_SMD:R_0805"))`;
6
+ let exp=sexpParse(input);
7
+ //console.log(exp);
8
+
9
+ let output=sexpStringify(exp);
10
+ //console.log(output);
11
+
12
+ expect(output).toEqual(`(symbol "R1" (value "10k") (footprint "Resistor_SMD:R_0805"))`);
13
+ });
14
+ });
@@ -0,0 +1,14 @@
1
+ export default {
2
+ spec_dir: "spec",
3
+ spec_files: [
4
+ "**/*[sS]pec.?(m)js"
5
+ ],
6
+ helpers: [
7
+ "helpers/**/*.?(m)js"
8
+ ],
9
+ env: {
10
+ stopSpecOnExpectationFailure: false,
11
+ random: true,
12
+ forbidDuplicateNames: true
13
+ }
14
+ }
package/src/Entity.js ADDED
@@ -0,0 +1,232 @@
1
+ import {Point} from "./cartesian-math.js";
2
+ import {sym, symName, sexpCallName} from "./sexp.js";
3
+ import {Rect} from "./cartesian-math.js";
4
+
5
+ class EntityPin {
6
+ constructor(sexpr, entity) {
7
+ this.sexpr=sexpr;
8
+ this.entity=entity;
9
+
10
+ //console.log(this.sexpr);
11
+ }
12
+
13
+ getNum() {
14
+ return this.sexpr[1];
15
+ }
16
+
17
+ getPoint() {
18
+ let librarySymbol=this.entity.getLibrarySymbol();
19
+ let librarySymbolPin=librarySymbol.getPin(Number(this.getNum()));
20
+ let pinAt=Point.from(librarySymbolPin.at);
21
+ pinAt[1]=-pinAt[1];
22
+
23
+ return Point.from(this.entity.getAt()).add(pinAt);
24
+ }
25
+
26
+ isConnected(p) {
27
+ if (typeof p=="string") {
28
+ for (let e of this.entity.schematic.getLabelEntities(p)) {
29
+ let p=e.getConnectionPoints()[0];
30
+ if (this.entity.schematic.arePointsConnected(this.getPoint(),p))
31
+ return true;
32
+ }
33
+
34
+ return false;
35
+ }
36
+
37
+ else {
38
+ return this.entity.schematic.arePointsConnected(this.getPoint(),p.getPoint());
39
+ }
40
+ }
41
+
42
+ connect(p) {
43
+ if (this.isConnected(p)) {
44
+ if (typeof p=="string") {
45
+ for (let e of this.entity.schematic.getLabelEntities(p)) {
46
+ let p=e.getConnectionPoints()[0];
47
+ if (this.entity.schematic.arePointsConnected(this.getPoint(),p)) {
48
+ e.declared=true;
49
+ this.entity.schematic.markConnectionDeclared(this.getPoint(),p);
50
+ }
51
+ }
52
+ }
53
+
54
+ else {
55
+ this.entity.schematic.markConnectionDeclared(this.getPoint(),p.getPoint());
56
+ }
57
+
58
+ return;
59
+ }
60
+
61
+ if (typeof p=="string") {
62
+ let l=this.entity.schematic.addLabel(this.getPoint(),p);
63
+ l.declared=true;
64
+ }
65
+
66
+ else {
67
+ this.entity.schematic.addConnectionWire(this.getPoint(),p.getPoint());
68
+ this.entity.schematic.markConnectionDeclared(this.getPoint(),p.getPoint());
69
+ }
70
+ }
71
+ }
72
+
73
+ export default class Entity {
74
+ constructor(sexpr, schematic) {
75
+ this.schematic=schematic;
76
+ this.sexpr=sexpr;
77
+ this.pins=[];
78
+
79
+ this.type=symName(this.sexpr[0]);
80
+ if (!["symbol","wire","label"].includes(this.type))
81
+ throw new Error("Unknown entity: "+this.type);
82
+
83
+ for (let a of this.sexpr)
84
+ if (sexpCallName(a)=="pin")
85
+ this.pins.push(new EntityPin(a,this));
86
+ }
87
+
88
+ getSexp() {
89
+ return this.sexpr;
90
+ }
91
+
92
+ init() {
93
+ if (this.getType()!="symbol")
94
+ return;
95
+
96
+ let id=this.getLibId();
97
+ if (!id)
98
+ throw new Error("Unable to load symbol");
99
+
100
+ //console.log("id: "+id);
101
+ this.librarySymbol=this.schematic.symbolLibrary.getLibrarySymbol(this.getLibId())
102
+ if (!this.librarySymbol)
103
+ throw new Error("Unable to load symbol");
104
+ }
105
+
106
+ async load() {
107
+ if (this.getType()!="symbol")
108
+ return;
109
+
110
+ let id=this.getLibId();
111
+ if (!id)
112
+ throw new Error("Unable to load symbol");
113
+
114
+ //console.log("id: "+id);
115
+ this.librarySymbol=await this.schematic.symbolLibrary.loadLibrarySymbol(this.getLibId())
116
+ if (!this.librarySymbol)
117
+ throw new Error("Unable to load symbol");
118
+ }
119
+
120
+ getReference() {
121
+ if (this.getType()!="symbol")
122
+ return;
123
+
124
+ let el=this.sexpr.find(a=>sexpCallName(a)=="property" && a[1]=="Reference");
125
+ return el[2];
126
+ }
127
+
128
+ getFootprint() {
129
+ if (this.getType()!="symbol")
130
+ throw new Error("Only symbols have footprints");
131
+
132
+ let el=this.sexpr.find(a=>sexpCallName(a)=="property" && a[1]=="Footprint");
133
+ if (!el)
134
+ return "";
135
+
136
+ return el[2];
137
+ }
138
+
139
+ setFootprint(footprint) {
140
+ if (this.getType()!="symbol")
141
+ throw new Error("Only symbols have footprints");
142
+
143
+ if (!footprint)
144
+ footprint="";
145
+
146
+ let el=this.sexpr.find(a=>sexpCallName(a)=="property" && a[1]=="Footprint");
147
+ if (!el) {
148
+ let exp=[sym("property"),"Footprint","",
149
+ [sym("effects"),
150
+ [sym("hide"),sym("yes")]
151
+ ]
152
+ ];
153
+
154
+ this.sexpr.push(exp);
155
+ el=exp;
156
+ }
157
+
158
+ el[2]=footprint;
159
+ }
160
+
161
+ getLibId() {
162
+ return this.sexpr.find(x=>sexpCallName(x)=="lib_id")[1];
163
+ }
164
+
165
+ getAt() {
166
+ for (let a of this.sexpr)
167
+ if (sexpCallName(a)=="at")
168
+ return a.slice(1).map(Number);
169
+ }
170
+
171
+ getLabel() {
172
+ if (this.getType()!="label")
173
+ throw new Error("Not a label");
174
+
175
+ return this.sexpr[1];
176
+ }
177
+
178
+ getBoundingRect() {
179
+ //console.log(this.librarySymbol);
180
+ let r=this.librarySymbol.getBoundingRect();
181
+ //console.log(r);
182
+ let p=Point.from(this.getAt());
183
+
184
+ return new Rect(p.add(r.corner),r.size);
185
+ }
186
+
187
+ getLibrarySymbol() {
188
+ return this.librarySymbol;
189
+ }
190
+
191
+ pin(num) {
192
+ if (!num)
193
+ throw new Error("Pin numbers start at 1");
194
+
195
+ for (let p of this.pins)
196
+ if (p.getNum()==num)
197
+ return p;
198
+ }
199
+
200
+ getType() {
201
+ return this.type;
202
+ /*let t=symName(this.sexpr[0]);
203
+
204
+ if (!["symbol","wire","label"].includes(t))
205
+ throw new Error("Unknown entity: "+t);
206
+
207
+ return t;*/
208
+ }
209
+
210
+ getConnectionPoints() {
211
+ switch (this.getType()) {
212
+ case "wire":
213
+ return [this.sexpr[1][1].slice(1),this.sexpr[1][2].slice(1)]
214
+ break;
215
+
216
+ case "label":
217
+ return [this.sexpr[2].slice(1)];
218
+ break;
219
+
220
+ case "symbol":
221
+ let p=[];
222
+ for (let i=1; i<=this.pins.length; i++)
223
+ p.push(this.pin(i).getPoint());
224
+
225
+ return p;
226
+ break;
227
+
228
+ default:
229
+ throw new Error("Unknown entity type: "+this.getType());
230
+ }
231
+ }
232
+ }
@@ -0,0 +1,100 @@
1
+ import {sym, isSym, symEq, symName, sexpStringify, sexpCallName} from "./sexp.js";
2
+ import {Rect} from "./cartesian-math.js";
3
+
4
+ export default class LibrarySymbol {
5
+ constructor(sexpr, qualifiedName) {
6
+ this.sexpr = sexpr;
7
+ this._pins = null; // lazy parse
8
+ this.qualifiedName=qualifiedName;
9
+ }
10
+
11
+ /**
12
+ * Return all pins as LibrarySymbolPin instances.
13
+ */
14
+ get pins() {
15
+ if (!this._pins) {
16
+ // 1. Find the first nested symbol
17
+ const nestedSymbol = this.sexpr.find(
18
+ (e) => Array.isArray(e) && symEq(e[0],"symbol") && e !== this.sexpr[0]
19
+ );
20
+
21
+ if (!nestedSymbol) {
22
+ this._pins = [];
23
+ } else {
24
+ // 2. Collect all (pin ...) inside nested symbol
25
+ this._pins = nestedSymbol
26
+ .filter((e) => Array.isArray(e) && symEq(e[0],"pin"))
27
+ .map((pinSexpr) => new LibrarySymbolPin(pinSexpr));
28
+ }
29
+ }
30
+ return this._pins;
31
+ }
32
+
33
+ /**
34
+ * Get pin by index (0-based) or by pin name.
35
+ */
36
+ getPin(numberOrName) {
37
+ return this.pins.find((p) => p.name === numberOrName || p.number==numberOrName);
38
+ }
39
+
40
+ getQualifiedSexpr() {
41
+ let expr=structuredClone(this.sexpr);
42
+ expr[1]=this.qualifiedName;
43
+ return expr;
44
+ }
45
+
46
+ getBoundingRect() {
47
+ let rect;
48
+
49
+ let symbolExp=this.sexpr.find(x=>sexpCallName(x)=="symbol")
50
+ for (let x of symbolExp) {
51
+ if (sexpCallName(x)=="rectangle") {
52
+ let start=x.find(x=>sexpCallName(x)=="start").slice(1);
53
+ let end=x.find(x=>sexpCallName(x)=="end").slice(1);
54
+ let r=Rect.fromCorners(start,end);
55
+
56
+ if (!rect)
57
+ rect=r;
58
+
59
+ rect=rect.union(r);
60
+ }
61
+ }
62
+
63
+ //console.log(sexpStringify(symbolExp));
64
+
65
+ return rect;
66
+ }
67
+ }
68
+
69
+ export class LibrarySymbolPin {
70
+ constructor(sexpr) {
71
+ this.sexpr = sexpr;
72
+ this.type = sexpr[1]; // e.g., "passive"
73
+ this.shape = sexpr[2]; // e.g., "line"
74
+
75
+ // parse attributes
76
+ this.at = [0, 0, 0];
77
+ this.length = 0;
78
+ this.name = null;
79
+ this.number = null;
80
+
81
+ for (const e of sexpr.slice(3)) {
82
+ if (!Array.isArray(e) || !isSym(e[0])) continue;
83
+
84
+ switch (symName(e[0])) {
85
+ case "at":
86
+ this.at = e.slice(1).map(Number); // [x, y, rotation]
87
+ break;
88
+ case "length":
89
+ this.length = Number(e[1]);
90
+ break;
91
+ case "name":
92
+ this.name = e[1];
93
+ break;
94
+ case "number":
95
+ this.number = e[1];
96
+ break;
97
+ }
98
+ }
99
+ }
100
+ }