@schemd/core 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.
@@ -0,0 +1,485 @@
1
+ import { classicalGateHeight, componentTextAnchors, distributedCoordinate, enumerateComponentPorts, positionIcPin, PORT_HOTSPOT_RADIUS, routeConnections, validateDocumentGeometry } from './layout.js';
2
+ import { MAX_SCHEMATIC_SVG_OUTPUT_BYTES, utf8ByteLength } from './limits.js';
3
+ import { assertParsedSchematicDocument } from './parser.js';
4
+ import { CLASSICAL_GATE_KINDS, SCHEMD_OUTPUT_MODES, SchematicSyntaxError } from './types.js';
5
+ import { renderMathLabelTspans } from './math-label.js';
6
+ export const MAX_SVG_OUTPUT_BYTES = MAX_SCHEMATIC_SVG_OUTPUT_BYTES;
7
+ const STATIC_SVG_STYLES = '.schematic-token{fill:none;stroke:var(--schematic-vector,var(--schematic-vector-fallback,currentColor));stroke-linecap:round;stroke-linejoin:round;stroke-width:var(--schematic-stroke-width,1.65);vector-effect:non-scaling-stroke}.schematic-node-fill{fill:var(--schematic-vector,var(--schematic-vector-fallback,currentColor))}.schematic-designator,.schematic-label,.schematic-gate-symbol,.schematic-quantum-detail,.schematic-pin-label{fill:currentColor}.schematic-surface{fill:var(--schematic-surface,transparent)}.schematic-grid-line{fill:none;stroke:var(--schematic-grid,currentColor);stroke-width:1;opacity:.12;vector-effect:non-scaling-stroke}';
8
+ const HOOK_SVG_STYLES = '.schematic-port-hotspot{fill:transparent!important;stroke:transparent!important;stroke-width:8;vector-effect:non-scaling-stroke;pointer-events:all;outline:none}.schematic-port-hotspot:focus-visible{fill:var(--schematic-vector,var(--schematic-vector-fallback,currentColor))!important;fill-opacity:.22!important;stroke:var(--schematic-vector,var(--schematic-vector-fallback,currentColor))!important;stroke-width:2}';
9
+ const INTERACTIVE_SVG_STYLES = '.schematic-component,.schematic-wire{outline:none;transition:opacity .2s ease,filter .2s ease}.schematic-component .schematic-token,.schematic-wire .schematic-token{transition:stroke .2s ease,fill .2s ease,color .2s ease,opacity .2s ease,stroke-width .2s ease}.schematic-glow-layer{opacity:0;pointer-events:none;transition:opacity .2s ease}.schematic-component:hover>.schematic-glow-layer,.schematic-component:focus>.schematic-glow-layer,.schematic-component:focus-within>.schematic-glow-layer,.schematic-component.is-hovered>.schematic-glow-layer,.schematic-component.is-active>.schematic-glow-layer,.schematic-component.is-selected>.schematic-glow-layer,.schematic-wire:hover>.schematic-glow-layer,.schematic-wire:focus>.schematic-glow-layer,.schematic-wire.is-hovered>.schematic-glow-layer,.schematic-wire.is-active>.schematic-glow-layer,.schematic-wire.is-selected>.schematic-glow-layer{opacity:1}.schematic-component.is-degraded,.schematic-wire.is-degraded{opacity:.45}.schematic-component.is-degraded>.schematic-glow-layer,.schematic-wire.is-degraded>.schematic-glow-layer{opacity:0}.schematic-component:hover .schematic-token,.schematic-component:focus .schematic-token,.schematic-component:focus-within .schematic-token,.schematic-component.is-hovered .schematic-token,.schematic-component.is-active .schematic-token,.schematic-component.is-selected .schematic-token,.schematic-wire:hover .schematic-token,.schematic-wire:focus .schematic-token,.schematic-wire.is-hovered .schematic-token,.schematic-wire.is-active .schematic-token,.schematic-wire.is-selected .schematic-token{stroke-width:var(--schematic-interactive-stroke-width,2.25)}.schematic-port-hotspot{fill:var(--schematic-vector,var(--schematic-vector-fallback,currentColor))!important;fill-opacity:0;transition:fill-opacity .2s ease,opacity .2s ease}.schematic-port-hotspot:hover,.schematic-port-hotspot:focus,.schematic-port-hotspot.is-hovered,.schematic-port-hotspot.is-active,.schematic-port-hotspot.is-selected{fill-opacity:.2}.schematic-port-hotspot.is-degraded{opacity:.45}@media(prefers-reduced-motion:reduce){.schematic-component,.schematic-wire,.schematic-token,.schematic-glow-layer,.schematic-port-hotspot{transition:none}}';
10
+ function validXmlCodePoint(codePoint) {
11
+ if (codePoint === 0x09 || codePoint === 0x0a || codePoint === 0x0d)
12
+ return true;
13
+ if (codePoint >= 0x20 && codePoint <= 0xd7ff)
14
+ return true;
15
+ if (codePoint >= 0xe000 && codePoint <= 0xfffd)
16
+ return true;
17
+ return codePoint >= 0x10000 && codePoint <= 0x10ffff;
18
+ }
19
+ function replaceInvalidXmlCharacters(value) {
20
+ let normalized = '';
21
+ for (const character of value) {
22
+ const codePoint = character.codePointAt(0);
23
+ normalized += validXmlCodePoint(codePoint) ? character : '\ufffd';
24
+ }
25
+ return normalized;
26
+ }
27
+ function escapeXml(value) {
28
+ return replaceInvalidXmlCharacters(value)
29
+ .replaceAll('&', '&amp;')
30
+ .replaceAll('<', '&lt;')
31
+ .replaceAll('>', '&gt;')
32
+ .replaceAll('"', '&quot;')
33
+ .replaceAll("'", '&#39;');
34
+ }
35
+ export class BoundedSvgWriter {
36
+ #chunks = [];
37
+ #bytes = 0;
38
+ append(chunk) {
39
+ this.#bytes += utf8ByteLength(chunk);
40
+ if (this.#bytes > MAX_SCHEMATIC_SVG_OUTPUT_BYTES) {
41
+ throw new SchematicSyntaxError(`Compiled SVG exceeds the ${MAX_SCHEMATIC_SVG_OUTPUT_BYTES.toLocaleString('en-US')} byte output limit.`);
42
+ }
43
+ this.#chunks.push(chunk);
44
+ }
45
+ finish() {
46
+ return this.#chunks.join('');
47
+ }
48
+ }
49
+ function normalizeCompileOptions(value) {
50
+ if (typeof value !== 'object' || value === null) {
51
+ throw new SchematicSyntaxError('Render options must be an object.');
52
+ }
53
+ const candidate = value;
54
+ const rawBounds = candidate.bounds;
55
+ if (typeof rawBounds !== 'object' || rawBounds === null) {
56
+ throw new SchematicSyntaxError('Render options require bounds.');
57
+ }
58
+ const bounds = rawBounds;
59
+ const width = bounds.width;
60
+ const height = bounds.height;
61
+ if (typeof width !== 'number' ||
62
+ !Number.isInteger(width) ||
63
+ typeof height !== 'number' ||
64
+ !Number.isInteger(height) ||
65
+ width < 64 ||
66
+ height < 64 ||
67
+ width > 4096 ||
68
+ height > 4096) {
69
+ throw new SchematicSyntaxError('Render bounds must be integers from 64 through 4096.');
70
+ }
71
+ const title = candidate.title;
72
+ if (typeof title !== 'string' || title.trim() === '' || title.length > 512) {
73
+ throw new SchematicSyntaxError('Render title must be a non-empty string of at most 512 characters.');
74
+ }
75
+ const idPrefix = candidate.idPrefix;
76
+ if (idPrefix !== undefined && (typeof idPrefix !== 'string' || idPrefix.length > 128)) {
77
+ throw new SchematicSyntaxError('Render idPrefix must be a string of at most 128 characters.');
78
+ }
79
+ const mode = candidate.mode;
80
+ if (mode !== undefined &&
81
+ (typeof mode !== 'string' || !SCHEMD_OUTPUT_MODES.includes(mode))) {
82
+ throw new SchematicSyntaxError('Render mode must be one of: default, embedded-css, or full.');
83
+ }
84
+ const normalizedMode = (mode ?? 'default');
85
+ return idPrefix === undefined
86
+ ? { bounds: { width, height }, title, mode: normalizedMode }
87
+ : { bounds: { width, height }, title, idPrefix, mode: normalizedMode };
88
+ }
89
+ function svgNumber(value) {
90
+ return String(Number(value.toFixed(3)));
91
+ }
92
+ function fittedTextLength(value, maximum, advance) {
93
+ return Math.max(1, Math.min(Array.from(value).length * advance, maximum));
94
+ }
95
+ function stableHash(value) {
96
+ let hash = 2166136261;
97
+ for (let index = 0; index < value.length; index += 1) {
98
+ hash ^= value.charCodeAt(index);
99
+ hash = Math.imul(hash, 16777619);
100
+ }
101
+ return (hash >>> 0).toString(36);
102
+ }
103
+ function isClassicalGate(component) {
104
+ return CLASSICAL_GATE_KINDS.includes(component.kind);
105
+ }
106
+ function colorAttributes(color, extraClass = '') {
107
+ const suffix = extraClass === '' ? '' : ` ${extraClass}`;
108
+ const fill = extraClass.split(' ').includes('schematic-node-fill') ? ' fill="currentColor"' : '';
109
+ if (color.kind === 'token') {
110
+ return `class="schematic-token schematic-token--${color.value}${suffix}"${fill}`;
111
+ }
112
+ if (color.kind === 'css') {
113
+ return `class="schematic-token schematic-token--custom${suffix}"${fill} style="color:${escapeXml(color.value)};--schematic-vector:${escapeXml(color.value)}"`;
114
+ }
115
+ const safeAlias = color.value.replace(/[^a-z0-9-]/gi, '-');
116
+ return `class="schematic-token schematic-token--alias schematic-color--${safeAlias}${suffix}"${fill} style="color:var(--schematic-color-${safeAlias},var(--schematic-vector-fallback,currentColor));--schematic-vector:var(--schematic-color-${safeAlias},var(--schematic-vector-fallback,currentColor))"`;
117
+ }
118
+ function orInputBoundaryX(kind, y, height) {
119
+ const halfHeight = height / 2;
120
+ const progress = (1 - y / halfHeight) / 2;
121
+ const start = kind === 'xor' ? -38 : -32;
122
+ return start + 40 * progress * (1 - progress);
123
+ }
124
+ function orOutputBoundaryX(y, height) {
125
+ const halfHeight = height / 2;
126
+ const progress = Math.sqrt(Math.max(0, 1 - Math.abs(y) / halfHeight));
127
+ const inverse = 1 - progress;
128
+ return -32 * inverse * inverse + 6 * inverse * progress + 32 * progress * progress;
129
+ }
130
+ function gateBoundaryX(component, direction, y) {
131
+ if (component.standard === 'iec')
132
+ return direction === 'input' ? -32 : 32;
133
+ const height = classicalGateHeight(component);
134
+ if (direction === 'input') {
135
+ if (component.kind === 'or' || component.kind === 'nor' || component.kind === 'xor') {
136
+ return orInputBoundaryX(component.kind, y, height);
137
+ }
138
+ return component.kind === 'not' ? -30 : -32;
139
+ }
140
+ if (component.kind === 'and' || component.kind === 'nand') {
141
+ const normalized = y / (height / 2);
142
+ return 32 * Math.sqrt(Math.max(0, 1 - normalized * normalized));
143
+ }
144
+ if (component.kind === 'or' || component.kind === 'nor' || component.kind === 'xor') {
145
+ return orOutputBoundaryX(y, height);
146
+ }
147
+ return 30 - 60 * (Math.abs(y) / (height / 2));
148
+ }
149
+ function hasOutputInversion(component) {
150
+ return component.kind === 'nand' || component.kind === 'nor' || component.kind === 'not';
151
+ }
152
+ function gateStubs(component, paint) {
153
+ const height = classicalGateHeight(component);
154
+ const inputs = Array.from({ length: component.inputs }, (_, index) => {
155
+ const y = distributedCoordinate(index, component.inputs, height);
156
+ return `<path ${paint} d="M -48 ${svgNumber(y)} H ${svgNumber(gateBoundaryX(component, 'input', y))}" />`;
157
+ }).join('');
158
+ const outputs = Array.from({ length: component.outputs }, (_, index) => {
159
+ const y = distributedCoordinate(index, component.outputs, height);
160
+ const boundary = gateBoundaryX(component, 'output', y);
161
+ const start = boundary + (hasOutputInversion(component) ? 8 : 0);
162
+ return `<path ${paint} d="M ${svgNumber(start)} ${svgNumber(y)} H 48" />`;
163
+ }).join('');
164
+ return inputs + outputs;
165
+ }
166
+ function inversionBubbles(component, paint) {
167
+ if (!hasOutputInversion(component))
168
+ return '';
169
+ const height = classicalGateHeight(component);
170
+ return Array.from({ length: component.outputs }, (_, index) => {
171
+ const y = distributedCoordinate(index, component.outputs, height);
172
+ const center = gateBoundaryX(component, 'output', y) + 4;
173
+ return `<circle ${paint} cx="${svgNumber(center)}" cy="${svgNumber(y)}" r="4" />`;
174
+ }).join('');
175
+ }
176
+ function iecGate(component, paint) {
177
+ const height = classicalGateHeight(component);
178
+ const symbol = {
179
+ and: '&amp;',
180
+ nand: '&amp;',
181
+ or: '≥1',
182
+ nor: '≥1',
183
+ xor: '=1',
184
+ not: '1'
185
+ };
186
+ return `<rect ${paint} x="-32" y="${-height / 2}" width="64" height="${height}" rx="2" /><text class="schematic-gate-symbol" fill="currentColor" stroke="none" x="0" y="4">${symbol[component.kind]}</text>${inversionBubbles(component, paint)}${gateStubs(component, paint)}`;
187
+ }
188
+ function ieeeGate(component, paint) {
189
+ const height = classicalGateHeight(component);
190
+ const top = -height / 2;
191
+ const bottom = height / 2;
192
+ let contour;
193
+ if (component.kind === 'not') {
194
+ contour = `<path ${paint} d="M -30 ${top} L 30 0 L -30 ${bottom} Z" />`;
195
+ }
196
+ else if (component.kind === 'and' || component.kind === 'nand') {
197
+ contour = `<path ${paint} d="M -32 ${top} H 0 A 32 ${height / 2} 0 0 1 0 ${bottom} H -32 Z" />`;
198
+ }
199
+ else {
200
+ const xorArc = component.kind === 'xor' ? `<path ${paint} d="M -38 ${top} Q -18 0 -38 ${bottom}" />` : '';
201
+ contour = `<path ${paint} d="M -32 ${top} Q 3 ${top} 32 0 Q 3 ${bottom} -32 ${bottom} Q -12 0 -32 ${top} Z" />${xorArc}`;
202
+ }
203
+ return `${contour}${inversionBubbles(component, paint)}${gateStubs(component, paint)}`;
204
+ }
205
+ function quantumText(component) {
206
+ const details = [component.parameter, component.phase, component.matrix].filter((value) => value !== undefined && value !== '');
207
+ const rows = [component.label, ...details];
208
+ const start = -((rows.length - 1) * 10) / 2 + 3;
209
+ return rows
210
+ .map((value, index) => `<text class="${index === 0 ? 'schematic-gate-symbol' : 'schematic-quantum-detail'}" fill="currentColor" stroke="none" x="0" y="${start + index * 10}" textLength="${fittedTextLength(value, 56, index === 0 ? 7 : 5)}" lengthAdjust="spacingAndGlyphs">${renderMathLabelTspans(value)}</text>`)
211
+ .join('');
212
+ }
213
+ function diodeShape(component, paint) {
214
+ const junction = `<path ${paint} d="M -42 0 H -14 M -14 -16 L 10 0 L -14 16 Z M 10 -16 V 16 M 10 0 H 42" />`;
215
+ switch (component.diodeType) {
216
+ case 'standard':
217
+ return junction;
218
+ case 'schottky':
219
+ return `${junction}<path ${paint} d="M 10 -16 H 16 V -10 M 10 16 H 4 V 10" />`;
220
+ case 'zener':
221
+ return `${junction}<path ${paint} d="M 4 -20 L 10 -16 M 10 16 L 16 20" />`;
222
+ case 'led':
223
+ return `${junction}<path ${paint} d="M -1 -18 L 11 -30 M 6 -30 H 11 V -25 M 8 -14 L 20 -26 M 15 -26 H 20 V -21" />`;
224
+ }
225
+ }
226
+ function bjtShape(component, paint) {
227
+ const emitterArrow = component.transistorType === 'npn' ? 'M 12 17 L 24 25 L 20 12' : 'M 0 7 L 12 17 L 0 20';
228
+ return `<circle ${paint} cx="0" cy="0" r="30" /><path ${paint} d="M -42 0 H -9 M -9 -18 V 18 M -9 -10 L 20 -22 H 42 M -9 10 L 20 22 H 42 ${emitterArrow}" />`;
229
+ }
230
+ function mosfetShape(component, paint) {
231
+ const pChannel = component.transistorType === 'pmos';
232
+ const control = pChannel
233
+ ? `<path ${paint} d="M -42 0 H -16" /><circle ${paint} cx="-11" cy="0" r="5" />`
234
+ : `<path ${paint} d="M -42 0 H -10" />`;
235
+ const arrow = pChannel ? 'M 16 -3 L 5 3 L 16 9' : 'M 5 -3 L 16 3 L 5 9';
236
+ return `<circle ${paint} cx="0" cy="0" r="30" />${control}<path ${paint} d="M -10 -19 V 19 M 4 -17 V 17 M 4 -11 L 20 -22 H 42 M 4 11 L 20 22 H 42 ${arrow}" />`;
237
+ }
238
+ function transistorShape(component, paint) {
239
+ return component.transistorType === 'npn' || component.transistorType === 'pnp'
240
+ ? bjtShape(component, paint)
241
+ : mosfetShape(component, paint);
242
+ }
243
+ function groundShape(component, paint) {
244
+ const stem = `<path ${paint} d="M 0 -42 V -10" />`;
245
+ switch (component.groundStyle) {
246
+ case 'signal':
247
+ return `${stem}<path ${paint} d="M -15 -10 H 15 L 0 15 Z" />`;
248
+ case 'earth':
249
+ return `${stem}<path ${paint} d="M -20 -10 H 20 M -13 -3 H 13 M -6 4 H 6" />`;
250
+ case 'chassis':
251
+ return `${stem}<path ${paint} d="M -20 -10 H 20 M -14 -10 L -20 2 M 0 -10 L -6 2 M 14 -10 L 8 2" />`;
252
+ }
253
+ }
254
+ function icPinMarkup(component, side, paint) {
255
+ const pins = component.pins[side];
256
+ return pins
257
+ .map((pin, index) => {
258
+ const point = positionIcPin(component, side, index);
259
+ const x = point.x - component.x;
260
+ const y = point.y - component.y;
261
+ const horizontalTextLength = fittedTextLength(pin, Math.max(8, component.bodyWidth / 2 - 10), 4);
262
+ const verticalTextLength = fittedTextLength(pin, Math.max(8, component.bodyHeight / 2 - 10), 4);
263
+ if (side === 'left') {
264
+ return `<path ${paint} d="M ${x} ${y} H ${-component.bodyWidth / 2}" /><text class="schematic-pin-label" fill="currentColor" stroke="none" x="${-component.bodyWidth / 2 + 5}" y="${y + 3}" text-anchor="start" textLength="${horizontalTextLength}" lengthAdjust="spacingAndGlyphs">${escapeXml(pin)}</text>`;
265
+ }
266
+ if (side === 'right') {
267
+ return `<path ${paint} d="M ${component.bodyWidth / 2} ${y} H ${x}" /><text class="schematic-pin-label" fill="currentColor" stroke="none" x="${component.bodyWidth / 2 - 5}" y="${y + 3}" text-anchor="end" textLength="${horizontalTextLength}" lengthAdjust="spacingAndGlyphs">${escapeXml(pin)}</text>`;
268
+ }
269
+ if (side === 'top') {
270
+ const labelY = -component.bodyHeight / 2 + 5;
271
+ return `<path ${paint} d="M ${x} ${y} V ${-component.bodyHeight / 2}" /><text class="schematic-pin-label" fill="currentColor" stroke="none" x="${x}" y="${labelY}" text-anchor="start" textLength="${verticalTextLength}" lengthAdjust="spacingAndGlyphs" transform="rotate(90 ${x} ${labelY})">${escapeXml(pin)}</text>`;
272
+ }
273
+ const labelY = component.bodyHeight / 2 - 5;
274
+ return `<path ${paint} d="M ${x} ${component.bodyHeight / 2} V ${y}" /><text class="schematic-pin-label" fill="currentColor" stroke="none" x="${x}" y="${labelY}" text-anchor="start" textLength="${verticalTextLength}" lengthAdjust="spacingAndGlyphs" transform="rotate(-90 ${x} ${labelY})">${escapeXml(pin)}</text>`;
275
+ })
276
+ .join('');
277
+ }
278
+ function integratedCircuitShape(component, paint) {
279
+ const left = -component.bodyWidth / 2;
280
+ const top = -component.bodyHeight / 2;
281
+ return `<rect ${paint} x="${left}" y="${top}" width="${component.bodyWidth}" height="${component.bodyHeight}" rx="3" />${icPinMarkup(component, 'left', paint)}${icPinMarkup(component, 'right', paint)}${icPinMarkup(component, 'top', paint)}${icPinMarkup(component, 'bottom', paint)}<text class="schematic-gate-symbol" fill="currentColor" stroke="none" x="0" y="4" textLength="${fittedTextLength(component.label, component.bodyWidth - 12, 7)}" lengthAdjust="spacingAndGlyphs">${renderMathLabelTspans(component.label)}</text>`;
282
+ }
283
+ function componentShape(component, paint = colorAttributes(component.color), nodePaint = colorAttributes(component.color, 'schematic-node-fill')) {
284
+ if (isClassicalGate(component)) {
285
+ return component.standard === 'iec' ? iecGate(component, paint) : ieeeGate(component, paint);
286
+ }
287
+ switch (component.kind) {
288
+ case 'resistor':
289
+ return `<path ${paint} d="M -42 0 H -26 L -20 -12 L -10 12 L 0 -12 L 10 12 L 20 -12 L 26 0 H 42" />`;
290
+ case 'capacitor':
291
+ return `<path ${paint} d="M -42 0 H -7 M -7 -18 V 18 M 7 -18 V 18 M 7 0 H 42" />`;
292
+ case 'inductor':
293
+ return `<path ${paint} d="M -42 0 H -28 C -28 -16 -14 -16 -14 0 C -14 -16 0 -16 0 0 C 0 -16 14 -16 14 0 C 14 -16 28 -16 28 0 H 42" />`;
294
+ case 'diode':
295
+ return diodeShape(component, paint);
296
+ case 'transistor':
297
+ return transistorShape(component, paint);
298
+ case 'port':
299
+ return `<path ${paint} d="M -42 0 H -24 M 24 0 H 42 M -24 -16 H 14 L 24 0 L 14 16 H -24 Z" />`;
300
+ case 'ground':
301
+ return groundShape(component, paint);
302
+ case 'hadamard':
303
+ return `<rect ${paint} x="-25" y="-25" width="50" height="50" rx="3" /><path ${paint} d="M -48 0 H -25 M 25 0 H 48 M -9 -13 V 13 M 9 -13 V 13 M -9 0 H 9" />`;
304
+ case 'cnot':
305
+ return `<path ${paint} d="M -42 0 H 42 M 0 -26 V 26 M -8 16 H 8 M 0 8 V 24" /><circle ${nodePaint} cx="0" cy="-16" r="4" /><circle ${paint} cx="0" cy="16" r="10" />`;
306
+ case 'qgate':
307
+ return `<rect ${paint} x="-34" y="-30" width="68" height="60" rx="4" /><path ${paint} d="M -48 0 H -34 M 34 0 H 48" />${quantumText(component)}`;
308
+ case 'ic':
309
+ return integratedCircuitShape(component, paint);
310
+ }
311
+ }
312
+ function reusableSymbolKey(component) {
313
+ if (isClassicalGate(component) || component.kind === 'qgate' || component.kind === 'ic') {
314
+ return undefined;
315
+ }
316
+ if (component.kind === 'diode')
317
+ return `diode-${component.diodeType}`;
318
+ if (component.kind === 'transistor')
319
+ return `transistor-${component.transistorType}`;
320
+ if (component.kind === 'ground')
321
+ return `ground-${component.groundStyle}`;
322
+ return component.kind;
323
+ }
324
+ function reusableSymbolDefinition(component, symbolId) {
325
+ const vectorPaint = 'class="schematic-token" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" style="stroke:var(--schematic-vector,currentColor)"';
326
+ const nodePaint = 'class="schematic-token schematic-node-fill" fill="currentColor" stroke="none" style="fill:var(--schematic-vector,currentColor)"';
327
+ return `<g id="${symbolId}">${componentShape(component, vectorPaint, nodePaint)}</g>`;
328
+ }
329
+ function connectionMarkup(connection, routed, index, idPrefix, glowId) {
330
+ const end = routed.points.at(-1);
331
+ const source = escapeXml(`${connection.from.componentId}.${connection.from.port}`);
332
+ const target = escapeXml(`${connection.to.componentId}.${connection.to.port}`);
333
+ const traceId = `${idPrefix}-wire-${index}-vector`;
334
+ const dataAttributes = ` data-wire-source="${source}" data-wire-target="${target}"`;
335
+ const accessibility = ` tabindex="0" role="group" aria-label="Signal from ${source} to ${target}"`;
336
+ const vectorId = ` id="${traceId}"`;
337
+ const markerAttributes = connectionMarkerAttributes(connection, idPrefix);
338
+ const glow = `<use class="schematic-glow-layer" href="#${traceId}" filter="url(#${glowId})" aria-hidden="true" pointer-events="none" />`;
339
+ return `<g class="schematic-wire"${dataAttributes}${accessibility}><path${vectorId} ${colorAttributes(connection.color, 'schematic-trace')} d="${routed.d}"${markerAttributes} />${glow}<circle ${colorAttributes(connection.color, 'schematic-node-fill')} cx="${svgNumber(end.x)}" cy="${svgNumber(end.y)}" r="3" /></g>`;
340
+ }
341
+ function connectionMarkerAttributes(connection, idPrefix) {
342
+ const start = connection.markerStart;
343
+ const end = connection.markerEnd;
344
+ return `${start === 'none' ? '' : ` marker-start="url(#${idPrefix}-marker-${start})"`}${end === 'none' ? '' : ` marker-end="url(#${idPrefix}-marker-${end})"`}`;
345
+ }
346
+ function colorKey(color) {
347
+ return `${color.kind}:${color.value}`;
348
+ }
349
+ function compactConnectionMarkup(connections, routes, idPrefix, embedVisuals, glowId) {
350
+ const batches = new Map();
351
+ for (const [index, connection] of connections.entries()) {
352
+ const markerAttributes = connectionMarkerAttributes(connection, idPrefix);
353
+ const key = `${colorKey(connection.color)}|${markerAttributes}`;
354
+ let batch = batches.get(key);
355
+ if (batch === undefined) {
356
+ batch = { color: connection.color, markerAttributes, traces: [], endpoints: [] };
357
+ batches.set(key, batch);
358
+ }
359
+ const routed = routes[index];
360
+ const end = routed.points.at(-1);
361
+ batch.traces.push(routed.d);
362
+ batch.endpoints.push(`M ${svgNumber(end.x - 3)} ${svgNumber(end.y)} a 3 3 0 1 0 6 0 a 3 3 0 1 0 -6 0`);
363
+ }
364
+ return Array.from(batches.values(), (batch, index) => {
365
+ const tracePaint = colorAttributes(batch.color, 'schematic-trace');
366
+ const nodePaint = colorAttributes(batch.color, 'schematic-node-fill');
367
+ const traceId = `${idPrefix}-wire-batch-${index}-vector`;
368
+ const trace = `<path${embedVisuals ? ` id="${traceId}"` : ''} ${tracePaint} d="${batch.traces.join(' ')}"${batch.markerAttributes} />`;
369
+ const endpoints = `<path ${nodePaint} d="${batch.endpoints.join(' ')}" />`;
370
+ if (!embedVisuals)
371
+ return trace + endpoints;
372
+ return `<g class="schematic-wire" tabindex="0" role="group" aria-label="${batch.traces.length} grouped signal trace${batch.traces.length === 1 ? '' : 's'}">${trace}<use class="schematic-glow-layer" href="#${traceId}" filter="url(#${glowId})" aria-hidden="true" pointer-events="none" />${endpoints}</g>`;
373
+ }).join('');
374
+ }
375
+ function componentMetadata(component) {
376
+ switch (component.kind) {
377
+ case 'qgate':
378
+ return [component.parameter, component.phase, component.matrix].filter(Boolean).join(', ');
379
+ case 'diode':
380
+ return component.diodeType;
381
+ case 'transistor':
382
+ return component.transistorType;
383
+ case 'ground':
384
+ return component.groundStyle;
385
+ case 'ic':
386
+ return `${Object.values(component.pins).reduce((total, pins) => total + pins.length, 0)} pins`;
387
+ default:
388
+ return '';
389
+ }
390
+ }
391
+ function componentPortMarkup(component) {
392
+ return enumerateComponentPorts(component)
393
+ .map((port) => {
394
+ const x = svgNumber(port.point.x - component.x);
395
+ const y = svgNumber(port.point.y - component.y);
396
+ const portId = escapeXml(port.id);
397
+ const parentId = escapeXml(component.id);
398
+ return `<circle ${colorAttributes(component.color, 'schematic-port-hotspot')} cx="${x}" cy="${y}" r="${PORT_HOTSPOT_RADIUS}" fill="transparent" stroke="transparent" stroke-width="8" vector-effect="non-scaling-stroke" pointer-events="all" data-port-id="${portId}" data-parent-node="${parentId}" tabindex="0" role="button" aria-label="${parentId} port ${portId}" />`;
399
+ })
400
+ .join('');
401
+ }
402
+ function componentMarkup(component, index, idPrefix, glowId, mode, symbolId) {
403
+ const label = escapeXml(component.label);
404
+ const renderedLabel = renderMathLabelTspans(component.label);
405
+ const anchors = componentTextAnchors(component);
406
+ const id = escapeXml(component.id);
407
+ const vectorId = `${idPrefix}-node-${index}-vector`;
408
+ const styles = mode === 'embedded-css' || mode === 'full';
409
+ const hooks = mode === 'full';
410
+ const dataAttributes = hooks
411
+ ? ` data-node-id="${id}" data-node-kind="${component.kind}" data-node-label="${label}" data-component="${id}" data-kind="${component.kind}"`
412
+ : '';
413
+ let accessibility = '';
414
+ if (styles || hooks) {
415
+ const metadata = componentMetadata(component);
416
+ const ariaLabel = escapeXml(`${component.id}, ${component.kind}, ${component.label}${metadata === '' ? '' : `, ${metadata}`}`);
417
+ accessibility = ` tabindex="0" role="group" aria-label="${ariaLabel}"`;
418
+ }
419
+ const vectorIdAttribute = styles ? ` id="${vectorId}"` : '';
420
+ const glow = styles
421
+ ? `<use class="schematic-glow-layer" href="#${vectorId}" filter="url(#${glowId})" aria-hidden="true" pointer-events="none" />`
422
+ : '';
423
+ const hotspots = hooks ? componentPortMarkup(component) : '';
424
+ const vector = symbolId === undefined
425
+ ? `<g${vectorIdAttribute} class="schematic-component-vector">${componentShape(component)}</g>`
426
+ : `<use${vectorIdAttribute} ${colorAttributes(component.color, 'schematic-component-vector')} href="#${symbolId}" />`;
427
+ return `<g class="schematic-component"${dataAttributes} transform="translate(${svgNumber(component.x)} ${svgNumber(component.y)})"${accessibility}>${vector}${glow}<text class="schematic-designator" fill="currentColor" stroke="none" x="0" y="${anchors.designatorY}" text-anchor="middle" textLength="${anchors.designatorWidth}" lengthAdjust="spacingAndGlyphs">${id}</text><text class="schematic-label" fill="currentColor" stroke="none" x="0" y="${anchors.labelY}" text-anchor="middle" textLength="${anchors.labelWidth}" lengthAdjust="spacingAndGlyphs">${renderedLabel}</text>${hotspots}</g>`;
428
+ }
429
+ export function renderSchematic(document, options) {
430
+ assertParsedSchematicDocument(document);
431
+ const normalized = normalizeCompileOptions(options);
432
+ validateDocumentGeometry(document, normalized);
433
+ const signature = `${normalized.bounds.width}x${normalized.bounds.height}:${normalized.title}:${JSON.stringify(document)}`;
434
+ const candidatePrefix = (normalized.idPrefix ?? `schematic-${stableHash(signature)}`).replace(/[^A-Za-z0-9_-]/g, '-');
435
+ const safePrefix = /[A-Za-z0-9]/.test(candidatePrefix)
436
+ ? candidatePrefix
437
+ : `schematic-${stableHash(signature)}`;
438
+ const titleId = `${safePrefix}-title`;
439
+ const descriptionId = `${safePrefix}-description`;
440
+ const gridId = `${safePrefix}-grid`;
441
+ const glowId = `${safePrefix}-schematic-glow-filter`;
442
+ const styles = normalized.mode === 'embedded-css' || normalized.mode === 'full';
443
+ const hooks = normalized.mode === 'full';
444
+ const components = new Map(document.components.map((component) => [component.id, component]));
445
+ const routedConnections = routeConnections(document.connections, components);
446
+ const reusableSymbols = new Map();
447
+ for (const component of document.components) {
448
+ const key = reusableSymbolKey(component);
449
+ if (key !== undefined && !reusableSymbols.has(key)) {
450
+ reusableSymbols.set(key, { id: `${safePrefix}-symbol-${key}`, component });
451
+ }
452
+ }
453
+ const title = escapeXml(normalized.title);
454
+ const componentCount = document.components.length;
455
+ const description = `${componentCount} component${componentCount === 1 ? '' : 's'} and ${document.connections.length} signal connection${document.connections.length === 1 ? '' : 's'}.`;
456
+ const writer = new BoundedSvgWriter();
457
+ const hookStyles = hooks ? HOOK_SVG_STYLES : '';
458
+ const interactionStyles = styles ? INTERACTIVE_SVG_STYLES : '';
459
+ const glowFilter = styles
460
+ ? `<filter id="${glowId}" x="-30%" y="-30%" width="160%" height="160%" color-interpolation-filters="sRGB"><feGaussianBlur stdDeviation="2.2" result="blur" /><feMerge><feMergeNode in="blur" /><feMergeNode in="SourceGraphic" /></feMerge></filter>`
461
+ : '';
462
+ const symbolDefinitions = Array.from(reusableSymbols.values(), ({ id, component }) => reusableSymbolDefinition(component, id)).join('');
463
+ const usedMarkers = new Set(document.connections.flatMap((connection) => [connection.markerStart, connection.markerEnd]));
464
+ const markerDefinitions = `${usedMarkers.has('arrow') ? `<marker id="${safePrefix}-marker-arrow" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M0 0 8 4 0 8Z" fill="context-stroke" /></marker>` : ''}${usedMarkers.has('dot') ? `<marker id="${safePrefix}-marker-dot" viewBox="0 0 8 8" refX="4" refY="4" markerWidth="5" markerHeight="5"><circle cx="4" cy="4" r="3" fill="context-stroke" /></marker>` : ''}`;
465
+ const embeddedDefinitions = styles
466
+ ? `<style>${STATIC_SVG_STYLES}${hookStyles}${interactionStyles}</style><pattern id="${gridId}" width="20" height="20" patternUnits="userSpaceOnUse"><path class="schematic-grid-line" d="M20 0H0V20" /></pattern>${glowFilter}`
467
+ : '';
468
+ const definitions = `${embeddedDefinitions}${markerDefinitions}${symbolDefinitions}`;
469
+ writer.append(`<figure class="schematic-frame"${hooks ? ' data-schematic' : ''}><svg class="schematic-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${normalized.bounds.width} ${normalized.bounds.height}" width="${normalized.bounds.width}" height="${normalized.bounds.height}" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" role="img" aria-labelledby="${titleId} ${descriptionId}" preserveAspectRatio="xMidYMid meet"><title id="${titleId}">${title}</title><desc id="${descriptionId}">${description}</desc>${definitions === '' ? '' : `<defs>${definitions}</defs>`}${styles ? `<rect class="schematic-surface" width="100%" height="100%" /><rect class="schematic-grid" width="100%" height="100%" fill="url(#${gridId})" />` : ''}<g class="schematic-vectors">`);
470
+ if (hooks) {
471
+ for (const [index, connection] of document.connections.entries()) {
472
+ writer.append(connectionMarkup(connection, routedConnections[index], index, safePrefix, glowId));
473
+ }
474
+ }
475
+ else {
476
+ writer.append(compactConnectionMarkup(document.connections, routedConnections, safePrefix, normalized.mode === 'embedded-css', glowId));
477
+ }
478
+ for (const [index, component] of document.components.entries()) {
479
+ const key = reusableSymbolKey(component);
480
+ const symbolId = key === undefined ? undefined : reusableSymbols.get(key)?.id;
481
+ writer.append(componentMarkup(component, index, safePrefix, glowId, normalized.mode, symbolId));
482
+ }
483
+ writer.append(`</g></svg><figcaption>${title}</figcaption></figure>`);
484
+ return writer.finish();
485
+ }