@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/dist/layout.js CHANGED
@@ -1,12 +1,68 @@
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;
65
+ const MIN_RENDERABLE_BRIDGE_RADIUS = 0.001;
10
66
  const DESIGNATOR_BASELINE_GAP = 10;
11
67
  const LABEL_BASELINE_GAP = 19;
12
68
  const TEXT_ASCENT_GUTTER = 12;
@@ -30,26 +86,96 @@ export function distributedCoordinate(index, count, span) {
30
86
  export function classicalGateHeight(component) {
31
87
  return Math.max(48, Math.max(component.inputs, component.outputs) * 16);
32
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
+ }
33
100
  function isClassicalGate(component) {
34
101
  return CLASSICAL_GATE_KINDS.includes(component.kind);
35
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
+ }
36
109
  function isUmlComponent(component) {
37
110
  return UML_COMPONENT_KINDS.includes(component.kind);
38
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
+ }
39
124
  function umlHalfExtents(component) {
40
125
  switch (component.kind) {
41
126
  case 'class':
127
+ case 'interface':
128
+ case 'enumeration':
129
+ case 'datatype':
130
+ case 'object':
42
131
  case 'state':
43
132
  case 'usecase':
44
133
  case 'lifeline':
45
134
  case 'note':
46
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':
47
149
  return { halfWidth: component.bodyWidth / 2, halfHeight: component.bodyHeight / 2 };
48
150
  case 'actor':
49
151
  return { halfWidth: 24, halfHeight: 50 };
50
152
  case 'initial':
51
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':
52
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 };
53
179
  }
54
180
  }
55
181
  function umlPortPoint(component, port) {
@@ -81,28 +207,30 @@ function indexedPortPoint(component, port) {
81
207
  const count = output ? component.outputs : component.inputs;
82
208
  const suffix = port.match(/\d+$/)?.[0];
83
209
  const index = suffix === undefined ? 0 : Number(suffix) - 1;
84
- return {
85
- x: component.x + (output ? 48 : -48),
86
- y: component.y + distributedCoordinate(index, count, classicalGateHeight(component))
87
- };
210
+ return positionedQuarterPoint(component, {
211
+ x: output ? 48 : -48,
212
+ y: distributedCoordinate(index, count, classicalGateHeight(component))
213
+ });
88
214
  }
89
215
  export function positionIcPin(component, side, index) {
90
216
  const pins = component.pins[side];
91
217
  if (!Number.isInteger(index) || index < 0 || index >= pins.length) {
92
218
  throw new RangeError(`IC pin index ${index} is outside ${component.id}.${side}.`);
93
219
  }
220
+ let local;
94
221
  if (side === 'left' || side === 'right') {
95
- return {
96
- x: component.x +
97
- (side === 'left' ? -component.bodyWidth / 2 - 16 : component.bodyWidth / 2 + 16),
98
- 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)
99
225
  };
100
226
  }
101
- return {
102
- x: component.x + distributedCoordinate(index, pins.length, component.bodyWidth),
103
- y: component.y +
104
- (side === 'top' ? -component.bodyHeight / 2 - 16 : component.bodyHeight / 2 + 16)
105
- };
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);
106
234
  }
107
235
  function findIcPinLocation(component, port) {
108
236
  const sides = ['left', 'right', 'top', 'bottom'];
@@ -149,73 +277,205 @@ export function resolvePortPoint(component, port) {
149
277
  return point;
150
278
  throw new Error(`Validated port ${component.id}.${port} is missing.`);
151
279
  }
152
- const left = { x: component.x - 42, y: component.y };
153
- 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 };
154
289
  switch (component.kind) {
155
290
  case 'resistor':
156
291
  case 'capacitor':
157
292
  case 'inductor':
158
293
  if (PASSIVE_INPUT_PORTS.has(port))
159
- return left;
294
+ local = left;
160
295
  if (PASSIVE_OUTPUT_PORTS.has(port))
161
- return right;
296
+ local = right;
162
297
  break;
163
298
  case 'diode':
164
299
  if (DIODE_ANODE_PORTS.has(port))
165
- return left;
300
+ local = left;
166
301
  if (DIODE_CATHODE_PORTS.has(port))
167
- return right;
302
+ local = right;
168
303
  break;
169
304
  case 'transistor':
170
305
  if (TRANSISTOR_CONTROL_PORTS.has(port))
171
- return left;
172
- if (TRANSISTOR_UPPER_PORTS.has(port)) {
173
- return { x: component.x + 42, y: component.y - 22 };
174
- }
175
- if (TRANSISTOR_LOWER_PORTS.has(port)) {
176
- return { x: component.x + 42, y: component.y + 22 };
177
- }
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 };
178
311
  break;
