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 CHANGED
@@ -2,12 +2,12 @@
2
2
 
3
3
  ## NAME
4
4
 
5
- kisch - schema‑transformation tool for KiCad schematics
5
+ kisch schema‑transformation tool for KiCad schematics
6
6
 
7
7
  ## SYNOPSIS
8
8
 
9
9
  ```
10
- kisch <schema.kicad_sch> [options]
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
- The schematic file is the only positional argument.
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
- kisch schema.kicad_sch --script=<file> [options]
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
- * `--script=<file>`
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
- JavaScript file that defines the transformation logic applied to the schematic.
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
- * `--symbol-library-path=<path>`
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
- Path to a KiCad symbol library used when adding or resolving symbols.
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
- (Additional options may be added over time.)
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 is implemented as a Node.js CLI and executes user‑provided JavaScript to perform transformations.
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
- * Writes the transformed schematic back to disk
124
+ * Returns a schematic that kisch will write back to disk (if an output is specified)
68
125
 
69
- The exact scripting API is intentionally minimal and focused on structural operations, not rendering or layout aesthetics.
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
- await sch.use([
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: "Peabrain:ScrewTerminals_4P",
134
+ footprint: "TerminalBlock:TerminalBlock_2P_5.08mm_Vertical"
87
135
  });
88
136
 
89
- let screw2 = sch.declare("J2", {
137
+ let j2 = sch.declare("J2", {
90
138
  symbol: "Connector_Generic:Conn_01x04",
91
- footprint: "Peabrain:ScrewTerminals_4P",
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
- screw1.pin(1).connect("GND");
110
- screw1.pin(2).connect("12V");
111
- screw1.pin(3).connect("CANH");
112
- screw1.pin(4).connect("CANL");
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 cares about schematic geometry just enough to be correct, but not enough to be pretty
150
- * It can place new symbols so that they function correctly (e.g. without overlapping other symbols), but it does not attempt to find the best or most readable placement
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 + codedriven design workflows
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 for now... Haven't thought about it... :)
180
+ GPL v3
package/package.json CHANGED
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "name": "kisch",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/limikael/kisch.git"
8
+ },
5
9
  "scripts": {
6
10
  "test": "jasmine"
7
11
  },
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 {
@@ -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 nestedSymbol = this.sexpr.find(
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
- if (!nestedSymbol) {
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(fn, options) {
11
- this.schematicFileName=fn;
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 load() {
17
- this.sexp=sexpParse(await fsp.readFile(this.schematicFileName,"utf8"))[0];
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(this.schematicFileName,content);
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 openSchematic(fn, options) {
325
- let schematic=new Schematic(fn, options);
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
+ }
@@ -1,4 +1,4 @@
1
- import fs from "fs/promises";
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 = new Map(); // Maps libraryName -> filename
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
- * Load the correct LibrarySymbol by full name "Lib:Symbol".
28
- * @param {string} qualifiedName
29
- * @returns {Promise<LibrarySymbol>}
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.size > 0) return;
106
+ if (this.index) return;
107
+
108
+ this.index = new Map();
70
109
 
71
- const entries = await fs.readdir(this.dirPath);
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 fs.readFile(fullPath, "utf8");
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
+ }