kisch 1.0.2 → 1.0.4

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/DSL.md ADDED
@@ -0,0 +1,209 @@
1
+ # Kisch Schematic Script Reference
2
+
3
+ ## Overview
4
+
5
+ A Kisch schematic is defined as a JavaScript module exporting a default async function:
6
+
7
+ ```js
8
+ export default async function (sch, defines) {
9
+ // schematic definition here
10
+ }
11
+ ```
12
+
13
+ The function receives:
14
+
15
+ - `sch` — the schematic builder API
16
+ - `defines` — an object containing CLI-defined parameters
17
+
18
+ The script declares symbols (components) and connects their pins using wires or net labels.
19
+
20
+ # Core Concepts
21
+
22
+ ## 1. Declaring a Component
23
+
24
+ Use:
25
+
26
+ ```js
27
+ sch.declare(reference, options)
28
+ ```
29
+
30
+ ### Parameters
31
+
32
+ - `reference` (string)
33
+ The component reference (e.g., `"R1"`, `"J1"`).
34
+
35
+ - `options` (object)
36
+ - `symbol` (string, required) — KiCad symbol library identifier
37
+ - `footprint` (string, optional) — KiCad footprint library identifier
38
+
39
+ ### Example
40
+
41
+ ```js
42
+ let J1 = sch.declare("J1", {
43
+ symbol: "Connector_Generic:Conn_01x02",
44
+ footprint: "Connector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_Horizontal"
45
+ });
46
+ ```
47
+
48
+ `declare()` returns a symbol object.
49
+
50
+ ## 2. Accessing an Existing Symbol
51
+
52
+ If you did not store the return value from `declare`, you can retrieve it later:
53
+
54
+ ```js
55
+ sch.sym("J1")
56
+ ```
57
+
58
+ Returns the symbol instance with reference `"J1"`.
59
+
60
+ ## 3. Working with Pins
61
+
62
+ Access a pin using:
63
+
64
+ ```js
65
+ symbol.pin(pinNumber)
66
+ ```
67
+
68
+ Example:
69
+
70
+ ```js
71
+ J1.pin(2)
72
+ ```
73
+
74
+ Returns a pin object.
75
+
76
+ ## 4. Connecting Pins
77
+
78
+ `connect()` must always be called on a pin object.
79
+
80
+ It accepts exactly **two argument types**:
81
+
82
+ - A string → connect to a named net (via net label)
83
+ - A pin object → connect directly to another pin (via wire)
84
+
85
+ No other argument types are supported.
86
+
87
+ ### A) Connecting to a Net (String Argument)
88
+
89
+ ```js
90
+ J1.pin(2).connect("GND");
91
+ ```
92
+
93
+ Behavior:
94
+
95
+ - A net label with the given name is created next to the pin (if not already present at that location).
96
+ - The pin is connected to that net label.
97
+ - The connection is implemented as a 0-length wire between the pin and the label.
98
+ - Multiple pins using the same string connect electrically through that net name.
99
+
100
+ Important:
101
+
102
+ - This explicitly creates or uses a net label in the schematic.
103
+ - This does not implicitly merge abstract nets.
104
+ - If you want a named signal, use a string.
105
+
106
+ ### B) Connecting to Another Pin (Pin Object Argument)
107
+
108
+ ```js
109
+ J2.pin(1).connect(J1.pin(2));
110
+ ```
111
+
112
+ Behavior:
113
+
114
+ - A wire is created directly between the two pins.
115
+ - No net label is created automatically.
116
+ - This represents a physical wire in the schematic.
117
+
118
+ Important:
119
+
120
+ - Connecting pin-to-pin does NOT create a named net.
121
+ - If you want a named net, you must connect using a string.
122
+
123
+ ## 5. Conditional Configuration via `defines`
124
+
125
+ Scripts can adapt based on CLI-defined values.
126
+
127
+ ### Passing Defines from CLI
128
+
129
+ ```
130
+ kisch schema.kicad_sch -s script.js -Dtest=123 --define test2=456
131
+ ```
132
+
133
+ Both forms are equivalent:
134
+
135
+ - `-Dkey=value`
136
+ - `--define key=value`
137
+
138
+ Inside the script:
139
+
140
+ ```js
141
+ defines.test // "123"
142
+ defines.test2 // "456"
143
+ ```
144
+
145
+ All define values are strings.
146
+
147
+ ### Example Usage
148
+
149
+ ```js
150
+ if (defines.test === "123") {
151
+ sch.declare("J6", {
152
+ symbol: "Connector_Generic:Conn_01x04"
153
+ });
154
+ }
155
+ ```
156
+
157
+ This enables variant-based schematic generation.
158
+
159
+ # Minimal Complete Example
160
+
161
+ ```js
162
+ export default async function (sch, defines) {
163
+
164
+ let J1 = sch.declare("J1", {
165
+ symbol: "Connector_Generic:Conn_01x02",
166
+ footprint: "Connector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_Horizontal"
167
+ });
168
+
169
+ J1.pin(2).connect("GND");
170
+
171
+ let J5 = sch.declare("J5", {
172
+ symbol: "Connector_Generic:Conn_01x04"
173
+ });
174
+
175
+ J5.pin(1).connect(J1.pin(1));
176
+
177
+ if (defines.test === "123") {
178
+ sch.declare("J6", {
179
+ symbol: "Connector_Generic:Conn_01x04"
180
+ });
181
+ }
182
+ }
183
+ ```
184
+
185
+ # Design Constraints (Important for AI Generation)
186
+
187
+ When generating Kisch scripts:
188
+
189
+ - Always export a default async function.
190
+ - Always use `sch.declare()` to create components.
191
+ - Always access pins using `.pin(n)`.
192
+ - Only use `.connect()` with:
193
+ - a string (net name), or
194
+ - a pin object.
195
+ - Do not assume additional API methods unless explicitly documented.
196
+ - All `defines` values are strings.
197
+ - Connecting to a string creates a net label.
198
+ - Connecting to a pin creates a direct wire.
199
+
200
+ # Philosophy
201
+
202
+ Kisch treats schematics as deterministic, programmable structures.
203
+
204
+ - Wires are explicit.
205
+ - Net labels are explicit.
206
+ - There is no hidden net merging.
207
+ - Behavior depends strictly on argument type.
208
+
209
+ This makes schematic generation reproducible, scriptable, and suitable for AI-assisted workflows.
package/README.md CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  kisch — schema‑transformation tool for KiCad schematics
6
6
 
