kisch 1.0.0 → 1.0.2
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.
- package/README.md +84 -74
- package/package.json +5 -1
- package/src/Entity.js +20 -0
- package/src/LibrarySymbol.js +13 -3
- package/src/Schematic.js +98 -12
- package/src/SymbolLibrary.js +70 -10
- package/src/js-util.js +15 -0
- package/src/kisch-cli.js +126 -20
- package/src/node-util.js +17 -0
- package/lab/kish-test.js +0 -35
- package/lab/kitest-org/kitest-backups/kitest-2026-02-03_180334.zip +0 -0
- package/lab/kitest-org/kitest.kicad_pcb +0 -2
- package/lab/kitest-org/kitest.kicad_prl +0 -98
- package/lab/kitest-org/kitest.kicad_pro +0 -418
- package/lab/kitest-org/kitest.kicad_sch +0 -663
- package/spec/Entity.spec.js +0 -31
- package/spec/Schematic.spec.js +0 -115
- package/spec/SymbolLibrary.spec.js +0 -17
- package/spec/cartesian-math.spec.js +0 -25
- package/spec/kitest.kicad_sch +0 -663
- package/spec/manhattan-router.spec.js +0 -152
- package/spec/sexp.spec.js +0 -14
- package/spec/support/jasmine.mjs +0 -14
package/README.md
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## NAME
|
|
4
4
|
|
|
5
|
-
kisch
|
|
5
|
+
kisch — schema‑transformation tool for KiCad schematics
|
|
6
6
|
|
|
7
7
|
## SYNOPSIS
|
|
8
8
|
|
|
9
9
|
```
|
|
10
|
-
kisch
|
|
10
|
+
kisch [options] [inout.kicad_sch]
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
## DESCRIPTION
|
|
@@ -33,103 +33,116 @@ Instead of exporting, regenerating, or synchronizing netlists, kisch transforms
|
|
|
33
33
|
* Large schematics can be refactored safely and repeatably
|
|
34
34
|
* Accidental GUI‑level wiring mistakes are avoided
|
|
35
35
|
|
|
36
|
-
Think of kisch as a schema transformation pass over a KiCad schematic.
|
|
36
|
+
Think of kisch as a **schema transformation pass** over a KiCad schematic.
|
|
37
37
|
|
|
38
38
|
## COMMAND LINE INTERFACE
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
kisch arguments can be understood along **two axes**:
|
|
41
|
+
|
|
42
|
+
| | Input | Output |
|
|
43
|
+
|---------------|-----------------|-----------------|
|
|
44
|
+
| **Schematic** | `-i` / positional | `-o` / positional |
|
|
45
|
+
| **JavaScript**| `-s` | `-e` |
|
|
46
|
+
|
|
47
|
+
- Positional argument `[inout.kicad_sch]` is a shorthand for both `-i` and `-o`.
|
|
48
|
+
- Flags cannot conflict; e.g., positional cannot be used together with `-i` or `-o`.
|
|
49
|
+
- Filling a quadrant defines the pipeline: kisch reads that representation, optionally transforms it, and emits the target representation.
|
|
50
|
+
|
|
51
|
+
### Arguments
|
|
41
52
|
|
|
42
53
|
```
|
|
43
|
-
|
|
54
|
+
[inout.kicad_sch] Schematic to be used as input and output (shorthand for -i and -o)
|
|
44
55
|
```
|
|
45
56
|
|
|
46
57
|
### Options
|
|
47
58
|
|
|
48
|
-
*
|
|
59
|
+
* `-i, --input <input.kicad_sch>`
|
|
60
|
+
Input schematic.
|
|
61
|
+
|
|
62
|
+
* `-o, --output <output.kicad_sch>`
|
|
63
|
+
Output schematic.
|
|
64
|
+
|
|
65
|
+
* `-L, --symbol-dir <path>`
|
|
66
|
+
Where to find KiCad symbols.
|
|
67
|
+
|
|
68
|
+
* `-s, --script <script.js>`
|
|
69
|
+
Input JavaScript script to apply to schematic.
|
|
70
|
+
|
|
71
|
+
* `-e, --emit <script.js>`
|
|
72
|
+
Emit JavaScript script based on schematic.
|
|
73
|
+
|
|
74
|
+
* `-D, --define <key=value>`
|
|
75
|
+
Define variable for the script.
|
|
49
76
|
|
|
50
|
-
|
|
77
|
+
* `-q, --quiet`
|
|
78
|
+
No output except errors.
|
|
79
|
+
|
|
80
|
+
* `-h, --help`
|
|
81
|
+
Display help.
|
|
82
|
+
|
|
83
|
+
* `--version`
|
|
84
|
+
Show version.
|
|
85
|
+
|
|
86
|
+
### Examples
|
|
87
|
+
|
|
88
|
+
**Transform existing schematic in-place:**
|
|
89
|
+
```
|
|
90
|
+
$ kisch design.sch --script x.js
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Generate script for later use:**
|
|
94
|
+
```
|
|
95
|
+
$ kisch --input fresh.sch --emit x.js
|
|
96
|
+
```
|
|
51
97
|
|
|
52
|
-
|
|
98
|
+
**Load from one file, apply script, and save in another file:**
|
|
99
|
+
```
|
|
100
|
+
$ kisch --input template.sch --script x.js --output out.sch
|
|
101
|
+
```
|
|
53
102
|
|
|
54
|
-
|
|
103
|
+
**Dry-run: just load schematic and apply script, do not save:**
|
|
104
|
+
```
|
|
105
|
+
$ kisch --input design.sch --script x.js
|
|
106
|
+
```
|
|
55
107
|
|
|
56
|
-
|
|
108
|
+
**Generate schematic from script (no input schematic):**
|
|
109
|
+
```
|
|
110
|
+
$ kisch --script new_design.js --output new_board.sch
|
|
111
|
+
```
|
|
57
112
|
|
|
58
113
|
## SCRIPTING MODEL
|
|
59
114
|
|
|
60
|
-
kisch
|
|
115
|
+
kisch executes user-provided JavaScript to transform a schematic loaded by the CLI.
|
|
116
|
+
|
|
117
|
+
The script is treated as the **source of truth**: any symbol or connection not present in the script will be removed.
|
|
118
|
+
If the script is empty, the resulting schematic will also be empty. To generate a script that reflects an existing schematic, use the `--emit` flag.
|
|
61
119
|
|
|
62
120
|
A script typically:
|
|
63
121
|
|
|
64
|
-
* Loads the schematic structure
|
|
65
122
|
* Adds or modifies symbols
|
|
66
123
|
* Connects pins and nets explicitly
|
|
67
|
-
*
|
|
124
|
+
* Returns a schematic that kisch will write back to disk (if an output is specified)
|
|
68
125
|
|
|
69
|
-
|
|
126
|
+
Scripts do **not** contain geometric layout information; placement is determined by KiCad or kisch heuristics.
|
|
70
127
|
|
|
71
|
-
### Example
|
|
72
|
-
|
|
73
|
-
The following example hooks up an ESP32-C3 supermini with a CAN transceiver.
|
|
128
|
+
### Example Script
|
|
74
129
|
|
|
75
130
|
```js
|
|
76
131
|
export default async function (sch) {
|
|
77
|
-
|
|
78
|
-
"Connector_Generic:Conn_01x04",
|
|
79
|
-
"Connector_Generic:Conn_02x02_Counter_Clockwise",
|
|
80
|
-
"Connector_Generic:Conn_02x08_Counter_Clockwise",
|
|
81
|
-
"Connector_Generic:Conn_02x04_Counter_Clockwise"
|
|
82
|
-
]);
|
|
83
|
-
|
|
84
|
-
let screw1 = sch.declare("J1", {
|
|
132
|
+
let j1 = sch.declare("J1", {
|
|
85
133
|
symbol: "Connector_Generic:Conn_01x04",
|
|
86
|
-
footprint: "
|
|
134
|
+
footprint: "TerminalBlock:TerminalBlock_2P_5.08mm_Vertical"
|
|
87
135
|
});
|
|
88
136
|
|
|
89
|
-
let
|
|
137
|
+
let j2 = sch.declare("J2", {
|
|
90
138
|
symbol: "Connector_Generic:Conn_01x04",
|
|
91
|
-
footprint: "
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
let vreg = sch.declare("U1", {
|
|
95
|
-
symbol: "Connector_Generic:Conn_02x02_Counter_Clockwise",
|
|
96
|
-
footprint: "Peabrain:VoltageRegulator",
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
let esp32 = sch.declare("U2", {
|
|
100
|
-
symbol: "Connector_Generic:Conn_02x08_Counter_Clockwise",
|
|
101
|
-
footprint: "Peabrain:ESP32",
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
let tja1050 = sch.declare("U3", {
|
|
105
|
-
symbol: "Connector_Generic:Conn_02x04_Counter_Clockwise",
|
|
106
|
-
footprint: "Peabrain:TJA1050",
|
|
139
|
+
footprint: "TerminalBlock:TerminalBlock_2P_5.08mm_Vertical"
|
|
107
140
|
});
|
|
108
141
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
screw2.pin(1).connect("GND");
|
|
115
|
-
screw2.pin(2).connect("12V");
|
|
116
|
-
screw2.pin(3).connect("CANH");
|
|
117
|
-
screw2.pin(4).connect("CANL");
|
|
118
|
-
|
|
119
|
-
tja1050.pin(1).connect("5V");
|
|
120
|
-
tja1050.pin(2).connect(esp32.pin(1)); // TX
|
|
121
|
-
tja1050.pin(3).connect(esp32.pin(13)); // RX
|
|
122
|
-
tja1050.pin(4).connect("GND");
|
|
123
|
-
tja1050.pin(6).connect("CANL");
|
|
124
|
-
tja1050.pin(7).connect("CANH");
|
|
125
|
-
|
|
126
|
-
esp32.pin(16).connect("5V");
|
|
127
|
-
esp32.pin(15).connect("GND");
|
|
128
|
-
|
|
129
|
-
vreg.pin(1).connect("12V");
|
|
130
|
-
vreg.pin(2).connect("GND");
|
|
131
|
-
vreg.pin(3).connect("GND");
|
|
132
|
-
vreg.pin(4).connect("5V");
|
|
142
|
+
j1.pin(1).connect("GND");
|
|
143
|
+
j1.pin(2).connect("12V");
|
|
144
|
+
j2.pin(1).connect("GND");
|
|
145
|
+
j2.pin(2).connect("12V");
|
|
133
146
|
}
|
|
134
147
|
```
|
|
135
148
|
|
|
@@ -144,10 +157,9 @@ export default async function (sch) {
|
|
|
144
157
|
|
|
145
158
|
## LIMITATIONS
|
|
146
159
|
|
|
147
|
-
* kisch targets **KiCad 9 only**
|
|
148
160
|
* Only schematic files are supported
|
|
149
|
-
* kisch
|
|
150
|
-
* It
|
|
161
|
+
* kisch targets **KiCad 9** and above only
|
|
162
|
+
* It ensures functional correctness, not placement aesthetics
|
|
151
163
|
* Graphical placement quality remains the responsibility of the user
|
|
152
164
|
|
|
153
165
|
## USE CASES
|
|
@@ -155,16 +167,14 @@ export default async function (sch) {
|
|
|
155
167
|
* Programmatic net and wire generation
|
|
156
168
|
* Repetitive schematic patterns
|
|
157
169
|
* Safe refactoring of large schematics
|
|
158
|
-
* Hybrid GUI + code
|
|
170
|
+
* Hybrid GUI + code-driven design workflows
|
|
159
171
|
|
|
160
172
|
## INSTALLATION
|
|
161
173
|
|
|
162
|
-
kisch is a Node.js‑based CLI. Install with:
|
|
163
|
-
|
|
164
174
|
```
|
|
165
175
|
npm install -g kisch
|
|
166
176
|
```
|
|
167
177
|
|
|
168
178
|
## LICENSE
|
|
169
179
|
|
|
170
|
-
GPL v3
|
|
180
|
+
GPL v3
|
package/package.json
CHANGED
package/src/Entity.js
CHANGED
|
@@ -68,6 +68,26 @@ class EntityPin {
|
|
|
68
68
|
this.entity.schematic.markConnectionDeclared(this.getPoint(),p.getPoint());
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
|
+
|
|
72
|
+
getConnections() {
|
|
73
|
+
let connections=[];
|
|
74
|
+
|
|
75
|
+
for (let net of this.entity.schematic.getNets()) {
|
|
76
|
+
if (this.isConnected(net))
|
|
77
|
+
connections.push(net);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (let e of this.entity.schematic.getSymbolEntities()) {
|
|
81
|
+
if (e==this.entity)
|
|
82
|
+
continue;
|
|
83
|
+
|
|
84
|
+
for (let p of e.pins)
|
|
85
|
+
if (this.isConnected(p))
|
|
86
|
+
connections.push(p);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return connections;
|
|
90
|
+
}
|
|
71
91
|
}
|
|
72
92
|
|
|
73
93
|
export default class Entity {
|
package/src/LibrarySymbol.js
CHANGED
|
@@ -13,19 +13,29 @@ export default class LibrarySymbol {
|
|
|
13
13
|
*/
|
|
14
14
|
get pins() {
|
|
15
15
|
if (!this._pins) {
|
|
16
|
+
this._pins = [];
|
|
17
|
+
|
|
16
18
|
// 1. Find the first nested symbol
|
|
17
|
-
const
|
|
19
|
+
const nestedSymbols = this.sexpr.filter(
|
|
18
20
|
(e) => Array.isArray(e) && symEq(e[0],"symbol") && e !== this.sexpr[0]
|
|
19
21
|
);
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
for (let nestedSymbol of nestedSymbols) {
|
|
24
|
+
this._pins.push(...nestedSymbol
|
|
25
|
+
.filter((e) => Array.isArray(e) && symEq(e[0],"pin"))
|
|
26
|
+
.map((pinSexpr) => new LibrarySymbolPin(pinSexpr))
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/*if (!nestedSymbol) {
|
|
22
32
|
this._pins = [];
|
|
23
33
|
} else {
|
|
24
34
|
// 2. Collect all (pin ...) inside nested symbol
|
|
25
35
|
this._pins = nestedSymbol
|
|
26
36
|
.filter((e) => Array.isArray(e) && symEq(e[0],"pin"))
|
|
27
37
|
.map((pinSexpr) => new LibrarySymbolPin(pinSexpr));
|
|
28
|
-
}
|
|
38
|
+
}*/
|
|
29
39
|
}
|
|
30
40
|
return this._pins;
|
|
31
41
|
}
|
package/src/Schematic.js
CHANGED
|
@@ -5,16 +5,36 @@ import {Point, pointKey, Rect} from "./cartesian-math.js";
|
|
|
5
5
|
import {findGridPath} from "../src/manhattan-router.js";
|
|
6
6
|
import {isSym, sym, sexpParse, sexpStringify, symName, sexpCallName} from "./sexp.js";
|
|
7
7
|
import {placeRect} from "./place-rect.js";
|
|
8
|
+
import {arrayUnique} from "./js-util.js";
|
|
8
9
|
|
|
9
10
|
export default class Schematic {
|
|
10
|
-
constructor(
|
|
11
|
-
|
|
11
|
+
constructor(options) {
|
|
12
|
+
if (typeof options=="string")
|
|
13
|
+
throw new Error("just pass options!!!");
|
|
14
|
+
|
|
12
15
|
this.symbolLibraryPath=options.symbolLibraryPath;
|
|
13
16
|
this.symbolLibrary=new SymbolLibrary(this.symbolLibraryPath);
|
|
14
17
|
}
|
|
15
18
|
|
|
16
|
-
async
|
|
17
|
-
|
|
19
|
+
async init() {
|
|
20
|
+
await this.symbolLibrary.loadIndex();
|
|
21
|
+
this.entities=[];
|
|
22
|
+
this.uuid=crypto.randomUUID();
|
|
23
|
+
|
|
24
|
+
this.sexp=[sym("kicad_sch"),
|
|
25
|
+
[sym("version"),sym("20250114")],
|
|
26
|
+
[sym("generator"),"eeschema"],
|
|
27
|
+
[sym("generator_version"),"9.0"],
|
|
28
|
+
[sym("paper"),"A4"],
|
|
29
|
+
[sym("lib_symbols")]
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async load(fn) {
|
|
34
|
+
await this.init();
|
|
35
|
+
|
|
36
|
+
//this.schematicFileName=fn;
|
|
37
|
+
this.sexp=sexpParse(await fsp.readFile(fn,"utf8"))[0];
|
|
18
38
|
this.entities=[];
|
|
19
39
|
|
|
20
40
|
for (let o of this.sexp) {
|
|
@@ -38,9 +58,9 @@ export default class Schematic {
|
|
|
38
58
|
return sexp;
|
|
39
59
|
}
|
|
40
60
|
|
|
41
|
-
async save() {
|
|
61
|
+
async save(fn) {
|
|
42
62
|
let content=sexpStringify([this.getSexp()],2);
|
|
43
|
-
await fsp.writeFile(
|
|
63
|
+
await fsp.writeFile(fn,content);
|
|
44
64
|
}
|
|
45
65
|
|
|
46
66
|
getEntities() {
|
|
@@ -75,6 +95,16 @@ export default class Schematic {
|
|
|
75
95
|
return entities;
|
|
76
96
|
}
|
|
77
97
|
|
|
98
|
+
getNets() {
|
|
99
|
+
let nets=this.getEntities()
|
|
100
|
+
.filter(e=>e.getType()=="label")
|
|
101
|
+
.map(e=>e.getLabel());
|
|
102
|
+
|
|
103
|
+
nets=arrayUnique(nets);
|
|
104
|
+
|
|
105
|
+
return nets;
|
|
106
|
+
}
|
|
107
|
+
|
|
78
108
|
getEntitiesByConnectionPoint(connectonPoint) {
|
|
79
109
|
connectonPoint=Point.from(connectonPoint);
|
|
80
110
|
let entities=[];
|
|
@@ -223,6 +253,17 @@ export default class Schematic {
|
|
|
223
253
|
libSymbolsExpr.push(librarySymbol.getQualifiedSexpr());
|
|
224
254
|
}
|
|
225
255
|
|
|
256
|
+
ensureLibSymbolSync(symbol) {
|
|
257
|
+
let libSymbolsExpr=this.getLibSymbolsExp();
|
|
258
|
+
for (let e of libSymbolsExpr)
|
|
259
|
+
if (sexpCallName(e)=="symbol" && e[1]==symbol)
|
|
260
|
+
return;
|
|
261
|
+
|
|
262
|
+
let librarySymbol=this.symbolLibrary.loadLibrarySymbolSync(symbol);
|
|
263
|
+
//console.log("adding: "+symbol);
|
|
264
|
+
libSymbolsExpr.push(librarySymbol.getQualifiedSexpr());
|
|
265
|
+
}
|
|
266
|
+
|
|
226
267
|
async use(...symbols) {
|
|
227
268
|
symbols=symbols.flat(Infinity);
|
|
228
269
|
for (let symbol of symbols)
|
|
@@ -235,7 +276,9 @@ export default class Schematic {
|
|
|
235
276
|
entity=this.addSymbol(ref,options);
|
|
236
277
|
|
|
237
278
|
if (entity.getLibId()!=options.symbol)
|
|
238
|
-
throw new Error("Symbol declaration mismatch.");
|
|
279
|
+
throw new Error("Symbol declaration mismatch, code: "+options.symbol+" existing in schema: "+entity.getLibId()+" ref: "+ref);
|
|
280
|
+
|
|
281
|
+
this.ensureLibSymbolSync(options.symbol);
|
|
239
282
|
|
|
240
283
|
entity.setFootprint(options.footprint);
|
|
241
284
|
entity.declared=true;
|
|
@@ -248,7 +291,9 @@ export default class Schematic {
|
|
|
248
291
|
if (entity)
|
|
249
292
|
throw new Error("Reference already exists: "+reference);
|
|
250
293
|
|
|
251
|
-
let librarySymbol=this.symbolLibrary.getLibrarySymbol(symbol);
|
|
294
|
+
//let librarySymbol=this.symbolLibrary.getLibrarySymbol(symbol);
|
|
295
|
+
let librarySymbol=this.symbolLibrary.loadLibrarySymbolSync(symbol);
|
|
296
|
+
|
|
252
297
|
//let librarySymbol=await this.symbolLibrary.loadLibrarySymbol(symbol);
|
|
253
298
|
let rects=this.getSymbolEntities().map(e=>e.getBoundingRect().pad(2.54*4));
|
|
254
299
|
|
|
@@ -319,11 +364,52 @@ export default class Schematic {
|
|
|
319
364
|
return entity.declared;
|
|
320
365
|
});
|
|
321
366
|
}
|
|
367
|
+
|
|
368
|
+
getSource() {
|
|
369
|
+
let src="";
|
|
370
|
+
src+=`export default async function(sch) {\n`;
|
|
371
|
+
for (let e of this.getSymbolEntities()) {
|
|
372
|
+
src+=` let ${e.getReference()}=sch.declare("${e.getReference()}",{\n`;
|
|
373
|
+
src+=` "symbol": "${e.getLibId()}",\n`
|
|
374
|
+
src+=` "footprint": "${e.getFootprint()}",\n`
|
|
375
|
+
src+=` });\n\n`;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
let allConnectionPoints=this.getConnectionPoints();
|
|
379
|
+
for (let e of this.getSymbolEntities()) {
|
|
380
|
+
for (let pin of e.pins) {
|
|
381
|
+
for (let c of pin.getConnections()) {
|
|
382
|
+
if (typeof c=="string") {
|
|
383
|
+
src+=` ${e.getReference()}.pin(${pin.getNum()}).connect("${c}");\n`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
else {
|
|
387
|
+
if (e.getReference()<c.entity.getReference()) {
|
|
388
|
+
src+=` ${e.getReference()}.pin(${pin.getNum()}).connect(`;
|
|
389
|
+
src+=`${c.entity.getReference()}.pin(${c.getNum()})`;
|
|
390
|
+
src+=`);\n`;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
src+=`}\n`;
|
|
398
|
+
|
|
399
|
+
return src;
|
|
400
|
+
}
|
|
322
401
|
}
|
|
323
402
|
|
|
324
|
-
export async function
|
|
325
|
-
let schematic=new Schematic(
|
|
326
|
-
await schematic.load();
|
|
403
|
+
export async function loadSchematic(fn, options) {
|
|
404
|
+
let schematic=new Schematic(options);
|
|
405
|
+
await schematic.load(fn);
|
|
327
406
|
|
|
328
407
|
return schematic;
|
|
329
|
-
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export async function createSchematic(options) {
|
|
411
|
+
let schematic=new Schematic(options);
|
|
412
|
+
await schematic.init();
|
|
413
|
+
|
|
414
|
+
return schematic;
|
|
415
|
+
}
|
package/src/SymbolLibrary.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import fs from "fs
|
|
1
|
+
import fs, {promises as fsp} from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import LibrarySymbol from "./LibrarySymbol.js";
|
|
4
4
|
import {sexpParse, sym, isSym, symEq} from "../src/sexp.js";
|
|
@@ -11,7 +11,7 @@ import {sexpParse, sym, isSym, symEq} from "../src/sexp.js";
|
|
|
11
11
|
export default class SymbolLibrary {
|
|
12
12
|
constructor(dirPath) {
|
|
13
13
|
this.dirPath = dirPath;
|
|
14
|
-
this.index =
|
|
14
|
+
this.index = null; // lazy create index
|
|
15
15
|
this.cache = new Map(); // Maps libraryName -> parsed list of symbols
|
|
16
16
|
this.librarySymbols={};
|
|
17
17
|
}
|
|
@@ -23,11 +23,39 @@ export default class SymbolLibrary {
|
|
|
23
23
|
return this.librarySymbols[qualifiedName];
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
loadLibrarySymbolSync(qualifiedName) {
|
|
27
|
+
if (this.librarySymbols[qualifiedName])
|
|
28
|
+
return this.librarySymbols[qualifiedName];
|
|
29
|
+
|
|
30
|
+
const [libraryName, symbolName] = qualifiedName.split(":");
|
|
31
|
+
if (!libraryName || !symbolName) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`Invalid qualified name "${qualifiedName}", expected "Library:Symbol"`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this._assertIndex();
|
|
38
|
+
|
|
39
|
+
const fileName = this.index.get(libraryName);
|
|
40
|
+
if (!fileName) {
|
|
41
|
+
throw new Error(`Library "${libraryName}" not found in ${this.dirPath}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
//console.log(libraryName,fileName);
|
|
45
|
+
|
|
46
|
+
const syms = this._loadLibraryFileSync(libraryName, fileName);
|
|
47
|
+
const sexpr = syms.find((s) => s[1] === symbolName);
|
|
48
|
+
if (!sexpr) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Symbol "${symbolName}" not found in library "${libraryName}"`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.librarySymbols[qualifiedName]=new LibrarySymbol(sexpr,qualifiedName);
|
|
55
|
+
|
|
56
|
+
return this.librarySymbols[qualifiedName];
|
|
57
|
+
}
|
|
58
|
+
|
|
31
59
|
async loadLibrarySymbol(qualifiedName) {
|
|
32
60
|
if (this.librarySymbols[qualifiedName])
|
|
33
61
|
return this.librarySymbols[qualifiedName];
|
|
@@ -61,14 +89,25 @@ export default class SymbolLibrary {
|
|
|
61
89
|
return this.librarySymbols[qualifiedName];
|
|
62
90
|
}
|
|
63
91
|
|
|
92
|
+
async loadIndex() {
|
|
93
|
+
await this._ensureIndex();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
_assertIndex() {
|
|
97
|
+
if (!this.index)
|
|
98
|
+
throw new Error("Symbol index not loaded");
|
|
99
|
+
}
|
|
100
|
+
|
|
64
101
|
/**
|
|
65
102
|
* Scan the directory to find all .kicad_sym files and index them
|
|
66
103
|
* by base name (without extension).
|
|
67
104
|
*/
|
|
68
105
|
async _ensureIndex() {
|
|
69
|
-
if (this.index
|
|
106
|
+
if (this.index) return;
|
|
107
|
+
|
|
108
|
+
this.index = new Map();
|
|
70
109
|
|
|
71
|
-
const entries = await
|
|
110
|
+
const entries = await fsp.readdir(this.dirPath);
|
|
72
111
|
for (const entry of entries) {
|
|
73
112
|
if (entry.endsWith(".kicad_sym")) {
|
|
74
113
|
const libName = path.basename(entry, ".kicad_sym");
|
|
@@ -86,7 +125,28 @@ export default class SymbolLibrary {
|
|
|
86
125
|
}
|
|
87
126
|
|
|
88
127
|
const fullPath = path.join(this.dirPath, fileName);
|
|
89
|
-
const raw = await
|
|
128
|
+
const raw = await fsp.readFile(fullPath, "utf8");
|
|
129
|
+
const sexprs = sexpParse(raw)[0]; // Your sexprParse
|
|
130
|
+
|
|
131
|
+
// Filter only top-level (symbol ...) forms
|
|
132
|
+
const symbols = sexprs.filter(
|
|
133
|
+
(e) => Array.isArray(e) && symEq(e[0],"symbol")
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
this.cache.set(libraryName, symbols);
|
|
137
|
+
return symbols;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Read and parse a .kicad_sym file if not cached.
|
|
142
|
+
*/
|
|
143
|
+
_loadLibraryFileSync(libraryName, fileName) {
|
|
144
|
+
if (this.cache.has(libraryName)) {
|
|
145
|
+
return this.cache.get(libraryName);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const fullPath = path.join(this.dirPath, fileName);
|
|
149
|
+
const raw = fs.readFileSync(fullPath, "utf8");
|
|
90
150
|
const sexprs = sexpParse(raw)[0]; // Your sexprParse
|
|
91
151
|
|
|
92
152
|
// Filter only top-level (symbol ...) forms
|
package/src/js-util.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export class DeclaredError extends Error {
|
|
2
|
+
constructor(...args) {
|
|
3
|
+
super(...args);
|
|
4
|
+
this.declared=true;
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function arrayUnique(array) {
|
|
9
|
+
let result=new Set();
|
|
10
|
+
|
|
11
|
+
for (let item of array)
|
|
12
|
+
result.add(item);
|
|
13
|
+
|
|
14
|
+
return Array.from(result);
|
|
15
|
+
}
|