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