179
312
  case 'port':
180
313
  if (port === 'in')
181
- return left;
314
+ local = left;
182
315
  if (port === 'out')
183
- return right;
316
+ local = right;
184
317
  break;
185
318
  case 'ground':
186
319
  if (port === 'in')
187
- return { x: component.x, y: component.y - 42 };
320
+ local = { x: 0, y: -42 };
188
321
  break;
189
322
  case 'hadamard':
190
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':
191
337
  if (port === 'in')
192
- return { x: component.x - 48, y: component.y };
338
+ local = { x: -quantumGateDimensions(component).stubExtent, y: 0 };
193
339
  if (port === 'out')
194
- return { x: component.x + 48, y: component.y };
340
+ local = { x: quantumGateDimensions(component).stubExtent, y: 0 };
195
341
  break;
196
342
  case 'cnot':
197
343
  if (port === 'in')
198
- return left;
344
+ local = left;
199
345
  if (port === 'out')
200
- return right;
346
+ local = right;
201
347
  if (port === 'control')
202
- return { x: component.x, y: component.y - 16 };
348
+ local = { x: 0, y: -16 };
203
349
  if (port === 'target')
204
- return { x: component.x, y: component.y + 16 };
350
+ local = { x: 0, y: 16 };
205
351
  break;
206
- case 'ic': {
207
- const location = icPinLocation(component, port);
208
- if (location !== undefined)
209
- 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
+ }
210
465
  break;
211
466
  }
212
467
  }
468
+ if (local !== undefined)
469
+ return positionedQuarterPoint(component, local);
213
470
  throw new Error(`Validated port ${component.id}.${port} is missing.`);
214
471
  }
215
472
  export function resolvePortGeometry(component, port) {
216
473
  const point = resolvePortPoint(component, port);
217
474
  if (isClassicalGate(component)) {
218
- 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
+ };
219
479
  }
220
480
  if (isUmlComponent(component)) {
221
481
  if (port === 'left' || port === 'in' || port.startsWith('left')) {
@@ -226,40 +486,103 @@ export function resolvePortGeometry(component, port) {
226
486
  }
227
487
  return { point, normal: port === 'top' ? { x: 0, y: -1 } : { x: 0, y: 1 } };
228
488
  }
489
+ const geometry = (normal) => ({
490
+ point,
491
+ normal: positionedQuarterNormal(component, normal)
492
+ });
229
493
  switch (component.kind) {
230
494
  case 'resistor':
231
495
  case 'capacitor':
232
496
  case 'inductor':
233
- return {
234
- point,
235
- normal: PASSIVE_OUTPUT_PORTS.has(port) ? { x: 1, y: 0 } : { x: -1, y: 0 }
236
- };
497
+ return geometry(PASSIVE_OUTPUT_PORTS.has(port) ? { x: 1, y: 0 } : { x: -1, y: 0 });
237
498
  case 'diode':
238
- return {
239
- point,
240
- normal: DIODE_CATHODE_PORTS.has(port) ? { x: 1, y: 0 } : { x: -1, y: 0 }
241
- };
499
+ return geometry(DIODE_CATHODE_PORTS.has(port) ? { x: 1, y: 0 } : { x: -1, y: 0 });
242
500
  case 'transistor':
243
- return {
244
- point,
245
- normal: TRANSISTOR_CONTROL_PORTS.has(port) ? { x: -1, y: 0 } : { x: 1, y: 0 }
246
- };
501
+ return geometry(TRANSISTOR_CONTROL_PORTS.has(port) ? { x: -1, y: 0 } : { x: 1, y: 0 });
247
502
  case 'port':
248
503
  case 'hadamard':
249
504
  case 'qgate':
250
- 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 });
251
528
  case 'ground':
252
- return { point, normal: { x: 0, y: -1 } };
529
+ return geometry({ x: 0, y: -1 });
253
530
  case 'cnot':
254
531
  if (port === 'control')
255
- return { point, normal: { x: 0, y: -1 } };
532
+ return geometry({ x: 0, y: -1 });
256
533
  if (port === 'target')
