@schemd/core 0.3.1 → 0.3.2

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,14 +1,16 @@
1
- import { classicalGateHeight, componentTextAnchors, distributedCoordinate, enumerateComponentPorts, PORT_HOTSPOT_RADIUS, quantumGateDimensions, quantumTrackCoordinate, quantumTrackSpan, routeConnections, validateDocumentGeometry } from './layout.js';
1
+ import { classicalGateHeight, componentTextAnchors, connectionLabelPoint, distributedCoordinate, enumerateComponentPorts, formatNumber as svgNumber, isClassicalGate, isDigitalComponent, isQuantumSpecial, isUmlComponent, 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, DIGITAL_COMPONENT_KINDS, QUANTUM_GATE_KINDS, QUANTUM_SPECIAL_KINDS, SCHEMD_OUTPUT_MODES, SCHEMD_SEMANTIC_HOOKS, UML_COMPONENT_KINDS, SchematicSyntaxError } from './types.js';
6
- import { mathLabelTextWidth, renderMathLabelTspans } from './math-label.js';
5
+ import { QUANTUM_GATE_KINDS, SCHEMD_OUTPUT_MODES, SCHEMD_SEMANTIC_HOOKS, SchematicSyntaxError } from './types.js';
6
+ import { mathLabelText, 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;
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}';
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-marker-carrier{stroke-width:0!important}.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}';
10
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}';
11
11
  const INTERACTIVE_SVG_STYLES = '.schematic-component,.schematic-wire{outline:none;transition:opacity .2s ease,filter .2s ease}.schematic-component .schematic-token,.schematic-wire .schematic-token{transition:stroke .2s ease,fill .2s ease,color .2s ease,opacity .2s ease,stroke-width .2s ease}.schematic-glow-layer{opacity:0;pointer-events:none;transition:opacity .2s ease}.schematic-component:hover>.schematic-glow-layer,.schematic-component:focus>.schematic-glow-layer,.schematic-component:focus-within>.schematic-glow-layer,.schematic-component.is-hovered>.schematic-glow-layer,.schematic-component.is-active>.schematic-glow-layer,.schematic-component.is-selected>.schematic-glow-layer,.schematic-wire:hover>.schematic-glow-layer,.schematic-wire:focus>.schematic-glow-layer,.schematic-wire.is-hovered>.schematic-glow-layer,.schematic-wire.is-active>.schematic-glow-layer,.schematic-wire.is-selected>.schematic-glow-layer{opacity:1}.schematic-component.is-degraded,.schematic-wire.is-degraded{opacity:.45}.schematic-component.is-degraded>.schematic-glow-layer,.schematic-wire.is-degraded>.schematic-glow-layer{opacity:0}.schematic-component:hover .schematic-token,.schematic-component:focus .schematic-token,.schematic-component:focus-within .schematic-token,.schematic-component.is-hovered .schematic-token,.schematic-component.is-active .schematic-token,.schematic-component.is-selected .schematic-token,.schematic-wire:hover .schematic-token,.schematic-wire:focus .schematic-token,.schematic-wire.is-hovered .schematic-token,.schematic-wire.is-active .schematic-token,.schematic-wire.is-selected .schematic-token{stroke-width:var(--schematic-interactive-stroke-width,2.25)}.schematic-port-hotspot{fill:var(--schematic-vector,var(--schematic-vector-fallback,currentColor))!important;fill-opacity:0;transition:fill-opacity .2s ease,opacity .2s ease}.schematic-port-hotspot:hover,.schematic-port-hotspot:focus,.schematic-port-hotspot.is-hovered,.schematic-port-hotspot.is-active,.schematic-port-hotspot.is-selected{fill-opacity:.2}.schematic-port-hotspot.is-degraded{opacity:.45}@media(prefers-reduced-motion:reduce){.schematic-component,.schematic-wire,.schematic-token,.schematic-glow-layer,.schematic-port-hotspot{transition:none}}';
12
+ const TEXT_PAINT = 'fill="currentColor" stroke="none"';
13
+ const LENGTH_FIT = 'lengthAdjust="spacingAndGlyphs"';
12
14
  const NODE_HOOK = 1;
13
15
  const PORT_HOOK = 2;
14
16
  const WIRE_HOOK = 4;
