@schemd/core 0.2.1 → 0.3.1

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