257
- return { point, normal: { x: 0, y: 1 } };
258
- 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 });
259
536
  case 'ic': {
260
537
  const location = icPinLocation(component, port);
261
- return { point, normal: sideNormal(location.side) };
538
+ return geometry(sideNormal(location.side));
262
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 });
263
586
  }
264
587
  throw new Error(`Validated port ${port} is missing.`);
265
588
  }
@@ -273,6 +596,50 @@ export function enumerateComponentPorts(component) {
273
596
  ...Array.from({ length: component.outputs }, (_, index) => namedPort(component, `out${index + 1}`))
274
597
  ];
275
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
+ }
276
643
  if (isUmlComponent(component)) {
277
644
  return ['left', 'right', 'top', 'bottom'].map((port) => namedPort(component, port));
278
645
  }
@@ -283,6 +650,24 @@ export function enumerateComponentPorts(component) {
283
650
  case 'port':
284
651
  case 'hadamard':
285
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':
286
671
  return [namedPort(component, 'in'), namedPort(component, 'out')];
287
672
  case 'diode':
288
673
  return [namedPort(component, 'anode'), namedPort(component, 'cathode')];
@@ -309,12 +694,34 @@ export function enumerateComponentPorts(component) {
309
694
  namedPort(component, 'control'),
310
695
  namedPort(component, 'target')
311
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));
312
719
  case 'ic': {
313
720
  const sides = ['left', 'right', 'top', 'bottom'];
314
721
  return sides.flatMap((side) => component.pins[side].map((id, index) => ({
315
722
  id,
316
723
  point: positionIcPin(component, side, index),
317
- normal: sideNormal(side)
724
+ normal: positionedQuarterNormal(component, sideNormal(side))
318
725
  })));
319
726
  }
320
727
  }
@@ -609,6 +1016,19 @@ function routeBetweenEscapes(start, end, obstacles, bounds, line) {
609
1016
  }
610
1017
  return best ?? searchOrthogonalRoute(start, end, obstacles, bounds, line);
611
1018
  }
1019
+ function endpointsFaceEachOther(from, to) {
1020
+ const dx = to.point.x - from.point.x;
1021
+ const dy = to.point.y - from.point.y;
1022
+ if (dy === 0 && dx !== 0) {
1023
+ const direction = Math.sign(dx);
1024
+ return from.normal.x === direction && to.normal.x === -direction;
1025
+ }
1026
+ if (dx === 0 && dy !== 0) {
1027
+ const direction = Math.sign(dy);
1028
+ return from.normal.y === direction && to.normal.y === -direction;
1029
+ }
1030
+ return false;
1031
+ }
612
1032
  function routeConnectionInternal(connection, components, bounds, cachedObstacles) {
613
1033
  const from = endpointGeometry(connection.from, components);
614
1034
  const to = endpointGeometry(connection.to, components);
@@ -635,6 +1055,13 @@ function routeConnectionInternal(connection, components, bounds, cachedObstacles
635
1055
  expanded: componentObstacleRectangle(component),
636
1056
  body: componentObstacleRectangle(component, 0)
637
1057
  }));
1058
+ if (from.component.id !== to.component.id &&
1059
+ endpointsFaceEachOther(from, to) &&
1060
+ !obstacleCache.some((entry) => entry.id !== from.component.id &&
1061
+ entry.id !== to.component.id &&
1062
+ segmentIntersectsRectangle(start, end, entry.expanded))) {
1063
+ return { curve: 'ortho', d: orthogonalPath([start, end]), points: [start, end] };
1064
+ }
638
1065
  const fromObstacle = componentObstacleRectangle(from.component);
639
1066
  const toObstacle = componentObstacleRectangle(to.component);
