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,329 @@
1
+ import SymbolLibrary from "./SymbolLibrary.js";
2
+ import fs, {promises as fsp} from "fs";
3
+ import Entity from "./Entity.js";
4
+ import {Point, pointKey, Rect} from "./cartesian-math.js";
5
+ import {findGridPath} from "../src/manhattan-router.js";
6
+ import {isSym, sym, sexpParse, sexpStringify, symName, sexpCallName} from "./sexp.js";
7
+ import {placeRect} from "./place-rect.js";
8
+
9
+ export default class Schematic {
10
+ constructor(fn, options) {
11
+ this.schematicFileName=fn;
12
+ this.symbolLibraryPath=options.symbolLibraryPath;
13
+ this.symbolLibrary=new SymbolLibrary(this.symbolLibraryPath);
14
+ }
15
+
16
+ async load() {
17
+ this.sexp=sexpParse(await fsp.readFile(this.schematicFileName,"utf8"))[0];
18
+ this.entities=[];
19
+
20
+ for (let o of this.sexp) {
21
+ if (["wire","label","symbol"].includes(sexpCallName(o))) {
22
+ let e=new Entity(o,this);
23
+ await e.load();
24
+ this.entities.push(e);
25
+ }
26
+
27
+ if (sexpCallName(o)=="uuid")
28
+ this.uuid=o[1];
29
+ }
30
+
31
+ this.sexp=this.sexp.filter(o=>!["wire","label","symbol","uuid"].includes(sexpCallName(o)));
32
+ }
33
+
34
+ getSexp() {
35
+ let sexp=structuredClone(this.sexp);
36
+ sexp.push(...this.entities.map(e=>e.getSexp()));
37
+ sexp.push([sym("uuid"),this.uuid]);
38
+ return sexp;
39
+ }
40
+
41
+ async save() {
42
+ let content=sexpStringify([this.getSexp()],2);
43
+ await fsp.writeFile(this.schematicFileName,content);
44
+ }
45
+
46
+ getEntities() {
47
+ return this.entities;
48
+ }
49
+
50
+ sym(ref) {
51
+ for (let e of this.entities)
52
+ if (e.getType()=="symbol" && e.getReference()==ref)
53
+ return e;
54
+
55
+ throw new Error("Undefined symbol reference: "+ref);
56
+ }
57
+
58
+ getSymbolEntities() {
59
+ let entities=[];
60
+
61
+ for (let e of this.entities)
62
+ if (e.getType()=="symbol")
63
+ entities.push(e);
64
+
65
+ return entities;
66
+ }
67
+
68
+ getLabelEntities(label) {
69
+ let entities=[];
70
+
71
+ for (let e of this.entities)
72
+ if (e.getType()=="label" && e.getLabel()==label)
73
+ entities.push(e);
74
+
75
+ return entities;
76
+ }
77
+
78
+ getEntitiesByConnectionPoint(connectonPoint) {
79
+ connectonPoint=Point.from(connectonPoint);
80
+ let entities=[];
81
+
82
+ for (let e of this.entities) {
83
+ for (let p of e.getConnectionPoints())
84
+ if (connectonPoint.equals(p))
85
+ entities.push(e)
86
+ }
87
+
88
+ return entities;
89
+ }
90
+
91
+ getConnectionPoints() {
92
+ let points=[];
93
+ for (let e of this.entities)
94
+ points.push(...e.getConnectionPoints());
95
+
96
+ points=points.map(p=>Point.from(p));
97
+ return points;
98
+ }
99
+
100
+ arePointsConnected(p, q) {
101
+ return (!!this.getConnectionPath(p,q));
102
+ }
103
+
104
+ getConnectionPath(p, q) {
105
+ const startKey = pointKey(p);
106
+ const targetKey = pointKey(q);
107
+
108
+ // trivial case
109
+ if (startKey === targetKey) return [];
110
+
111
+ const visitedPoints = new Set();
112
+ const queue = [];
113
+
114
+ // each item: { point, path }
115
+ queue.push({
116
+ point: p,
117
+ path: []
118
+ });
119
+
120
+ while (queue.length > 0) {
121
+ const { point, path } = queue.shift();
122
+ const key = pointKey(point);
123
+
124
+ if (visitedPoints.has(key)) continue;
125
+ visitedPoints.add(key);
126
+
127
+ // find all entities touching this point
128
+ const entities = this.getEntitiesByConnectionPoint(point).filter(e=>e.getType()=="wire");
129
+
130
+ for (const entity of entities) {
131
+ const connectionPoints = entity.getConnectionPoints();
132
+
133
+ for (const cp of connectionPoints) {
134
+ const cpKey = pointKey(cp);
135
+
136
+ if (visitedPoints.has(cpKey)) continue;
137
+
138
+ const newPath = path.concat(entity);
139
+
140
+ // found target
141
+ if (cpKey === targetKey) {
142
+ return newPath;
143
+ }
144
+
145
+ queue.push({
146
+ point: cp,
147
+ path: newPath
148
+ });
149
+ }
150
+ }
151
+ }
152
+
153
+ // no path found
154
+ return null;
155
+ }
156
+
157
+ addConnectionWire(fromPoint, toPoint) {
158
+ let connectionPoints=this.getConnectionPoints();
159
+ connectionPoints=connectionPoints.filter(p=>!p.equals(fromPoint) && !p.equals(toPoint));
160
+ let avoidRects=connectionPoints.map(p=>new Rect(p.sub([0.635,0.635]),[1.27,1.27]));
161
+
162
+ let avoidLines=this.entities.filter(e=>e.getType()=="wire").map(e=>{
163
+ return ({
164
+ a: e.getConnectionPoints()[0],
165
+ b: e.getConnectionPoints()[1],
166
+ })
167
+ });
168
+
169
+ //console.log(avoidLines);
170
+
171
+ let points=findGridPath({
172
+ from: fromPoint,
173
+ to: toPoint,
174
+ gridSize: 1.27,
175
+ avoidRects: avoidRects,
176
+ avoidLines: avoidLines
177
+ });
178
+
179
+ for (let i=0; i<points.length-1; i++) {
180
+ let p1=points[i], p2=points[i+1];
181
+ let expr=[sym("wire"),
182
+ [sym("pts"), [sym("xy"),p1[0],p1[1]], [sym("xy"),p2[0],p2[1]]],
183
+ [sym("stroke"), [sym("width"),0], [sym("type"), sym("default")]],
184
+ [sym("uuid"),crypto.randomUUID()]
185
+ ];
186
+
187
+ let e=new Entity(expr,this);
188
+ this.entities.push(e);
189
+ }
190
+ }
191
+
192
+ addLabel(point, label) {
193
+ let expr=[sym("label"),label,
194
+ [sym("at"),point[0],point[1],180],
195
+ [sym("effects"),
196
+ [sym("font"),[sym("size"),1.27,1.27]],
197
+ [sym("justify"),sym("right"),sym("bottom")]
198
+ ],
199
+ [sym("uuid"),crypto.randomUUID()]
200
+ ]
201
+
202
+ let e=new Entity(expr,this);
203
+ this.entities.push(e);
204
+ return e;
205
+ }
206
+
207
+ getLibSymbolsExp() {
208
+ //return sexpFirst(this.sexpr,x=>sexpCallName(x)=="lib_symbols")
209
+
210
+ for (let exp of this.sexp)
211
+ if (sexpCallName(exp)=="lib_symbols")
212
+ return exp;
213
+ }
214
+
215
+ async ensureLibSymbol(symbol) {
216
+ let libSymbolsExpr=this.getLibSymbolsExp();
217
+ for (let e of libSymbolsExpr)
218
+ if (sexpCallName(e)=="symbol" && e[1]==symbol)
219
+ return;
220
+
221
+ let librarySymbol=await this.symbolLibrary.loadLibrarySymbol(symbol);
222
+ //console.log("adding: "+symbol);
223
+ libSymbolsExpr.push(librarySymbol.getQualifiedSexpr());
224
+ }
225
+
226
+ async use(...symbols) {
227
+ symbols=symbols.flat(Infinity);
228
+ for (let symbol of symbols)
229
+ await this.ensureLibSymbol(symbol);
230
+ }
231
+
232
+ declare(ref, options) {
233
+ let entity=this.entities.find(e=>e.getType()=="symbol" && e.getReference()==ref);
234
+ if (!entity)
235
+ entity=this.addSymbol(ref,options);
236
+
237
+ if (entity.getLibId()!=options.symbol)
238
+ throw new Error("Symbol declaration mismatch.");
239
+
240
+ entity.setFootprint(options.footprint);
241
+ entity.declared=true;
242
+
243
+ return entity;
244
+ }
245
+
246
+ addSymbol(reference, {symbol, at}) {
247
+ let entity=this.entities.find(e=>e.getType()=="symbol" && e.getReference()==reference);
248
+ if (entity)
249
+ throw new Error("Reference already exists: "+reference);
250
+
251
+ let librarySymbol=this.symbolLibrary.getLibrarySymbol(symbol);
252
+ //let librarySymbol=await this.symbolLibrary.loadLibrarySymbol(symbol);
253
+ let rects=this.getSymbolEntities().map(e=>e.getBoundingRect().pad(2.54*4));
254
+
255
+ let center=new Point(101.6,101.6);
256
+ if (rects.length)
257
+ center=rects.reduce((r,q)=>r.union(q)).getCenter().snap(2.54);
258
+
259
+ if (!at) {
260
+ at=placeRect({
261
+ start: center,
262
+ rect: librarySymbol.getBoundingRect(),
263
+ avoid: rects,
264
+ step: 2.54,
265
+ });
266
+ }
267
+
268
+ let expr=[sym("symbol"),
269
+ [sym("lib_id"),symbol],
270
+ [sym("at"),at[0],at[1],0],
271
+ [sym("unit"),1],
272
+ [sym("exclude_from_sim"),sym("no")],
273
+ [sym("in_bom"),sym("yes")],
274
+ [sym("on_board"),sym("yes")],
275
+ [sym("dnp"),sym("no")],
276
+ [sym("fields_autoplaced"),sym("yes")],
277
+ [sym("uuid"),crypto.randomUUID()]
278
+ ];
279
+
280
+ expr.push([sym("property"),"Reference",reference,
281
+ [sym("at"),at[0],at[1],0],
282
+ [sym("effects"),
283
+ [sym("font"),[sym("size"),1.27,1.27]],
284
+ [sym("justify"),sym("left")],
285
+ ]
286
+ ]);
287
+
288
+ for (let i=1; i<=librarySymbol.pins.length; i++)
289
+ expr.push([sym("pin"),String(i),[sym("uuid"),crypto.randomUUID()]]);
290
+
291
+ expr.push([sym("instances"),
292
+ [sym("project"),"",
293
+ [sym("path"),"/"+this.uuid,
294
+ [sym("reference"),reference],
295
+ [sym("unit"),1]
296
+ ]
297
+ ]
298
+ ]);
299
+
300
+ let e=new Entity(expr,this);
301
+ e.init();
302
+ this.entities.push(e);
303
+
304
+ return e;
305
+ }
306
+
307
+ markConnectionDeclared(from, to) {
308
+ let wires=this.getConnectionPath(from,to);
309
+ for (let wire of wires) {
310
+ if (wire.getType()!="wire")
311
+ throw new Error("Sanity check... Wire is not a wire...");
312
+
313
+ wire.declared=true;
314
+ }
315
+ }
316
+
317
+ removeUndeclared() {
318
+ this.entities=this.entities.filter(entity=>{
319
+ return entity.declared;
320
+ });
321
+ }
322
+ }
323
+
324
+ export async function openSchematic(fn, options) {
325
+ let schematic=new Schematic(fn, options);
326
+ await schematic.load();
327
+
328
+ return schematic;
329
+ }
@@ -0,0 +1,101 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import LibrarySymbol from "./LibrarySymbol.js";
4
+ import {sexpParse, sym, isSym, symEq} from "../src/sexp.js";
5
+
6
+ /**
7
+ * SymbolLibrary represents a directory of .kicad_sym files.
8
+ * You can load a LibrarySymbol by its qualified name like
9
+ * "Connector_Generic:Conn_01x02".
10
+ */
11
+ export default class SymbolLibrary {
12
+ constructor(dirPath) {
13
+ this.dirPath = dirPath;
14
+ this.index = new Map(); // Maps libraryName -> filename
15
+ this.cache = new Map(); // Maps libraryName -> parsed list of symbols
16
+ this.librarySymbols={};
17
+ }
18
+
19
+ getLibrarySymbol(qualifiedName) {
20
+ if (!this.librarySymbols[qualifiedName])
21
+ throw new Error("Symbol not loaded: "+qualifiedName);
22
+
23
+ return this.librarySymbols[qualifiedName];
24
+ }
25
+
26
+ /**
27
+ * Load the correct LibrarySymbol by full name "Lib:Symbol".
28
+ * @param {string} qualifiedName
29
+ * @returns {Promise<LibrarySymbol>}
30
+ */
31
+ async loadLibrarySymbol(qualifiedName) {
32
+ if (this.librarySymbols[qualifiedName])
33
+ return this.librarySymbols[qualifiedName];
34
+
35
+ const [libraryName, symbolName] = qualifiedName.split(":");
36
+ if (!libraryName || !symbolName) {
37
+ throw new Error(
38
+ `Invalid qualified name "${qualifiedName}", expected "Library:Symbol"`
39
+ );
40
+ }
41
+
42
+ await this._ensureIndex();
43
+
44
+ const fileName = this.index.get(libraryName);
45
+ if (!fileName) {
46
+ throw new Error(`Library "${libraryName}" not found in ${this.dirPath}`);
47
+ }
48
+
49
+ //console.log(libraryName,fileName);
50
+
51
+ const syms = await this._loadLibraryFile(libraryName, fileName);
52
+ const sexpr = syms.find((s) => s[1] === symbolName);
53
+ if (!sexpr) {
54
+ throw new Error(
55
+ `Symbol "${symbolName}" not found in library "${libraryName}"`
56
+ );
57
+ }
58
+
59
+ this.librarySymbols[qualifiedName]=new LibrarySymbol(sexpr,qualifiedName);
60
+
61
+ return this.librarySymbols[qualifiedName];
62
+ }
63
+
64
+ /**
65
+ * Scan the directory to find all .kicad_sym files and index them
66
+ * by base name (without extension).
67
+ */
68
+ async _ensureIndex() {
69
+ if (this.index.size > 0) return;
70
+
71
+ const entries = await fs.readdir(this.dirPath);
72
+ for (const entry of entries) {
73
+ if (entry.endsWith(".kicad_sym")) {
74
+ const libName = path.basename(entry, ".kicad_sym");
75
+ this.index.set(libName, entry);
76
+ }
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Read and parse a .kicad_sym file if not cached.
82
+ */
83
+ async _loadLibraryFile(libraryName, fileName) {
84
+ if (this.cache.has(libraryName)) {
85
+ return this.cache.get(libraryName);
86
+ }
87
+
88
+ const fullPath = path.join(this.dirPath, fileName);
89
+ const raw = await fs.readFile(fullPath, "utf8");
90
+ const sexprs = sexpParse(raw)[0]; // Your sexprParse
91
+
92
+ // Filter only top-level (symbol ...) forms
93
+ const symbols = sexprs.filter(
94
+ (e) => Array.isArray(e) && symEq(e[0],"symbol")
95
+ );
96
+
97
+ this.cache.set(libraryName, symbols);
98
+ return symbols;
99
+ }
100
+ }
101
+
@@ -0,0 +1,127 @@
1
+ export class Point extends Array {
2
+ constructor(...a) {
3
+ if (a)
4
+ super(...[a].flat(Infinity));
5
+
6
+ else
7
+ super();
8
+ }
9
+
10
+ add(p) {
11
+ return Point.from([
12
+ this[0]+p[0],
13
+ this[1]+p[1]
14
+ ]);
15
+ }
16
+
17
+ sub(p) {
18
+ return Point.from([
19
+ this[0]-p[0],
20
+ this[1]-p[1]
21
+ ]);
22
+ }
23
+
24
+ equals(p) {
25
+ return (pointKey(this)==pointKey(p));
26
+ }
27
+
28
+ snap(gridSize) {
29
+ return new Point([
30
+ Math.round(this[0]/gridSize)*gridSize,
31
+ Math.round(this[1]/gridSize)*gridSize,
32
+ ]);
33
+ }
34
+ }
35
+
36
+ export class Rect {
37
+ constructor(corner, size) {
38
+ this.corner=Point.from(corner);
39
+ this.size=Point.from(size);
40
+ }
41
+
42
+ static fromCorners(p1, p2) {
43
+ p1=new Point(p1);
44
+ p2=new Point(p2);
45
+
46
+ return new Rect(p1,p2.sub(p1));
47
+ }
48
+
49
+ getLeft() {
50
+ if (this.size[0]<0)
51
+ return this.corner[0]+this.size[0];
52
+
53
+ return this.corner[0]
54
+ }
55
+
56
+ getRight() {
57
+ if (this.size[0]<0)
58
+ return this.corner[0];
59
+
60
+ return this.corner[0]+this.size[0];
61
+ }
62
+
63
+ getTop() {
64
+ if (this.size[1]<0)
65
+ return this.corner[1]+this.size[1];
66
+
67
+ return this.corner[1]
68
+ }
69
+
70
+ getBottom() {
71
+ if (this.size[1]<0)
72
+ return this.corner[1];
73
+
74
+ return this.corner[1]+this.size[1];
75
+ }
76
+
77
+ getTopLeft() {
78
+ return new Point(this.getLeft(),this.getTop());
79
+ }
80
+
81
+ getBottomRight() {
82
+ return new Point(this.getRight(),this.getBottom());
83
+ }
84
+
85
+ union(r) {
86
+ let left=Math.min(this.getLeft(),r.getLeft());
87
+ let top=Math.min(this.getTop(),r.getTop());
88
+ let right=Math.max(this.getRight(),r.getRight());
89
+ let bottom=Math.max(this.getBottom(),r.getBottom());
90
+
91
+ return Rect.fromCorners([left,top],[right,bottom]);
92
+ }
93
+
94
+ overlaps(other) {
95
+ return !(
96
+ this.getRight() <= other.getLeft() ||
97
+ this.getLeft() >= other.getRight() ||
98
+ this.getBottom() <= other.getTop() ||
99
+ this.getTop() >= other.getBottom()
100
+ );
101
+ }
102
+
103
+ translate(p) {
104
+ return new Rect(
105
+ this.corner.add(p),
106
+ this.size
107
+ );
108
+ }
109
+
110
+ pad(border) {
111
+ return Rect.fromCorners(
112
+ this.getTopLeft().sub([border,border]),
113
+ this.getBottomRight().add([border,border])
114
+ );
115
+ }
116
+
117
+ getCenter() {
118
+ return new Point(
119
+ (this.getLeft()+this.getRight())/2,
120
+ (this.getTop()+this.getBottom())/2
121
+ );
122
+ }
123
+ }
124
+
125
+ export function pointKey(p) {
126
+ return `${p[0].toFixed(2)},${p[1].toFixed(2)}`;
127
+ }
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {program} from "commander";
4
+ import Schematic from "./Schematic.js";
5
+ import path from "node:path";
6
+
7
+ program
8
+ .description("Create or update KiCad schematic based on programmatic description.")
9
+ .option("--symbol-library-path <path>","Where to find KiCad symbols.")
10
+ .requiredOption("--script <script>","JavaScript file to transform the schematic.")
11
+ .argument("<schematic>","KiCad Schematic file.")
12
+
13
+ await program.parseAsync();
14
+ let options={
15
+ ...program.opts(),
16
+ schematic: program.args[0],
17
+ }
18
+
19
+ //console.log(options);
20
+
21
+ let schematic=new Schematic(options.schematic,{
22
+ symbolLibraryPath: options.symbolLibraryPath
23
+ });
24
+
25
+ await schematic.load();
26
+
27
+ global.schematic=schematic;
28
+ let mod=await import(path.resolve(options.script));
29
+ if (typeof mod.default=="function")
30
+ await mod.default(schematic);
31
+
32
+ schematic.removeUndeclared();
33
+ await schematic.save();