@schemd/core 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/layout.js CHANGED
@@ -1,9 +1,64 @@
1
- import { CLASSICAL_GATE_KINDS, UML_COMPONENT_KINDS, SchematicSyntaxError } from './types.js';
1
+ import { CLASSICAL_GATE_KINDS, DIGITAL_COMPONENT_KINDS, QUANTUM_SPECIAL_KINDS, UML_COMPONENT_KINDS, SchematicSyntaxError } from './types.js';
2
2
  import { mathLabelTextWidth } from './math-label.js';
3
3
  import { MAX_SCHEMATIC_WIRE_CROSSINGS } from './limits.js';
4
4
  export const PORT_HOTSPOT_RADIUS = 4;
5
5
  export const SCHEMATIC_OBSTACLE_CLEARANCE = 12;
6
6
  export const SCHEMATIC_BRIDGE_RADIUS = 5;
7
+ export function orientationQuarterTurn(orientation) {
8
+ switch (orientation) {
9
+ case 'right':
10
+ return 0;
11
+ case 'down':
12
+ return 1;
13
+ case 'left':
14
+ return 2;
15
+ case 'up':
16
+ return 3;
17
+ }
18
+ }
19
+ function normalizedQuarterTurn(value) {
20
+ switch (value & 3) {
21
+ case 0:
22
+ return 0;
23
+ case 1:
24
+ return 1;
25
+ case 2:
26
+ return 2;
27
+ default:
28
+ return 3;
29
+ }
30
+ }
31
+ export function composeQuarterTurns(first, second) {
32
+ return normalizedQuarterTurn(first + second);
33
+ }
34
+ export function inverseQuarterTurn(turn) {
35
+ return normalizedQuarterTurn(4 - turn);
36
+ }
37
+ function negatedCoordinate(value) {
38
+ return value === 0 ? 0 : -value;
39
+ }
40
+ function canonicalCoordinate(value) {
41
+ return value === 0 ? 0 : value;
42
+ }
43
+ export function rotateQuarterPoint(point, turn) {
44
+ switch (turn) {
45
+ case 0:
46
+ return { x: canonicalCoordinate(point.x), y: canonicalCoordinate(point.y) };
47
+ case 1:
48
+ return { x: negatedCoordinate(point.y), y: canonicalCoordinate(point.x) };
49
+ case 2:
50
+ return { x: negatedCoordinate(point.x), y: negatedCoordinate(point.y) };
51
+ case 3:
52
+ return { x: canonicalCoordinate(point.y), y: negatedCoordinate(point.x) };
53
+ }
54
+ }
55
+ export function rotateQuarterExtents(extents, turn) {
56
+ const halfWidth = canonicalCoordinate(extents.halfWidth);
57
+ const halfHeight = canonicalCoordinate(extents.halfHeight);
58
+ return turn === 1 || turn === 3
59
+ ? { halfWidth: halfHeight, halfHeight: halfWidth }
60
+ : { halfWidth, halfHeight };
61
+ }
7
62
  const MAX_ROUTER_STATES = 40_000;
8
63
  const ROUTER_BEND_PENALTY = 0.25;
9
64
  const CROSSING_BUCKET_SIZE = 64;