7
+ [Watch video tutorial](https://www.youtube.com/watch?v=VylXziX1D8U)
8
+
7
9
  ## SYNOPSIS
8
10
 
9
11
  ```
@@ -125,6 +127,8 @@ A script typically:
125
127
 
126
128
  Scripts do **not** contain geometric layout information; placement is determined by KiCad or kisch heuristics.
127
129
 
130
+ Full scripting reference: [DSL.md](./DSL.md)
131
+
128
132
  ### Example Script
129
133
 
130
134
  ```js
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kisch",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -10,7 +10,7 @@
10
10
  "test": "jasmine"
11
11
  },
12
12
  "bin": {
13
- "kisch": "src/kisch-cli.js"
13
+ "kisch": "src/app/kisch-cli.js"
14
14
  },
15
15
  "keywords": [],
16
16
  "author": "",
@@ -22,5 +22,8 @@
22
22
  },
23
23
  "dependencies": {
24
24
  "commander": "^14.0.3"
25
+ },
26
+ "exports": {
27
+ ".": "./src/exports.js"
25
28
  }
26
29
  }
@@ -0,0 +1 @@
1
+ export {default as SymbolLibrary} from "./SymbolLibrary.js";
@@ -1,16 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import {program} from "commander";
4
- import Schematic, {loadSchematic, createSchematic} from "./Schematic.js";
4
+ import Schematic, {loadSchematic, createSchematic} from "../schematic/Schematic.js";
5
5
  import path from "node:path";
6
- import pkg from "../package.json" with { type: "json" };
7
- import {DeclaredError} from "./js-util.js";
6
+ import pkg from "../../package.json" with { type: "json" };
7
+ import {DeclaredError} from "../utils/js-util.js";
8
8
  import fs, {promises as fsp} from "fs";
9
-
10
- /*todo...
11
-
12
- defines
13
- wires*/
9
+ import {compoundSymbol} from "../schematic/CompoundSymbol.js";
14
10
 
