@schemd/core 0.2.1 → 0.3.1

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,8 @@
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, quantumTrackCoordinate, quantumTrackSpan, 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
4
  import { parsedSchematicRoutes } from './route-cache.js';
5
- import { CLASSICAL_GATE_KINDS, SCHEMD_OUTPUT_MODES, SCHEMD_SEMANTIC_HOOKS, UML_COMPONENT_KINDS, SchematicSyntaxError } from './types.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';
6
6
  import { mathLabelTextWidth, renderMathLabelTspans } from './math-label.js';
7
7
  import { escapeXml } from './xml.js';
8
8
  export const MAX_SVG_OUTPUT_BYTES = MAX_SCHEMATIC_SVG_OUTPUT_BYTES;
@@ -105,6 +105,29 @@ function stableHash(value) {
105
105
  function isClassicalGate(component) {
106
106
  return CLASSICAL_GATE_KINDS.includes(component.kind);
107
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
+ }
108
131
  function isUmlComponent(component) {
109
132
  return UML_COMPONENT_KINDS.includes(component.kind);
110
133
  }
@@ -123,7 +146,7 @@ function colorAttributes(color, extraClass = '') {
123
146
  function orInputBoundaryX(kind, y, height) {
124
147
  const halfHeight = height / 2;
125
148
  const progress = (1 - y / halfHeight) / 2;
126
- const start = kind === 'xor' ? -38 : -32;
149
+ const start = kind === 'xor' || kind === 'xnor' ? -38 : -32;
127
150
  return start + 40 * progress * (1 - progress);
128
151
  }
129
152
  function orOutputBoundaryX(y, height) {
@@ -137,7 +160,7 @@ function gateBoundaryX(component, direction, y) {
137
160
  return direction === 'input' ? -32 : 32;
138
161
  const height = classicalGateHeight(component);
139
162
  if (direction === 'input') {
140
- if (component.kind === 'or' || component.kind === 'nor' || component.kind === 'xor') {
163
+ if (component.kind === 'or' || component.kind === 'nor' || component.kind === 'xor' || component.kind === 'xnor') {
141
164
  return orInputBoundaryX(component.kind, y, height);
142
165
  }
143
166
  return component.kind === 'not' ? -30 : -32;
@@ -146,13 +169,13 @@ function gateBoundaryX(component, direction, y) {
146
169
  const normalized = y / (height / 2);
147
170
  return 32 * Math.sqrt(Math.max(0, 1 - normalized * normalized));
148
171
  }
149
- if (component.kind === 'or' || component.kind === 'nor' || component.kind === 'xor') {
172
+ if (component.kind === 'or' || component.kind === 'nor' || component.kind === 'xor' || component.kind === 'xnor') {
150
173
  return orOutputBoundaryX(y, height);
151
174
  }
152
175
  return 30 - 60 * (Math.abs(y) / (height / 2));
153
176
  }
154
177
  function hasOutputInversion(component) {
155
- return component.kind === 'nand' || component.kind === 'nor' || component.kind === 'not';
178
+ return component.kind === 'nand' || component.kind === 'nor' || component.kind === 'xnor' || component.kind === 'not';
156
179
  }
157
180
  function gateStubs(component, paint) {
158
181
  const height = classicalGateHeight(component);
@@ -186,6 +209,7 @@ function iecGate(component, paint) {
186
209
  or: '≥1',
187
210
  nor: '≥1',
188
211
  xor: '=1',
212
+ xnor: '=1',
189
213
  not: '1'
190
214
  };
191
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)}`;
@@ -202,19 +226,30 @@ function ieeeGate(component, paint) {
202
226
  contour = `<path ${paint} d="M -32 ${top} H 0 A 32 ${height / 2} 0 0 1 0 ${bottom} H -32 Z" />`;
203
227
  }
204
228
  else {
205
- 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}" />` : '';
206
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}`;
207
231
  }
208
232
  return `${contour}${inversionBubbles(component, paint)}${gateStubs(component, paint)}`;
209
233
  }
210
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
+ };
211
240
  const details = [component.parameter, component.phase, component.matrix].filter((value) => value !== undefined && value !== '');
212
- const rows = [component.label, ...details];
213
- 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;
214
244
  return rows
215
- .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>`)
216
246
  .join('');
217
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
+ }
218
253
  function diodeShape(component, paint) {
219
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" />`;
220
255
  switch (component.diodeType) {
@@ -226,6 +261,14 @@ function diodeShape(component, paint) {
226
261
  return `${junction}<path ${paint} d="M 4 -20 L 10 -16 M 10 16 L 16 20" />`;
227
262
  case 'led':
228
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" />`;
229
272
  }
230
273
  }
231
274
  function bjtShape(component, paint) {
@@ -233,12 +276,18 @@ function bjtShape(component, paint) {
233
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}" />`;
234
277
  }
235
278
  function mosfetShape(component, paint) {
236
- 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
+ }
237
286
  const control = pChannel
238
287
  ? `<path ${paint} d="M -42 0 H -16" /><circle ${paint} cx="-11" cy="0" r="5" />`
239
288
  : `<path ${paint} d="M -42 0 H -10" />`;
240
289
  const arrow = pChannel ? 'M 16 -3 L 5 3 L 16 9' : 'M 5 -3 L 16 3 L 5 9';
241
- 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' : ''}" />`;
242
291
  }
243
292
  function transistorShape(component, paint) {
244
293
  return component.transistorType === 'npn' || component.transistorType === 'pnp'
@@ -260,9 +309,13 @@ function icPinMarkup(component, side, paint) {
260
309
  const pins = component.pins[side];
261
310
  return pins
262
311
  .map((pin, index) => {
263
- const point = positionIcPin(component, side, index);
264
- const x = point.x - component.x;
265
- 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);
266
319
  const horizontalTextLength = fittedTextLength(pin, Math.max(8, component.bodyWidth / 2 - 10), 4);
267
320
  const verticalTextLength = fittedTextLength(pin, Math.max(8, component.bodyHeight / 2 - 10), 4);
268
321
  if (side === 'left') {
@@ -283,7 +336,9 @@ function icPinMarkup(component, side, paint) {
283
336
  function integratedCircuitShape(component, paint) {
284
337
  const left = -component.bodyWidth / 2;
285
338
  const top = -component.bodyHeight / 2;
286
- 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>`}`;
287
342
  }
288
343
  function umlRow(value, x, y) {
289
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>`;
@@ -317,7 +372,8 @@ function umlStateShape(component, paint) {
317
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}`;
318
373
  }
319
374
  function umlComponentShape(component, paint, nodePaint) {
320
- if (component.kind === 'class')
375
+ if (component.kind === 'class' || component.kind === 'interface' ||
376
+ component.kind === 'enumeration' || component.kind === 'datatype' || component.kind === 'object')
321
377
  return umlClassShape(component, paint);
322
378
  if (component.kind === 'state')
323
379
  return umlStateShape(component, paint);
@@ -342,25 +398,230 @@ function umlComponentShape(component, paint, nodePaint) {
342
398
  const top = -component.bodyHeight / 2;
343
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>`;
344
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" />`;
345
470
  case 'initial':
346
471
  return `<circle ${nodePaint} cx="0" cy="0" r="10" />`;
347
472
  case 'final':
348
473
  return `<circle ${paint} cx="0" cy="0" r="11" /><circle ${nodePaint} cx="0" cy="0" r="6" />`;
349
474
  }
350
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 = quantumTrackSpan(tracks);
564
+ const lines = Array.from({ length: tracks }, (_, index) => `<path ${paint} d="M -42 ${svgNumber(quantumTrackCoordinate(index, tracks))} 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 = quantumTrackCoordinate(index, tracks);
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
+ }
351
610
  function componentShape(component, paint = colorAttributes(component.color), nodePaint = colorAttributes(component.color, 'schematic-node-fill')) {
352
611
  if (isClassicalGate(component)) {
353
612
  return component.standard === 'iec' ? iecGate(component, paint) : ieeeGate(component, paint);
354
613
  }
614
+ if (isDigitalComponent(component))
615
+ return digitalShape(component, paint);
616
+ if (isQuantumSpecial(component))
617
+ return quantumSpecialShape(component, paint, nodePaint);
355
618
  if (isUmlComponent(component))
356
619
  return umlComponentShape(component, paint, nodePaint);
357
620
  switch (component.kind) {
358
621
  case 'resistor':
359
- 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" />`;
360
622
  case 'capacitor':
361
- return `<path ${paint} d="M -42 0 H -7 M -7 -18 V 18 M 7 -18 V 18 M 7 0 H 42" />`;
362
623
  case 'inductor':
363
- 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);
364
625
  case 'diode':
365
626
  return diodeShape(component, paint);
366
627
  case 'transistor':
@@ -370,28 +631,66 @@ function componentShape(component, paint = colorAttributes(component.color), nod
370
631
  case 'ground':
371
632
  return groundShape(component, paint);
372
633
  case 'hadamard':
373
- 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);
374
649
  case 'cnot':
375
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" />`;
376
- case 'qgate':
377
- 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)}`;
378
651
  case 'ic':
379
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);
380
665
  }
381
666
  }
382
667
  function reusableSymbolKey(component) {
383
668
  if (isClassicalGate(component) ||
384
669
  isUmlComponent(component) ||
385
- component.kind === 'qgate' ||
386
670
  component.kind === 'ic') {
387
671
  return undefined;
388
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}`;
389
681
  if (component.kind === 'diode')
390
682
  return `diode-${component.diodeType}`;
391
683
  if (component.kind === 'transistor')
392
684
  return `transistor-${component.transistorType}`;
393
685
  if (component.kind === 'ground')
394
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 ?? ''}`;
395
694
  return component.kind;
396
695
  }
397
696
  function reusableSymbolDefinition(component, symbolId) {
@@ -404,7 +703,9 @@ function connectionMarkup(connection, routed, index, idPrefix, glowId) {
404
703
  const source = escapeXml(`${connection.from.componentId}.${connection.from.port}`);
405
704
  const target = escapeXml(`${connection.to.componentId}.${connection.to.port}`);
406
705
  const traceId = `${idPrefix}-wire-${index}-vector`;
407
- 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}"`}`;
408
709
  const relation = connection.relation ?? 'signal';
409
710
  const accessibility = ` tabindex="0" role="group" aria-label="${escapeXml(relation)} from ${source} to ${target}"`;
410
711
  const vectorId = ` id="${traceId}"`;
@@ -413,7 +714,7 @@ function connectionMarkup(connection, routed, index, idPrefix, glowId) {
413
714
  const endpoint = relation === 'signal' && connection.markerEnd === 'none'
414
715
  ? `<circle ${colorAttributes(connection.color, 'schematic-node-fill')} cx="${svgNumber(end.x)}" cy="${svgNumber(end.y)}" r="3" />`
415
716
  : '';
416
- 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>`;
417
718
  }
418
719
  function connectionMarkerAttributes(connection, idPrefix) {
419
720
  const start = connection.markerStart;
@@ -463,10 +764,11 @@ function compactConnectionMarkup(connections, routes, idPrefix, embedVisuals, gl
463
764
  const batches = new Map();
464
765
  for (const [index, connection] of connections.entries()) {
465
766
  const markerAttributes = connectionMarkerAttributes(connection, idPrefix);
466
- const key = `${colorKey(connection.color)}|${markerAttributes}`;
767
+ const signalKind = connection.signalKind ?? 'electrical';
768
+ const key = `${colorKey(connection.color)}|${markerAttributes}|${signalKind}|${connection.width ?? 1}`;
467
769
  let batch = batches.get(key);
468
770
  if (batch === undefined) {
469
- batch = { color: connection.color, markerAttributes, traces: [], endpoints: [], labels: [] };
771
+ batch = { color: connection.color, markerAttributes, signalKind, traces: [], endpoints: [], labels: [] };
470
772
  batches.set(key, batch);
471
773
  }
472
774
  const routed = routes[index];
@@ -480,7 +782,7 @@ function compactConnectionMarkup(connections, routes, idPrefix, embedVisuals, gl
480
782
  batch.labels.push(label);
481
783
  }
482
784
  return Array.from(batches.values(), (batch, index) => {
483
- const tracePaint = colorAttributes(batch.color, 'schematic-trace');
785
+ const tracePaint = colorAttributes(batch.color, `schematic-trace${batch.signalKind === 'electrical' ? '' : ` schematic-signal--${batch.signalKind}`}`);
484
786
  const nodePaint = colorAttributes(batch.color, 'schematic-node-fill');
485
787
  const traceId = `${idPrefix}-wire-batch-${index}-vector`;
486
788
  const trace = `<path${embedVisuals ? ` id="${traceId}"` : ''} ${tracePaint} d="${batch.traces.join(' ')}"${batch.markerAttributes} />`;
@@ -488,19 +790,47 @@ function compactConnectionMarkup(connections, routes, idPrefix, embedVisuals, gl
488
790
  const labels = batch.labels.join('');
489
791
  if (!embedVisuals)
490
792
  return trace + endpoints + labels;
491
- return `<g class="schematic-wire" tabindex="0" role="group" aria-label="${batch.traces.length} grouped connection${batch.traces.length === 1 ? '' : 's'}">${trace}<use class="schematic-glow-layer" href="#${traceId}" filter="url(#${glowId})" aria-hidden="true" pointer-events="none" />${endpoints}${labels}</g>`;
793
+ return `<g class="schematic-wire">${trace}<use class="schematic-glow-layer" href="#${traceId}" filter="url(#${glowId})" aria-hidden="true" pointer-events="none" />${endpoints}${labels}</g>`;
492
794
  }).join('');
493
795
  }
494
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
+ }
495
814
  switch (component.kind) {
496
- case 'qgate':
497
- return [component.parameter, component.phase, component.matrix].filter(Boolean).join(', ');
815
+ case 'resistor':
816
+ case 'capacitor':
817
+ case 'inductor':
818
+ return component.passiveType ?? 'fixed';
498
819
  case 'diode':
499
820
  return component.diodeType;
500
821
  case 'transistor':
501
822
  return component.transistorType;
502
823
  case 'ground':
503
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);
504
834
  case 'ic':
505
835
  return `${Object.values(component.pins).reduce((total, pins) => total + pins.length, 0)} pins`;
506
836
  default:
@@ -524,15 +854,17 @@ function componentMarkup(component, index, idPrefix, glowId, mode, nodeHooks, po
524
854
  const anchors = componentTextAnchors(component);
525
855
  const id = escapeXml(component.id);
526
856
  const vectorId = `${idPrefix}-node-${index}-vector`;
857
+ const rotation = componentRotation(component);
858
+ const vectorTransform = rotation === 0 ? '' : ` transform="rotate(${rotation})"`;
527
859
  const styles = mode === 'embedded-css' || mode === 'full';
528
860
  const dataAttributes = nodeHooks
529
- ? ` 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}"` : ''}`
530
862
  : '';
531
863
  let accessibility = '';
532
- if (styles) {
864
+ if (mode === 'full') {
533
865
  const metadata = componentMetadata(component);
534
866
  const ariaLabel = escapeXml(`${component.id}, ${component.kind}, ${component.label}${metadata === '' ? '' : `, ${metadata}`}`);
535
- accessibility = ` tabindex="0" role="group" aria-label="${ariaLabel}"`;
867
+ accessibility = `${portHooks ? '' : ' tabindex="0"'} role="group" aria-label="${ariaLabel}"`;
536
868
  }
537
869
  const vectorIdAttribute = styles ? ` id="${vectorId}"` : '';
538
870
  const glow = styles
@@ -540,25 +872,24 @@ function componentMarkup(component, index, idPrefix, glowId, mode, nodeHooks, po
540
872
  : '';
541
873
  const hotspots = portHooks ? componentPortMarkup(component) : '';
542
874
  const vector = symbolId === undefined
543
- ? `<g${vectorIdAttribute} class="schematic-component-vector">${componentShape(component)}</g>`
544
- : `<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);
545
878
  const externalLabels = isUmlComponent(component)
546
879
  ? ''
547
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>`;
548
- 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>`;
549
882
  }
550
883
  export function renderSchematic(document, options) {
551
884
  assertParsedSchematicDocument(document);
552
885
  const normalized = normalizeCompileOptions(options);
553
- const components = new Map(document.components.map((component) => [component.id, component]));
554
886
  const routedConnections = parsedSchematicRoutes(document, normalized.bounds) ??
555
- routeConnections(document.connections, components, normalized.bounds);
556
- validateDocumentGeometry(document, normalized, routedConnections);
557
- const signature = `${normalized.bounds.width}x${normalized.bounds.height}:${normalized.title}:${JSON.stringify(document)}`;
558
- const candidatePrefix = (normalized.idPrefix ?? `schematic-${stableHash(signature)}`).replace(/[^A-Za-z0-9_-]/g, '-');
887
+ validateDocumentGeometry(document, normalized, routeConnections(document.connections, new Map(document.components.map((component) => [component.id, component])), normalized.bounds));
888
+ const signatureHash = () => stableHash(`${normalized.bounds.width}x${normalized.bounds.height}:${normalized.title}:${JSON.stringify(document)}`);
889
+ const candidatePrefix = (normalized.idPrefix ?? `schematic-${signatureHash()}`).replace(/[^A-Za-z0-9_-]/g, '-');
559
890
  const safePrefix = /[A-Za-z0-9]/.test(candidatePrefix)
560
891
  ? candidatePrefix
561
- : `schematic-${stableHash(signature)}`;
892
+ : `schematic-${signatureHash()}`;
562
893
  const titleId = `${safePrefix}-title`;
563
894
  const descriptionId = `${safePrefix}-description`;
564
895
  const gridId = `${safePrefix}-grid`;
@@ -591,7 +922,8 @@ export function renderSchematic(document, options) {
591
922
  ? `<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}`
592
923
  : '';
593
924
  const definitions = `${embeddedDefinitions}${markerDefinitions}${symbolDefinitions}`;
594
- 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">`);
925
+ const svgRole = hooks ? 'group' : 'img';
926
+ 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">`);
595
927
  if (wireHooks) {
596
928
  for (const [index, connection] of document.connections.entries()) {
597
929
  writer.append(connectionMarkup(connection, routedConnections[index], index, safePrefix, glowId));