@@ -31,26 +86,96 @@ export function distributedCoordinate(index, count, span) {
31
86
  export function classicalGateHeight(component) {
32
87
  return Math.max(48, Math.max(component.inputs, component.outputs) * 16);
33
88
  }
89
+ export function quantumGateDimensions(component) {
90
+ const details = [component.parameter, component.phase, component.matrix].filter((value) => value !== undefined);
91
+ let widest = 16;
92
+ if (details.length > 0 && component.kind === 'qgate')
93
+ widest = mathLabelTextWidth(component.label, 8);
94
+ for (const detail of details)
95
+ widest = Math.max(widest, mathLabelTextWidth(detail, 7));
96
+ const bodyWidth = Math.max(50, Math.min(104, widest + 20));
97
+ const bodyHeight = 50 + details.length * 15;
98
+ return { bodyWidth, bodyHeight, stubExtent: bodyWidth / 2 + 23 };
99
+ }
34
100
  function isClassicalGate(component) {
35
101
  return CLASSICAL_GATE_KINDS.includes(component.kind);
36
102
  }
103
+ function isDigitalComponent(component) {
104
+ return DIGITAL_COMPONENT_KINDS.includes(component.kind);
105
+ }
106
+ function isQuantumSpecial(component) {
107
+ return QUANTUM_SPECIAL_KINDS.includes(component.kind);
108
+ }
37
109
  function isUmlComponent(component) {
38
110
  return UML_COMPONENT_KINDS.includes(component.kind);
39
111
  }
112
+ function componentTurn(component) {
113
+ return 'orientation' in component && component.orientation !== undefined
114
+ ? orientationQuarterTurn(component.orientation)
115
+ : 0;
116
+ }
117
+ function positionedQuarterPoint(component, local) {
118
+ const rotated = rotateQuarterPoint(local, componentTurn(component));
119
+ return { x: component.x + rotated.x, y: component.y + rotated.y };
120
+ }
121
+ function positionedQuarterNormal(component, normal) {
122
+ return rotateQuarterPoint(normal, componentTurn(component));
123
+ }
40
124
  function umlHalfExtents(component) {
41
125
  switch (component.kind) {
42
126
  case 'class':
127
+ case 'interface':
128
+ case 'enumeration':
129
+ case 'datatype':
130
+ case 'object':
43
131
  case 'state':
44
132
  case 'usecase':
45
133
  case 'lifeline':
46
134
  case 'note':
47
135
  case 'package':
136
+ case 'component':
137
+ case 'artifact':
138
+ case 'node':
139
+ case 'device':
140
+ case 'execution':
141
+ case 'system':
142
+ case 'action':
143
+ case 'object-node':
144
+ case 'partition':
145
+ case 'activation':
146
+ case 'fragment':
147
+ case 'interaction':
148
+ case 'region':
48
149
  return { halfWidth: component.bodyWidth / 2, halfHeight: component.bodyHeight / 2 };
49
150
  case 'actor':
50
151
  return { halfWidth: 24, halfHeight: 50 };
51
152
  case 'initial':
52
153
  case 'final':
154
+ case 'activity-final':
155
+ case 'flow-final':
156
+ case 'provided-interface':
157
+ case 'required-interface':
158
+ case 'component-port':
159
+ case 'destruction':
160
+ case 'gate':
161
+ case 'found':
162
+ case 'lost':
163
+ case 'state-junction':
164
+ case 'history':
165
+ case 'entry':
166
+ case 'exit':
167
+ case 'terminate':
53
168
  return { halfWidth: 12, halfHeight: 12 };
169
+ case 'decision':
170
+ case 'merge':
171
+ case 'choice':
172
+ return { halfWidth: 18, halfHeight: 18 };
173
+ case 'fork':
174
+ case 'join':
175
+ return { halfWidth: 38, halfHeight: 5 };
176
+ case 'send-signal':
177
+ case 'receive-signal':
178
+ return { halfWidth: 22, halfHeight: 15 };
54
179
  }
55
180
  }
56
181
  function umlPortPoint(component, port) {
@@ -82,28 +207,30 @@ function indexedPortPoint(component, port) {
82
207
  const count = output ? component.outputs : component.inputs;
83
208
  const suffix = port.match(/\d+$/)?.[0];
84
209
  const index = suffix === undefined ? 0 : Number(suffix) - 1;
85
- return {
86
- x: component.x + (output ? 48 : -48),
87
- y: component.y + distributedCoordinate(index, count, classicalGateHeight(component))
88
- };
210
+ return positionedQuarterPoint(component, {
211
+ x: output ? 48 : -48,
212
+ y: distributedCoordinate(index, count, classicalGateHeight(component))
213
+ });
89
214
  }
90
215
  export function positionIcPin(component, side, index) {
91
216
  const pins = component.pins[side];
92
217
  if (!Number.isInteger(index) || index < 0 || index >= pins.length) {
93
218
  throw new RangeError(`IC pin index ${index} is outside ${component.id}.${side}.`);
94
219
  }
220
+ let local;
95
221
  if (side === 'left' || side === 'right') {
96
- return {
97
- x: component.x +
98
- (side === 'left' ? -component.bodyWidth / 2 - 16 : component.bodyWidth / 2 + 16),
99
- y: component.y + distributedCoordinate(index, pins.length, component.bodyHeight)
222
+ local = {
223
+ x: side === 'left' ? -component.bodyWidth / 2 - 16 : component.bodyWidth / 2 + 16,
224
+ y: distributedCoordinate(index, pins.length, component.bodyHeight)
100
225
  };
101
226
  }
102
- return {
103
- x: component.x + distributedCoordinate(index, pins.length, component.bodyWidth),
104
- y: component.y +
105
- (side === 'top' ? -component.bodyHeight / 2 - 16 : component.bodyHeight / 2 + 16)
106
- };
227
+ else {
228
+ local = {
229
+ x: distributedCoordinate(index, pins.length, component.bodyWidth),
230
+ y: side === 'top' ? -component.bodyHeight / 2 - 16 : component.bodyHeight / 2 + 16
231
+ };
232
+ }
233
+ return positionedQuarterPoint(component, local);
107
234
  }
108
235
  function findIcPinLocation(component, port) {
109
236
  const sides = ['left', 'right', 'top', 'bottom'];
@@ -150,73 +277,205 @@ export function resolvePortPoint(component, port) {
150
277
  return point;
151
278
  throw new Error(`Validated port ${component.id}.${port} is missing.`);
152
279
  }
153
- const left = { x: component.x - 42, y: component.y };
154
- const right = { x: component.x + 42, y: component.y };
280
+ if (component.kind === 'ic') {
281
+ const location = icPinLocation(component, port);
282
+ if (location !== undefined)
283
+ return location.point;
284
+ throw new Error(`Validated port ${component.id}.${port} is missing.`);
285
+ }
286
+ let local;
287
+ const left = { x: -42, y: 0 };
288
+ const right = { x: 42, y: 0 };
155
289
  switch (component.kind) {
156
290
  case 'resistor':
157
291
  case 'capacitor':
158
292
  case 'inductor':
159
293
  if (PASSIVE_INPUT_PORTS.has(port))
160
- return left;
294
+ local = left;
161
295
  if (PASSIVE_OUTPUT_PORTS.has(port))
162
- return right;
296
+ local = right;
163
297
  break;
164
298
  case 'diode':
165
299
  if (DIODE_ANODE_PORTS.has(port))
166
- return left;
300
+ local = left;
167
301
  if (DIODE_CATHODE_PORTS.has(port))
168
- return right;
302
+ local = right;
169
303
  break;
170
304
  case 'transistor':
171
305
  if (TRANSISTOR_CONTROL_PORTS.has(port))
172
- return left;
173
- if (TRANSISTOR_UPPER_PORTS.has(port)) {
174
- return { x: component.x + 42, y: component.y - 22 };
175
- }
176
- if (TRANSISTOR_LOWER_PORTS.has(port)) {
177
- return { x: component.x + 42, y: component.y + 22 };
178
- }
306
+ local = left;
307
+ if (TRANSISTOR_UPPER_PORTS.has(port))
308
+ local = { x: 42, y: -22 };
309
+ if (TRANSISTOR_LOWER_PORTS.has(port))
310
+ local = { x: 42, y: 22 };
179
311
  break;
180
312
  case 'port':
181
313
  if (port === 'in')
182
- return left;
314
+ local = left;
183
315
  if (port === 'out')
184
- return right;
316
+ local = right;
185
317
  break;
186
318
  case 'ground':
187
319
  if (port === 'in')
188
- return { x: component.x, y: component.y - 42 };
320
+ local = { x: 0, y: -42 };
189
321
  break;
190
322
  case 'hadamard':
191
323
  case 'qgate':
324
+ case 'xgate':
325
+ case 'ygate':
326
+ case 'zgate':
327
+ case 'sgate':
328
+ case 'sdg':
329
+ case 'tgate':
330
+ case 'tdg':
331
+ case 'sx':
332
+ case 'phase':
333
+ case 'rx':
334
+ case 'ry':
335
+ case 'rz':
336
+ case 'ugate':
192
337
  if (port === 'in')
193
- return { x: component.x - 48, y: component.y };
338
+ local = { x: -quantumGateDimensions(component).stubExtent, y: 0 };
194
339
  if (port === 'out')
195
- return { x: component.x + 48, y: component.y };
340
+ local = { x: quantumGateDimensions(component).stubExtent, y: 0 };
196
341
  break;
197
342
  case 'cnot':
198
343
  if (port === 'in')
199
- return left;
344
+ local = left;
200
345
  if (port === 'out')
201
- return right;
346
+ local = right;
202
347
  if (port === 'control')
203
- return { x: component.x, y: component.y - 16 };
348
+ local = { x: 0, y: -16 };
204
349
  if (port === 'target')
205
- return { x: component.x, y: component.y + 16 };
350
+ local = { x: 0, y: 16 };
206
351
  break;
207
- case 'ic': {
208
- const location = icPinLocation(component, port);
209
- if (location !== undefined)
210
- return location.point;
352
+ case 'junction':
353
+ case 'testpoint':
354
+ local = { x: 0, y: 0 };
355
+ break;
356
+ case 'connector':
357
+ local = port === 'out' ? { x: 32, y: 0 } : { x: -32, y: 0 };
358
+ break;
359
+ case 'source':
360
+ if (port === 'in' || port === 'negative')
361
+ local = left;
362
+ else if (port === 'out' || port === 'positive')
363
+ local = right;
364
+ else
365
+ local = { x: 0, y: port === 'control-positive' ? -34 : 34 };
366
+ break;
367
+ case 'power':
368
+ local = { x: -32, y: 0 };
369
+ break;
370
+ case 'switch':
371
+ if (port === 'in' || port === 'common')
372
+ local = left;
373
+ else if (port === 'normally-open')
374
+ local = { x: 42, y: -12 };
375
+ else if (port === 'normally-closed')
376
+ local = { x: 42, y: 12 };
377
+ else if (port === 'coil1')
378
+ local = { x: -12, y: 34 };
379
+ else if (port === 'coil2')
380
+ local = { x: 12, y: 34 };
381
+ else
382
+ local = right;
383
+ break;
384
+ case 'amplifier':
385
+ if (port === 'out')
386
+ local = { x: 48, y: 0 };
387
+ else if (port === 'v+')
388
+ local = { x: 0, y: -36 };
389
+ else if (port === 'v-')
390
+ local = { x: 0, y: 36 };
391
+ else
392
+ local = { x: -48, y: port === 'negative' ? 13 : -13 };
393
+ break;
394
+ case 'protection':
395
+ case 'resonator':
396
+ case 'meter':
397
+ case 'load':
398
+ local = port === 'out' ? right : left;
399
+ break;
400
+ case 'buffer':
401
+ case 'logic':
402
+ case 'clock':
403
+ case 'flipflop':
404
+ case 'mux':
405
+ case 'encoder':
406
+ case 'decoder':
407
+ case 'register':
408
+ case 'counter':
409
+ case 'adder':
410
+ case 'comparator':
411
+ case 'bus': {
412
+ const output = port === 'out' || port.startsWith('out') || ['q', 'nq', 'gt', 'eq', 'lt', 'tap'].includes(port) || (component.kind === 'bus' && component.variant === 'joiner' && port === 'bus');
413
+ const match = port.match(/\d+$/);
414
+ const count = output ? component.outputs : component.inputs;
415
+ const index = match === null ? 0 : Number(match[0]) - 1;
416
+ if (port === 'clock' || port === 'enable' || port === 'select')
417
+ local = { x: 0, y: component.bodyHeight / 2 + 16 };
418
+ else if (port === 'preset' || port === 'clear')
419
+ local = { x: 0, y: -component.bodyHeight / 2 - 16 };
420
+ else
421
+ local = {
422
+ x: (output ? 1 : -1) * (component.bodyWidth / 2 + 16),
423
+ y: distributedCoordinate(index, Math.max(1, count), component.bodyHeight)
424
+ };
425
+ break;
426
+ }
427
+ case 'measure':
428
+ if (port === 'classical')
429
+ local = { x: 0, y: 34 };
430
+ else
431
+ local = port === 'out' ? { x: 40, y: 0 } : { x: -40, y: 0 };
432
+ break;
433
+ case 'prepare':
434
+ local = { x: 40, y: 0 };
435
+ break;
436
+ case 'control':
437
+ local = port === 'control' ? { x: 0, y: 0 } : port === 'out' ? right : left;
438
+ break;
439
+ case 'reset':
440
+ case 'classical-bit':
441
+ case 'classical-register':
442
+ local = port === 'out' ? { x: 40, y: 0 } : { x: -40, y: 0 };
443
+ break;
444
+ case 'swap':
445
+ case 'cz':
446
+ case 'cphase':
447
+ case 'toffoli':
448
+ case 'controlled':
449
+ case 'barrier':
450
+ case 'delay': {
451
+ const indexed = port.match(/^(?:in|out|control|target)([1-9]\d*)$/);
452
+ let index = indexed === null ? 0 : Number(indexed[1]) - 1;
453
+ if (port.startsWith('target'))
454
+ index += component.controls;
455
+ const tracks = Math.max(component.wires, component.controls + component.targets);
456
+ if (port.startsWith('control') || port.startsWith('target')) {
457
+ local = { x: 0, y: distributedCoordinate(index, Math.max(1, tracks), Math.max(16, (tracks - 1) * 18)) };
458
+ }
459
+ else {
460
+ local = {
461
+ x: port.startsWith('out') ? 42 : -42,
462
+ y: distributedCoordinate(index, Math.max(1, tracks), Math.max(16, (tracks - 1) * 18))
463
+ };
464
+ }
211
465
  break;
212
466
  }
213
467
  }
468
+ if (local !== undefined)
469
+ return positionedQuarterPoint(component, local);
214
470
  throw new Error(`Validated port ${component.id}.${port} is missing.`);
215
471
  }
216
472
  export function resolvePortGeometry(component, port) {
217
473
  const point = resolvePortPoint(component, port);
218
474
  if (isClassicalGate(component)) {
219
- return { point, normal: port.startsWith('out') ? { x: 1, y: 0 } : { x: -1, y: 0 } };
475
+ return {
476
+ point,
477
+ normal: positionedQuarterNormal(component, port.startsWith('out') ? { x: 1, y: 0 } : { x: -1, y: 0 })
478
+ };
220
479
  }
221
480
  if (isUmlComponent(component)) {
222
481
  if (port === 'left' || port === 'in' || port.startsWith('left')) {
@@ -227,40 +486,103 @@ export function resolvePortGeometry(component, port) {
227
486
  }
228
487
  return { point, normal: port === 'top' ? { x: 0, y: -1 } : { x: 0, y: 1 } };
229
488
  }
489
+ const geometry = (normal) => ({
490
+ point,
491
+ normal: positionedQuarterNormal(component, normal)
492
+ });
230
493
  switch (component.kind) {
231
494
  case 'resistor':
232
495
  case 'capacitor':
233
496
  case 'inductor':
234
- return {
235
- point,
236
- normal: PASSIVE_OUTPUT_PORTS.has(port) ? { x: 1, y: 0 } : { x: -1, y: 0 }
237
- };
497
+ return geometry(PASSIVE_OUTPUT_PORTS.has(port) ? { x: 1, y: 0 } : { x: -1, y: 0 });
238
498
  case 'diode':
239
- return {
240
- point,
241
- normal: DIODE_CATHODE_PORTS.has(port) ? { x: 1, y: 0 } : { x: -1, y: 0 }
242
- };
499
+ return geometry(DIODE_CATHODE_PORTS.has(port) ? { x: 1, y: 0 } : { x: -1, y: 0 });
243
500
  case 'transistor':
244
- return {
245
- point,
246
- normal: TRANSISTOR_CONTROL_PORTS.has(port) ? { x: -1, y: 0 } : { x: 1, y: 0 }
247
- };
501
+ return geometry(TRANSISTOR_CONTROL_PORTS.has(port) ? { x: -1, y: 0 } : { x: 1, y: 0 });
248
502
  case 'port':
249
503
  case 'hadamard':
250
504
  case 'qgate':
251
- return { point, normal: port === 'out' ? { x: 1, y: 0 } : { x: -1, y: 0 } };
505
+ case 'xgate':
506
+ case 'ygate':
507
+ case 'zgate':
508
+ case 'sgate':
509
+ case 'sdg':
510
+ case 'tgate':
511
+ case 'tdg':
512
+ case 'sx':
513
+ case 'phase':
514
+ case 'rx':
515
+ case 'ry':
516
+ case 'rz':
517
+ case 'ugate':
518
+ case 'connector':
519
+ case 'protection':
520
+ case 'resonator':
521
+ case 'meter':
522
+ case 'load':
523
+ case 'reset':
524
+ case 'prepare':
525
+ case 'classical-bit':
526
+ case 'classical-register':
527
+ return geometry(port === 'out' ? { x: 1, y: 0 } : { x: -1, y: 0 });
252
528
  case 'ground':
253
- return { point, normal: { x: 0, y: -1 } };
529
+ return geometry({ x: 0, y: -1 });
254
530
  case 'cnot':
255
531
  if (port === 'control')
256
- return { point, normal: { x: 0, y: -1 } };
532
+ return geometry({ x: 0, y: -1 });
257
533
  if (port === 'target')
258
- return { point, normal: { x: 0, y: 1 } };
259
- return { point, normal: port === 'out' ? { x: 1, y: 0 } : { x: -1, y: 0 } };
534
+ return geometry({ x: 0, y: 1 });
535
+ return geometry(port === 'out' ? { x: 1, y: 0 } : { x: -1, y: 0 });
260
536
  case 'ic': {
261
537
  const location = icPinLocation(component, port);
262
- return { point, normal: sideNormal(location.side) };
538
+ return geometry(sideNormal(location.side));
263
539
  }
540
+ case 'junction':
541
+ case 'testpoint':
542
+ return geometry(port === 'out' ? { x: 1, y: 0 } : { x: -1, y: 0 });
543
+ case 'source':
544
+ if (port.startsWith('control-'))
545
+ return geometry({ x: 0, y: port.endsWith('positive') ? -1 : 1 });
546
+ return geometry(port === 'out' || port === 'positive' ? { x: 1, y: 0 } : { x: -1, y: 0 });
547
+ case 'power':
548
+ return geometry({ x: -1, y: 0 });
549
+ case 'switch':
550
+ if (port.startsWith('coil'))
551
+ return geometry({ x: 0, y: 1 });
552
+ return geometry(port === 'in' || port === 'common' ? { x: -1, y: 0 } : { x: 1, y: 0 });
553
+ case 'amplifier':
554
+ if (port === 'v+' || port === 'v-')
555
+ return geometry({ x: 0, y: port === 'v+' ? -1 : 1 });
556
+ return geometry(port === 'out' ? { x: 1, y: 0 } : { x: -1, y: 0 });
557
+ case 'buffer':
558
+ case 'logic':
559
+ case 'clock':
560
+ case 'flipflop':
561
+ case 'mux':
562
+ case 'encoder':
563
+ case 'decoder':
564
+ case 'register':
565
+ case 'counter':
566
+ case 'adder':
567
+ case 'comparator':
568
+ case 'bus':
569
+ if (['clock', 'enable', 'select'].includes(port))
570
+ return geometry({ x: 0, y: 1 });
571
+ if (['preset', 'clear'].includes(port))
572
+ return geometry({ x: 0, y: -1 });
573
+ return geometry(port === 'out' || port.startsWith('out') || ['q', 'nq', 'gt', 'eq', 'lt', 'tap'].includes(port) || (component.kind === 'bus' && component.variant === 'joiner' && port === 'bus') ? { x: 1, y: 0 } : { x: -1, y: 0 });
574
+ case 'measure':
575
+ return geometry(port === 'classical' ? { x: 0, y: 1 } : port === 'out' ? { x: 1, y: 0 } : { x: -1, y: 0 });
576
+ case 'control':
577
+ return geometry(port === 'control' ? { x: 0, y: -1 } : port === 'out' ? { x: 1, y: 0 } : { x: -1, y: 0 });
578
+ case 'swap':
579
+ case 'cz':
580
+ case 'cphase':
581
+ case 'toffoli':
582
+ case 'controlled':
583
+ case 'barrier':
584
+ case 'delay':
585
+ return geometry(port.startsWith('out') ? { x: 1, y: 0 } : port.startsWith('in') ? { x: -1, y: 0 } : { x: 0, y: -1 });
264
586
  }
265
587
  throw new Error(`Validated port ${port} is missing.`);
266
588
  }
@@ -274,6 +596,50 @@ export function enumerateComponentPorts(component) {
274
596
  ...Array.from({ length: component.outputs }, (_, index) => namedPort(component, `out${index + 1}`))
275
597
  ];
276
598
  }
599
+ if (isDigitalComponent(component)) {
600
+ let ports = [
601
+ ...Array.from({ length: component.inputs }, (_, index) => `in${index + 1}`),
602
+ ...Array.from({ length: component.outputs }, (_, index) => `out${index + 1}`)
603
+ ];
604
+ if (component.kind === 'logic' || component.kind === 'clock')
605
+ ports = ['out'];
606
+ else if (component.kind === 'flipflop') {
607
+ const inputs = component.variant === 'jk' ? ['j', 'k', 'clock'] : component.variant === 't' ? ['t', 'clock', 'clear'] : component.variant === 'sr-latch' ? ['s', 'r', 'enable'] : ['d', 'clock', 'clear'];
608
+ ports = [...inputs, 'q', 'nq', 'preset'];
609
+ }
610
+ else if (component.kind === 'register')
611
+ ports = ['in', 'out', 'clock', 'enable', 'clear'];
612
+ else if (component.kind === 'counter')
613
+ ports.push('clock', 'enable', 'clear');
614
+ else if (component.kind === 'mux')
615
+ ports.push('select', 'enable');
616
+ else if (component.kind === 'comparator')
617
+ ports = ['in1', 'in2', 'gt', 'eq', 'lt'];
618
+ else if (component.kind === 'bus') {
619
+ ports = component.variant === 'tap' ? ['bus', 'tap'] : component.variant === 'joiner' ? [...Array.from({ length: component.inputs }, (_, index) => `in${index + 1}`), 'bus'] : ['bus', ...Array.from({ length: component.outputs }, (_, index) => `out${index + 1}`)];
620
+ }
621
+ else if (component.kind === 'buffer' && component.variant?.startsWith('tristate') === true)
622
+ ports.push('enable');
623
+ return ports.map((port) => namedPort(component, port));
624
+ }
625
+ if (isQuantumSpecial(component)) {
626
+ if (component.kind === 'prepare')
627
+ return [namedPort(component, 'out')];
628
+ if (component.kind === 'measure')
629
+ return ['in', 'out', 'classical'].map((port) => namedPort(component, port));
630
+ if (component.kind === 'control')
631
+ return ['in', 'out', 'control'].map((port) => namedPort(component, port));
632
+ if (component.kind === 'reset' || component.kind === 'classical-bit' || component.kind === 'classical-register') {
633
+ return ['in', 'out'].map((port) => namedPort(component, port));
634
+ }
635
+ const tracks = Math.max(component.wires, component.controls + component.targets);
636
+ return [
637
+ ...Array.from({ length: tracks }, (_, index) => namedPort(component, `in${index + 1}`)),
638
+ ...Array.from({ length: tracks }, (_, index) => namedPort(component, `out${index + 1}`)),
639
+ ...Array.from({ length: component.controls }, (_, index) => namedPort(component, `control${index + 1}`)),
640
+ ...Array.from({ length: component.targets }, (_, index) => namedPort(component, `target${index + 1}`))
641
+ ];
642
+ }
277
643
  if (isUmlComponent(component)) {
278
644
  return ['left', 'right', 'top', 'bottom'].map((port) => namedPort(component, port));
279
645
  }
@@ -284,6 +650,24 @@ export function enumerateComponentPorts(component) {
284
650
  case 'port':
285
651
  case 'hadamard':
286
652
  case 'qgate':
653
+ case 'xgate':
654
+ case 'ygate':
655
+ case 'zgate':
656
+ case 'sgate':
657
+ case 'sdg':
658
+ case 'tgate':
659
+ case 'tdg':
660
+ case 'sx':
661
+ case 'phase':
662
+ case 'rx':
663
+ case 'ry':
664
+ case 'rz':
665
+ case 'ugate':
666
+ case 'connector':
667
+ case 'protection':
668
+ case 'resonator':
669
+ case 'meter':
670
+ case 'load':
287
671
  return [namedPort(component, 'in'), namedPort(component, 'out')];
288
672
  case 'diode':
289
673
  return [namedPort(component, 'anode'), namedPort(component, 'cathode')];
@@ -310,12 +694,34 @@ export function enumerateComponentPorts(component) {
310
694
  namedPort(component, 'control'),
311
695
  namedPort(component, 'target')
312
696
  ];
697
+ case 'junction':
698
+ case 'testpoint':
699
+ return [namedPort(component, 'node')];
700
+ case 'source': {
701
+ const ports = ['negative', 'positive'];
702
+ if (component.variant !== undefined && ['vcvs', 'vccs', 'ccvs', 'cccs'].includes(component.variant)) {
703
+ ports.push('control-positive', 'control-negative');
704
+ }
705
+ return ports.map((port) => namedPort(component, port));
706
+ }
707
+ case 'power':
708
+ return [namedPort(component, 'in')];
709
+ case 'switch': {
710
+ const ports = component.variant === 'spdt'
711
+ ? ['common', 'normally-open', 'normally-closed']
712
+ : component.variant === 'relay'
713
+ ? ['in', 'out', 'coil1', 'coil2']
714
+ : ['in', 'out'];
715
+ return ports.map((port) => namedPort(component, port));
716
+ }
717
+ case 'amplifier':
718
+ return ['positive', 'negative', 'out', 'v+', 'v-'].map((port) => namedPort(component, port));
313
719
  case 'ic': {
314
720
  const sides = ['left', 'right', 'top', 'bottom'];
315
721
  return sides.flatMap((side) => component.pins[side].map((id, index) => ({
316
722
  id,
317
723
  point: positionIcPin(component, side, index),
318
- normal: sideNormal(side)
724
+ normal: positionedQuarterNormal(component, sideNormal(side))
319
725
  })));
320
726
  }
321
727
  }
@@ -871,6 +1277,17 @@ function componentHalfExtents(component) {
871
1277
  else if (isUmlComponent(component)) {
872
1278
  return umlHalfExtents(component);
873
1279
  }
1280
+ else if (isDigitalComponent(component)) {
1281
+ halfWidth = component.bodyWidth / 2 + 16;
1282
+ halfHeight = component.bodyHeight / 2 + 16;
1283
+ }
1284
+ else if (isQuantumSpecial(component)) {
1285
+ const tracks = Math.max(component.wires, component.controls + component.targets);
1286
+ halfWidth = 42;
1287
+ halfHeight = Math.max(24, (tracks - 1) * 9 + 14);
1288
+ if (component.kind === 'measure')
1289
+ halfHeight = 34;
1290
+ }
874
1291
  else {
875
1292
  switch (component.kind) {
876
1293
  case 'resistor':
@@ -881,7 +1298,7 @@ function componentHalfExtents(component) {
881
1298
  break;
882
1299
  case 'diode':
883
1300
  halfWidth = 42;
884
- halfHeight = component.diodeType === 'led' ? 30 : 20;
1301
+ halfHeight = component.diodeType === 'led' || component.diodeType === 'photodiode' ? 30 : 20;
885
1302
  break;
886
1303
  case 'transistor':
887
1304
  halfWidth = 42;
@@ -897,9 +1314,24 @@ function componentHalfExtents(component) {
897
1314
  break;
898
1315
  case 'hadamard':
899
1316
  case 'qgate':
900
- halfWidth = 48;
901
- halfHeight = 30;
1317
+ case 'xgate':
1318
+ case 'ygate':
1319
+ case 'zgate':
1320
+ case 'sgate':
1321
+ case 'sdg':
1322
+ case 'tgate':
1323
+ case 'tdg':
1324
+ case 'sx':
1325
+ case 'phase':
1326
+ case 'rx':
1327
+ case 'ry':
1328
+ case 'rz':
1329
+ case 'ugate': {
1330
+ const dimensions = quantumGateDimensions(component);
1331
+ halfWidth = dimensions.stubExtent;
1332
+ halfHeight = dimensions.bodyHeight / 2;
902
1333
  break;
1334
+ }
903
1335
  case 'cnot':
904
1336
  halfWidth = 42;
905
1337
  halfHeight = 26;
@@ -908,9 +1340,39 @@ function componentHalfExtents(component) {
908
1340
  halfWidth = component.bodyWidth / 2 + 16;
909
1341
  halfHeight = component.bodyHeight / 2 + 16;
910
1342
  break;
1343
+ case 'source':
1344
+ halfWidth = 42;
1345
+ halfHeight = 34;
1346
+ break;
1347
+ case 'junction':
1348
+ halfWidth = halfHeight = 5;
1349
+ break;
1350
+ case 'testpoint':
1351
+ halfWidth = halfHeight = 12;
1352
+ break;
1353
+ case 'connector':
1354
+ case 'power':
1355
+ halfWidth = 32;
1356
+ halfHeight = 18;
1357
+ break;
1358
+ case 'switch':
1359
+ halfWidth = 42;
1360
+ halfHeight = component.variant === 'relay' ? 34 : 20;
1361
+ break;
1362
+ case 'amplifier':
1363
+ halfWidth = 48;
1364
+ halfHeight = 36;
1365
+ break;
1366
+ case 'protection':
1367
+ case 'resonator':
1368
+ case 'meter':
1369
+ case 'load':
1370
+ halfWidth = 42;
1371
+ halfHeight = 24;
1372
+ break;
911
1373
  }
912
1374
  }
913
- return { halfWidth, halfHeight };
1375
+ return rotateQuarterExtents({ halfWidth, halfHeight }, componentTurn(component));
914
1376
  }
915
1377
  export function componentTextAnchors(component) {
916
1378
  const { halfHeight } = componentHalfExtents(component);