@schemd/core 0.2.0 → 0.3.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/renderer.js CHANGED
@@ -1,8 +1,10 @@
1
- import { classicalGateHeight, componentTextAnchors, distributedCoordinate, enumerateComponentPorts, positionIcPin, PORT_HOTSPOT_RADIUS, routeConnections, validateDocumentGeometry } from './layout.js';
1
+ import { classicalGateHeight, componentTextAnchors, distributedCoordinate, enumerateComponentPorts, PORT_HOTSPOT_RADIUS, quantumGateDimensions, routeConnections, validateDocumentGeometry } from './layout.js';
2
2
  import { MAX_SCHEMATIC_SVG_OUTPUT_BYTES, utf8ByteLength } from './limits.js';
3
3
  import { assertParsedSchematicDocument } from './parser.js';
4
- import { CLASSICAL_GATE_KINDS, SCHEMD_OUTPUT_MODES, SCHEMD_SEMANTIC_HOOKS, UML_COMPONENT_KINDS, SchematicSyntaxError } from './types.js';
4
+ import { parsedSchematicRoutes } from './route-cache.js';
5
+ import { CLASSICAL_GATE_KINDS, DIGITAL_COMPONENT_KINDS, QUANTUM_GATE_KINDS, QUANTUM_SPECIAL_KINDS, SCHEMD_OUTPUT_MODES, SCHEMD_SEMANTIC_HOOKS, UML_COMPONENT_KINDS, SchematicSyntaxError } from './types.js';
5
6
  import { mathLabelTextWidth, renderMathLabelTspans } from './math-label.js';
7
+ import { escapeXml } from './xml.js';
6
8
  export const MAX_SVG_OUTPUT_BYTES = MAX_SCHEMATIC_SVG_OUTPUT_BYTES;
