@sketchmark/plugin-circuit 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,830 @@
1
+ const SUPPORTED_COMMANDS = new Set([
2
+ "comp",
3
+ "port",
4
+ "junction",
5
+ "wire",
6
+ ]);
7
+ const SUPPORTED_COMPONENTS = new Set([
8
+ "resistor",
9
+ "capacitor",
10
+ "inductor",
11
+ "diode",
12
+ "source",
13
+ "ground",
14
+ "switch",
15
+ ]);
16
+ const DEFAULTS = {
17
+ autoAbsoluteLayout: true,
18
+ stroke: "#2b190d",
19
+ strokeWidth: 2,
20
+ labelColor: "#5f4937",
21
+ labelOffset: 16,
22
+ valueOffset: 18,
23
+ portRadius: 4,
24
+ junctionRadius: 5,
25
+ wireMode: "auto",
26
+ };
27
+ function circuit(options = {}) {
28
+ return {
29
+ name: "circuit",
30
+ preprocess(source) {
31
+ return compileCircuit(source, options);
32
+ },
33
+ };
34
+ }
35
+ function compileCircuit(source, options = {}) {
36
+ const settings = { ...DEFAULTS, ...options };
37
+ const lines = source.split(/\r?\n/);
38
+ const commandByLine = new Map();
39
+ const entities = new Map();
40
+ let hasCircuit = false;
41
+ let inTripleQuoteBlock = false;
42
+ for (let index = 0; index < lines.length; index += 1) {
43
+ const trimmed = lines[index]?.trim() ?? "";
44
+ if (trimmed === '"""') {
45
+ inTripleQuoteBlock = !inTripleQuoteBlock;
46
+ continue;
47
+ }
48
+ if (inTripleQuoteBlock || !trimmed.startsWith("ckt."))
49
+ continue;
50
+ const command = parseCircuitCommand(trimmed, index + 1);
51
+ commandByLine.set(index, command);
52
+ hasCircuit = true;
53
+ if (command.type === "comp") {
54
+ if (entities.has(command.id)) {
55
+ throw new Error(`Duplicate ckt.comp "${command.id}" on line ${command.lineNumber}`);
56
+ }
57
+ entities.set(command.id, buildComponentSpec(command));
58
+ }
59
+ else if (command.type === "port" || command.type === "junction") {
60
+ if (entities.has(command.id)) {
61
+ throw new Error(`Duplicate ckt.${command.type} "${command.id}" on line ${command.lineNumber}`);
62
+ }
63
+ entities.set(command.id, buildPointEntity(command));
64
+ }
65
+ }
66
+ if (!hasCircuit)
67
+ return source;
68
+ const layoutDecision = resolveLayout(lines, settings.autoAbsoluteLayout);
69
+ const output = [];
70
+ for (let index = 0; index < lines.length; index += 1) {
71
+ output.push(lines[index] ?? "");
72
+ if (layoutDecision.insertAfterDiagramIndex === index) {
73
+ output.push("layout absolute");
74
+ }
75
+ const command = commandByLine.get(index);
76
+ if (!command)
77
+ continue;
78
+ output.pop();
79
+ output.push(...emitCommand(command, entities, settings));
80
+ }
81
+ return output.join("\n");
82
+ }
83
+ function parseCircuitCommand(line, lineNumber) {
84
+ const tokens = tokenizeLine(line);
85
+ if (tokens.length < 2) {
86
+ throw new Error(`Invalid circuit command on line ${lineNumber}`);
87
+ }
88
+ const commandToken = tokens[0] ?? "";
89
+ const type = commandToken.slice("ckt.".length);
90
+ if (!SUPPORTED_COMMANDS.has(type)) {
91
+ throw new Error(`Unsupported circuit command "${commandToken}" on line ${lineNumber}`);
92
+ }
93
+ const id = tokens[1] ?? "";
94
+ if (!id || id.includes("=")) {
95
+ throw new Error(`Circuit command "${commandToken}" requires an explicit id on line ${lineNumber}`);
96
+ }
97
+ const props = {};
98
+ for (const token of tokens.slice(2)) {
99
+ const eqIndex = token.indexOf("=");
100
+ if (eqIndex < 1) {
101
+ throw new Error(`Invalid circuit property "${token}" on line ${lineNumber}`);
102
+ }
103
+ props[token.slice(0, eqIndex)] = stripWrapping(token.slice(eqIndex + 1));
104
+ }
105
+ return { type, id, props, lineNumber };
106
+ }
107
+ function tokenizeLine(line) {
108
+ const tokens = [];
109
+ let index = 0;
110
+ while (index < line.length) {
111
+ while (index < line.length && /\s/.test(line[index] ?? ""))
112
+ index += 1;
113
+ if (index >= line.length)
114
+ break;
115
+ const start = index;
116
+ let inQuote = false;
117
+ let listDepth = 0;
118
+ while (index < line.length) {
119
+ const ch = line[index] ?? "";
120
+ if (ch === '"' && line[index - 1] !== "\\") {
121
+ inQuote = !inQuote;
122
+ index += 1;
123
+ continue;
124
+ }
125
+ if (!inQuote) {
126
+ if (ch === "[")
127
+ listDepth += 1;
128
+ if (ch === "]" && listDepth > 0)
129
+ listDepth -= 1;
130
+ if (/\s/.test(ch) && listDepth === 0)
131
+ break;
132
+ }
133
+ index += 1;
134
+ }
135
+ tokens.push(line.slice(start, index));
136
+ }
137
+ return tokens;
138
+ }
139
+ function stripWrapping(value) {
140
+ if (value.length >= 2 && value[0] === '"' && value[value.length - 1] === '"') {
141
+ return value.slice(1, -1).replace(/\\"/g, '"');
142
+ }
143
+ return value;
144
+ }
145
+ function resolveLayout(lines, autoAbsoluteLayout) {
146
+ const diagramIndex = lines.findIndex((line) => (line.trim() ?? "") === "diagram");
147
+ if (diagramIndex < 0) {
148
+ throw new Error('Circuit plugin requires a root "diagram" block');
149
+ }
150
+ const layoutIndex = lines.findIndex((line, index) => {
151
+ if (index <= diagramIndex)
152
+ return false;
153
+ return line.trim().startsWith("layout ");
154
+ });
155
+ if (layoutIndex < 0) {
156
+ if (!autoAbsoluteLayout) {
157
+ throw new Error('Circuit plugin requires "layout absolute"');
158
+ }
159
+ return { insertAfterDiagramIndex: diagramIndex };
160
+ }
161
+ if (lines[layoutIndex]?.trim() !== "layout absolute") {
162
+ throw new Error('Circuit commands require the root diagram to use "layout absolute"');
163
+ }
164
+ return {};
165
+ }
166
+ function buildComponentSpec(command) {
167
+ const kind = normalizeComponentKind(command.props.kind, command.lineNumber);
168
+ const orient = normalizeOrientation(command.props.orient, kind);
169
+ const x = requireNumber(command.props, "x", command.lineNumber);
170
+ const y = requireNumber(command.props, "y", command.lineNumber);
171
+ const dims = componentDimensions(kind, orient, readNumber(command.props.length), readNumber(command.props.size));
172
+ const left = x - dims.w / 2;
173
+ const top = y - dims.h / 2;
174
+ const pins = buildPins(kind, orient, x, y, left, top, dims.w, dims.h);
175
+ return {
176
+ id: command.id,
177
+ kind,
178
+ orient,
179
+ x,
180
+ y,
181
+ w: dims.w,
182
+ h: dims.h,
183
+ left,
184
+ top,
185
+ pins,
186
+ props: command.props,
187
+ };
188
+ }
189
+ function buildPointEntity(command) {
190
+ if (command.type !== "port" && command.type !== "junction") {
191
+ throw new Error(`Expected ckt.port or ckt.junction, received ckt.${command.type}`);
192
+ }
193
+ return {
194
+ id: command.id,
195
+ kind: command.type,
196
+ x: requireNumber(command.props, "x", command.lineNumber),
197
+ y: requireNumber(command.props, "y", command.lineNumber),
198
+ props: command.props,
199
+ };
200
+ }
201
+ function componentDimensions(kind, orient, length, size) {
202
+ switch (kind) {
203
+ case "resistor":
204
+ return orient === "h"
205
+ ? { w: Math.round(length ?? 120), h: 24 }
206
+ : { w: 24, h: Math.round(length ?? 120) };
207
+ case "capacitor":
208
+ return orient === "h"
209
+ ? { w: Math.round(length ?? 100), h: 28 }
210
+ : { w: 28, h: Math.round(length ?? 100) };
211
+ case "inductor":
212
+ return orient === "h"
213
+ ? { w: Math.round(length ?? 116), h: 24 }
214
+ : { w: 24, h: Math.round(length ?? 116) };
215
+ case "diode":
216
+ return orient === "h"
217
+ ? { w: Math.round(length ?? 100), h: 24 }
218
+ : { w: 24, h: Math.round(length ?? 100) };
219
+ case "switch":
220
+ return orient === "h"
221
+ ? { w: Math.round(length ?? 116), h: 24 }
222
+ : { w: 24, h: Math.round(length ?? 116) };
223
+ case "source": {
224
+ const base = Math.round(size ?? 64);
225
+ return { w: base, h: base };
226
+ }
227
+ case "ground":
228
+ return { w: 36, h: 26 };
229
+ }
230
+ }
231
+ function buildPins(kind, orient, x, y, left, top, w, h) {
232
+ const pins = {
233
+ left: { x: left, y },
234
+ right: { x: left + w, y },
235
+ top: { x, y: top },
236
+ bottom: { x, y: top + h },
237
+ center: { x, y },
238
+ };
239
+ if (kind === "ground") {
240
+ pins.pin = { ...pins.top };
241
+ }
242
+ if (kind === "diode") {
243
+ if (orient === "h") {
244
+ pins.anode = { ...pins.left };
245
+ pins.cathode = { ...pins.right };
246
+ }
247
+ else {
248
+ pins.anode = { ...pins.top };
249
+ pins.cathode = { ...pins.bottom };
250
+ }
251
+ }
252
+ return pins;
253
+ }
254
+ function emitCommand(command, entities, settings) {
255
+ switch (command.type) {
256
+ case "comp":
257
+ return emitComponent(entities.get(command.id), settings);
258
+ case "port":
259
+ return emitPort(entities.get(command.id), settings);
260
+ case "junction":
261
+ return emitJunction(entities.get(command.id), settings);
262
+ case "wire":
263
+ return emitWire(command, entities, settings);
264
+ }
265
+ }
266
+ function emitComponent(spec, settings) {
267
+ const bodyId = helperId(spec.id, "body");
268
+ const items = [bodyId];
269
+ const output = [];
270
+ const stroke = spec.props.stroke ?? settings.stroke;
271
+ const strokeWidth = spec.props["stroke-width"] ?? String(settings.strokeWidth);
272
+ const theme = spec.props.theme;
273
+ output.push(emitPathNode(bodyId, {
274
+ x: 0,
275
+ y: 0,
276
+ width: spec.w,
277
+ height: spec.h,
278
+ pathData: componentPath(spec.kind, spec.orient, spec.w, spec.h),
279
+ stroke,
280
+ strokeWidth,
281
+ fill: spec.props.fill ?? "none",
282
+ opacity: spec.props.opacity,
283
+ dash: spec.props.dash,
284
+ theme,
285
+ }));
286
+ if (spec.props.label) {
287
+ const labelId = helperId(spec.id, "label");
288
+ items.push(labelId);
289
+ const labelDx = readNumber(spec.props["label-dx"]) ?? 0;
290
+ const labelDy = readNumber(spec.props["label-dy"]) ?? 0;
291
+ output.push(emitTextNode(labelId, {
292
+ label: spec.props.label,
293
+ x: -40 + labelDx,
294
+ y: -Math.round(settings.labelOffset + 12) + labelDy,
295
+ width: spec.w + 80,
296
+ color: spec.props.color ?? settings.labelColor,
297
+ fontSize: spec.props["font-size"],
298
+ fontWeight: spec.props["font-weight"],
299
+ font: spec.props.font,
300
+ opacity: spec.props.opacity,
301
+ theme,
302
+ }));
303
+ }
304
+ if (spec.props.value) {
305
+ const valueId = helperId(spec.id, "value");
306
+ items.push(valueId);
307
+ output.push(emitTextNode(valueId, {
308
+ label: spec.props.value,
309
+ x: -40,
310
+ y: spec.h + settings.valueOffset - 10,
311
+ width: spec.w + 80,
312
+ color: spec.props.color ?? settings.labelColor,
313
+ fontSize: spec.props["value-size"] ?? spec.props["font-size"],
314
+ fontWeight: spec.props["font-weight"],
315
+ font: spec.props.font,
316
+ opacity: spec.props.opacity,
317
+ theme,
318
+ }));
319
+ }
320
+ output.push(emitBareGroup(spec.id, {
321
+ x: spec.left,
322
+ y: spec.top,
323
+ width: spec.w,
324
+ height: spec.h,
325
+ items,
326
+ }));
327
+ return output;
328
+ }
329
+ function emitPort(entity, settings) {
330
+ const r = readNumber(entity.props.r) ?? settings.portRadius;
331
+ const bodyId = helperId(entity.id, "body");
332
+ const items = [bodyId];
333
+ const output = [
334
+ emitCircleNode(bodyId, {
335
+ x: 0,
336
+ y: 0,
337
+ size: r * 2,
338
+ stroke: entity.props.stroke ?? settings.stroke,
339
+ strokeWidth: entity.props["stroke-width"] ?? String(settings.strokeWidth),
340
+ fill: entity.props.fill ?? settings.stroke,
341
+ opacity: entity.props.opacity,
342
+ theme: entity.props.theme,
343
+ }),
344
+ ];
345
+ if (entity.props.label) {
346
+ const labelId = helperId(entity.id, "label");
347
+ items.push(labelId);
348
+ const side = entity.props.side ?? "right";
349
+ const pos = portLabelPosition(side, r);
350
+ const labelDx = readNumber(entity.props["label-dx"]) ?? 0;
351
+ const labelDy = readNumber(entity.props["label-dy"]) ?? 0;
352
+ output.push(emitTextNode(labelId, {
353
+ label: entity.props.label,
354
+ x: pos.x + labelDx,
355
+ y: pos.y + labelDy,
356
+ width: pos.width,
357
+ color: entity.props.color ?? settings.labelColor,
358
+ fontSize: entity.props["font-size"],
359
+ fontWeight: entity.props["font-weight"],
360
+ font: entity.props.font,
361
+ opacity: entity.props.opacity,
362
+ textAlign: side === "left" ? "right" : side === "right" ? "left" : "center",
363
+ theme: entity.props.theme,
364
+ }));
365
+ }
366
+ output.push(emitBareGroup(entity.id, {
367
+ x: entity.x - r,
368
+ y: entity.y - r,
369
+ width: r * 2,
370
+ height: r * 2,
371
+ items,
372
+ }));
373
+ return output;
374
+ }
375
+ function emitJunction(entity, settings) {
376
+ const r = readNumber(entity.props.r) ?? settings.junctionRadius;
377
+ const bodyId = helperId(entity.id, "body");
378
+ const items = [bodyId];
379
+ const output = [
380
+ emitCircleNode(bodyId, {
381
+ x: 0,
382
+ y: 0,
383
+ size: r * 2,
384
+ stroke: entity.props.stroke ?? settings.stroke,
385
+ strokeWidth: entity.props["stroke-width"] ?? "1",
386
+ fill: entity.props.fill ?? settings.stroke,
387
+ opacity: entity.props.opacity,
388
+ theme: entity.props.theme,
389
+ }),
390
+ ];
391
+ if (entity.props.label) {
392
+ const labelId = helperId(entity.id, "label");
393
+ items.push(labelId);
394
+ const labelDx = readNumber(entity.props["label-dx"]) ?? 0;
395
+ const labelDy = readNumber(entity.props["label-dy"]) ?? 0;
396
+ output.push(emitTextNode(labelId, {
397
+ label: entity.props.label,
398
+ x: 8 + labelDx,
399
+ y: -18 + labelDy,
400
+ width: 100,
401
+ color: entity.props.color ?? settings.labelColor,
402
+ fontSize: entity.props["font-size"],
403
+ fontWeight: entity.props["font-weight"],
404
+ font: entity.props.font,
405
+ opacity: entity.props.opacity,
406
+ textAlign: "left",
407
+ theme: entity.props.theme,
408
+ }));
409
+ }
410
+ output.push(emitBareGroup(entity.id, {
411
+ x: entity.x - r,
412
+ y: entity.y - r,
413
+ width: r * 2,
414
+ height: r * 2,
415
+ items,
416
+ }));
417
+ return output;
418
+ }
419
+ function emitWire(command, entities, settings) {
420
+ const fromRef = command.props.from;
421
+ const toRef = command.props.to;
422
+ if (!fromRef || !toRef) {
423
+ throw new Error(`ckt.wire "${command.id}" requires from= and to= on line ${command.lineNumber}`);
424
+ }
425
+ const from = resolveCircuitRef(fromRef, entities, command.lineNumber);
426
+ const to = resolveCircuitRef(toRef, entities, command.lineNumber);
427
+ const mode = normalizeWireMode(command.props.mode ?? settings.wireMode, command.lineNumber);
428
+ const points = routeWire(from, to, mode);
429
+ const bounds = wireBounds(points);
430
+ const localPoints = points.map((point) => ({
431
+ x: point.x - bounds.minX,
432
+ y: point.y - bounds.minY,
433
+ }));
434
+ const bodyId = helperId(command.id, "body");
435
+ const items = [bodyId];
436
+ const output = [
437
+ emitPathNode(bodyId, {
438
+ x: 0,
439
+ y: 0,
440
+ width: Math.max(1, bounds.maxX - bounds.minX),
441
+ height: Math.max(1, bounds.maxY - bounds.minY),
442
+ pathData: pointsToPath(localPoints),
443
+ stroke: command.props.stroke ?? settings.stroke,
444
+ strokeWidth: command.props["stroke-width"] ?? String(settings.strokeWidth),
445
+ fill: "none",
446
+ opacity: command.props.opacity,
447
+ dash: command.props.dash,
448
+ theme: command.props.theme,
449
+ }),
450
+ ];
451
+ if (command.props.label) {
452
+ const labelId = helperId(command.id, "label");
453
+ items.push(labelId);
454
+ const labelPoint = wireLabelPoint(localPoints);
455
+ const labelDx = readNumber(command.props["label-dx"]) ?? 0;
456
+ const labelDy = readNumber(command.props["label-dy"]) ?? 0;
457
+ output.push(emitTextNode(labelId, {
458
+ label: command.props.label,
459
+ x: labelPoint.x - 60 + labelDx,
460
+ y: labelPoint.y - 20 + labelDy,
461
+ width: 120,
462
+ color: command.props.color ?? settings.labelColor,
463
+ fontSize: command.props["font-size"],
464
+ fontWeight: command.props["font-weight"],
465
+ font: command.props.font,
466
+ opacity: command.props.opacity,
467
+ theme: command.props.theme,
468
+ }));
469
+ }
470
+ output.push(emitBareGroup(command.id, {
471
+ x: bounds.minX,
472
+ y: bounds.minY,
473
+ width: Math.max(1, bounds.maxX - bounds.minX),
474
+ height: Math.max(1, bounds.maxY - bounds.minY),
475
+ items,
476
+ }));
477
+ return output;
478
+ }
479
+ function emitPathNode(id, options) {
480
+ const parts = ["path", id];
481
+ appendProp(parts, "label", "");
482
+ appendProp(parts, "x", String(Math.round(options.x)));
483
+ appendProp(parts, "y", String(Math.round(options.y)));
484
+ appendProp(parts, "width", String(Math.max(1, Math.round(options.width))));
485
+ appendProp(parts, "height", String(Math.max(1, Math.round(options.height))));
486
+ appendProp(parts, "value", options.pathData);
487
+ appendProp(parts, "fill", options.fill);
488
+ appendProp(parts, "stroke", options.stroke);
489
+ appendProp(parts, "stroke-width", options.strokeWidth);
490
+ appendProp(parts, "opacity", options.opacity);
491
+ appendProp(parts, "dash", options.dash);
492
+ appendProp(parts, "theme", options.theme);
493
+ return parts.join(" ");
494
+ }
495
+ function emitCircleNode(id, options) {
496
+ const parts = ["circle", id];
497
+ appendProp(parts, "label", "");
498
+ appendProp(parts, "x", String(Math.round(options.x)));
499
+ appendProp(parts, "y", String(Math.round(options.y)));
500
+ appendProp(parts, "width", String(Math.max(1, Math.round(options.size))));
501
+ appendProp(parts, "height", String(Math.max(1, Math.round(options.size))));
502
+ appendProp(parts, "fill", options.fill);
503
+ appendProp(parts, "stroke", options.stroke);
504
+ appendProp(parts, "stroke-width", options.strokeWidth);
505
+ appendProp(parts, "opacity", options.opacity);
506
+ appendProp(parts, "theme", options.theme);
507
+ return parts.join(" ");
508
+ }
509
+ function emitTextNode(id, options) {
510
+ const parts = ["text", id];
511
+ appendProp(parts, "label", options.label);
512
+ appendProp(parts, "x", String(Math.round(options.x)));
513
+ appendProp(parts, "y", String(Math.round(options.y)));
514
+ appendProp(parts, "width", String(Math.max(1, Math.round(options.width))));
515
+ appendProp(parts, "color", options.color);
516
+ appendProp(parts, "font-size", options.fontSize);
517
+ appendProp(parts, "font-weight", options.fontWeight);
518
+ appendProp(parts, "font", options.font);
519
+ appendProp(parts, "opacity", options.opacity);
520
+ appendProp(parts, "text-align", options.textAlign ?? "center");
521
+ appendProp(parts, "theme", options.theme);
522
+ return parts.join(" ");
523
+ }
524
+ function emitBareGroup(id, options) {
525
+ const parts = ["bare", id];
526
+ appendProp(parts, "layout", "absolute");
527
+ appendProp(parts, "padding", "0");
528
+ appendProp(parts, "gap", "0");
529
+ appendProp(parts, "x", String(Math.round(options.x)));
530
+ appendProp(parts, "y", String(Math.round(options.y)));
531
+ appendProp(parts, "width", String(Math.max(1, Math.round(options.width))));
532
+ appendProp(parts, "height", String(Math.max(1, Math.round(options.height))));
533
+ appendProp(parts, "items", `[${options.items.join(",")}]`);
534
+ return parts.join(" ");
535
+ }
536
+ function componentPath(kind, orient, w, h) {
537
+ switch (kind) {
538
+ case "resistor":
539
+ return orient === "h" ? resistorPathH(w, h) : resistorPathV(w, h);
540
+ case "capacitor":
541
+ return orient === "h" ? capacitorPathH(w, h) : capacitorPathV(w, h);
542
+ case "inductor":
543
+ return orient === "h" ? inductorPathH(w, h) : inductorPathV(w, h);
544
+ case "diode":
545
+ return orient === "h" ? diodePathH(w, h) : diodePathV(w, h);
546
+ case "source":
547
+ return orient === "h" ? sourcePathH(w, h) : sourcePathV(w, h);
548
+ case "ground":
549
+ return groundPath(w);
550
+ case "switch":
551
+ return orient === "h" ? switchPathH(w, h) : switchPathV(w, h);
552
+ }
553
+ }
554
+ function resistorPathH(w, h) {
555
+ const cy = h / 2;
556
+ const lead = Math.round(w * 0.15);
557
+ const bodyStart = lead;
558
+ const bodyEnd = w - lead;
559
+ const bodyWidth = bodyEnd - bodyStart;
560
+ const amp = h / 2 - 2;
561
+ const step = bodyWidth / 6;
562
+ const points = [
563
+ [0, cy],
564
+ [bodyStart, cy],
565
+ [bodyStart + step * 0.5, cy - amp],
566
+ [bodyStart + step * 1.5, cy + amp],
567
+ [bodyStart + step * 2.5, cy - amp],
568
+ [bodyStart + step * 3.5, cy + amp],
569
+ [bodyStart + step * 4.5, cy - amp],
570
+ [bodyStart + step * 5.5, cy + amp],
571
+ [bodyEnd, cy],
572
+ [w, cy],
573
+ ];
574
+ return pointsToPath(points.map(([x, y]) => ({ x, y })));
575
+ }
576
+ function resistorPathV(w, h) {
577
+ const cx = w / 2;
578
+ const lead = Math.round(h * 0.15);
579
+ const bodyStart = lead;
580
+ const bodyEnd = h - lead;
581
+ const bodyHeight = bodyEnd - bodyStart;
582
+ const amp = w / 2 - 2;
583
+ const step = bodyHeight / 6;
584
+ const points = [
585
+ [cx, 0],
586
+ [cx, bodyStart],
587
+ [cx - amp, bodyStart + step * 0.5],
588
+ [cx + amp, bodyStart + step * 1.5],
589
+ [cx - amp, bodyStart + step * 2.5],
590
+ [cx + amp, bodyStart + step * 3.5],
591
+ [cx - amp, bodyStart + step * 4.5],
592
+ [cx + amp, bodyStart + step * 5.5],
593
+ [cx, bodyEnd],
594
+ [cx, h],
595
+ ];
596
+ return pointsToPath(points.map(([x, y]) => ({ x, y })));
597
+ }
598
+ function capacitorPathH(w, h) {
599
+ const cy = h / 2;
600
+ const a = Math.round(w * 0.36);
601
+ const b = Math.round(w * 0.52);
602
+ return `M 0 ${cy} L ${a} ${cy} M ${a} 0 L ${a} ${h} M ${b} 0 L ${b} ${h} M ${b} ${cy} L ${w} ${cy}`;
603
+ }
604
+ function capacitorPathV(w, h) {
605
+ const cx = w / 2;
606
+ const a = Math.round(h * 0.36);
607
+ const b = Math.round(h * 0.52);
608
+ return `M ${cx} 0 L ${cx} ${a} M 0 ${a} L ${w} ${a} M 0 ${b} L ${w} ${b} M ${cx} ${b} L ${cx} ${h}`;
609
+ }
610
+ function inductorPathH(w, h) {
611
+ const cy = h / 2;
612
+ const lead = 16;
613
+ const bodyEnd = w - lead;
614
+ const bodyStart = lead;
615
+ const section = (bodyEnd - bodyStart) / 4;
616
+ return [
617
+ `M 0 ${cy} L ${bodyStart} ${cy}`,
618
+ `C ${bodyStart} ${cy - h / 2} ${bodyStart + section} ${cy - h / 2} ${bodyStart + section} ${cy}`,
619
+ `C ${bodyStart + section} ${cy + h / 2} ${bodyStart + section * 2} ${cy + h / 2} ${bodyStart + section * 2} ${cy}`,
620
+ `C ${bodyStart + section * 2} ${cy - h / 2} ${bodyStart + section * 3} ${cy - h / 2} ${bodyStart + section * 3} ${cy}`,
621
+ `C ${bodyStart + section * 3} ${cy + h / 2} ${bodyEnd} ${cy + h / 2} ${bodyEnd} ${cy}`,
622
+ `L ${w} ${cy}`,
623
+ ].join(" ");
624
+ }
625
+ function inductorPathV(w, h) {
626
+ const cx = w / 2;
627
+ const lead = 16;
628
+ const bodyEnd = h - lead;
629
+ const bodyStart = lead;
630
+ const section = (bodyEnd - bodyStart) / 4;
631
+ return [
632
+ `M ${cx} 0 L ${cx} ${bodyStart}`,
633
+ `C ${cx - w / 2} ${bodyStart} ${cx - w / 2} ${bodyStart + section} ${cx} ${bodyStart + section}`,
634
+ `C ${cx + w / 2} ${bodyStart + section} ${cx + w / 2} ${bodyStart + section * 2} ${cx} ${bodyStart + section * 2}`,
635
+ `C ${cx - w / 2} ${bodyStart + section * 2} ${cx - w / 2} ${bodyStart + section * 3} ${cx} ${bodyStart + section * 3}`,
636
+ `C ${cx + w / 2} ${bodyStart + section * 3} ${cx + w / 2} ${bodyEnd} ${cx} ${bodyEnd}`,
637
+ `L ${cx} ${h}`,
638
+ ].join(" ");
639
+ }
640
+ function diodePathH(w, h) {
641
+ const cy = h / 2;
642
+ const start = 24;
643
+ const bar = 60;
644
+ return `M 0 ${cy} L ${start} ${cy} M ${start} 0 L ${bar - 8} ${cy} L ${start} ${h} Z M ${bar} 0 L ${bar} ${h} M ${bar} ${cy} L ${w} ${cy}`;
645
+ }
646
+ function diodePathV(w, h) {
647
+ const cx = w / 2;
648
+ const start = 24;
649
+ const bar = 60;
650
+ return `M ${cx} 0 L ${cx} ${start} M 0 ${start} L ${cx} ${bar - 8} L ${w} ${start} Z M 0 ${bar} L ${w} ${bar} M ${cx} ${bar} L ${cx} ${h}`;
651
+ }
652
+ function sourcePathH(w, h) {
653
+ const cx = w / 2;
654
+ const cy = h / 2;
655
+ const r = Math.max(10, Math.min(w, h) / 2 - 18);
656
+ return [
657
+ `M 0 ${cy} L ${cx - r} ${cy}`,
658
+ `M ${cx - r} ${cy} A ${r} ${r} 0 1 1 ${cx + r} ${cy}`,
659
+ `A ${r} ${r} 0 1 1 ${cx - r} ${cy}`,
660
+ `M ${cx + r} ${cy} L ${w} ${cy}`,
661
+ `M ${cx} ${cy - 10} L ${cx} ${cy + 10}`,
662
+ `M ${cx - 10} ${cy} L ${cx + 10} ${cy}`,
663
+ ].join(" ");
664
+ }
665
+ function sourcePathV(w, h) {
666
+ const cx = w / 2;
667
+ const cy = h / 2;
668
+ const r = Math.max(10, Math.min(w, h) / 2 - 18);
669
+ return [
670
+ `M ${cx} 0 L ${cx} ${cy - r}`,
671
+ `M ${cx - r} ${cy} A ${r} ${r} 0 1 1 ${cx + r} ${cy}`,
672
+ `A ${r} ${r} 0 1 1 ${cx - r} ${cy}`,
673
+ `M ${cx} ${cy + r} L ${cx} ${h}`,
674
+ `M ${cx} ${cy - 10} L ${cx} ${cy + 10}`,
675
+ `M ${cx - 10} ${cy} L ${cx + 10} ${cy}`,
676
+ ].join(" ");
677
+ }
678
+ function groundPath(w, h) {
679
+ const cx = w / 2;
680
+ return [
681
+ `M ${cx} 0 L ${cx} 8`,
682
+ `M 2 8 L ${w - 2} 8`,
683
+ `M 6 14 L ${w - 6} 14`,
684
+ `M 10 20 L ${w - 10} 20`,
685
+ ].join(" ");
686
+ }
687
+ function switchPathH(w, h) {
688
+ const cy = h / 2;
689
+ return `M 0 ${cy} L 34 ${cy} M 34 ${cy} L 76 2 M ${w - 34} ${cy} L ${w} ${cy}`;
690
+ }
691
+ function switchPathV(w, h) {
692
+ const cx = w / 2;
693
+ return `M ${cx} 0 L ${cx} 34 M ${cx} 34 L 2 76 M ${cx} ${h - 34} L ${cx} ${h}`;
694
+ }
695
+ function resolveCircuitRef(ref, entities, lineNumber) {
696
+ if (entities.has(ref)) {
697
+ const entity = entities.get(ref);
698
+ if ("pins" in entity) {
699
+ throw new Error(`Component "${ref}" requires an explicit pin like ${ref}.left on line ${lineNumber}`);
700
+ }
701
+ return { x: entity.x, y: entity.y };
702
+ }
703
+ const dot = ref.lastIndexOf(".");
704
+ if (dot > 0) {
705
+ const base = ref.slice(0, dot);
706
+ const pin = ref.slice(dot + 1);
707
+ const entity = entities.get(base);
708
+ if (entity && "pins" in entity) {
709
+ const point = entity.pins[pin];
710
+ if (!point) {
711
+ throw new Error(`Unknown pin "${pin}" on "${base}" on line ${lineNumber}`);
712
+ }
713
+ return point;
714
+ }
715
+ }
716
+ throw new Error(`Unknown circuit reference "${ref}" on line ${lineNumber}`);
717
+ }
718
+ function normalizeComponentKind(value, lineNumber) {
719
+ if (!value) {
720
+ throw new Error(`ckt.comp requires kind= on line ${lineNumber}`);
721
+ }
722
+ if (!SUPPORTED_COMPONENTS.has(value)) {
723
+ throw new Error(`Unsupported circuit component kind "${value}" on line ${lineNumber}`);
724
+ }
725
+ return value;
726
+ }
727
+ function normalizeOrientation(value, kind) {
728
+ if (!value)
729
+ return kind === "ground" ? "v" : "h";
730
+ return value === "v" || value === "vertical" ? "v" : "h";
731
+ }
732
+ function normalizeWireMode(value, lineNumber) {
733
+ if (value === "auto" || value === "straight" || value === "hv" || value === "vh") {
734
+ return value;
735
+ }
736
+ throw new Error(`Unsupported wire mode "${value}" on line ${lineNumber}`);
737
+ }
738
+ function routeWire(from, to, mode) {
739
+ const resolvedMode = mode === "auto"
740
+ ? from.x === to.x || from.y === to.y
741
+ ? "straight"
742
+ : Math.abs(to.x - from.x) >= Math.abs(to.y - from.y)
743
+ ? "hv"
744
+ : "vh"
745
+ : mode;
746
+ if (resolvedMode === "straight")
747
+ return [from, to];
748
+ if (resolvedMode === "hv")
749
+ return [from, { x: to.x, y: from.y }, to];
750
+ return [from, { x: from.x, y: to.y }, to];
751
+ }
752
+ function wireBounds(points) {
753
+ return {
754
+ minX: Math.min(...points.map((point) => point.x)),
755
+ minY: Math.min(...points.map((point) => point.y)),
756
+ maxX: Math.max(...points.map((point) => point.x)),
757
+ maxY: Math.max(...points.map((point) => point.y)),
758
+ };
759
+ }
760
+ function pointsToPath(points) {
761
+ return points
762
+ .map((point, index) => `${index === 0 ? "M" : "L"} ${formatNumber(point.x)} ${formatNumber(point.y)}`)
763
+ .join(" ");
764
+ }
765
+ function wireLabelPoint(points) {
766
+ if (points.length === 2) {
767
+ return {
768
+ x: (points[0].x + points[1].x) / 2,
769
+ y: (points[0].y + points[1].y) / 2,
770
+ };
771
+ }
772
+ const midIndex = Math.floor((points.length - 1) / 2);
773
+ const a = points[midIndex];
774
+ const b = points[midIndex + 1] ?? a;
775
+ return {
776
+ x: (a.x + b.x) / 2,
777
+ y: (a.y + b.y) / 2,
778
+ };
779
+ }
780
+ function portLabelPosition(side, r) {
781
+ switch (side) {
782
+ case "left":
783
+ return { x: -124, y: -10, width: 120 };
784
+ case "top":
785
+ return { x: -40, y: -24, width: 80 };
786
+ case "bottom":
787
+ return { x: -40, y: r * 2 + 4, width: 80 };
788
+ default:
789
+ return { x: r * 2 + 6, y: -10, width: 120 };
790
+ }
791
+ }
792
+ function appendProp(parts, key, value) {
793
+ if (value === undefined)
794
+ return;
795
+ parts.push(`${key}=${formatDslValue(value)}`);
796
+ }
797
+ function formatDslValue(value) {
798
+ if (/^-?\d+(?:\.\d+)?$/.test(value))
799
+ return value;
800
+ if (value === "true" || value === "false")
801
+ return value;
802
+ if (/^\[[^\]]*]$/.test(value))
803
+ return value;
804
+ if (/^[A-Za-z_][A-Za-z0-9_.-]*$/.test(value))
805
+ return value;
806
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
807
+ }
808
+ function readNumber(value) {
809
+ if (value === undefined)
810
+ return undefined;
811
+ const parsed = Number(value);
812
+ return Number.isFinite(parsed) ? parsed : undefined;
813
+ }
814
+ function requireNumber(props, key, lineNumber) {
815
+ const value = readNumber(props[key]);
816
+ if (value === undefined) {
817
+ throw new Error(`Circuit command requires ${key}=<number> on line ${lineNumber}`);
818
+ }
819
+ return value;
820
+ }
821
+ function formatNumber(value) {
822
+ const rounded = Math.round(value * 100) / 100;
823
+ return Number.isInteger(rounded) ? String(rounded) : String(rounded);
824
+ }
825
+ function helperId(baseId, suffix) {
826
+ return `__ckt_${baseId.replace(/[^A-Za-z0-9_-]/g, "_")}_${suffix}`;
827
+ }
828
+
829
+ export { circuit, compileCircuit };
830
+ //# sourceMappingURL=index.js.map