640
1067
  const escape = (geometry, obstacle) => ({
@@ -651,13 +1078,8 @@ function routeConnectionInternal(connection, components, bounds, cachedObstacles
651
1078
  });
652
1079
  const startEscape = escape(from, fromObstacle);
653
1080
  const endEscape = escape(to, toObstacle);
654
- const obstacleEntries = obstacleCache.map((entry) => ({
655
- id: entry.id,
656
- rectangle: entry.id === from.component.id || entry.id === to.component.id
657
- ? entry.body
658
- : entry.expanded
659
- }));
660
- const obstacleRectangles = obstacleEntries.map((entry) => entry.rectangle);
1081
+ const obstacleRectangle = (entry) => entry.id === from.component.id || entry.id === to.component.id ? entry.body : entry.expanded;
1082
+ const obstacleRectangles = obstacleCache.map(obstacleRectangle);
661
1083
  const middle = routeBetweenEscapes(startEscape, endEscape, obstacleRectangles, bounds, connection.line);
662
1084
  if (middle === undefined) {
663
1085
  throw new SchematicSyntaxError('No collision-free orthogonal route exists.', connection.line);
@@ -666,10 +1088,10 @@ function routeConnectionInternal(connection, components, bounds, cachedObstacles
666
1088
  for (let index = 1; index < points.length; index += 1) {
667
1089
  const allowFrom = index === 1;
668
1090
  const allowTo = index === points.length - 1;
669
- for (const obstacle of obstacleEntries) {
1091
+ for (const obstacle of obstacleCache) {
670
1092
  if (!(allowFrom && obstacle.id === from.component.id) &&
671
1093
  !(allowTo && obstacle.id === to.component.id) &&
672
- segmentIntersectsRectangle(points[index - 1], points[index], obstacle.rectangle)) {
1094
+ segmentIntersectsRectangle(points[index - 1], points[index], obstacleRectangle(obstacle))) {
673
1095
  throw new SchematicSyntaxError(`Orthogonal route intersects ${obstacle.id} after routing.`, connection.line);
674
1096
  }
675
1097
  }
@@ -743,6 +1165,8 @@ function bridgedOrthogonalPath(route, crossings) {
743
1165
  let cursorX = start.x;
744
1166
  for (const [crossingIndex, crossing] of segmentCrossings.entries()) {
745
1167
  const radius = radii[crossingIndex];
1168
+ if (radius < MIN_RENDERABLE_BRIDGE_RADIUS)
1169
+ continue;
746
1170
  const before = crossing.x - direction * radius;
747
1171
  const after = crossing.x + direction * radius;
748
1172
  if (before !== cursorX)
@@ -758,6 +1182,8 @@ function bridgedOrthogonalPath(route, crossings) {
758
1182
  let cursorY = start.y;
759
1183
  for (const [crossingIndex, crossing] of segmentCrossings.entries()) {
760
1184
  const radius = radii[crossingIndex];
1185
+ if (radius < MIN_RENDERABLE_BRIDGE_RADIUS)
1186
+ continue;
761
1187
  const before = crossing.y - direction * radius;
762
1188
  const after = crossing.y + direction * radius;
763
1189
  if (before !== cursorY)
@@ -851,6 +1277,17 @@ function componentHalfExtents(component) {
851
1277
  else if (isUmlComponent(component)) {
852
1278
  return umlHalfExtents(component);
853
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
+ }
854
1291
  else {
855
1292
  switch (component.kind) {
856
1293
  case 'resistor':
@@ -861,7 +1298,7 @@ function componentHalfExtents(component) {
861
1298
  break;
862
1299
  case 'diode':
863
1300
  halfWidth = 42;
864
- halfHeight = component.diodeType === 'led' ? 30 : 20;
1301
+ halfHeight = component.diodeType === 'led' || component.diodeType === 'photodiode' ? 30 : 20;
865
1302
  break;
866
1303
  case 'transistor':
867
1304
  halfWidth = 42;
@@ -877,9 +1314,24 @@ function componentHalfExtents(component) {
877
1314
  break;
878
1315
  case 'hadamard':
879
1316
  case 'qgate':
880
- halfWidth = 48;
881
- 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;
882
1333
  break;
1334
+ }
883
1335
  case 'cnot':
884
1336
  halfWidth = 42;
885
1337
  halfHeight = 26;
@@ -888,9 +1340,39 @@ function componentHalfExtents(component) {
888
1340
  halfWidth = component.bodyWidth / 2 + 16;
889
1341
  halfHeight = component.bodyHeight / 2 + 16;
890
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;
891
1373
  }
892
1374
  }
893
- return { halfWidth, halfHeight };
1375
+ return rotateQuarterExtents({ halfWidth, halfHeight }, componentTurn(component));
894
1376
  }
895
1377
  export function componentTextAnchors(component) {
896
1378
  const { halfHeight } = componentHalfExtents(component);
@@ -961,4 +1443,5 @@ export function validateDocumentGeometry(document, fence, routedConnections) {
961
1443
  throw new SchematicSyntaxError('Connection trace exceeds the declared schematic bounds.', connection.line);
962
1444
  }
963
1445
  }
1446
+ return routes;
964
1447
  }