@schemd/core 0.3.0 → 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/CHANGELOG.md +55 -0
- package/README.md +19 -11
- package/dist/compiler.d.ts +2 -0
- package/dist/compiler.js +4 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/layout.d.ts +38 -3
- package/dist/layout.js +633 -85
- package/dist/math-label.d.ts +5 -3
- package/dist/math-label.js +14 -28
- package/dist/parser.d.ts +1 -1
- package/dist/parser.js +137 -20
- package/dist/renderer.js +105 -99
- package/dist/types.d.ts +4 -0
- package/package.json +8 -2
package/dist/renderer.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import { classicalGateHeight, componentTextAnchors, distributedCoordinate, enumerateComponentPorts, PORT_HOTSPOT_RADIUS, quantumGateDimensions, 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 {
|
|
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
|
|
27
|
-
if (
|
|
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"
|
|
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'}"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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" />` : ''}`;
|
|
@@ -560,14 +548,14 @@ function quantumSpecialShape(component, paint, nodePaint) {
|
|
|
560
548
|
if (component.kind === 'classical-bit' || component.kind === 'classical-register')
|
|
561
549
|
return `<path ${paint} d="M -40 -2 H 40 M -40 2 H 40" />`;
|
|
562
550
|
const tracks = Math.max(component.wires, component.controls + component.targets);
|
|
563
|
-
const span =
|
|
564
|
-
const lines = Array.from({ length: tracks }, (_, index) => `<path ${paint} d="M -42 ${svgNumber(
|
|
551
|
+
const span = quantumTrackSpan(tracks);
|
|
552
|
+
const lines = Array.from({ length: tracks }, (_, index) => `<path ${paint} d="M -42 ${svgNumber(quantumTrackCoordinate(index, tracks))} H 42" />`).join('');
|
|
565
553
|
if (component.kind === 'barrier')
|
|
566
554
|
return `${lines}<path ${paint} stroke-dasharray="4 3" d="M 0 ${svgNumber(-span / 2 - 8)} V ${svgNumber(span / 2 + 8)}" />`;
|
|
567
555
|
if (component.kind === 'delay')
|
|
568
556
|
return `${lines}<rect ${paint} x="-12" y="${svgNumber(-span / 2 - 7)}" width="24" height="${svgNumber(span + 14)}" rx="2" />`;
|
|
569
557
|
const marks = Array.from({ length: tracks }, (_, index) => {
|
|
570
|
-
const y =
|
|
558
|
+
const y = quantumTrackCoordinate(index, tracks);
|
|
571
559
|
if (component.kind === 'swap')
|
|
572
560
|
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
561
|
if (component.kind === 'cz')
|
|
@@ -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 =
|
|
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}
|
|
718
|
-
}
|
|
719
|
-
function
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
const
|
|
744
|
-
const
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
}
|
|
752
|
-
|
|
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"
|
|
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
|
|
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"
|
|
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) {
|
|
@@ -861,9 +869,9 @@ function componentMarkup(component, index, idPrefix, glowId, mode, nodeHooks, po
|
|
|
861
869
|
? ` 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}"` : ''}`
|
|
862
870
|
: '';
|
|
863
871
|
let accessibility = '';
|
|
864
|
-
if (
|
|
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,21 +885,19 @@ 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"
|
|
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) {
|
|
884
892
|
assertParsedSchematicDocument(document);
|
|
885
893
|
const normalized = normalizeCompileOptions(options);
|
|
886
|
-
const components = new Map(document.components.map((component) => [component.id, component]));
|
|
887
894
|
const routedConnections = parsedSchematicRoutes(document, normalized.bounds) ??
|
|
888
|
-
routeConnections(document.connections, components, normalized.bounds);
|
|
889
|
-
|
|
890
|
-
const
|
|
891
|
-
const candidatePrefix = (normalized.idPrefix ?? `schematic-${stableHash(signature)}`).replace(/[^A-Za-z0-9_-]/g, '-');
|
|
895
|
+
validateDocumentGeometry(document, normalized, routeConnections(document.connections, new Map(document.components.map((component) => [component.id, component])), normalized.bounds));
|
|
896
|
+
const signatureHash = () => stableHash(`${normalized.bounds.width}x${normalized.bounds.height}:${normalized.title}:${JSON.stringify(document)}`);
|
|
897
|
+
const candidatePrefix = (normalized.idPrefix ?? `schematic-${signatureHash()}`).replace(/[^A-Za-z0-9_-]/g, '-');
|
|
892
898
|
const safePrefix = /[A-Za-z0-9]/.test(candidatePrefix)
|
|
893
899
|
? candidatePrefix
|
|
894
|
-
: `schematic-${
|
|
900
|
+
: `schematic-${signatureHash()}`;
|
|
895
901
|
const titleId = `${safePrefix}-title`;
|
|
896
902
|
const descriptionId = `${safePrefix}-description`;
|
|
897
903
|
const gridId = `${safePrefix}-grid`;
|
|
@@ -919,12 +925,12 @@ export function renderSchematic(document, options) {
|
|
|
919
925
|
: '';
|
|
920
926
|
const symbolDefinitions = Array.from(reusableSymbols.values(), ({ id, component }) => reusableSymbolDefinition(component, id)).join('');
|
|
921
927
|
const usedMarkers = new Set(document.connections.flatMap((connection) => [connection.markerStart, connection.markerEnd]));
|
|
922
|
-
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="
|
|
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>` : ''}`;
|
|
923
929
|
const embeddedDefinitions = styles
|
|
924
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}`
|
|
925
931
|
: '';
|
|
926
932
|
const definitions = `${embeddedDefinitions}${markerDefinitions}${symbolDefinitions}`;
|
|
927
|
-
const svgRole =
|
|
933
|
+
const svgRole = hooks ? 'group' : 'img';
|
|
928
934
|
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">`);
|
|
929
935
|
if (wireHooks) {
|
|
930
936
|
for (const [index, connection] of document.connections.entries()) {
|
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.
|
|
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",
|