@@ -23,10 +25,11 @@ export class BoundedSvgWriter {
23
25
  #chunks = [];
24
26
  #bytes = 0;
25
27
  append(chunk) {
26
- this.#bytes += utf8ByteLength(chunk);
27
- if (this.#bytes > MAX_SCHEMATIC_SVG_OUTPUT_BYTES) {
28
+ const nextBytes = this.#bytes + utf8ByteLength(chunk);
29
+ if (nextBytes > MAX_SCHEMATIC_SVG_OUTPUT_BYTES) {
28
30
  throw new SchematicSyntaxError(`Compiled SVG exceeds the ${MAX_SCHEMATIC_SVG_OUTPUT_BYTES.toLocaleString('en-US')} byte output limit.`);
29
31
  }
32
+ this.#bytes = nextBytes;
30
33
  this.#chunks.push(chunk);
31
34
  }
32
35
  finish() {
@@ -88,9 +91,6 @@ function normalizeCompileOptions(value) {
88
91
  ? { bounds: { width, height }, title, mode: normalizedMode, semanticHookMask }
89
92
  : { bounds: { width, height }, title, idPrefix, mode: normalizedMode, semanticHookMask };
90
93
  }
91
- function svgNumber(value) {
92
- return String(Number(value.toFixed(3)));
93
- }
94
94
  function fittedTextLength(value, maximum, advance) {
95
95
  return Math.max(1, Math.min(Math.ceil(mathLabelTextWidth(value, advance)), maximum));
96
96
  }
@@ -102,15 +102,6 @@ function stableHash(value) {
102
102
  }
103
103
  return (hash >>> 0).toString(36);
104
104
  }
105
- function isClassicalGate(component) {
106
- return CLASSICAL_GATE_KINDS.includes(component.kind);
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
105
  function isQuantumGate(component) {
115
106
  return QUANTUM_GATE_KINDS.includes(component.kind);
116
107
  }
@@ -128,9 +119,6 @@ function componentRotation(component) {
128
119
  return 0;
129
120
  }
130
121
  }
131
- function isUmlComponent(component) {
132
- return UML_COMPONENT_KINDS.includes(component.kind);
133
- }
134
122
  function colorAttributes(color, extraClass = '') {
135
123
  const suffix = extraClass === '' ? '' : ` ${extraClass}`;
136
124
  const fill = extraClass.split(' ').includes('schematic-node-fill') ? ' fill="currentColor"' : '';
@@ -212,7 +200,7 @@ function iecGate(component, paint) {
212
200
  xnor: '=1',
213
201
  not: '1'
214
202
  };
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)}`;
203
+ return `<rect ${paint} x="-32" y="${-height / 2}" width="64" height="${height}" rx="2" /><text class="schematic-gate-symbol" ${TEXT_PAINT} x="0" y="4" text-anchor="middle" font-size="14">${symbol[component.kind]}</text>${inversionBubbles(component, paint)}${gateStubs(component, paint)}`;
216
204
  }
217
205
  function ieeeGate(component, paint) {
218
206
  const height = classicalGateHeight(component);
@@ -242,7 +230,7 @@ function quantumText(component) {
242
230
  const { bodyWidth } = quantumGateDimensions(component);
243
231
  const start = -((rows.length - 1) * 15) / 2 + 4;
244
232
  return rows
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>`)
233
+ .map((value, index) => `<text class="${index === 0 ? 'schematic-gate-symbol' : 'schematic-quantum-detail'}" ${TEXT_PAINT} x="0" y="${start + index * 15}" text-anchor="middle" font-size="${index === 0 ? 14 : 10}" textLength="${fittedTextLength(value, bodyWidth - 12, index === 0 ? 7 : 5)}" ${LENGTH_FIT}>${renderMathLabelTspans(value)}</text>`)
246
234
  .join('');
247
235
  }
248
236
  function quantumGateShell(component, paint) {
@@ -319,17 +307,17 @@ function icPinMarkup(component, side, paint) {
319
307
  const horizontalTextLength = fittedTextLength(pin, Math.max(8, component.bodyWidth / 2 - 10), 4);
320
308
  const verticalTextLength = fittedTextLength(pin, Math.max(8, component.bodyHeight / 2 - 10), 4);
321
309
  if (side === 'left') {
322
- return `<path ${paint} d="M ${x} ${y} H ${-component.bodyWidth / 2}" /><text class="schematic-pin-label" fill="currentColor" stroke="none" x="${-component.bodyWidth / 2 + 5}" y="${y + 3}" text-anchor="start" font-size="10" textLength="${horizontalTextLength}" lengthAdjust="spacingAndGlyphs">${escapeXml(pin)}</text>`;
310
+ return `<path ${paint} d="M ${x} ${y} H ${-component.bodyWidth / 2}" /><text class="schematic-pin-label" ${TEXT_PAINT} x="${-component.bodyWidth / 2 + 5}" y="${y + 3}" text-anchor="start" font-size="10" textLength="${horizontalTextLength}" ${LENGTH_FIT}>${escapeXml(pin)}</text>`;
323
311
  }
324
312
  if (side === 'right') {
325
- return `<path ${paint} d="M ${component.bodyWidth / 2} ${y} H ${x}" /><text class="schematic-pin-label" fill="currentColor" stroke="none" x="${component.bodyWidth / 2 - 5}" y="${y + 3}" text-anchor="end" font-size="10" textLength="${horizontalTextLength}" lengthAdjust="spacingAndGlyphs">${escapeXml(pin)}</text>`;
313
+ return `<path ${paint} d="M ${component.bodyWidth / 2} ${y} H ${x}" /><text class="schematic-pin-label" ${TEXT_PAINT} x="${component.bodyWidth / 2 - 5}" y="${y + 3}" text-anchor="end" font-size="10" textLength="${horizontalTextLength}" ${LENGTH_FIT}>${escapeXml(pin)}</text>`;
326
314
  }
327
315
  if (side === 'top') {
328
316
  const labelY = -component.bodyHeight / 2 + 5;
329
- return `<path ${paint} d="M ${x} ${y} V ${-component.bodyHeight / 2}" /><text class="schematic-pin-label" fill="currentColor" stroke="none" x="${x}" y="${labelY}" text-anchor="start" font-size="10" textLength="${verticalTextLength}" lengthAdjust="spacingAndGlyphs" transform="rotate(90 ${x} ${labelY})">${escapeXml(pin)}</text>`;
317
+ return `<path ${paint} d="M ${x} ${y} V ${-component.bodyHeight / 2}" /><text class="schematic-pin-label" ${TEXT_PAINT} x="${x}" y="${labelY}" text-anchor="start" font-size="10" textLength="${verticalTextLength}" ${LENGTH_FIT} transform="rotate(90 ${x} ${labelY})">${escapeXml(pin)}</text>`;
330
318
  }
331
319
  const labelY = component.bodyHeight / 2 - 5;
332
- return `<path ${paint} d="M ${x} ${component.bodyHeight / 2} V ${y}" /><text class="schematic-pin-label" fill="currentColor" stroke="none" x="${x}" y="${labelY}" text-anchor="start" font-size="10" textLength="${verticalTextLength}" lengthAdjust="spacingAndGlyphs" transform="rotate(-90 ${x} ${labelY})">${escapeXml(pin)}</text>`;
320
+ return `<path ${paint} d="M ${x} ${component.bodyHeight / 2} V ${y}" /><text class="schematic-pin-label" ${TEXT_PAINT} x="${x}" y="${labelY}" text-anchor="start" font-size="10" textLength="${verticalTextLength}" ${LENGTH_FIT} transform="rotate(-90 ${x} ${labelY})">${escapeXml(pin)}</text>`;
333
321
  })