15
11
  let HELP_TEXT=`
16
12
  Examples:
@@ -45,6 +41,8 @@ program
45
41
  .option("-s, --script <script.js>", "Input script to apply to schematic.")
46
42
  .option("-e, --emit <script.js>", "Emit script based on schematic.")
47
43
  .option("-q, --quiet", "No output, except for errors.")
44
+ .option("-a, --append", "Append mode: don't remove undeclared.")
45
+ .option("--bounding-boxes", "Remove wires and draw bounding boxes instead (only for debug!).")
48
46
  //.option("-v, --verbose", "Print detailed execution info")
49
47
  .option(
50
48
  "-D, --define <key=value>",
@@ -111,11 +109,29 @@ try {
111
109
  ([e.slice(0,e.indexOf("=")),e.slice(e.indexOf("=")+1)])
112
110
  ));
113
111
 
112
+ global.compoundSymbol=compoundSymbol;
114
113
  let mod=await import(path.resolve(options.script));
115
114
  if (typeof mod.default=="function")
116
115
  await mod.default(schematic,defines);
117
116
 
118
- schematic.removeUndeclared();
117
+ if (!options.append)
118
+ schematic.removeUndeclared();
119
+ }
120
+
121
+ if (options.boundingBoxes) {
122
+ cons.info("Drawing bounding boxes...");
123
+ for (let e of schematic.getEntities({type: "wire"})) {
124
+ schematic.removeEntity(e);
125
+ }
126
+
127
+ for (let s of schematic.getEntities({type: "symbol"})) {
128
+ schematic.drawWireRect(s.getBoundingRect());
129
+ for (let p of s.getPins()) {
130
+ schematic.drawWirePoint(p.getPoint());
131
+ schematic.drawWirePoint(p.getLegPoint());
132
+ schematic.drawWireLine(p.getPoint(),p.getLegPoint());
133
+ }
134
+ }
119
135
  }
120
136
 
121
137
  if (options.emit) {
@@ -0,0 +1,35 @@
1
+ export default class CompoundSymbol {
2
+ constructor(...symbols) {
3
+ this.symbols=symbols;
4
+ this.pins=[];
5
+
6
+ for (let symbol of symbols) {
7
+ for (let pin of symbol.pins) {
8
+ //console.log(pin.getNum());
9
+
10
+ this.pins.push(pin);
11
+ }
12
+ }
13
+ }
14
+
15
+ pin(num) {
16
+ if (!num)
17
+ throw new Error("Pins start at 1");
18
+
19
+ return this.pins[num-1];
20
+ }
21
+
22
+ namePins(names) {
23
+ if (names.length!=this.pins.length)
24
+ throw new Error("pin count mismatch");
25
+
26
+ for (let i=0; i<this.pins.length; i++)
27
+ this[names[i]]=this.pins[i];
28
+
29
+ return this;
30
+ }
31
+ }
32
+
33
+ export function compoundSymbol(...args) {
34
+ return new CompoundSymbol(...args);
35
+ }
@@ -1,31 +1,50 @@
1
- import {Point} from "./cartesian-math.js";
2
- import {sym, symName, sexpCallName} from "./sexp.js";
3
- import {Rect} from "./cartesian-math.js";
1
+ import {Point, Rect} from "../utils/cartesian-math.js";
2
+ import {sym, symName, sexpCallName} from "../utils/sexp.js";
4
3
 
5
4
  class EntityPin {
6
5
  constructor(sexpr, entity) {
7
6
  this.sexpr=sexpr;
8
7
  this.entity=entity;
9
-
10
- //console.log(this.sexpr);
11
8
  }
12
9
 
13
10
  getNum() {
14
11
  return this.sexpr[1];
15
12
  }
16
13
 
17
- getPoint() {
14
+ initPoint() {
18
15
  let librarySymbol=this.entity.getLibrarySymbol();
19
16
  let librarySymbolPin=librarySymbol.getPin(Number(this.getNum()));
20
17
  let pinAt=Point.from(librarySymbolPin.at);
18
+ let symbolAt=this.entity.getAt();
19
+ let symbolRot=this.entity.getRotation();
21
20
  pinAt[1]=-pinAt[1];
22
21
 
23
- return Point.from(this.entity.getAt()).add(pinAt);
22
+ //this.point=Point.from(symbolAt).add(pinAt.rotateDegrees(-symbolAt[2]));
23
+ //this.point=Point.from(symbolAt).add(pinAt.rotateDegrees(-this.entity.getRotation()));
24
+ this.point=Point.from(symbolAt).add(pinAt.rotateDegrees(-symbolRot));
25
+
26
+ let leg=new Point(librarySymbolPin.length,0);
27
+ leg=leg.rotateDegrees(-(librarySymbolPin.rotation+symbolRot));
28
+ this.legPoint=this.point.add(leg);
29
+ }
30
+
31
+ getLegPoint() {
32
+ if (!this.legPoint)
33
+ this.initPoint();
34
+
35
+ return this.legPoint;
36
+ }
37
+
38
+ getPoint() {
39
+ if (!this.point)
40
+ this.initPoint();
41
+
42
+ return this.point;
24
43
  }
25
44
 
26
45
  isConnected(p) {
27
46
  if (typeof p=="string") {
28
- for (let e of this.entity.schematic.getLabelEntities(p)) {
47
+ for (let e of this.entity.schematic.getEntities({label: p})) {
29
48
  let p=e.getConnectionPoints()[0];
30
49
  if (this.entity.schematic.arePointsConnected(this.getPoint(),p))
31
50
  return true;
@@ -40,9 +59,12 @@ class EntityPin {
40
59
  }
41
60
 
42
61
  connect(p) {
62
+ if (!p)
63
+ return;
64
+
43
65
  if (this.isConnected(p)) {
44
66
  if (typeof p=="string") {
45
- for (let e of this.entity.schematic.getLabelEntities(p)) {
67
+ for (let e of this.entity.schematic.getEntities({label: p})) {
46
68
  let p=e.getConnectionPoints()[0];
47
69
  if (this.entity.schematic.arePointsConnected(this.getPoint(),p)) {
48
70
  e.declared=true;
@@ -64,11 +86,16 @@ class EntityPin {
64
86
  }
65
87
 
66
88
  else {
89
+ //console.log("connecting wire",this.toString(),"->",p.toString());
67
90
  this.entity.schematic.addConnectionWire(this.getPoint(),p.getPoint());
68
91
  this.entity.schematic.markConnectionDeclared(this.getPoint(),p.getPoint());
69
92
  }
70
93
  }
71
94
 
95
+ toString() {
96
+ return this.entity.getReference()+":"+this.getNum();
97
+ }
98
+
72
99
  getConnections() {
73
100
  let connections=[];
74
101
 
@@ -77,7 +104,7 @@ class EntityPin {
77
104
  connections.push(net);
78
105
  }
79
106
 
80
- for (let e of this.entity.schematic.getSymbolEntities()) {
107
+ for (let e of this.entity.schematic.getEntities({type: "symbol"})) {
81
108
  if (e==this.entity)
82
109
  continue;
83
110
 
@@ -97,7 +124,7 @@ export default class Entity {
97
124
  this.pins=[];
98
125
 
99
126
  this.type=symName(this.sexpr[0]);
100
- if (!["symbol","wire","label"].includes(this.type))
127
+ if (!["symbol","wire","label","junction"].includes(this.type))
101
128
  throw new Error("Unknown entity: "+this.type);
102
129
 
103
130
  for (let a of this.sexpr)
@@ -105,6 +132,14 @@ export default class Entity {
105
132
  this.pins.push(new EntityPin(a,this));
106
133
  }
107
134
 
135
+ getRotation() {
136
+ return this.getAt()[2];
137
+ }
138
+
139
+ getPins() {
140
+ return this.pins;
141
+ }
142
+
108
143
  getSexp() {
109
144
  return this.sexpr;
110
145
  }
@@ -178,6 +213,36 @@ export default class Entity {
178
213
  el[2]=footprint;
179
214
  }
180
215
 
216
+ removeProp(name) {
217
+ if (this.getType()!="symbol")
218
+ throw new Error("Only symbols have props");
219
+
220
+ let index=this.sexpr.findIndex(a=>sexpCallName(a)=="property" && a[1]==name);
221
+ if (index<0)
222
+ return;
223
+
224
+ this.sexpr.splice(index,1);
225
+ }
226
+
227
+ setProp(name, value) {
228
+ if (this.getType()!="symbol")
229
+ throw new Error("Only symbols have props");
230
+
231
+ let el=this.sexpr.find(a=>sexpCallName(a)=="property" && a[1]==name);
232
+ if (!el) {
233
+ let exp=[sym("property"),name,"",
234
+ [sym("effects"),
235
+ [sym("hide"),sym("yes")]
236
+ ]
237
+ ];
238
+
239
+ this.sexpr.push(exp);
240
+ el=exp;
241
+ }
242
+
243
+ el[2]=value;
244
+ }
245
+
181
246
  getLibId() {
182
247
  return this.sexpr.find(x=>sexpCallName(x)=="lib_id")[1];
183
248
  }
@@ -196,12 +261,18 @@ export default class Entity {
196
261
  }
197
262
 
198
263
  getBoundingRect() {
264
+ if (!this.librarySymbol)
265
+ throw new Error("Can't get bounding rect, no library symbol");
266
+
199
267
  //console.log(this.librarySymbol);
200
268
  let r=this.librarySymbol.getBoundingRect();
269
+ let corner=r.corner.rotateDegrees(-this.getRotation());
270
+ let size=r.size.rotateDegrees(-this.getRotation());
271
+
201
272
  //console.log(r);
202
273
  let p=Point.from(this.getAt());
203
274
 
204
- return new Rect(p.add(r.corner),r.size);
275
+ return new Rect(p.add(corner),size);
205
276
  }
206
277
 
207
278
  getLibrarySymbol() {
@@ -219,12 +290,6 @@ export default class Entity {
219
290
 
220
291
  getType() {
221
292
  return this.type;
222
- /*let t=symName(this.sexpr[0]);
223
-
224
- if (!["symbol","wire","label"].includes(t))
225
- throw new Error("Unknown entity: "+t);
226
-
227
- return t;*/
228
293
  }
229
294
 
230
295
  getConnectionPoints() {
@@ -249,4 +314,43 @@ export default class Entity {
249
314
  throw new Error("Unknown entity type: "+this.getType());
250
315
  }
251
316
  }
317
+
318
+ connect(...pins) {
319
+ if (this.getType()!="symbol")
320
+ throw new Error("can only connect sybols");
321
+
322
+ if (pins.length!=this.pins.length)
323
+ throw new Error("pin count mismatch");
324
+
325
+ for (let i=0; i<this.pins.length; i++)
326
+ this.pins[i].connect(pins[i]);
327
+ }
328
+
329
+ containsPoint(p) {
330
+ function isNumberInRangeInclusive(num, a, b) {
331
+ return ((num >= Math.min(a, b)) && (num <= Math.max(a, b)));
332
+ }
333
+
334
+ if (this.getType()!="wire")
335
+ throw new Error("Only a wire can contain points");
336
+
337
+ p=Point.from(p);
338
+ let cp=this.getConnectionPoints();
339
+ if (cp[0][0]==cp[1][0]) { // vertical
340
+ if (p[0]!=cp[0][0])
341
+ return false;
342
+
343
+ return isNumberInRangeInclusive(p[1],cp[0][1],cp[1][1]);
344
+ }
345
+
346
+ else if (cp[0][1]==cp[1][1]) { // horizontal
347
+ if (p[1]!=cp[0][1])
348
+ return false;
349
+
350
+ return isNumberInRangeInclusive(p[0],cp[0][0],cp[1][0]);
351
+ }
352
+
353
+ else
354
+ throw new Error("wire is not horizontal or vertical");
355
+ }
252
356
  }
@@ -1,5 +1,5 @@
1
- import {sym, isSym, symEq, symName, sexpStringify, sexpCallName} from "./sexp.js";
2
- import {Rect} from "./cartesian-math.js";
1
+ import {sym, isSym, symEq, symName, sexpStringify, sexpCallName} from "../utils/sexp.js";
2
+ import {Rect} from "../utils/cartesian-math.js";
3
3
 
4
4
  export default class LibrarySymbol {
5
5
  constructor(sexpr, qualifiedName) {
@@ -61,6 +61,13 @@ export default class LibrarySymbol {
61
61
  if (sexpCallName(x)=="rectangle") {
62
62
  let start=x.find(x=>sexpCallName(x)=="start").slice(1);
63
63
  let end=x.find(x=>sexpCallName(x)=="end").slice(1);
64
+
65
+ start[1]=-start[1];
66
+ end[1]=-end[1];
67
+
68
+ /*console.log(start);
69
+ console.log(end);*/
70
+
64
71
  let r=Rect.fromCorners(start,end);
65
72
 
66
73
  if (!rect)
@@ -68,6 +75,32 @@ export default class LibrarySymbol {
68
75
 
69
76
  rect=rect.union(r);
70
77
  }
78
+
79
+ if (sexpCallName(x)=="polyline") {
80
+ //console.log("poly line...");
81
+ for (let sub of x) {
82
+ if (sexpCallName(sub)=="pts") {
83
+ for (let p of sub) {
84
+ if (sexpCallName(p)=="xy") {
85
+ let point=[p[1],p[2]];
86
+
87
+ if (!rect)
88
+ rect=new Rect(point,[0,0]);
89
+
90
+ //console.log("rect",rect);
91
+ //console.log("point",point);
92
+
93
+ rect=rect.includePoint(point);
94
+ }
95
+ }
96
+ }
97
+ }
98
+ }
99
+ }
100
+
101
+ if (!rect) {
102
+ //console.log(symbolExp);
103
+ throw new Error("Symbol doesn't have any rect!");
71
104
  }
72
105
 
73
106
  //console.log(sexpStringify(symbolExp));
@@ -93,7 +126,9 @@ export class LibrarySymbolPin {
93
126
 
94
127
  switch (symName(e[0])) {
95
128
  case "at":
129
+ //console.log(e);
96
130
  this.at = e.slice(1).map(Number); // [x, y, rotation]
131
+ this.rotation=Number(e[3]);
97
132
  break;
98
133
  case "length":
99
134
  this.length = Number(e[1]);