7
9
  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,.schematic-uml-text,.schematic-connection-label{fill:currentColor}.schematic-uml-row{font-size:12px}.schematic-uml-stereotype{font-size:11px}.schematic-connection-label{font-size:11px;paint-order:stroke;stroke:var(--schematic-surface,#fff);stroke-width:4;stroke-linejoin:round}.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
10
  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}';
@@ -17,31 +19,6 @@ function semanticHookBit(hook) {
17
19
  return PORT_HOOK;
18
20
  return WIRE_HOOK;
19
21
  }
20
- function validXmlCodePoint(codePoint) {
21
- if (codePoint === 0x09 || codePoint === 0x0a || codePoint === 0x0d)
22
- return true;
23
- if (codePoint >= 0x20 && codePoint <= 0xd7ff)
24
- return true;
25
- if (codePoint >= 0xe000 && codePoint <= 0xfffd)
26
- return true;
27
- return codePoint >= 0x10000 && codePoint <= 0x10ffff;
28
- }
29
- function replaceInvalidXmlCharacters(value) {
30
- let normalized = '';
31
- for (const character of value) {
32
- const codePoint = character.codePointAt(0);
33
- normalized += validXmlCodePoint(codePoint) ? character : '\ufffd';
34
- }
35
- return normalized;
36
- }
37
- function escapeXml(value) {
38
- return replaceInvalidXmlCharacters(value)
39
- .replaceAll('&', '&amp;')
40
- .replaceAll('<', '&lt;')
41
- .replaceAll('>', '&gt;')
42
- .replaceAll('"', '&quot;')
43
- .replaceAll("'", '&#39;');
44
- }
45
22
  export class BoundedSvgWriter {
46
23
  #chunks = [];
47
24
  #bytes = 0;
@@ -128,6 +105,29 @@ function stableHash(value) {
128
105
  function isClassicalGate(component) {
129
106
  return CLASSICAL_GATE_KINDS.includes(component.kind);
130
107
  }
108
+ function isDigitalComponent(component) {
109
+ return DIGITAL_COMPONENT_KINDS.includes(component.kind);
110
+ }
111
+ function isQuantumSpecial(component) {
112
+ return QUANTUM_SPECIAL_KINDS.includes(component.kind);
113
+ }
114
+ function isQuantumGate(component) {
115
+ return QUANTUM_GATE_KINDS.includes(component.kind);
116
+ }
117
+ function componentRotation(component) {
118
+ if (!('orientation' in component))
119
+ return 0;
120
+ switch (component.orientation) {
121
+ case 'down':
122
+ return 90;
123
+ case 'left':
124
+ return 180;
125
+ case 'up':
126
+ return 270;
127
+ default:
128
+ return 0;
129
+ }
130
+ }
131
131
  function isUmlComponent(component) {
132
132
  return UML_COMPONENT_KINDS.includes(component.kind);
133
133
  }
@@ -146,7 +146,7 @@ function colorAttributes(color, extraClass = '') {
146
146
  function orInputBoundaryX(kind, y, height) {
147
147
  const halfHeight = height / 2;
148
148
  const progress = (1 - y / halfHeight) / 2;
149
- const start = kind === 'xor' ? -38 : -32;
149
+ const start = kind === 'xor' || kind === 'xnor' ? -38 : -32;
150
150
  return start + 40 * progress * (1 - progress);
151
151
  }
152
152
  function orOutputBoundaryX(y, height) {
@@ -160,7 +160,7 @@ function gateBoundaryX(component, direction, y) {
160
160
  return direction === 'input' ? -32 : 32;
161
161
  const height = classicalGateHeight(component);
162
162
  if (direction === 'input') {
163
- if (component.kind === 'or' || component.kind === 'nor' || component.kind === 'xor') {
163
+ if (component.kind === 'or' || component.kind === 'nor' || component.kind === 'xor' || component.kind === 'xnor') {
164
164
  return orInputBoundaryX(component.kind, y, height);
165
165
  }
166
166
  return component.kind === 'not' ? -30 : -32;
@@ -169,13 +169,13 @@ function gateBoundaryX(component, direction, y) {
169
169
  const normalized = y / (height / 2);
170
170
  return 32 * Math.sqrt(Math.max(0, 1 - normalized * normalized));
171
171
  }
172
- if (component.kind === 'or' || component.kind === 'nor' || component.kind === 'xor') {
172
+ if (component.kind === 'or' || component.kind === 'nor' || component.kind === 'xor' || component.kind === 'xnor') {
173
173
  return orOutputBoundaryX(y, height);
174
174
  }
175
175
  return 30 - 60 * (Math.abs(y) / (height / 2));
176
176
  }
177
177
  function hasOutputInversion(component) {
178
- return component.kind === 'nand' || component.kind === 'nor' || component.kind === 'not';
178
+ return component.kind === 'nand' || component.kind === 'nor' || component.kind === 'xnor' || component.kind === 'not';
179
179
  }
180
180
  function gateStubs(component, paint) {
181
181
  const height = classicalGateHeight(component);
@@ -209,6 +209,7 @@ function iecGate(component, paint) {
209
209
  or: '≥1',
210
210
  nor: '≥1',
211
211
  xor: '=1',
212
+ xnor: '=1',
212
213
  not: '1'
213
214
  };
214
215
  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" text-anchor="middle" font-size="14">${symbol[component.kind]}</text>${inversionBubbles(component, paint)}${gateStubs(component, paint)}`;
@@ -225,19 +226,30 @@ function ieeeGate(component, paint) {
225
226
  contour = `<path ${paint} d="M -32 ${top} H 0 A 32 ${height / 2} 0 0 1 0 ${bottom} H -32 Z" />`;
226
227
  }
227
228
  else {
228
- const xorArc = component.kind === 'xor' ? `<path ${paint} d="M -38 ${top} Q -18 0 -38 ${bottom}" />` : '';
229
+ const xorArc = component.kind === 'xor' || component.kind === 'xnor' ? `<path ${paint} d="M -38 ${top} Q -18 0 -38 ${bottom}" />` : '';
229
230
  contour = `<path ${paint} d="M -32 ${top} Q 3 ${top} 32 0 Q 3 ${bottom} -32 ${bottom} Q -12 0 -32 ${top} Z" />${xorArc}`;
230
231
  }
231
232
  return `${contour}${inversionBubbles(component, paint)}${gateStubs(component, paint)}`;
232
233
  }
233
234
  function quantumText(component) {
235
+ const symbols = {
236
+ qgate: '', cnot: '⊕',
237
+ hadamard: 'H', xgate: 'X', ygate: 'Y', zgate: 'Z', sgate: 'S', sdg: 'S†',
238
+ tgate: 'T', tdg: 'T†', sx: '√X', phase: 'P', rx: 'Rx', ry: 'Ry', rz: 'Rz', ugate: 'U'
239
+ };
234
240
  const details = [component.parameter, component.phase, component.matrix].filter((value) => value !== undefined && value !== '');
235
- const rows = [component.label, ...details];
236
- const start = -((rows.length - 1) * 12) / 2 + 4;
241
+ const rows = [component.kind === 'qgate' ? component.label : symbols[component.kind], ...details];
242
+ const { bodyWidth } = quantumGateDimensions(component);
243
+ const start = -((rows.length - 1) * 15) / 2 + 4;
237
244
  return rows
238
- .map((value, index) => `<text class="${index === 0 ? 'schematic-gate-symbol' : 'schematic-quantum-detail'}" fill="currentColor" stroke="none" x="0" y="${start + index * 12}" text-anchor="middle" font-size="${index === 0 ? 14 : 10}" textLength="${fittedTextLength(value, 56, index === 0 ? 7 : 5)}" lengthAdjust="spacingAndGlyphs">${renderMathLabelTspans(value)}</text>`)
245
+ .map((value, index) => `<text class="${index === 0 ? 'schematic-gate-symbol' : 'schematic-quantum-detail'}" fill="currentColor" stroke="none" x="0" y="${start + index * 15}" text-anchor="middle" font-size="${index === 0 ? 14 : 10}" textLength="${fittedTextLength(value, bodyWidth - 12, index === 0 ? 7 : 5)}" lengthAdjust="spacingAndGlyphs">${renderMathLabelTspans(value)}</text>`)
239
246
  .join('');
240
247
  }
248
+ function quantumGateShell(component, paint) {
249
+ const { bodyWidth, bodyHeight, stubExtent } = quantumGateDimensions(component);
250
+ const halfWidth = bodyWidth / 2;
251
+ return `<rect ${paint} x="${svgNumber(-halfWidth)}" y="${svgNumber(-bodyHeight / 2)}" width="${svgNumber(bodyWidth)}" height="${svgNumber(bodyHeight)}" rx="3" /><path ${paint} d="M ${svgNumber(-stubExtent)} 0 H ${svgNumber(-halfWidth)} M ${svgNumber(halfWidth)} 0 H ${svgNumber(stubExtent)}" />`;
252
+ }
241
253
  function diodeShape(component, paint) {
242
254
  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" />`;
243
255
  switch (component.diodeType) {
@@ -249,6 +261,14 @@ function diodeShape(component, paint) {
249
261
  return `${junction}<path ${paint} d="M 4 -20 L 10 -16 M 10 16 L 16 20" />`;
250
262
  case 'led':
251
263
  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" />`;
264
+ case 'photodiode':
265
+ return `${junction}<path ${paint} d="M 18 -30 L 6 -18 M 6 -18 H 11 V -23 M 27 -22 L 15 -10 M 15 -10 H 20 V -15" />`;
266
+ case 'varactor':
267
+ return `${junction}<path ${paint} d="M 16 -16 V 16" />`;
268
+ case 'scr':
269
+ return `${junction}<path ${paint} d="M 10 10 L 24 22 H 34" />`;
270
+ case 'triac':
271
+ return `<path ${paint} d="M -42 0 H -15 L 9 -15 V 15 L -15 0 M 42 0 H 15 L -9 15 V -15 L 15 0 M 5 12 L 23 25 H 34" />`;
252
272
  }
253
273
  }
254
274
  function bjtShape(component, paint) {
@@ -256,12 +276,18 @@ function bjtShape(component, paint) {
256
276
  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}" />`;
257
277
  }
258
278
  function mosfetShape(component, paint) {
259
- const pChannel = component.transistorType === 'pmos';
279
+ const pChannel = component.transistorType.startsWith('p');
280
+ const jfet = component.transistorType.endsWith('jfet');
281
+ const igbt = component.transistorType.endsWith('igbt');
282
+ if (jfet) {
283
+ const arrow = pChannel ? 'M -5 -5 L 5 0 L -5 5' : 'M 5 -5 L -5 0 L 5 5';
284
+ return `<circle ${paint} cx="0" cy="0" r="30" /><path ${paint} d="M -42 0 H 4 M 4 -18 V 18 M 4 -12 L 20 -22 H 42 M 4 12 L 20 22 H 42 ${arrow}" />`;
285
+ }
260
286
  const control = pChannel
261
287
  ? `<path ${paint} d="M -42 0 H -16" /><circle ${paint} cx="-11" cy="0" r="5" />`
262
288
  : `<path ${paint} d="M -42 0 H -10" />`;
263
289
  const arrow = pChannel ? 'M 16 -3 L 5 3 L 16 9' : 'M 5 -3 L 16 3 L 5 9';
264
- 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}" />`;
290
+ 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}${igbt ? ' M 9 -11 V 11' : ''}" />`;
265
291
  }
266
292
  function transistorShape(component, paint) {
267
293
  return component.transistorType === 'npn' || component.transistorType === 'pnp'
@@ -283,9 +309,13 @@ function icPinMarkup(component, side, paint) {
283
309
  const pins = component.pins[side];
284
310
  return pins
285
311
  .map((pin, index) => {
286
- const point = positionIcPin(component, side, index);
287
- const x = point.x - component.x;
288
- const y = point.y - component.y;
312
+ const vertical = side === 'left' || side === 'right';
313
+ const x = vertical
314
+ ? (side === 'left' ? -1 : 1) * (component.bodyWidth / 2 + 16)
315
+ : distributedCoordinate(index, pins.length, component.bodyWidth);
316
+ const y = vertical
317
+ ? distributedCoordinate(index, pins.length, component.bodyHeight)
318
+ : (side === 'top' ? -1 : 1) * (component.bodyHeight / 2 + 16);
289
319
  const horizontalTextLength = fittedTextLength(pin, Math.max(8, component.bodyWidth / 2 - 10), 4);
290
320
  const verticalTextLength = fittedTextLength(pin, Math.max(8, component.bodyHeight / 2 - 10), 4);
291
321
  if (side === 'left') {
@@ -306,10 +336,12 @@ function icPinMarkup(component, side, paint) {
306
336
  function integratedCircuitShape(component, paint) {
307
337
  const left = -component.bodyWidth / 2;
308
338
  const top = -component.bodyHeight / 2;
309
- 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" text-anchor="middle" font-size="12" textLength="${fittedTextLength(component.label, component.bodyWidth - 12, 7)}" lengthAdjust="spacingAndGlyphs">${renderMathLabelTspans(component.label)}</text>`;
339
+ const rotation = componentRotation(component);
340
+ const label = `<text class="schematic-gate-symbol" fill="currentColor" stroke="none" x="0" y="4" text-anchor="middle" font-size="12" textLength="${fittedTextLength(component.label, component.bodyWidth - 12, 7)}" lengthAdjust="spacingAndGlyphs">${renderMathLabelTspans(component.label)}</text>`;
341
+ 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)}${rotation === 0 ? label : `<g transform="rotate(${-rotation})">${label}</g>`}`;
310
342
  }
311
343
  function umlRow(value, x, y) {
312
- return `<text class="schematic-uml-text schematic-uml-row" fill="currentColor" stroke="none" x="${svgNumber(x)}" y="${svgNumber(y)}" font-size="12">${escapeXml(value)}</text>`;
344
+ return `<text class="schematic-uml-text schematic-uml-row" fill="currentColor" stroke="none" x="${svgNumber(x)}" y="${svgNumber(y)}" font-size="12">${renderMathLabelTspans(value)}</text>`;
313
345
  }
314
346
  function umlClassShape(component, paint) {
315
347
  const left = -component.bodyWidth / 2;
@@ -320,7 +352,7 @@ function umlClassShape(component, paint) {
320
352
  const operationSeparator = attributeSeparator + attributeHeight;
321
353
  const stereotype = component.stereotype === undefined
322
354
  ? ''
323
- : `<text class="schematic-uml-text schematic-uml-stereotype" fill="currentColor" stroke="none" x="0" y="${svgNumber(top + 13)}" text-anchor="middle" font-size="11">«${escapeXml(component.stereotype)}»</text>`;
355
+ : `<text class="schematic-uml-text schematic-uml-stereotype" fill="currentColor" stroke="none" x="0" y="${svgNumber(top + 13)}" text-anchor="middle" font-size="11">«${renderMathLabelTspans(component.stereotype)}»</text>`;
324
356
  const nameY = top + (component.stereotype === undefined ? 23 : 32);
325
357
  const attributes = component.attributes
326
358
  .map((row, index) => umlRow(row, left + 8, attributeSeparator + 17 + index * 16))
@@ -340,7 +372,8 @@ function umlStateShape(component, paint) {
340
372
  return `<rect ${paint} x="${svgNumber(left)}" y="${svgNumber(top)}" width="${svgNumber(component.bodyWidth)}" height="${svgNumber(component.bodyHeight)}" rx="10" />${component.details.length === 0 ? '' : `<path ${paint} d="M ${svgNumber(left)} ${svgNumber(separator)} H ${svgNumber(-left)}" />`}<text class="schematic-uml-text" fill="currentColor" stroke="none" x="0" y="${svgNumber(top + 21)}" text-anchor="middle" font-size="13" font-weight="600">${renderMathLabelTspans(component.label)}</text>${rows}`;
341
373
  }
342
374
  function umlComponentShape(component, paint, nodePaint) {
343
- if (component.kind === 'class')
375
+ if (component.kind === 'class' || component.kind === 'interface' ||
376
+ component.kind === 'enumeration' || component.kind === 'datatype' || component.kind === 'object')
344
377
  return umlClassShape(component, paint);
345
378
  if (component.kind === 'state')
346
379
  return umlStateShape(component, paint);
@@ -365,25 +398,230 @@ function umlComponentShape(component, paint, nodePaint) {
365
398
  const top = -component.bodyHeight / 2;
366
399
  return `<path ${paint} d="M ${svgNumber(left)} ${svgNumber(top + 12)} V ${svgNumber(-top)} H ${svgNumber(-left)} V ${svgNumber(top)} H ${svgNumber(left + Math.min(54, component.bodyWidth / 2))} L ${svgNumber(left + Math.min(64, component.bodyWidth / 2 + 10))} ${svgNumber(top + 12)} Z" /><text class="schematic-uml-text" fill="currentColor" stroke="none" x="0" y="4" text-anchor="middle" font-size="13">${renderMathLabelTspans(component.label)}</text>`;
367
400
  }
401
+ case 'component':
402
+ case 'artifact':
403
+ case 'node':
404
+ case 'device':
405
+ case 'execution':
406
+ case 'system':
407
+ case 'action':
408
+ case 'object-node':
409
+ case 'partition':
410
+ case 'activation':
411
+ case 'fragment':
412
+ case 'interaction':
413
+ case 'region': {
414
+ const left = -component.bodyWidth / 2;
415
+ const top = -component.bodyHeight / 2;
416
+ const right = -left;
417
+ const bottom = -top;
418
+ if (component.kind === 'activation')
419
+ return `<rect ${paint} x="${svgNumber(left)}" y="${svgNumber(top)}" width="${svgNumber(component.bodyWidth)}" height="${svgNumber(component.bodyHeight)}" />`;
420
+ if (component.kind === 'fragment' || component.kind === 'interaction') {
421
+ return `<rect ${paint} x="${svgNumber(left)}" y="${svgNumber(top)}" width="${svgNumber(component.bodyWidth)}" height="${svgNumber(component.bodyHeight)}" /><path ${paint} d="M ${svgNumber(left)} ${svgNumber(top + 20)} H ${svgNumber(left + 58)} L ${svgNumber(left + 68)} ${svgNumber(top + 10)} V ${svgNumber(top)}" /><text class="schematic-uml-text" fill="currentColor" stroke="none" x="${svgNumber(left + 6)}" y="${svgNumber(top + 14)}" font-size="11">${component.kind === 'interaction' ? 'ref' : 'alt'}</text><text class="schematic-uml-text" fill="currentColor" stroke="none" x="0" y="4" text-anchor="middle" font-size="12">${renderMathLabelTspans(component.label)}</text>`;
422
+ }
423
+ const rx = component.kind === 'action' ? 10 : 1;
424
+ const dash = component.kind === 'region' ? ' stroke-dasharray="6 4"' : '';
425
+ const icon = component.kind === 'component'
426
+ ? `<path ${paint} d="M ${svgNumber(right - 22)} ${svgNumber(top + 10)} h 14 v 10 h -14 z M ${svgNumber(right - 22)} ${svgNumber(top + 25)} h 14 v 10 h -14 z" />`
427
+ : component.kind === 'artifact'
428
+ ? `<path ${paint} d="M ${svgNumber(right - 16)} ${svgNumber(top)} v 16 h 16" />`
429
+ : component.kind === 'node' || component.kind === 'device' || component.kind === 'execution'
430
+ ? `<path ${paint} d="M ${svgNumber(left)} ${svgNumber(top + 10)} l 12 -10 h ${svgNumber(component.bodyWidth)} l -12 10 M ${svgNumber(right)} ${svgNumber(top)} v ${svgNumber(component.bodyHeight - 10)} l -12 10" />`
431
+ : component.kind === 'partition' ? `<path ${paint} d="M ${svgNumber(left + 28)} ${svgNumber(top)} V ${svgNumber(bottom)}" />` : '';
432
+ return `<rect ${paint}${dash} x="${svgNumber(left)}" y="${svgNumber(top)}" width="${svgNumber(component.bodyWidth)}" height="${svgNumber(component.bodyHeight)}" rx="${rx}" />${icon}<text class="schematic-uml-text" fill="currentColor" stroke="none" x="0" y="4" text-anchor="middle" font-size="12">${renderMathLabelTspans(component.label)}</text>`;
433
+ }
434
+ case 'provided-interface':
435
+ return `<circle ${paint} cx="0" cy="0" r="10" />`;
436
+ case 'required-interface':
437
+ return `<path ${paint} d="M 0 -10 A 10 10 0 0 0 0 10" />`;
438
+ case 'component-port':
439
+ return `<rect ${paint} x="-7" y="-7" width="14" height="14" />`;
440
+ case 'decision':
441
+ case 'merge':
442
+ case 'choice':
443
+ return `<path ${paint} d="M 0 -17 L 17 0 0 17 -17 0 Z" />`;
444
+ case 'fork':
445
+ case 'join':
446
+ return `<rect ${nodePaint} x="-38" y="-4" width="76" height="8" rx="1" />`;
447
+ case 'activity-final':
448
+ return `<circle ${paint} cx="0" cy="0" r="11" /><circle ${nodePaint} cx="0" cy="0" r="6" />`;
449
+ case 'flow-final':
450
+ return `<circle ${paint} cx="0" cy="0" r="11" /><path ${paint} d="M -7 -7 L 7 7 M 7 -7 L -7 7" />`;
451
+ case 'send-signal':
452
+ return `<path ${paint} d="M -21 -14 H 5 L 21 0 5 14 H -21 Z" />`;
453
+ case 'receive-signal':
454
+ return `<path ${paint} d="M -21 -14 H 21 L 5 0 21 14 H -21 Z" />`;
455
+ case 'destruction':
456
+ return `<path ${paint} d="M -10 -10 L 10 10 M 10 -10 L -10 10" />`;
457
+ case 'gate':
458
+ case 'found':
459
+ case 'lost':
460
+ return `<circle ${component.kind === 'lost' ? paint : nodePaint} cx="0" cy="0" r="5" />`;
461
+ case 'state-junction':
462
+ return `<circle ${nodePaint} cx="0" cy="0" r="7" />`;
463
+ case 'history':
464
+ return `<circle ${paint} cx="0" cy="0" r="11" /><text class="schematic-uml-text" fill="currentColor" stroke="none" x="0" y="4" text-anchor="middle" font-size="11">H${component.variant === 'deep' ? '*' : ''}</text>`;
465
+ case 'entry':
466
+ case 'exit':
467
+ return `<circle ${paint} cx="0" cy="0" r="10" />${component.kind === 'exit' ? `<path ${paint} d="M -6 -6 L 6 6 M 6 -6 L -6 6" />` : ''}`;
468
+ case 'terminate':
469
+ return `<path ${paint} d="M -9 -9 L 9 9 M 9 -9 L -9 9" />`;
368
470
  case 'initial':
369
471
  return `<circle ${nodePaint} cx="0" cy="0" r="10" />`;
370
472
  case 'final':
371
473
  return `<circle ${paint} cx="0" cy="0" r="11" /><circle ${nodePaint} cx="0" cy="0" r="6" />`;
372
474
  }
373
475
  }
476
+ function passiveShape(component, paint) {
477
+ const variant = component.passiveType ?? 'fixed';
478
+ if (component.kind === 'resistor') {
479
+ const base = `<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" />`;
480
+ if (variant === 'variable' || variant === 'rheostat' || variant === 'potentiometer')
481
+ return `${base}<path ${paint} d="M -18 20 L 18 -20 M 11 -20 H 18 V -13${variant === 'potentiometer' ? ' M 0 28 V 12' : ''}" />`;
482
+ if (variant === 'thermistor')
483
+ return `${base}<path ${paint} d="M -18 19 L 18 -19 M 10 -19 H 18 V -11" />`;
484
+ if (variant === 'ldr')
485
+ return `${base}<path ${paint} d="M 15 -27 L 4 -16 M 4 -16 H 9 V -21 M 27 -20 L 16 -9 M 16 -9 H 21 V -14" />`;
486
+ return base;
487
+ }
488
+ if (component.kind === 'capacitor') {
489
+ const rightPlate = variant === 'polarized' ? 'M 7 -18 Q 13 0 7 18' : 'M 7 -18 V 18';
490
+ const variable = variant === 'variable' ? `<path ${paint} d="M -18 20 L 18 -20 M 11 -20 H 18 V -13" />` : '';
491
+ const positive = variant === 'polarized' ? `<path ${paint} d="M -17 -12 H -9 M -13 -16 V -8" />` : '';
492
+ return `<path ${paint} d="M -42 0 H -7 M -7 -18 V 18 ${rightPlate} M 7 0 H 42" />${variable}${positive}`;
493
+ }
494
+ const base = `<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" />`;
495
+ return variant === 'fixed' ? base : `${base}<path ${paint} d="M -28 12 C -28 28 -14 28 -14 12 C -14 28 0 28 0 12 C 0 28 14 28 14 12 C 14 28 28 28 28 12${variant === 'transformer' ? ' M -33 5 H 33 M -33 8 H 33' : ''}" />`;
496
+ }
497
+ function electricalShape(component, paint, nodePaint) {
498
+ const variant = component.variant ?? '';
499
+ switch (component.kind) {
500
+ case 'junction':
501
+ return `<circle ${nodePaint} cx="0" cy="0" r="4" />`;
502
+ case 'testpoint':
503
+ return `<circle ${paint} cx="0" cy="0" r="9" /><circle ${nodePaint} cx="0" cy="0" r="2" />`;
504
+ case 'connector':
505
+ return `<path ${paint} d="M -32 0 H -9 M 9 0 H 32" /><circle ${paint} cx="0" cy="0" r="9" />`;
506
+ case 'source': {
507
+ if (variant === 'battery')
508
+ return `<path ${paint} d="M -42 0 H -8 M -8 -16 V 16 M 8 -10 V 10 M 8 0 H 42" />`;
509
+ const dependent = ['vcvs', 'vccs', 'ccvs', 'cccs'].includes(variant);
510
+ const body = dependent ? `<path ${paint} d="M -24 0 L 0 -24 24 0 0 24 Z" />` : `<circle ${paint} cx="0" cy="0" r="24" />`;
511
+ const controls = dependent ? `<path ${paint} d="M 0 -34 V -24 M 0 24 V 34" />` : '';
512
+ const current = variant.startsWith('current') || variant.endsWith('cs');
513
+ const glyph = variant.endsWith('ac')
514
+ ? `<path ${paint} d="M -13 0 C -8 -12 -4 -12 0 0 C 4 12 8 12 13 0" />`
515
+ : variant === 'voltage-pulse'
516
+ ? `<path ${paint} d="M -14 8 H -7 V -8 H 7 V 8 H 14" />`
517
+ : current ? `<path ${paint} d="M 0 12 V -12 M -6 -5 L 0 -12 6 -5" />` : `<path ${paint} d="M -7 -8 H 7 M 0 -15 V -1 M -7 9 H 7" />`;
518
+ return `<path ${paint} d="M -42 0 H -24 M 24 0 H 42" />${body}${controls}${glyph}`;
519
+ }
520
+ case 'power':
521
+ return `<path ${paint} d="M -32 0 H 0 M 0 -13 V 13 M 0 -13 L 15 0 0 13" />`;
522
+ case 'switch': {
523
+ const relay = variant === 'relay' ? `<path ${paint} d="M -12 34 C -12 18 12 18 12 34 M 0 19 V 8" />` : '';
524
+ const throw2 = variant === 'spdt' ? `<path ${paint} d="M 26 12 H 42" />` : '';
525
+ return `<path ${paint} d="M -42 0 H -24 M -24 0 L 22 -12 M 26 -12 H 42" />${throw2}${relay}`;
526
+ }
527
+ case 'protection':
528
+ return variant === 'breaker'
529
+ ? `<path ${paint} d="M -42 0 H -18 M -18 0 L 18 -12 M 18 0 H 42" />`
530
+ : `<path ${paint} d="M -42 0 H -24 M 24 0 H 42" /><rect ${paint} x="-24" y="-9" width="48" height="18" rx="4" />`;
531
+ case 'amplifier':
532
+ return `<path ${paint} d="M -48 -13 H -30 M -48 13 H -30 M -30 -30 L 30 0 -30 30 Z M 30 0 H 48 M 0 -36 V -15 M 0 15 V 36 M -25 -17 H -17 M -21 -21 V -13 M -25 13 H -17" />`;
533
+ case 'resonator':
534
+ return `<path ${paint} d="M -42 0 H -12 M -12 -18 V 18 M 12 -18 V 18 M 12 0 H 42" /><rect ${paint} x="-7" y="-14" width="14" height="28" />`;
535
+ case 'meter':
536
+ case 'load':
537
+ return `<path ${paint} d="M -42 0 H -24 M 24 0 H 42" /><circle ${paint} cx="0" cy="0" r="24" />${variant === 'lamp' ? `<path ${paint} d="M -15 -15 L 15 15 M 15 -15 L -15 15" />` : variant === 'speaker' ? `<path ${paint} d="M -12 -9 H -3 L 8 -19 V 19 L -3 9 H -12 Z M 13 -10 Q 22 0 13 10" />` : variant === 'buzzer' ? `<path ${paint} d="M -12 -9 H -3 L 8 -18 V 18 L -3 9 H -12 Z M 13 -8 L 20 -14 M 15 0 H 24 M 13 8 L 20 14" />` : ''}`;
538
+ }
539
+ }
540
+ function digitalShape(component, paint) {
541
+ if (component.kind === 'buffer') {
542
+ const bubble = component.variant === 'tristate-inverter' || component.variant === 'schmitt-inverter';
543
+ return `<path ${paint} d="M -44 0 H -28 M -28 -22 L 24 0 -28 22 Z M ${bubble ? 32 : 24} 0 H 44${component.variant?.startsWith('schmitt') === true ? ' M -17 -8 H -6 V 8 H 5' : ''}" />${bubble ? `<circle ${paint} cx="28" cy="0" r="4" />` : ''}${component.variant?.startsWith('tristate') === true ? `<path ${paint} d="M 0 34 V 12" />` : ''}`;
544
+ }
545
+ const left = -component.bodyWidth / 2;
546
+ const right = -left;
547
+ const top = -component.bodyHeight / 2;
548
+ const stubs = `${Array.from({ length: component.inputs }, (_, index) => `<path ${paint} d="M ${svgNumber(left - 16)} ${svgNumber(distributedCoordinate(index, Math.max(1, component.inputs), component.bodyHeight))} H ${svgNumber(left)}" />`).join('')}${Array.from({ length: component.outputs }, (_, index) => `<path ${paint} d="M ${svgNumber(right)} ${svgNumber(distributedCoordinate(index, component.outputs, component.bodyHeight))} H ${svgNumber(right + 16)}" />`).join('')}`;
549
+ return `<rect ${paint} x="${svgNumber(left)}" y="${svgNumber(top)}" width="${component.bodyWidth}" height="${component.bodyHeight}" rx="3" />${stubs}`;
550
+ }
551
+ function quantumSpecialShape(component, paint, nodePaint) {
552
+ if (component.kind === 'measure')
553
+ return `<path ${paint} d="M -40 0 H -22 M 22 0 H 40 M 0 22 V 34" /><rect ${paint} x="-22" y="-22" width="44" height="44" rx="3" /><path ${paint} d="M -12 8 A 15 15 0 0 1 12 8 M 0 8 L 11 -9" />`;
554
+ if (component.kind === 'prepare')
555
+ return `<path ${paint} d="M 0 0 H 40 M -10 -10 L 10 0 -10 10 Z" />`;
556
+ if (component.kind === 'reset')
557
+ return `<path ${paint} d="M -40 0 H -18 M 18 0 H 40" /><rect ${paint} x="-18" y="-18" width="36" height="36" rx="3" /><path ${paint} d="M 9 -8 A 12 12 0 1 0 10 7 M 9 -8 H 1 M 9 -8 V 0" />`;
558
+ if (component.kind === 'control')
559
+ return `<path ${paint} d="M -42 0 H 42" />${component.controlType === 'negative' ? `<circle ${paint} cx="0" cy="0" r="5" />` : `<circle ${nodePaint} cx="0" cy="0" r="5" />`}`;
560
+ if (component.kind === 'classical-bit' || component.kind === 'classical-register')
561
+ return `<path ${paint} d="M -40 -2 H 40 M -40 2 H 40" />`;
562
+ const tracks = Math.max(component.wires, component.controls + component.targets);
563
+ const span = Math.max(16, (tracks - 1) * 18);
564
+ const lines = Array.from({ length: tracks }, (_, index) => `<path ${paint} d="M -42 ${svgNumber(distributedCoordinate(index, tracks, span))} H 42" />`).join('');
565
+ if (component.kind === 'barrier')
566
+ return `${lines}<path ${paint} stroke-dasharray="4 3" d="M 0 ${svgNumber(-span / 2 - 8)} V ${svgNumber(span / 2 + 8)}" />`;
567
+ if (component.kind === 'delay')
568
+ return `${lines}<rect ${paint} x="-12" y="${svgNumber(-span / 2 - 7)}" width="24" height="${svgNumber(span + 14)}" rx="2" />`;
569
+ const marks = Array.from({ length: tracks }, (_, index) => {
570
+ const y = distributedCoordinate(index, tracks, span);
571
+ if (component.kind === 'swap')
572
+ return `<path ${paint} d="M -6 ${svgNumber(y - 6)} L 6 ${svgNumber(y + 6)} M 6 ${svgNumber(y - 6)} L -6 ${svgNumber(y + 6)}" />`;
573
+ if (component.kind === 'cz')
574
+ return `<circle ${nodePaint} cx="0" cy="${svgNumber(y)}" r="4" />`;
575
+ if (index < component.controls) {
576
+ if (component.controlType === 'negative')
577
+ return `<circle ${paint} cx="0" cy="${svgNumber(y)}" r="5" />`;
578
+ if (component.controlType === 'classical')
579
+ return `<rect ${nodePaint} x="-5" y="${svgNumber(y - 5)}" width="10" height="10" />`;
580
+ return `<circle ${nodePaint} cx="0" cy="${svgNumber(y)}" r="4" />`;
581
+ }
582
+ if (component.kind === 'controlled' || component.kind === 'cphase')
583
+ return `<rect ${paint} x="-10" y="${svgNumber(y - 10)}" width="20" height="20" rx="2" />`;
584
+ return `<circle ${paint} cx="0" cy="${svgNumber(y)}" r="10" /><path ${paint} d="M -7 ${svgNumber(y)} H 7 M 0 ${svgNumber(y - 7)} V ${svgNumber(y + 7)}" />`;
585
+ }).join('');
586
+ return `${lines}<path ${paint} d="M 0 ${svgNumber(-span / 2)} V ${svgNumber(span / 2)}" />${marks}`;
587
+ }
588
+ function componentInnerText(component) {
589
+ if (isQuantumGate(component) && component.kind !== 'cnot') {
590
+ return `<g ${colorAttributes(component.color)}>${quantumText(component)}</g>`;
591
+ }
592
+ if (isDigitalComponent(component)) {
593
+ const labels = {
594
+ buffer: '', logic: component.variant === 'high' ? '1' : component.variant === 'low' ? '0' : component.variant === 'high-z' ? 'Z' : 'X',
595
+ clock: 'CLK', flipflop: String(component.variant).toUpperCase(), mux: component.variant === 'demux' ? 'DEMUX' : 'MUX',
596
+ encoder: 'ENC', decoder: 'DEC', register: 'REG', counter: 'CTR', adder: component.variant === 'full' ? 'FA' : 'HA', comparator: 'CMP', bus: String(component.variant).toUpperCase()
597
+ };
598
+ const value = labels[component.kind];
599
+ return value === '' ? '' : `<text ${colorAttributes(component.color, 'schematic-gate-symbol')} stroke="none" x="0" y="4" text-anchor="middle" font-size="11">${value}</text>`;
600
+ }
601
+ if ((component.kind === 'meter' || component.kind === 'load') && component.variant !== 'lamp' && component.variant !== 'speaker' && component.variant !== 'buzzer') {
602
+ const value = component.variant === 'voltmeter' ? 'V' : component.variant === 'ammeter' ? 'A' : 'M';
603
+ return `<text ${colorAttributes(component.color, 'schematic-gate-symbol')} stroke="none" x="0" y="5" text-anchor="middle" font-size="14">${value}</text>`;
604
+ }
605
+ if (isQuantumSpecial(component) && component.kind === 'controlled' && component.operator !== undefined) {
606
+ return `<text ${colorAttributes(component.color, 'schematic-gate-symbol')} stroke="none" x="0" y="4" text-anchor="middle" font-size="11">${renderMathLabelTspans(component.operator)}</text>`;
607
+ }
608
+ return '';
609
+ }
374
610
  function componentShape(component, paint = colorAttributes(component.color), nodePaint = colorAttributes(component.color, 'schematic-node-fill')) {
375
611
  if (isClassicalGate(component)) {
376
612
  return component.standard === 'iec' ? iecGate(component, paint) : ieeeGate(component, paint);
377
613
  }
614
+ if (isDigitalComponent(component))
615
+ return digitalShape(component, paint);
616
+ if (isQuantumSpecial(component))
617
+ return quantumSpecialShape(component, paint, nodePaint);
378
618
  if (isUmlComponent(component))
379
619
  return umlComponentShape(component, paint, nodePaint);
380
620
  switch (component.kind) {
381
621
  case 'resistor':
382
- 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" />`;
383
622
  case 'capacitor':
384
- return `<path ${paint} d="M -42 0 H -7 M -7 -18 V 18 M 7 -18 V 18 M 7 0 H 42" />`;
385
623
  case 'inductor':
386
- 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" />`;
624
+ return passiveShape(component, paint);
387
625
  case 'diode':
388
626
  return diodeShape(component, paint);
389
627
  case 'transistor':
@@ -393,28 +631,66 @@ function componentShape(component, paint = colorAttributes(component.color), nod
393
631
  case 'ground':
394
632
  return groundShape(component, paint);
395
633
  case 'hadamard':
396
- 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" />`;
634
+ case 'qgate':
635
+ case 'xgate':
636
+ case 'ygate':
637
+ case 'zgate':
638
+ case 'sgate':
639
+ case 'sdg':
640
+ case 'tgate':
641
+ case 'tdg':
642
+ case 'sx':
643
+ case 'phase':
644
+ case 'rx':
645
+ case 'ry':
646
+ case 'rz':
647
+ case 'ugate':
648
+ return quantumGateShell(component, paint);
397
649
  case 'cnot':
398
650
  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" />`;
399
- case 'qgate':
400
- 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)}`;
401
651
  case 'ic':
402
652
  return integratedCircuitShape(component, paint);
653
+ case 'source':
654
+ case 'junction':
655
+ case 'testpoint':
656
+ case 'connector':
657
+ case 'power':
658
+ case 'switch':
659
+ case 'protection':
660
+ case 'amplifier':
661
+ case 'resonator':
662
+ case 'meter':
663
+ case 'load':
664
+ return electricalShape(component, paint, nodePaint);
403
665
  }
404
666
  }
405
667
  function reusableSymbolKey(component) {
406
668
  if (isClassicalGate(component) ||
407
669
  isUmlComponent(component) ||
408
- component.kind === 'qgate' ||
409
670
  component.kind === 'ic') {
410
671
  return undefined;
411
672
  }
673
+ if (isQuantumGate(component) && component.kind !== 'cnot') {
674
+ const dimensions = quantumGateDimensions(component);
675
+ return `quantum-shell-${dimensions.bodyWidth}-${dimensions.bodyHeight}`;
676
+ }
677
+ if (isDigitalComponent(component))
678
+ return `digital-${component.kind}-${component.variant ?? ''}-${component.inputs}-${component.outputs}-${component.bodyWidth}-${component.bodyHeight}`;
679
+ if (isQuantumSpecial(component))
680
+ return `quantum-${component.kind}-${component.controlType ?? ''}-${component.controls}-${component.targets}-${component.wires}`;
412
681
  if (component.kind === 'diode')
413
682
  return `diode-${component.diodeType}`;
414
683
  if (component.kind === 'transistor')
415
684
  return `transistor-${component.transistorType}`;
416
685
  if (component.kind === 'ground')
417
686
  return `ground-${component.groundStyle}`;
687
+ if (component.kind === 'resistor' || component.kind === 'capacitor' || component.kind === 'inductor')
688
+ return component.passiveType === undefined ? component.kind : `${component.kind}-${component.passiveType}`;
689
+ if (component.kind === 'source' || component.kind === 'junction' || component.kind === 'testpoint' ||
690
+ component.kind === 'connector' || component.kind === 'power' || component.kind === 'switch' ||
691
+ component.kind === 'protection' || component.kind === 'amplifier' || component.kind === 'resonator' ||
692
+ component.kind === 'meter' || component.kind === 'load')
693
+ return `${component.kind}-${component.variant ?? ''}`;
418
694
  return component.kind;
419
695
  }
420
696
  function reusableSymbolDefinition(component, symbolId) {
@@ -427,7 +703,9 @@ function connectionMarkup(connection, routed, index, idPrefix, glowId) {
427
703
  const source = escapeXml(`${connection.from.componentId}.${connection.from.port}`);
428
704
  const target = escapeXml(`${connection.to.componentId}.${connection.to.port}`);
429
705
  const traceId = `${idPrefix}-wire-${index}-vector`;
430
- const dataAttributes = ` data-wire-source="${source}" data-wire-target="${target}"`;
706
+ const signalKind = connection.signalKind ?? 'electrical';
707
+ const signalClass = connection.signalKind === undefined ? '' : ` schematic-signal--${signalKind}`;
708
+ const dataAttributes = ` data-wire-source="${source}" data-wire-target="${target}" data-source-line="${connection.line}"${connection.signalKind === undefined ? '' : ` data-signal-kind="${signalKind}"`}${connection.width === undefined ? '' : ` data-bus-width="${connection.width}"`}`;
431
709
  const relation = connection.relation ?? 'signal';
432
710
  const accessibility = ` tabindex="0" role="group" aria-label="${escapeXml(relation)} from ${source} to ${target}"`;
433
711
  const vectorId = ` id="${traceId}"`;
@@ -436,7 +714,7 @@ function connectionMarkup(connection, routed, index, idPrefix, glowId) {
436
714
  const endpoint = relation === 'signal' && connection.markerEnd === 'none'
437
715
  ? `<circle ${colorAttributes(connection.color, 'schematic-node-fill')} cx="${svgNumber(end.x)}" cy="${svgNumber(end.y)}" r="3" />`
438
716
  : '';
439
- return `<g class="schematic-wire"${dataAttributes}${accessibility}><path${vectorId} ${colorAttributes(connection.color, 'schematic-trace')} d="${routed.d}"${markerAttributes} />${glow}${endpoint}${connectionLabelMarkup(connection, routed)}</g>`;
717
+ return `<g class="schematic-wire${signalClass}"${dataAttributes}${accessibility}><path${vectorId} ${colorAttributes(connection.color, `schematic-trace${signalClass}`)} d="${routed.d}"${markerAttributes} />${glow}${endpoint}${connectionLabelMarkup(connection, routed)}</g>`;
440
718
  }
441
719
  function connectionMarkerAttributes(connection, idPrefix) {
442
720
  const start = connection.markerStart;
@@ -486,10 +764,11 @@ function compactConnectionMarkup(connections, routes, idPrefix, embedVisuals, gl
486
764
  const batches = new Map();
487
765
  for (const [index, connection] of connections.entries()) {
488
766
  const markerAttributes = connectionMarkerAttributes(connection, idPrefix);
489
- const key = `${colorKey(connection.color)}|${markerAttributes}`;
767
+ const signalKind = connection.signalKind ?? 'electrical';
768
+ const key = `${colorKey(connection.color)}|${markerAttributes}|${signalKind}|${connection.width ?? 1}`;
490
769
  let batch = batches.get(key);
491
770
  if (batch === undefined) {
492
- batch = { color: connection.color, markerAttributes, traces: [], endpoints: [], labels: [] };
771
+ batch = { color: connection.color, markerAttributes, signalKind, traces: [], endpoints: [], labels: [] };
493
772
  batches.set(key, batch);
494
773
  }
495
774
  const routed = routes[index];
@@ -503,7 +782,7 @@ function compactConnectionMarkup(connections, routes, idPrefix, embedVisuals, gl
503
782
  batch.labels.push(label);
504
783
  }
505
784
  return Array.from(batches.values(), (batch, index) => {
506
- const tracePaint = colorAttributes(batch.color, 'schematic-trace');
785
+ const tracePaint = colorAttributes(batch.color, `schematic-trace${batch.signalKind === 'electrical' ? '' : ` schematic-signal--${batch.signalKind}`}`);
507
786
  const nodePaint = colorAttributes(batch.color, 'schematic-node-fill');
508
787
  const traceId = `${idPrefix}-wire-batch-${index}-vector`;
509
788
  const trace = `<path${embedVisuals ? ` id="${traceId}"` : ''} ${tracePaint} d="${batch.traces.join(' ')}"${batch.markerAttributes} />`;
@@ -515,15 +794,43 @@ function compactConnectionMarkup(connections, routes, idPrefix, embedVisuals, gl
515
794
  }).join('');
516
795
  }
517
796
  function componentMetadata(component) {
797
+ if (isDigitalComponent(component)) {
798
+ return [component.variant, component.width > 1 ? `${component.width} bit` : undefined]
799
+ .filter(Boolean)
800
+ .join(', ');
801
+ }
802
+ if (isQuantumSpecial(component)) {
803
+ return [
804
+ component.operator,
805
+ component.controlType,
806
+ component.controls > 0 ? `${component.controls} controls` : undefined,
807
+ component.targets > 0 ? `${component.targets} targets` : undefined,
808
+ component.width > 1 ? `${component.width} bit` : undefined
809
+ ].filter(Boolean).join(', ');
810
+ }
811
+ if (isQuantumGate(component)) {
812
+ return [component.parameter, component.phase, component.matrix].filter(Boolean).join(', ');
813
+ }
518
814
  switch (component.kind) {
519
- case 'qgate':
520
- return [component.parameter, component.phase, component.matrix].filter(Boolean).join(', ');
815
+ case 'resistor':
816
+ case 'capacitor':
817
+ case 'inductor':
818
+ return component.passiveType ?? 'fixed';
521
819
  case 'diode':
522
820
  return component.diodeType;
523
821
  case 'transistor':
524
822
  return component.transistorType;
525
823
  case 'ground':
526
824
  return component.groundStyle;
825
+ case 'source':
826
+ case 'power':
827
+ case 'switch':
828
+ case 'protection':
829
+ case 'amplifier':
830
+ case 'resonator':
831
+ case 'meter':
832
+ case 'load':
833
+ return String(component.variant);
527
834
  case 'ic':
528
835
  return `${Object.values(component.pins).reduce((total, pins) => total + pins.length, 0)} pins`;
529
836
  default:
@@ -547,15 +854,17 @@ function componentMarkup(component, index, idPrefix, glowId, mode, nodeHooks, po
547
854
  const anchors = componentTextAnchors(component);
548
855
  const id = escapeXml(component.id);
549
856
  const vectorId = `${idPrefix}-node-${index}-vector`;
857
+ const rotation = componentRotation(component);
858
+ const vectorTransform = rotation === 0 ? '' : ` transform="rotate(${rotation})"`;
550
859
  const styles = mode === 'embedded-css' || mode === 'full';
551
860
  const dataAttributes = nodeHooks
552
- ? ` data-node-id="${id}" data-node-kind="${component.kind}" data-node-label="${label}" data-component="${id}" data-kind="${component.kind}"`
861
+ ? ` data-node-id="${id}" data-node-kind="${component.kind}" data-node-label="${label}" data-component="${id}" data-kind="${component.kind}" data-source-line="${component.line}"${'orientation' in component && component.orientation !== undefined ? ` data-orientation="${component.orientation}"` : ''}`
553
862
  : '';
554
863
  let accessibility = '';
555
864
  if (styles) {
556
865
  const metadata = componentMetadata(component);
557
866
  const ariaLabel = escapeXml(`${component.id}, ${component.kind}, ${component.label}${metadata === '' ? '' : `, ${metadata}`}`);
558
- accessibility = ` tabindex="0" role="group" aria-label="${ariaLabel}"`;
867
+ accessibility = `${portHooks ? '' : ' tabindex="0"'} role="group" aria-label="${ariaLabel}"`;
559
868
  }
560
869
  const vectorIdAttribute = styles ? ` id="${vectorId}"` : '';
561
870
  const glow = styles
@@ -563,18 +872,20 @@ function componentMarkup(component, index, idPrefix, glowId, mode, nodeHooks, po
563
872
  : '';
564
873
  const hotspots = portHooks ? componentPortMarkup(component) : '';
565
874
  const vector = symbolId === undefined
566
- ? `<g${vectorIdAttribute} class="schematic-component-vector">${componentShape(component)}</g>`
567
- : `<use${vectorIdAttribute} ${colorAttributes(component.color, 'schematic-component-vector')} href="#${symbolId}" />`;
875
+ ? `<g${vectorIdAttribute} class="schematic-component-vector"${vectorTransform}>${componentShape(component)}</g>`
876
+ : `<use${vectorIdAttribute} ${colorAttributes(component.color, 'schematic-component-vector')} href="#${symbolId}"${vectorTransform} />`;
877
+ const innerText = componentInnerText(component);
568
878
  const externalLabels = isUmlComponent(component)
569
879
  ? ''
570
880
  : `<text class="schematic-designator" fill="currentColor" stroke="none" x="0" y="${anchors.designatorY}" text-anchor="middle" font-size="12" textLength="${anchors.designatorWidth}" lengthAdjust="spacingAndGlyphs">${id}</text><text class="schematic-label" fill="currentColor" stroke="none" x="0" y="${anchors.labelY}" text-anchor="middle" font-size="12" textLength="${anchors.labelWidth}" lengthAdjust="spacingAndGlyphs">${renderedLabel}</text>`;
571
- return `<g class="schematic-component"${dataAttributes} transform="translate(${svgNumber(component.x)} ${svgNumber(component.y)})"${accessibility}>${vector}${glow}${externalLabels}${hotspots}</g>`;
881
+ return `<g class="schematic-component"${dataAttributes} transform="translate(${svgNumber(component.x)} ${svgNumber(component.y)})"${accessibility}>${vector}${glow}${innerText}${externalLabels}${hotspots}</g>`;
572
882
  }
573
883
  export function renderSchematic(document, options) {
574
884
  assertParsedSchematicDocument(document);
575
885
  const normalized = normalizeCompileOptions(options);
576
886
  const components = new Map(document.components.map((component) => [component.id, component]));
577
- const routedConnections = routeConnections(document.connections, components, normalized.bounds);
887
+ const routedConnections = parsedSchematicRoutes(document, normalized.bounds) ??
888
+ routeConnections(document.connections, components, normalized.bounds);
578
889
  validateDocumentGeometry(document, normalized, routedConnections);
579
890
  const signature = `${normalized.bounds.width}x${normalized.bounds.height}:${normalized.title}:${JSON.stringify(document)}`;
580
891
  const candidatePrefix = (normalized.idPrefix ?? `schematic-${stableHash(signature)}`).replace(/[^A-Za-z0-9_-]/g, '-');
@@ -613,7 +924,8 @@ export function renderSchematic(document, options) {
613
924
  ? `<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}`
614
925
  : '';
615
926
  const definitions = `${embeddedDefinitions}${markerDefinitions}${symbolDefinitions}`;
616
- 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">`);
927
+ const svgRole = portHooks ? 'group' : 'img';
928
+ 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="${svgRole}" 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">`);
617
929
  if (wireHooks) {
618
930
  for (const [index, connection] of document.connections.entries()) {
619
931
  writer.append(connectionMarkup(connection, routedConnections[index], index, safePrefix, glowId));