334
322
  .join('');
335
323
  }
@@ -337,11 +325,11 @@ function integratedCircuitShape(component, paint) {
337
325
  const left = -component.bodyWidth / 2;
338
326
  const top = -component.bodyHeight / 2;
339
327
  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>`;
328
+ const label = `<text class="schematic-gate-symbol" ${TEXT_PAINT} x="0" y="4" text-anchor="middle" font-size="12" textLength="${fittedTextLength(component.label, component.bodyWidth - 12, 7)}" ${LENGTH_FIT}>${renderMathLabelTspans(component.label)}</text>`;
341
329
  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>`}`;
342
330
  }
343
331
  function umlRow(value, x, y) {
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>`;
332
+ return `<text class="schematic-uml-text schematic-uml-row" ${TEXT_PAINT} x="${svgNumber(x)}" y="${svgNumber(y)}" font-size="12">${renderMathLabelTspans(value)}</text>`;
345
333
  }
346
334
  function umlClassShape(component, paint) {
347
335
  const left = -component.bodyWidth / 2;
@@ -352,7 +340,7 @@ function umlClassShape(component, paint) {
352
340
  const operationSeparator = attributeSeparator + attributeHeight;
353
341
  const stereotype = component.stereotype === undefined
354
342
  ? ''
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>`;
343
+ : `<text class="schematic-uml-text schematic-uml-stereotype" ${TEXT_PAINT} x="0" y="${svgNumber(top + 13)}" text-anchor="middle" font-size="11">«${renderMathLabelTspans(component.stereotype)}»</text>`;
356
344
  const nameY = top + (component.stereotype === undefined ? 23 : 32);
357
345
  const attributes = component.attributes
358
346
  .map((row, index) => umlRow(row, left + 8, attributeSeparator + 17 + index * 16))
@@ -360,7 +348,7 @@ function umlClassShape(component, paint) {
360
348
  const operations = component.operations
361
349
  .map((row, index) => umlRow(row, left + 8, operationSeparator + 17 + index * 16))
362
350
  .join('');
363
- return `<rect ${paint} x="${svgNumber(left)}" y="${svgNumber(top)}" width="${svgNumber(component.bodyWidth)}" height="${svgNumber(component.bodyHeight)}" rx="2" /><path ${paint} d="M ${svgNumber(left)} ${svgNumber(attributeSeparator)} H ${svgNumber(-left)} M ${svgNumber(left)} ${svgNumber(operationSeparator)} H ${svgNumber(-left)}" />${stereotype}<text class="schematic-uml-text" fill="currentColor" stroke="none" x="0" y="${svgNumber(nameY)}" text-anchor="middle" font-size="13" font-weight="600">${renderMathLabelTspans(component.label)}</text>${attributes}${operations}`;
351
+ return `<rect ${paint} x="${svgNumber(left)}" y="${svgNumber(top)}" width="${svgNumber(component.bodyWidth)}" height="${svgNumber(component.bodyHeight)}" rx="2" /><path ${paint} d="M ${svgNumber(left)} ${svgNumber(attributeSeparator)} H ${svgNumber(-left)} M ${svgNumber(left)} ${svgNumber(operationSeparator)} H ${svgNumber(-left)}" />${stereotype}<text class="schematic-uml-text" ${TEXT_PAINT} x="0" y="${svgNumber(nameY)}" text-anchor="middle" font-size="13" font-weight="600">${renderMathLabelTspans(component.label)}</text>${attributes}${operations}`;
364
352
  }
365
353
  function umlStateShape(component, paint) {
366
354
  const left = -component.bodyWidth / 2;
@@ -369,7 +357,7 @@ function umlStateShape(component, paint) {
369
357
  const rows = component.details
370
358
  .map((row, index) => umlRow(row, left + 8, separator + 17 + index * 16))
371
359
  .join('');
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}`;
360
+ 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" ${TEXT_PAINT} x="0" y="${svgNumber(top + 21)}" text-anchor="middle" font-size="13" font-weight="600">${renderMathLabelTspans(component.label)}</text>${rows}`;
373
361
  }
374
362
  function umlComponentShape(component, paint, nodePaint) {
375
363
  if (component.kind === 'class' || component.kind === 'interface' ||
@@ -379,24 +367,24 @@ function umlComponentShape(component, paint, nodePaint) {
379
367
  return umlStateShape(component, paint);
380
368
  switch (component.kind) {
381
369
  case 'actor':
382
- return `<circle ${paint} cx="0" cy="-28" r="9" /><path ${paint} d="M 0 -19 V 12 M -20 -5 H 20 M 0 12 L -17 37 M 0 12 L 17 37" /><text class="schematic-uml-text" fill="currentColor" stroke="none" x="0" y="49" text-anchor="middle" font-size="12">${renderMathLabelTspans(component.label)}</text>`;
370
+ return `<circle ${paint} cx="0" cy="-28" r="9" /><path ${paint} d="M 0 -19 V 12 M -20 -5 H 20 M 0 12 L -17 37 M 0 12 L 17 37" /><text class="schematic-uml-text" ${TEXT_PAINT} x="0" y="49" text-anchor="middle" font-size="12">${renderMathLabelTspans(component.label)}</text>`;
383
371
  case 'usecase':
384
- return `<ellipse ${paint} cx="0" cy="0" rx="${svgNumber(component.bodyWidth / 2)}" ry="${svgNumber(component.bodyHeight / 2)}" /><text class="schematic-uml-text" fill="currentColor" stroke="none" x="0" y="4" text-anchor="middle" font-size="13">${renderMathLabelTspans(component.label)}</text>`;
372
+ return `<ellipse ${paint} cx="0" cy="0" rx="${svgNumber(component.bodyWidth / 2)}" ry="${svgNumber(component.bodyHeight / 2)}" /><text class="schematic-uml-text" ${TEXT_PAINT} x="0" y="4" text-anchor="middle" font-size="13">${renderMathLabelTspans(component.label)}</text>`;
385
373
  case 'lifeline': {
386
374
  const top = -component.bodyHeight / 2;
387
375
  const left = -component.bodyWidth / 2;
388
- return `<rect ${paint} x="${svgNumber(left)}" y="${svgNumber(top)}" width="${svgNumber(component.bodyWidth)}" height="32" rx="2" /><text class="schematic-uml-text" fill="currentColor" stroke="none" x="0" y="${svgNumber(top + 21)}" text-anchor="middle" font-size="12">${renderMathLabelTspans(component.label)}</text><path ${paint} stroke-dasharray="6 5" d="M 0 ${svgNumber(top + 32)} V ${svgNumber(component.bodyHeight / 2)}" />`;
376
+ return `<rect ${paint} x="${svgNumber(left)}" y="${svgNumber(top)}" width="${svgNumber(component.bodyWidth)}" height="32" rx="2" /><text class="schematic-uml-text" ${TEXT_PAINT} x="0" y="${svgNumber(top + 21)}" text-anchor="middle" font-size="12">${renderMathLabelTspans(component.label)}</text><path ${paint} stroke-dasharray="6 5" d="M 0 ${svgNumber(top + 32)} V ${svgNumber(component.bodyHeight / 2)}" />`;
389
377
  }
390
378
  case 'note': {
391
379
  const left = -component.bodyWidth / 2;
392
380
  const top = -component.bodyHeight / 2;
393
381
  const right = component.bodyWidth / 2;
394
- return `<path ${paint} d="M ${svgNumber(left)} ${svgNumber(top)} H ${svgNumber(right - 14)} L ${svgNumber(right)} ${svgNumber(top + 14)} V ${svgNumber(-top)} H ${svgNumber(left)} Z M ${svgNumber(right - 14)} ${svgNumber(top)} V ${svgNumber(top + 14)} H ${svgNumber(right)}" /><text class="schematic-uml-text" fill="currentColor" stroke="none" x="0" y="4" text-anchor="middle" font-size="12">${renderMathLabelTspans(component.label)}</text>`;
382
+ return `<path ${paint} d="M ${svgNumber(left)} ${svgNumber(top)} H ${svgNumber(right - 14)} L ${svgNumber(right)} ${svgNumber(top + 14)} V ${svgNumber(-top)} H ${svgNumber(left)} Z M ${svgNumber(right - 14)} ${svgNumber(top)} V ${svgNumber(top + 14)} H ${svgNumber(right)}" /><text class="schematic-uml-text" ${TEXT_PAINT} x="0" y="4" text-anchor="middle" font-size="12">${renderMathLabelTspans(component.label)}</text>`;
395
383
  }
396
384
  case 'package': {
397
385
  const left = -component.bodyWidth / 2;
398
386
  const top = -component.bodyHeight / 2;
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>`;
387
+ 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" ${TEXT_PAINT} x="0" y="4" text-anchor="middle" font-size="13">${renderMathLabelTspans(component.label)}</text>`;
400
388
  }
401
389
  case 'component':
402
390
  case 'artifact':
@@ -418,7 +406,7 @@ function umlComponentShape(component, paint, nodePaint) {
418
406
  if (component.kind === 'activation')
419
407
  return `<rect ${paint} x="${svgNumber(left)}" y="${svgNumber(top)}" width="${svgNumber(component.bodyWidth)}" height="${svgNumber(component.bodyHeight)}" />`;
420
408
  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>`;
409
+ 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" ${TEXT_PAINT} x="${svgNumber(left + 6)}" y="${svgNumber(top + 14)}" font-size="11">${component.kind === 'interaction' ? 'ref' : 'alt'}</text><text class="schematic-uml-text" ${TEXT_PAINT} x="0" y="4" text-anchor="middle" font-size="12">${renderMathLabelTspans(component.label)}</text>`;
422
410
  }
423
411
  const rx = component.kind === 'action' ? 10 : 1;
424
412
  const dash = component.kind === 'region' ? ' stroke-dasharray="6 4"' : '';
@@ -429,7 +417,7 @@ function umlComponentShape(component, paint, nodePaint) {
429
417
  : component.kind === 'node' || component.kind === 'device' || component.kind === 'execution'
430
418
  ? `<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
419
  : 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>`;
420
+ 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" ${TEXT_PAINT} x="0" y="4" text-anchor="middle" font-size="12">${renderMathLabelTspans(component.label)}</text>`;
433
421
  }
434
422
  case 'provided-interface':
435
423
  return `<circle ${paint} cx="0" cy="0" r="10" />`;
@@ -461,7 +449,7 @@ function umlComponentShape(component, paint, nodePaint) {
461
449
  case 'state-junction':
462
450
  return `<circle ${nodePaint} cx="0" cy="0" r="7" />`;
463
451
  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>`;
452
+ return `<circle ${paint} cx="0" cy="0" r="11" /><text class="schematic-uml-text" ${TEXT_PAINT} x="0" y="4" text-anchor="middle" font-size="11">H${component.variant === 'deep' ? '*' : ''}</text>`;
465
453
  case 'entry':
466
454
  case 'exit':
467
455
  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" />` : ''}`;
@@ -695,7 +683,7 @@ function reusableSymbolKey(component) {
695
683
  }
696
684
  function reusableSymbolDefinition(component, symbolId) {
697
685
  const vectorPaint = 'class="schematic-token" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" style="stroke:var(--schematic-vector,currentColor)"';
698
- const nodePaint = 'class="schematic-token schematic-node-fill" fill="currentColor" stroke="none" style="fill:var(--schematic-vector,currentColor)"';
686
+ const nodePaint = `class="schematic-token schematic-node-fill" ${TEXT_PAINT} style="fill:var(--schematic-vector,currentColor)"`;
699
687
  return `<g id="${symbolId}">${componentShape(component, vectorPaint, nodePaint)}</g>`;
700
688
  }
701
689
  function connectionMarkup(connection, routed, index, idPrefix, glowId) {
@@ -705,57 +693,73 @@ function connectionMarkup(connection, routed, index, idPrefix, glowId) {
705
693
  const traceId = `${idPrefix}-wire-${index}-vector`;
706
694
  const signalKind = connection.signalKind ?? 'electrical';
707
695
  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}"`}`;
696
+ const dataAttributes = ` data-wire-source="${source}" data-wire-target="${target}" data-source-line="${connection.line}"${connection.netId === undefined ? '' : ` data-net-id="${escapeXml(connection.netId)}"`}${connection.signalKind === undefined ? '' : ` data-signal-kind="${signalKind}"`}${connection.width === undefined ? '' : ` data-bus-width="${connection.width}"`}`;
709
697
  const relation = connection.relation ?? 'signal';
710
698
  const accessibility = ` tabindex="0" role="group" aria-label="${escapeXml(relation)} from ${source} to ${target}"`;
711
699
  const vectorId = ` id="${traceId}"`;
712
- const markerAttributes = connectionMarkerAttributes(connection, idPrefix);
700
+ const markerAttributes = connectionMarkerAttributes(connection, idPrefix, false);
701
+ const markerCarrier = openMarkerCarrier(connection, routed, idPrefix);
713
702
  const glow = `<use class="schematic-glow-layer" href="#${traceId}" filter="url(#${glowId})" aria-hidden="true" pointer-events="none" />`;
714
703
  const endpoint = relation === 'signal' && connection.markerEnd === 'none'
715
704
  ? `<circle ${colorAttributes(connection.color, 'schematic-node-fill')} cx="${svgNumber(end.x)}" cy="${svgNumber(end.y)}" r="3" />`
716
705
  : '';
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>`;
718
- }
719
- function connectionMarkerAttributes(connection, idPrefix) {
720
- const start = connection.markerStart;
721
- const end = connection.markerEnd;
722
- return `${start === 'none' ? '' : ` marker-start="url(#${idPrefix}-marker-${start})"`}${end === 'none' ? '' : ` marker-end="url(#${idPrefix}-marker-${end})"`}${connection.dashed === true ? ' stroke-dasharray="7 5"' : ''}`;
723
- }
724
- function connectionLabelPoint(route) {
725
- if (route.curve === 'bezier' && route.points.length >= 4) {
726
- const a = route.points[0];
727
- const b = route.points[1];
728
- const c = route.points[2];
729
- const d = route.points[3];
730
- return {
731
- x: (a.x + 3 * b.x + 3 * c.x + d.x) / 8,
732
- y: (a.y + 3 * b.y + 3 * c.y + d.y) / 8
733
- };
734
- }
735
- let total = 0;
736
- for (let index = 1; index < route.points.length; index += 1) {
737
- total +=
738
- Math.abs(route.points[index].x - route.points[index - 1].x) +
739
- Math.abs(route.points[index].y - route.points[index - 1].y);
740
- }
741
- let remaining = total / 2;
742
- for (let index = 1; index < route.points.length; index += 1) {
743
- const start = route.points[index - 1];
744
- const end = route.points[index];
745
- const length = Math.abs(end.x - start.x) + Math.abs(end.y - start.y);
746
- if (remaining <= length) {
747
- const ratio = length === 0 ? 0 : remaining / length;
748
- return { x: start.x + (end.x - start.x) * ratio, y: start.y + (end.y - start.y) * ratio };
749
- }
750
- remaining -= length;
751
- }
752
- return route.points.at(-1);
706
+ return `<g class="schematic-wire${signalClass}"${dataAttributes}${accessibility}>${markerCarrier}<path${vectorId} ${colorAttributes(connection.color, `schematic-trace${signalClass}`)} d="${visibleConnectionPath(connection, routed)}"${markerAttributes} />${glow}${endpoint}${connectionLabelMarkup(connection, routed)}</g>`;
707
+ }
708
+ function isOpenMarker(marker) {
709
+ return marker === 'open-arrow' || marker === 'triangle' || marker === 'diamond';
710
+ }
711
+ function connectionMarkerAttributes(connection, idPrefix, open) {
712
+ const start = isOpenMarker(connection.markerStart) === open ? connection.markerStart : 'none';
713
+ const end = isOpenMarker(connection.markerEnd) === open ? connection.markerEnd : 'none';
714
+ return `${start === 'none' ? '' : ` marker-start="url(#${idPrefix}-marker-${start})"`}${end === 'none' ? '' : ` marker-end="url(#${idPrefix}-marker-${end})"`}${!open && connection.dashed === true ? ' stroke-dasharray="7 5"' : ''}`;
715
+ }
716
+ function visibleConnectionPath(connection, route) {
717
+ const startTrim = isOpenMarker(connection.markerStart)
718
+ ? connection.markerStart === 'open-arrow'
719
+ ? 9
720
+ : 12
721
+ : 0;
722
+ const endTrim = isOpenMarker(connection.markerEnd)
723
+ ? connection.markerEnd === 'open-arrow'
724
+ ? 9
725
+ : 12
726
+ : 0;
727
+ if (startTrim === 0 && endTrim === 0)
728
+ return route.d;
729
+ const inset = (endpoint, adjacent, distance) => {
730
+ const dx = adjacent.x - endpoint.x;
731
+ const dy = adjacent.y - endpoint.y;
732
+ const magnitude = Math.hypot(dx, dy);
733
+ const scale = Math.min(1, distance / magnitude);
734
+ return { x: endpoint.x + dx * scale, y: endpoint.y + dy * scale };
735
+ };
736
+ let path = route.d;
737
+ if (startTrim > 0) {
738
+ const start = inset(route.points[0], route.points[1], startTrim);
739
+ path = path.replace(/^M -?[\d.]+ -?[\d.]+/, `M ${svgNumber(start.x)} ${svgNumber(start.y)}`);
740
+ }
741
+ if (endTrim > 0) {
742
+ const end = inset(route.points.at(-1), route.points.at(-2), endTrim);
743
+ if (/ H -?[\d.]+$/.test(path))
744
+ path = path.replace(/-?[\d.]+$/, svgNumber(end.x));
745
+ else if (/ V -?[\d.]+$/.test(path))
746
+ path = path.replace(/-?[\d.]+$/, svgNumber(end.y));
747
+ else
748
+ path = path.replace(/-?[\d.]+ -?[\d.]+$/, `${svgNumber(end.x)} ${svgNumber(end.y)}`);
749
+ }
750
+ return path;
751
+ }
752
+ function openMarkerCarrier(connection, route, idPrefix) {
753
+ const markers = connectionMarkerAttributes(connection, idPrefix, true);
754
+ return markers === ''
755
+ ? ''
756
+ : `<path ${colorAttributes(connection.color, 'schematic-marker-carrier')} d="${route.d}" stroke-width="0"${markers} />`;
753
757
  }
754
758
  function connectionLabelMarkup(connection, route) {
755
759
  if (connection.label === undefined)
756
760
  return '';
757
761
  const point = connectionLabelPoint(route);
758
- return `<text class="schematic-connection-label" fill="currentColor" stroke="none" x="${svgNumber(point.x)}" y="${svgNumber(point.y - 7)}" text-anchor="middle" font-size="11">${renderMathLabelTspans(connection.label)}</text>`;
762
+ return `<text class="schematic-connection-label" ${TEXT_PAINT} x="${svgNumber(point.x)}" y="${svgNumber(point.y - 7)}" text-anchor="middle" font-size="11">${renderMathLabelTspans(connection.label)}</text>`;
759
763
  }
760
764
  function colorKey(color) {
761
765
  return `${color.kind}:${color.value}`;
@@ -763,17 +767,20 @@ function colorKey(color) {
763
767
  function compactConnectionMarkup(connections, routes, idPrefix, embedVisuals, glowId) {
764
768
  const batches = new Map();
765
769
  for (const [index, connection] of connections.entries()) {
766
- const markerAttributes = connectionMarkerAttributes(connection, idPrefix);
770
+ const markerAttributes = connectionMarkerAttributes(connection, idPrefix, false);
767
771
  const signalKind = connection.signalKind ?? 'electrical';
768
772
  const key = `${colorKey(connection.color)}|${markerAttributes}|${signalKind}|${connection.width ?? 1}`;
769
773
  let batch = batches.get(key);
770
774
  if (batch === undefined) {
771
- batch = { color: connection.color, markerAttributes, signalKind, traces: [], endpoints: [], labels: [] };
775
+ batch = { color: connection.color, markerAttributes, signalKind, traces: [], endpoints: [], labels: [], carriers: [] };
772
776
  batches.set(key, batch);
773
777
  }
774
778
  const routed = routes[index];
775
779
  const end = routed.points.at(-1);
776
- batch.traces.push(routed.d);
780
+ batch.traces.push(visibleConnectionPath(connection, routed));
781
+ const carrier = openMarkerCarrier(connection, routed, idPrefix);
782
+ if (carrier !== '')
783
+ batch.carriers.push(carrier);
777
784
  if ((connection.relation ?? 'signal') === 'signal' && connection.markerEnd === 'none') {
778
785
  batch.endpoints.push(`M ${svgNumber(end.x - 3)} ${svgNumber(end.y)} a 3 3 0 1 0 6 0 a 3 3 0 1 0 -6 0`);
779
786
  }
@@ -788,9 +795,10 @@ function compactConnectionMarkup(connections, routes, idPrefix, embedVisuals, gl
788
795
  const trace = `<path${embedVisuals ? ` id="${traceId}"` : ''} ${tracePaint} d="${batch.traces.join(' ')}"${batch.markerAttributes} />`;
789
796
  const endpoints = batch.endpoints.length === 0 ? '' : `<path ${nodePaint} d="${batch.endpoints.join(' ')}" />`;
790
797
  const labels = batch.labels.join('');
798
+ const carriers = batch.carriers.join('');
791
799
  if (!embedVisuals)
792
- return trace + endpoints + labels;
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>`;
800
+ return carriers + trace + endpoints + labels;
801
+ return `<g class="schematic-wire">${carriers}${trace}<use class="schematic-glow-layer" href="#${traceId}" filter="url(#${glowId})" aria-hidden="true" pointer-events="none" />${endpoints}${labels}</g>`;
794
802
  }).join('');
795
803
  }
796
804
  function componentMetadata(component) {
@@ -863,7 +871,7 @@ function componentMarkup(component, index, idPrefix, glowId, mode, nodeHooks, po
863
871
  let accessibility = '';
864
872
  if (mode === 'full') {
865
873
  const metadata = componentMetadata(component);
866
- const ariaLabel = escapeXml(`${component.id}, ${component.kind}, ${component.label}${metadata === '' ? '' : `, ${metadata}`}`);
874
+ const ariaLabel = escapeXml(`${component.id}, ${component.kind}, ${mathLabelText(component.label)}${metadata === '' ? '' : `, ${metadata}`}`);
867
875
  accessibility = `${portHooks ? '' : ' tabindex="0"'} role="group" aria-label="${ariaLabel}"`;
868
876
  }
869
877
  const vectorIdAttribute = styles ? ` id="${vectorId}"` : '';
@@ -877,7 +885,7 @@ function componentMarkup(component, index, idPrefix, glowId, mode, nodeHooks, po
877
885
  const innerText = componentInnerText(component);
878
886
  const externalLabels = isUmlComponent(component)
879
887
  ? ''
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>`;
888
+ : `<text class="schematic-designator" ${TEXT_PAINT} x="0" y="${anchors.designatorY}" text-anchor="middle" font-size="12" textLength="${anchors.designatorWidth}" ${LENGTH_FIT}>${id}</text><text class="schematic-label" ${TEXT_PAINT} x="0" y="${anchors.labelY}" text-anchor="middle" font-size="12" textLength="${anchors.labelWidth}" ${LENGTH_FIT}>${renderedLabel}</text>`;
881
889
  return `<g class="schematic-component"${dataAttributes} transform="translate(${svgNumber(component.x)} ${svgNumber(component.y)})"${accessibility}>${vector}${glow}${innerText}${externalLabels}${hotspots}</g>`;
882
890
  }
883
891
  export function renderSchematic(document, options) {
@@ -917,7 +925,7 @@ export function renderSchematic(document, options) {
917
925
  : '';
918
926
  const symbolDefinitions = Array.from(reusableSymbols.values(), ({ id, component }) => reusableSymbolDefinition(component, id)).join('');
919
927
  const usedMarkers = new Set(document.connections.flatMap((connection) => [connection.markerStart, connection.markerEnd]));
920
- const markerDefinitions = `${usedMarkers.has('arrow') ? `<marker id="${safePrefix}-marker-arrow" viewBox="0 0 8 8" refX="8" refY="4" markerWidth="8" markerHeight="8" markerUnits="userSpaceOnUse" orient="auto-start-reverse" overflow="visible"><path d="M0 0 8 4 0 8Z" fill="context-stroke" /></marker>` : ''}${usedMarkers.has('open-arrow') ? `<marker id="${safePrefix}-marker-open-arrow" viewBox="0 0 9 10" refX="9" refY="5" markerWidth="9" markerHeight="10" markerUnits="userSpaceOnUse" orient="auto-start-reverse" overflow="visible"><path d="M0 0 9 5 0 10" fill="none" stroke="context-stroke" stroke-width="1.5" /></marker>` : ''}${usedMarkers.has('dot') ? `<marker id="${safePrefix}-marker-dot" viewBox="0 0 8 8" refX="4" refY="4" markerWidth="8" markerHeight="8" markerUnits="userSpaceOnUse"><circle cx="4" cy="4" r="3" fill="context-stroke" /></marker>` : ''}${usedMarkers.has('triangle') ? `<marker id="${safePrefix}-marker-triangle" viewBox="0 0 12 12" refX="11" refY="6" markerWidth="12" markerHeight="12" markerUnits="userSpaceOnUse" orient="auto-start-reverse" overflow="visible"><path d="M0 1 11 6 0 11Z" fill="var(--schematic-surface,#fff)" stroke="context-stroke" stroke-width="1.5" /></marker>` : ''}${usedMarkers.has('diamond') ? `<marker id="${safePrefix}-marker-diamond" viewBox="0 0 13 12" refX="12" refY="6" markerWidth="13" markerHeight="12" markerUnits="userSpaceOnUse" orient="auto-start-reverse" overflow="visible"><path d="M0 6 6 1 12 6 6 11Z" fill="var(--schematic-surface,#fff)" stroke="context-stroke" stroke-width="1.5" /></marker>` : ''}${usedMarkers.has('diamond-filled') ? `<marker id="${safePrefix}-marker-diamond-filled" viewBox="0 0 13 12" refX="12" refY="6" markerWidth="13" markerHeight="12" markerUnits="userSpaceOnUse" orient="auto-start-reverse" overflow="visible"><path d="M0 6 6 1 12 6 6 11Z" fill="context-stroke" stroke="context-stroke" /></marker>` : ''}`;
928
+ const markerDefinitions = `${usedMarkers.has('arrow') ? `<marker id="${safePrefix}-marker-arrow" viewBox="0 0 8 8" refX="8" refY="4" markerWidth="8" markerHeight="8" markerUnits="userSpaceOnUse" orient="auto-start-reverse" overflow="visible"><path d="M0 0 8 4 0 8Z" fill="context-stroke" /></marker>` : ''}${usedMarkers.has('open-arrow') ? `<marker id="${safePrefix}-marker-open-arrow" viewBox="0 0 9 10" refX="9" refY="5" markerWidth="9" markerHeight="10" markerUnits="userSpaceOnUse" orient="auto-start-reverse" overflow="visible"><path d="M0 0 9 5 0 10" fill="none" stroke="context-stroke" stroke-width="1.5" /></marker>` : ''}${usedMarkers.has('dot') ? `<marker id="${safePrefix}-marker-dot" viewBox="0 0 8 8" refX="4" refY="4" markerWidth="8" markerHeight="8" markerUnits="userSpaceOnUse"><circle cx="4" cy="4" r="3" fill="context-stroke" /></marker>` : ''}${usedMarkers.has('triangle') ? `<marker id="${safePrefix}-marker-triangle" viewBox="0 0 12 12" refX="11" refY="6" markerWidth="12" markerHeight="12" markerUnits="userSpaceOnUse" orient="auto-start-reverse" overflow="visible"><path d="M0 1 11 6 0 11Z" fill="none" stroke="context-stroke" stroke-width="1.5" /></marker>` : ''}${usedMarkers.has('diamond') ? `<marker id="${safePrefix}-marker-diamond" viewBox="0 0 13 12" refX="12" refY="6" markerWidth="13" markerHeight="12" markerUnits="userSpaceOnUse" orient="auto-start-reverse" overflow="visible"><path d="M0 6 6 1 12 6 6 11Z" fill="none" stroke="context-stroke" stroke-width="1.5" /></marker>` : ''}${usedMarkers.has('diamond-filled') ? `<marker id="${safePrefix}-marker-diamond-filled" viewBox="0 0 13 12" refX="12" refY="6" markerWidth="13" markerHeight="12" markerUnits="userSpaceOnUse" orient="auto-start-reverse" overflow="visible"><path d="M0 6 6 1 12 6 6 11Z" fill="context-stroke" stroke="context-stroke" /></marker>` : ''}`;
921
929
  const embeddedDefinitions = styles
922
930
  ? `<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}`
923
931
  : '';
package/dist/types.d.ts CHANGED
@@ -346,6 +346,10 @@ export interface SchematicConnection {
346
346
  relation?: SchematicRelationKind;
347
347
  /** Non-default signal domain; absence preserves legacy electrical behavior. */
348
348
  signalKind?: SchematicSignalKind;
349
+ /** Author-supplied net name joining otherwise disconnected signal segments. */
350
+ net?: string;
351
+ /** Parser-resolved topology identity; unnamed nets use a stable `$N` identifier. */
352
+ netId?: string;
349
353
  /** Validated bus/register width; absence denotes a scalar connection. */
350
354
  width?: number;
351
355
  /** Optional text centered beside the routed connector. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@schemd/core",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Zero-dependency text-to-SVG compiler for schematics and UML.",
5
5
  "homepage": "https://schemd.johnowolabiidogun.dev",
6
6
  "bugs": {
@@ -60,11 +60,17 @@
60
60
  "benchmark": "bun run build && node scripts/benchmark.mjs",
61
61
  "check": "tsc -p tsconfig.json --noEmit",
62
62
  "prepack": "bun run build",
63
+ "release:check": "bun run check && bun run test:coverage && bun run test:mutation && bun run test:visual && bun run build && bun run size && node scripts/benchmark.mjs",
63
64
  "size": "node scripts/check-bundle-size.mjs",
64
65
  "test": "vitest run",
65
- "test:coverage": "vitest run --coverage"
66
+ "test:coverage": "vitest run --coverage",
67
+ "test:fuzz": "vitest run tests/fuzz.test.ts",
68
+ "test:mutation": "node scripts/mutation.mjs",
69
+ "test:visual": "playwright test --config=playwright.config.ts",
70
+ "test:visual:install": "playwright install chromium"
66
71
  },
67
72
  "devDependencies": {
73
+ "@playwright/test": "^1.61.1",
68
74
  "@vitest/coverage-v8": "^4.1.10",
69
75
  "typescript": "^6.0.3",
70
76
  "vite": "^8.1.4",