@schemd/core 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -82
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/layout.d.ts +14 -4
- package/dist/layout.js +510 -138
- package/dist/limits.d.ts +3 -0
- package/dist/limits.js +2 -0
- package/dist/math-label.d.ts +20 -3
- package/dist/math-label.js +149 -56
- package/dist/parser.js +248 -27
- package/dist/renderer.js +147 -27
- package/dist/types.d.ts +58 -3
- package/dist/types.js +33 -1
- package/package.json +33 -3
package/dist/layout.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { CLASSICAL_GATE_KINDS, SchematicSyntaxError } from './types.js';
|
|
2
|
-
import {
|
|
1
|
+
import { CLASSICAL_GATE_KINDS, UML_COMPONENT_KINDS, SchematicSyntaxError } from './types.js';
|
|
2
|
+
import { mathLabelTextWidth } from './math-label.js';
|
|
3
|
+
import { MAX_SCHEMATIC_WIRE_CROSSINGS } from './limits.js';
|
|
3
4
|
export const PORT_HOTSPOT_RADIUS = 4;
|
|
4
5
|
export const SCHEMATIC_OBSTACLE_CLEARANCE = 12;
|
|
5
6
|
export const SCHEMATIC_BRIDGE_RADIUS = 5;
|
|
6
|
-
const
|
|
7
|
+
const MAX_ROUTER_STATES = 40_000;
|
|
8
|
+
const ROUTER_BEND_PENALTY = 0.25;
|
|
7
9
|
const CROSSING_BUCKET_SIZE = 64;
|
|
8
10
|
const DESIGNATOR_BASELINE_GAP = 10;
|
|
9
11
|
const LABEL_BASELINE_GAP = 19;
|
|
@@ -31,6 +33,49 @@ export function classicalGateHeight(component) {
|
|
|
31
33
|
function isClassicalGate(component) {
|
|
32
34
|
return CLASSICAL_GATE_KINDS.includes(component.kind);
|
|
33
35
|
}
|
|
36
|
+
function isUmlComponent(component) {
|
|
37
|
+
return UML_COMPONENT_KINDS.includes(component.kind);
|
|
38
|
+
}
|
|
39
|
+
function umlHalfExtents(component) {
|
|
40
|
+
switch (component.kind) {
|
|
41
|
+
case 'class':
|
|
42
|
+
case 'state':
|
|
43
|
+
case 'usecase':
|
|
44
|
+
case 'lifeline':
|
|
45
|
+
case 'note':
|
|
46
|
+
case 'package':
|
|
47
|
+
return { halfWidth: component.bodyWidth / 2, halfHeight: component.bodyHeight / 2 };
|
|
48
|
+
case 'actor':
|
|
49
|
+
return { halfWidth: 24, halfHeight: 50 };
|
|
50
|
+
case 'initial':
|
|
51
|
+
case 'final':
|
|
52
|
+
return { halfWidth: 12, halfHeight: 12 };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function umlPortPoint(component, port) {
|
|
56
|
+
const { halfWidth, halfHeight } = umlHalfExtents(component);
|
|
57
|
+
if (port === 'left' || port === 'in')
|
|
58
|
+
return { x: component.x - halfWidth, y: component.y };
|
|
59
|
+
if (port === 'right' || port === 'out')
|
|
60
|
+
return { x: component.x + halfWidth, y: component.y };
|
|
61
|
+
if (port === 'top')
|
|
62
|
+
return { x: component.x, y: component.y - halfHeight };
|
|
63
|
+
if (port === 'bottom')
|
|
64
|
+
return { x: component.x, y: component.y + halfHeight };
|
|
65
|
+
if (component.kind === 'lifeline') {
|
|
66
|
+
const match = port.match(/^(left|right)(\d+)$/);
|
|
67
|
+
if (match) {
|
|
68
|
+
const offset = Number(match[2]);
|
|
69
|
+
if (offset >= 0 && offset <= component.bodyHeight) {
|
|
70
|
+
return {
|
|
71
|
+
x: component.x + (match[1] === 'left' ? -halfWidth : halfWidth),
|
|
72
|
+
y: component.y - halfHeight + offset
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
34
79
|
function indexedPortPoint(component, port) {
|
|
35
80
|
const output = port.startsWith('out');
|
|
36
81
|
const count = output ? component.outputs : component.inputs;
|
|
@@ -59,32 +104,51 @@ export function positionIcPin(component, side, index) {
|
|
|
59
104
|
(side === 'top' ? -component.bodyHeight / 2 - 16 : component.bodyHeight / 2 + 16)
|
|
60
105
|
};
|
|
61
106
|
}
|
|
62
|
-
function
|
|
107
|
+
function findIcPinLocation(component, port) {
|
|
63
108
|
const sides = ['left', 'right', 'top', 'bottom'];
|
|
64
109
|
for (const side of sides) {
|
|
65
110
|
const index = component.pins[side].indexOf(port);
|
|
66
111
|
if (index >= 0)
|
|
67
|
-
return positionIcPin(component, side, index);
|
|
112
|
+
return { side, index, point: positionIcPin(component, side, index) };
|
|
68
113
|
}
|
|
69
114
|
return undefined;
|
|
70
115
|
}
|
|
71
|
-
function
|
|
116
|
+
function icPinLocation(component, port) {
|
|
72
117
|
if (port === 'in' || port === 'out') {
|
|
73
|
-
const canonical =
|
|
118
|
+
const canonical = findIcPinLocation(component, `${port}1`);
|
|
74
119
|
if (canonical !== undefined)
|
|
75
120
|
return canonical;
|
|
76
121
|
const aliasSides = port === 'in' ? ['left', 'top'] : ['right', 'bottom'];
|
|
77
122
|
for (const side of aliasSides) {
|
|
78
|
-
if (component.pins[side].length > 0)
|
|
79
|
-
return positionIcPin(component, side, 0);
|
|
123
|
+
if (component.pins[side].length > 0) {
|
|
124
|
+
return { side, point: positionIcPin(component, side, 0) };
|
|
125
|
+
}
|
|
80
126
|
}
|
|
81
127
|
return undefined;
|
|
82
128
|
}
|
|
83
|
-
return
|
|
129
|
+
return findIcPinLocation(component, port);
|
|
130
|
+
}
|
|
131
|
+
function sideNormal(side) {
|
|
132
|
+
switch (side) {
|
|
133
|
+
case 'left':
|
|
134
|
+
return { x: -1, y: 0 };
|
|
135
|
+
case 'right':
|
|
136
|
+
return { x: 1, y: 0 };
|
|
137
|
+
case 'top':
|
|
138
|
+
return { x: 0, y: -1 };
|
|
139
|
+
case 'bottom':
|
|
140
|
+
return { x: 0, y: 1 };
|
|
141
|
+
}
|
|
84
142
|
}
|
|
85
143
|
export function resolvePortPoint(component, port) {
|
|
86
144
|
if (isClassicalGate(component))
|
|
87
145
|
return indexedPortPoint(component, port);
|
|
146
|
+
if (isUmlComponent(component)) {
|
|
147
|
+
const point = umlPortPoint(component, port);
|
|
148
|
+
if (point !== undefined)
|
|
149
|
+
return point;
|
|
150
|
+
throw new Error(`Validated port ${component.id}.${port} is missing.`);
|
|
151
|
+
}
|
|
88
152
|
const left = { x: component.x - 42, y: component.y };
|
|
89
153
|
const right = { x: component.x + 42, y: component.y };
|
|
90
154
|
switch (component.kind) {
|
|
@@ -140,16 +204,67 @@ export function resolvePortPoint(component, port) {
|
|
|
140
204
|
return { x: component.x, y: component.y + 16 };
|
|
141
205
|
break;
|
|
142
206
|
case 'ic': {
|
|
143
|
-
const
|
|
144
|
-
if (
|
|
145
|
-
return point;
|
|
207
|
+
const location = icPinLocation(component, port);
|
|
208
|
+
if (location !== undefined)
|
|
209
|
+
return location.point;
|
|
146
210
|
break;
|
|
147
211
|
}
|
|
148
212
|
}
|
|
149
213
|
throw new Error(`Validated port ${component.id}.${port} is missing.`);
|
|
150
214
|
}
|
|
215
|
+
export function resolvePortGeometry(component, port) {
|
|
216
|
+
const point = resolvePortPoint(component, port);
|
|
217
|
+
if (isClassicalGate(component)) {
|
|
218
|
+
return { point, normal: port.startsWith('out') ? { x: 1, y: 0 } : { x: -1, y: 0 } };
|
|
219
|
+
}
|
|
220
|
+
if (isUmlComponent(component)) {
|
|
221
|
+
if (port === 'left' || port === 'in' || port.startsWith('left')) {
|
|
222
|
+
return { point, normal: { x: -1, y: 0 } };
|
|
223
|
+
}
|
|
224
|
+
if (port === 'right' || port === 'out' || port.startsWith('right')) {
|
|
225
|
+
return { point, normal: { x: 1, y: 0 } };
|
|
226
|
+
}
|
|
227
|
+
return { point, normal: port === 'top' ? { x: 0, y: -1 } : { x: 0, y: 1 } };
|
|
228
|
+
}
|
|
229
|
+
switch (component.kind) {
|
|
230
|
+
case 'resistor':
|
|
231
|
+
case 'capacitor':
|
|
232
|
+
case 'inductor':
|
|
233
|
+
return {
|
|
234
|
+
point,
|
|
235
|
+
normal: PASSIVE_OUTPUT_PORTS.has(port) ? { x: 1, y: 0 } : { x: -1, y: 0 }
|
|
236
|
+
};
|
|
237
|
+
case 'diode':
|
|
238
|
+
return {
|
|
239
|
+
point,
|
|
240
|
+
normal: DIODE_CATHODE_PORTS.has(port) ? { x: 1, y: 0 } : { x: -1, y: 0 }
|
|
241
|
+
};
|
|
242
|
+
case 'transistor':
|
|
243
|
+
return {
|
|
244
|
+
point,
|
|
245
|
+
normal: TRANSISTOR_CONTROL_PORTS.has(port) ? { x: -1, y: 0 } : { x: 1, y: 0 }
|
|
246
|
+
};
|
|
247
|
+
case 'port':
|
|
248
|
+
case 'hadamard':
|
|
249
|
+
case 'qgate':
|
|
250
|
+
return { point, normal: port === 'out' ? { x: 1, y: 0 } : { x: -1, y: 0 } };
|
|
251
|
+
case 'ground':
|
|
252
|
+
return { point, normal: { x: 0, y: -1 } };
|
|
253
|
+
case 'cnot':
|
|
254
|
+
if (port === 'control')
|
|
255
|
+
return { point, normal: { x: 0, y: -1 } };
|
|
256
|
+
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 } };
|
|
259
|
+
case 'ic': {
|
|
260
|
+
const location = icPinLocation(component, port);
|
|
261
|
+
return { point, normal: sideNormal(location.side) };
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
throw new Error(`Validated port ${port} is missing.`);
|
|
265
|
+
}
|
|
151
266
|
function namedPort(component, id) {
|
|
152
|
-
return { id,
|
|
267
|
+
return { id, ...resolvePortGeometry(component, id) };
|
|
153
268
|
}
|
|
154
269
|
export function enumerateComponentPorts(component) {
|
|
155
270
|
if (isClassicalGate(component)) {
|
|
@@ -158,6 +273,9 @@ export function enumerateComponentPorts(component) {
|
|
|
158
273
|
...Array.from({ length: component.outputs }, (_, index) => namedPort(component, `out${index + 1}`))
|
|
159
274
|
];
|
|
160
275
|
}
|
|
276
|
+
if (isUmlComponent(component)) {
|
|
277
|
+
return ['left', 'right', 'top', 'bottom'].map((port) => namedPort(component, port));
|
|
278
|
+
}
|
|
161
279
|
switch (component.kind) {
|
|
162
280
|
case 'resistor':
|
|
163
281
|
case 'capacitor':
|
|
@@ -195,17 +313,18 @@ export function enumerateComponentPorts(component) {
|
|
|
195
313
|
const sides = ['left', 'right', 'top', 'bottom'];
|
|
196
314
|
return sides.flatMap((side) => component.pins[side].map((id, index) => ({
|
|
197
315
|
id,
|
|
198
|
-
point: positionIcPin(component, side, index)
|
|
316
|
+
point: positionIcPin(component, side, index),
|
|
317
|
+
normal: sideNormal(side)
|
|
199
318
|
})));
|
|
200
319
|
}
|
|
201
320
|
}
|
|
202
321
|
}
|
|
203
|
-
function
|
|
322
|
+
function endpointGeometry(endpoint, components) {
|
|
204
323
|
const component = components.get(endpoint.componentId);
|
|
205
324
|
if (component === undefined) {
|
|
206
325
|
throw new Error(`Validated component ${endpoint.componentId} is missing.`);
|
|
207
326
|
}
|
|
208
|
-
return
|
|
327
|
+
return { component, ...resolvePortGeometry(component, endpoint.port) };
|
|
209
328
|
}
|
|
210
329
|
function samePoint(left, right) {
|
|
211
330
|
return Math.abs(left.x - right.x) < 0.0005 && Math.abs(left.y - right.y) < 0.0005;
|
|
@@ -271,15 +390,14 @@ function segmentIntersectsRectangle(start, end, rectangle) {
|
|
|
271
390
|
Math.max(start.y, end.y) > rectangle.minY &&
|
|
272
391
|
Math.min(start.y, end.y) < rectangle.maxY);
|
|
273
392
|
}
|
|
274
|
-
function
|
|
275
|
-
let collisions = 0;
|
|
393
|
+
function routeIntersectsObstacles(points, obstacles) {
|
|
276
394
|
for (let index = 1; index < points.length; index += 1) {
|
|
277
395
|
for (const obstacle of obstacles) {
|
|
278
396
|
if (segmentIntersectsRectangle(points[index - 1], points[index], obstacle))
|
|
279
|
-
|
|
397
|
+
return true;
|
|
280
398
|
}
|
|
281
399
|
}
|
|
282
|
-
return
|
|
400
|
+
return false;
|
|
283
401
|
}
|
|
284
402
|
function candidateLength(points) {
|
|
285
403
|
let length = 0;
|
|
@@ -290,61 +408,212 @@ function candidateLength(points) {
|
|
|
290
408
|
}
|
|
291
409
|
return length;
|
|
292
410
|
}
|
|
293
|
-
function
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
411
|
+
function candidateBends(points) {
|
|
412
|
+
return Math.max(0, points.length - 2);
|
|
413
|
+
}
|
|
414
|
+
class RouteHeap {
|
|
415
|
+
#items = [];
|
|
416
|
+
get size() {
|
|
417
|
+
return this.#items.length;
|
|
418
|
+
}
|
|
419
|
+
push(item) {
|
|
420
|
+
this.#items.push(item);
|
|
421
|
+
let index = this.#items.length - 1;
|
|
422
|
+
while (index > 0) {
|
|
423
|
+
const parent = (index - 1) >> 1;
|
|
424
|
+
if (!RouteHeap.before(item, this.#items[parent]))
|
|
425
|
+
break;
|
|
426
|
+
this.#items[index] = this.#items[parent];
|
|
427
|
+
index = parent;
|
|
428
|
+
}
|
|
429
|
+
this.#items[index] = item;
|
|
430
|
+
}
|
|
431
|
+
pop() {
|
|
432
|
+
const first = this.#items[0];
|
|
433
|
+
const last = this.#items.pop();
|
|
434
|
+
if (first === undefined || last === undefined || this.#items.length === 0)
|
|
435
|
+
return first;
|
|
436
|
+
let index = 0;
|
|
437
|
+
while (true) {
|
|
438
|
+
const left = index * 2 + 1;
|
|
439
|
+
if (left >= this.#items.length)
|
|
440
|
+
break;
|
|
441
|
+
const right = left + 1;
|
|
442
|
+
const child = right < this.#items.length && RouteHeap.before(this.#items[right], this.#items[left])
|
|
443
|
+
? right
|
|
444
|
+
: left;
|
|
445
|
+
if (!RouteHeap.before(this.#items[child], last))
|
|
446
|
+
break;
|
|
447
|
+
this.#items[index] = this.#items[child];
|
|
448
|
+
index = child;
|
|
449
|
+
}
|
|
450
|
+
this.#items[index] = last;
|
|
451
|
+
return first;
|
|
452
|
+
}
|
|
453
|
+
static before(left, right) {
|
|
454
|
+
return left.f !== right.f
|
|
455
|
+
? left.f < right.f
|
|
456
|
+
: left.g !== right.g
|
|
457
|
+
? left.g < right.g
|
|
458
|
+
: left.state < right.state;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
function uniqueSorted(values) {
|
|
462
|
+
return [...new Set(values)].sort((left, right) => left - right);
|
|
463
|
+
}
|
|
464
|
+
function searchOrthogonalRoute(start, end, obstacles, bounds, line) {
|
|
465
|
+
const withinX = (value) => bounds === undefined || (value >= 0 && value <= bounds.width);
|
|
466
|
+
const withinY = (value) => bounds === undefined || (value >= 0 && value <= bounds.height);
|
|
467
|
+
const boundaryXs = bounds === undefined ? [] : [0, bounds.width];
|
|
468
|
+
const boundaryYs = bounds === undefined ? [] : [0, bounds.height];
|
|
469
|
+
const xs = uniqueSorted([
|
|
470
|
+
start.x,
|
|
471
|
+
end.x,
|
|
472
|
+
...boundaryXs,
|
|
473
|
+
...obstacles.flatMap((obstacle) => [obstacle.minX, obstacle.maxX]).filter(withinX)
|
|
474
|
+
]);
|
|
475
|
+
const ys = uniqueSorted([
|
|
476
|
+
start.y,
|
|
477
|
+
end.y,
|
|
478
|
+
...boundaryYs,
|
|
479
|
+
...obstacles.flatMap((obstacle) => [obstacle.minY, obstacle.maxY]).filter(withinY)
|
|
480
|
+
]);
|
|
481
|
+
const width = xs.length;
|
|
482
|
+
const startX = xs.indexOf(start.x);
|
|
483
|
+
const startY = ys.indexOf(start.y);
|
|
484
|
+
const endX = xs.indexOf(end.x);
|
|
485
|
+
const endY = ys.indexOf(end.y);
|
|
486
|
+
const startCell = startY * width + startX;
|
|
487
|
+
const endCell = endY * width + endX;
|
|
488
|
+
const stateId = (cell, direction) => cell * 3 + direction;
|
|
489
|
+
const gScore = new Map();
|
|
490
|
+
const previous = new Map();
|
|
491
|
+
const heap = new RouteHeap();
|
|
492
|
+
const startState = stateId(startCell, 0);
|
|
493
|
+
gScore.set(startState, 0);
|
|
494
|
+
heap.push({
|
|
495
|
+
state: startState,
|
|
496
|
+
cell: startCell,
|
|
497
|
+
direction: 0,
|
|
498
|
+
g: 0,
|
|
499
|
+
f: Math.abs(start.x - end.x) + Math.abs(start.y - end.y)
|
|
500
|
+
});
|
|
501
|
+
let expanded = 0;
|
|
502
|
+
let finalState;
|
|
503
|
+
while (heap.size > 0 && expanded < MAX_ROUTER_STATES) {
|
|
504
|
+
const current = heap.pop();
|
|
505
|
+
if (current.g !== gScore.get(current.state))
|
|
506
|
+
continue;
|
|
507
|
+
if (current.cell === endCell) {
|
|
508
|
+
finalState = current.state;
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
expanded += 1;
|
|
512
|
+
const xIndex = current.cell % width;
|
|
513
|
+
const yIndex = Math.floor(current.cell / width);
|
|
514
|
+
const from = { x: xs[xIndex], y: ys[yIndex] };
|
|
515
|
+
const neighbors = [
|
|
516
|
+
[xIndex - 1, yIndex, 1],
|
|
517
|
+
[xIndex + 1, yIndex, 1],
|
|
518
|
+
[xIndex, yIndex - 1, 2],
|
|
519
|
+
[xIndex, yIndex + 1, 2]
|
|
520
|
+
];
|
|
521
|
+
for (const [nextX, nextY, direction] of neighbors) {
|
|
522
|
+
if (nextX < 0 || nextY < 0 || nextX >= width || nextY >= ys.length)
|
|
523
|
+
continue;
|
|
524
|
+
const to = { x: xs[nextX], y: ys[nextY] };
|
|
525
|
+
let blocked = false;
|
|
332
526
|
for (const obstacle of obstacles) {
|
|
333
|
-
if (segmentIntersectsRectangle(
|
|
334
|
-
|
|
527
|
+
if (segmentIntersectsRectangle(from, to, obstacle)) {
|
|
528
|
+
blocked = true;
|
|
335
529
|
break;
|
|
336
530
|
}
|
|
337
531
|
}
|
|
532
|
+
if (blocked)
|
|
533
|
+
continue;
|
|
534
|
+
const cell = nextY * width + nextX;
|
|
535
|
+
const state = stateId(cell, direction);
|
|
536
|
+
const bend = current.direction !== 0 && current.direction !== direction;
|
|
537
|
+
const g = current.g + Math.abs(from.x - to.x) + Math.abs(from.y - to.y) + (bend ? ROUTER_BEND_PENALTY : 0);
|
|
538
|
+
if (g >= (gScore.get(state) ?? Number.POSITIVE_INFINITY))
|
|
539
|
+
continue;
|
|
540
|
+
gScore.set(state, g);
|
|
541
|
+
previous.set(state, current.state);
|
|
542
|
+
heap.push({
|
|
543
|
+
state,
|
|
544
|
+
cell,
|
|
545
|
+
direction,
|
|
546
|
+
g,
|
|
547
|
+
f: g + Math.abs(to.x - end.x) + Math.abs(to.y - end.y)
|
|
548
|
+
});
|
|
338
549
|
}
|
|
339
|
-
if (collision === undefined)
|
|
340
|
-
break;
|
|
341
|
-
routed = compactOrthogonalPoints(detourRoute(routed[0], routed.at(-1), collision.obstacle, obstacles));
|
|
342
550
|
}
|
|
343
|
-
|
|
551
|
+
if (finalState === undefined && heap.size > 0 && expanded >= MAX_ROUTER_STATES) {
|
|
552
|
+
throw new SchematicSyntaxError(`Orthogonal routing complexity exceeds ${MAX_ROUTER_STATES.toLocaleString('en-US')} search states.`, line);
|
|
553
|
+
}
|
|
554
|
+
if (finalState === undefined)
|
|
555
|
+
return undefined;
|
|
556
|
+
const reversed = [];
|
|
557
|
+
let state = finalState;
|
|
558
|
+
while (state !== undefined) {
|
|
559
|
+
const cell = Math.floor(state / 3);
|
|
560
|
+
reversed.push({ x: xs[cell % width], y: ys[Math.floor(cell / width)] });
|
|
561
|
+
state = previous.get(state);
|
|
562
|
+
}
|
|
563
|
+
return compactOrthogonalPoints(reversed.reverse());
|
|
564
|
+
}
|
|
565
|
+
function routeBetweenEscapes(start, end, obstacles, bounds, line) {
|
|
566
|
+
const middleX = (start.x + end.x) / 2;
|
|
567
|
+
const direct = compactOrthogonalPoints([
|
|
568
|
+
start,
|
|
569
|
+
{ x: middleX, y: start.y },
|
|
570
|
+
{ x: middleX, y: end.y },
|
|
571
|
+
end
|
|
572
|
+
]);
|
|
573
|
+
if (!routeIntersectsObstacles(direct, obstacles))
|
|
574
|
+
return direct;
|
|
575
|
+
const candidates = [];
|
|
576
|
+
const yLanes = uniqueSorted([
|
|
577
|
+
start.y,
|
|
578
|
+
end.y,
|
|
579
|
+
...(bounds === undefined ? [] : [0, bounds.height]),
|
|
580
|
+
...obstacles.flatMap((obstacle) => [obstacle.minY, obstacle.maxY])
|
|
581
|
+
]);
|
|
582
|
+
for (const y of yLanes) {
|
|
583
|
+
if (bounds !== undefined && (y < 0 || y > bounds.height))
|
|
584
|
+
continue;
|
|
585
|
+
candidates.push([start, { x: start.x, y }, { x: end.x, y }, end]);
|
|
586
|
+
}
|
|
587
|
+
const xLanes = uniqueSorted([
|
|
588
|
+
start.x,
|
|
589
|
+
end.x,
|
|
590
|
+
...(bounds === undefined ? [] : [0, bounds.width]),
|
|
591
|
+
...obstacles.flatMap((obstacle) => [obstacle.minX, obstacle.maxX])
|
|
592
|
+
]);
|
|
593
|
+
for (const x of xLanes) {
|
|
594
|
+
if (bounds !== undefined && (x < 0 || x > bounds.width))
|
|
595
|
+
continue;
|
|
596
|
+
candidates.push([start, { x, y: start.y }, { x, y: end.y }, end]);
|
|
597
|
+
}
|
|
598
|
+
let best;
|
|
599
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
600
|
+
for (const raw of candidates) {
|
|
601
|
+
const candidate = compactOrthogonalPoints(raw);
|
|
602
|
+
if (routeIntersectsObstacles(candidate, obstacles))
|
|
603
|
+
continue;
|
|
604
|
+
const score = candidateLength(candidate) + candidateBends(candidate) * ROUTER_BEND_PENALTY;
|
|
605
|
+
if (score < bestScore) {
|
|
606
|
+
best = candidate;
|
|
607
|
+
bestScore = score;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return best ?? searchOrthogonalRoute(start, end, obstacles, bounds, line);
|
|
344
611
|
}
|
|
345
|
-
|
|
346
|
-
const
|
|
347
|
-
const
|
|
612
|
+
function routeConnectionInternal(connection, components, bounds, cachedObstacles) {
|
|
613
|
+
const from = endpointGeometry(connection.from, components);
|
|
614
|
+
const to = endpointGeometry(connection.to, components);
|
|
615
|
+
const start = from.point;
|
|
616
|
+
const end = to.point;
|
|
348
617
|
const sx = formatNumber(start.x);
|
|
349
618
|
const sy = formatNumber(start.y);
|
|
350
619
|
const ex = formatNumber(end.x);
|
|
@@ -354,54 +623,85 @@ export function routeConnection(connection, components) {
|
|
|
354
623
|
const controlA = { x: middleX, y: start.y };
|
|
355
624
|
const controlB = { x: middleX, y: end.y };
|
|
356
625
|
return {
|
|
626
|
+
curve: 'bezier',
|
|
357
627
|
d: `M ${sx} ${sy} C ${formatNumber(controlA.x)} ${formatNumber(controlA.y)}, ${formatNumber(controlB.x)} ${formatNumber(controlB.y)}, ${ex} ${ey}`,
|
|
358
628
|
points: [start, controlA, controlB, end]
|
|
359
629
|
};
|
|
360
630
|
}
|
|
361
631
|
if (connection.curve === 'ortho') {
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const
|
|
632
|
+
const obstacleCache = cachedObstacles ??
|
|
633
|
+
Array.from(components.values(), (component) => ({
|
|
634
|
+
id: component.id,
|
|
635
|
+
expanded: componentObstacleRectangle(component),
|
|
636
|
+
body: componentObstacleRectangle(component, 0)
|
|
637
|
+
}));
|
|
638
|
+
const fromObstacle = componentObstacleRectangle(from.component);
|
|
639
|
+
const toObstacle = componentObstacleRectangle(to.component);
|
|
640
|
+
const escape = (geometry, obstacle) => ({
|
|
641
|
+
x: geometry.normal.x < 0
|
|
642
|
+
? obstacle.minX
|
|
643
|
+
: geometry.normal.x > 0
|
|
644
|
+
? obstacle.maxX
|
|
645
|
+
: geometry.point.x,
|
|
646
|
+
y: geometry.normal.y < 0
|
|
647
|
+
? obstacle.minY
|
|
648
|
+
: geometry.normal.y > 0
|
|
649
|
+
? obstacle.maxY
|
|
650
|
+
: geometry.point.y
|
|
651
|
+
});
|
|
652
|
+
const startEscape = escape(from, fromObstacle);
|
|
653
|
+
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);
|
|
661
|
+
const middle = routeBetweenEscapes(startEscape, endEscape, obstacleRectangles, bounds, connection.line);
|
|
662
|
+
if (middle === undefined) {
|
|
663
|
+
throw new SchematicSyntaxError('No collision-free orthogonal route exists.', connection.line);
|
|
664
|
+
}
|
|
665
|
+
const points = compactOrthogonalPoints([start, ...middle, end]);
|
|
666
|
+
for (let index = 1; index < points.length; index += 1) {
|
|
667
|
+
const allowFrom = index === 1;
|
|
668
|
+
const allowTo = index === points.length - 1;
|
|
669
|
+
for (const obstacle of obstacleEntries) {
|
|
670
|
+
if (!(allowFrom && obstacle.id === from.component.id) &&
|
|
671
|
+
!(allowTo && obstacle.id === to.component.id) &&
|
|
672
|
+
segmentIntersectsRectangle(points[index - 1], points[index], obstacle.rectangle)) {
|
|
673
|
+
throw new SchematicSyntaxError(`Orthogonal route intersects ${obstacle.id} after routing.`, connection.line);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
369
677
|
return {
|
|
678
|
+
curve: 'ortho',
|
|
370
679
|
d: orthogonalPath(points),
|
|
371
680
|
points: points
|
|
372
681
|
};
|
|
373
682
|
}
|
|
374
|
-
return { d: `M ${sx} ${sy} L ${ex} ${ey}`, points: [start, end] };
|
|
683
|
+
return { curve: 'line', d: `M ${sx} ${sy} L ${ex} ${ey}`, points: [start, end] };
|
|
684
|
+
}
|
|
685
|
+
export function routeConnection(connection, components, bounds) {
|
|
686
|
+
return routeConnectionInternal(connection, components, bounds, undefined);
|
|
375
687
|
}
|
|
376
688
|
function axisSegments(route, routeIndex) {
|
|
377
689
|
const segments = [];
|
|
690
|
+
if (route.curve !== 'ortho')
|
|
691
|
+
return segments;
|
|
378
692
|
for (let index = 1; index < route.points.length; index += 1) {
|
|
379
693
|
const start = route.points[index - 1];
|
|
380
694
|
const end = route.points[index];
|
|
381
695
|
if (start.x === end.x && start.y !== end.y) {
|
|
382
696
|
segments.push({ routeIndex, segmentIndex: index - 1, start, end, orientation: 'vertical' });
|
|
383
697
|
}
|
|
384
|
-
else
|
|
698
|
+
else {
|
|
385
699
|
segments.push({ routeIndex, segmentIndex: index - 1, start, end, orientation: 'horizontal' });
|
|
386
700
|
}
|
|
387
701
|
}
|
|
388
702
|
return segments;
|
|
389
703
|
}
|
|
390
|
-
function segmentBucketKeys(segment) {
|
|
391
|
-
const minX = Math.floor(Math.min(segment.start.x, segment.end.x) / CROSSING_BUCKET_SIZE);
|
|
392
|
-
const maxX = Math.floor(Math.max(segment.start.x, segment.end.x) / CROSSING_BUCKET_SIZE);
|
|
393
|
-
const minY = Math.floor(Math.min(segment.start.y, segment.end.y) / CROSSING_BUCKET_SIZE);
|
|
394
|
-
const maxY = Math.floor(Math.max(segment.start.y, segment.end.y) / CROSSING_BUCKET_SIZE);
|
|
395
|
-
const keys = [];
|
|
396
|
-
for (let x = minX; x <= maxX; x += 1) {
|
|
397
|
-
for (let y = minY; y <= maxY; y += 1)
|
|
398
|
-
keys.push(`${x}:${y}`);
|
|
399
|
-
}
|
|
400
|
-
return keys;
|
|
401
|
-
}
|
|
402
704
|
function segmentCrossing(left, right) {
|
|
403
|
-
if (left.orientation === right.orientation)
|
|
404
|
-
return undefined;
|
|
405
705
|
const horizontal = left.orientation === 'horizontal' ? left : right;
|
|
406
706
|
const vertical = left.orientation === 'vertical' ? left : right;
|
|
407
707
|
const point = { x: vertical.start.x, y: horizontal.start.y };
|
|
@@ -427,65 +727,116 @@ function bridgedOrthogonalPath(route, crossings) {
|
|
|
427
727
|
const direction = Math.sign(horizontal ? end.x - start.x : end.y - start.y);
|
|
428
728
|
const coordinate = horizontal ? 'x' : 'y';
|
|
429
729
|
segmentCrossings.sort((left, right) => direction * (left[coordinate] - right[coordinate]));
|
|
730
|
+
const radii = segmentCrossings.map((crossing, crossingIndex) => {
|
|
731
|
+
const position = crossing[coordinate];
|
|
732
|
+
const startDistance = Math.abs(position - start[coordinate]);
|
|
733
|
+
const endDistance = Math.abs(end[coordinate] - position);
|
|
734
|
+
const previousDistance = crossingIndex === 0
|
|
735
|
+
? Number.POSITIVE_INFINITY
|
|
736
|
+
: Math.abs(position - segmentCrossings[crossingIndex - 1][coordinate]) / 2;
|
|
737
|
+
const nextDistance = crossingIndex === segmentCrossings.length - 1
|
|
738
|
+
? Number.POSITIVE_INFINITY
|
|
739
|
+
: Math.abs(segmentCrossings[crossingIndex + 1][coordinate] - position) / 2;
|
|
740
|
+
return Math.min(SCHEMATIC_BRIDGE_RADIUS, startDistance, endDistance, previousDistance, nextDistance);
|
|
741
|
+
});
|
|
430
742
|
if (horizontal) {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
743
|
+
let cursorX = start.x;
|
|
744
|
+
for (const [crossingIndex, crossing] of segmentCrossings.entries()) {
|
|
745
|
+
const radius = radii[crossingIndex];
|
|
746
|
+
const before = crossing.x - direction * radius;
|
|
747
|
+
const after = crossing.x + direction * radius;
|
|
748
|
+
if (before !== cursorX)
|
|
749
|
+
path += ` H ${formatNumber(before)}`;
|
|
750
|
+
path += ` A ${formatNumber(radius)} ${formatNumber(radius)} 0 0 ${direction > 0 ? 1 : 0} ${formatNumber(after)} ${formatNumber(crossing.y)}`;
|
|
751
|
+
cursorX = after;
|
|
752
|
+
boundsPoints.push({ x: before, y: crossing.y }, { x: after, y: crossing.y }, { x: crossing.x, y: crossing.y - radius });
|
|
436
753
|
}
|
|
437
|
-
|
|
754
|
+
if (cursorX !== end.x)
|
|
755
|
+
path += ` H ${formatNumber(end.x)}`;
|
|
438
756
|
}
|
|
439
757
|
else {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
758
|
+
let cursorY = start.y;
|
|
759
|
+
for (const [crossingIndex, crossing] of segmentCrossings.entries()) {
|
|
760
|
+
const radius = radii[crossingIndex];
|
|
761
|
+
const before = crossing.y - direction * radius;
|
|
762
|
+
const after = crossing.y + direction * radius;
|
|
763
|
+
if (before !== cursorY)
|
|
764
|
+
path += ` V ${formatNumber(before)}`;
|
|
765
|
+
path += ` A ${formatNumber(radius)} ${formatNumber(radius)} 0 0 ${direction > 0 ? 0 : 1} ${formatNumber(crossing.x)} ${formatNumber(after)}`;
|
|
766
|
+
cursorY = after;
|
|
767
|
+
boundsPoints.push({ x: crossing.x, y: before }, { x: crossing.x, y: after }, { x: crossing.x - radius, y: crossing.y });
|
|
445
768
|
}
|
|
446
|
-
|
|
769
|
+
if (cursorY !== end.y)
|
|
770
|
+
path += ` V ${formatNumber(end.y)}`;
|
|
447
771
|
}
|
|
448
772
|
}
|
|
449
773
|
return {
|
|
774
|
+
curve: route.curve,
|
|
450
775
|
d: path,
|
|
451
776
|
points: boundsPoints
|
|
452
777
|
};
|
|
453
778
|
}
|
|
454
|
-
export function routeConnections(connections, components) {
|
|
455
|
-
const
|
|
456
|
-
|
|
779
|
+
export function routeConnections(connections, components, bounds) {
|
|
780
|
+
const cachedObstacles = Array.from(components.values(), (component) => ({
|
|
781
|
+
id: component.id,
|
|
782
|
+
expanded: componentObstacleRectangle(component),
|
|
783
|
+
body: componentObstacleRectangle(component, 0)
|
|
784
|
+
}));
|
|
785
|
+
const routes = connections.map((connection) => routeConnectionInternal(connection, components, bounds, cachedObstacles));
|
|
786
|
+
const horizontalBuckets = new Map();
|
|
787
|
+
const verticalBuckets = new Map();
|
|
457
788
|
const crossingsByRoute = new Map();
|
|
789
|
+
const recordedCrossings = new Set();
|
|
790
|
+
let crossingCount = 0;
|
|
791
|
+
const recordCrossing = (routeIndex, segmentIndex, crossing) => {
|
|
792
|
+
let routeCrossings = crossingsByRoute.get(routeIndex);
|
|
793
|
+
if (routeCrossings === undefined) {
|
|
794
|
+
routeCrossings = new Map();
|
|
795
|
+
crossingsByRoute.set(routeIndex, routeCrossings);
|
|
796
|
+
}
|
|
797
|
+
const points = routeCrossings.get(segmentIndex) ?? [];
|
|
798
|
+
const key = `${routeIndex}:${segmentIndex}:${crossing.x}:${crossing.y}`;
|
|
799
|
+
if (!recordedCrossings.has(key)) {
|
|
800
|
+
recordedCrossings.add(key);
|
|
801
|
+
crossingCount += 1;
|
|
802
|
+
if (crossingCount > MAX_SCHEMATIC_WIRE_CROSSINGS) {
|
|
803
|
+
throw new SchematicSyntaxError(`Wire crossing complexity exceeds ${MAX_SCHEMATIC_WIRE_CROSSINGS.toLocaleString('en-US')} intersections.`, connections[routeIndex]?.line);
|
|
804
|
+
}
|
|
805
|
+
points.push(crossing);
|
|
806
|
+
}
|
|
807
|
+
routeCrossings.set(segmentIndex, points);
|
|
808
|
+
};
|
|
458
809
|
for (let routeIndex = 0; routeIndex < routes.length; routeIndex += 1) {
|
|
459
810
|
const route = routes[routeIndex];
|
|
460
811
|
for (const segment of axisSegments(route, routeIndex)) {
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
if (crossing === undefined)
|
|
470
|
-
continue;
|
|
471
|
-
let routeCrossings = crossingsByRoute.get(routeIndex);
|
|
472
|
-
if (routeCrossings === undefined) {
|
|
473
|
-
routeCrossings = new Map();
|
|
474
|
-
crossingsByRoute.set(routeIndex, routeCrossings);
|
|
812
|
+
if (segment.orientation === 'horizontal') {
|
|
813
|
+
const minBucket = Math.floor(Math.min(segment.start.x, segment.end.x) / CROSSING_BUCKET_SIZE);
|
|
814
|
+
const maxBucket = Math.floor(Math.max(segment.start.x, segment.end.x) / CROSSING_BUCKET_SIZE);
|
|
815
|
+
for (let bucket = minBucket; bucket <= maxBucket; bucket += 1) {
|
|
816
|
+
for (const previous of verticalBuckets.get(bucket) ?? []) {
|
|
817
|
+
const crossing = segmentCrossing(segment, previous);
|
|
818
|
+
if (crossing !== undefined)
|
|
819
|
+
recordCrossing(routeIndex, segment.segmentIndex, crossing);
|
|
475
820
|
}
|
|
476
|
-
const points = routeCrossings.get(segment.segmentIndex) ?? [];
|
|
477
|
-
if (!points.some((point) => samePoint(point, crossing)))
|
|
478
|
-
points.push(crossing);
|
|
479
|
-
routeCrossings.set(segment.segmentIndex, points);
|
|
480
821
|
}
|
|
481
822
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
823
|
+
else {
|
|
824
|
+
const minBucket = Math.floor(Math.min(segment.start.y, segment.end.y) / CROSSING_BUCKET_SIZE);
|
|
825
|
+
const maxBucket = Math.floor(Math.max(segment.start.y, segment.end.y) / CROSSING_BUCKET_SIZE);
|
|
826
|
+
for (let bucket = minBucket; bucket <= maxBucket; bucket += 1) {
|
|
827
|
+
for (const previous of horizontalBuckets.get(bucket) ?? []) {
|
|
828
|
+
const crossing = segmentCrossing(segment, previous);
|
|
829
|
+
if (crossing !== undefined)
|
|
830
|
+
recordCrossing(routeIndex, segment.segmentIndex, crossing);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
488
833
|
}
|
|
834
|
+
const bucketIndex = Math.floor((segment.orientation === 'horizontal' ? segment.start.y : segment.start.x) /
|
|
835
|
+
CROSSING_BUCKET_SIZE);
|
|
836
|
+
const index = segment.orientation === 'horizontal' ? horizontalBuckets : verticalBuckets;
|
|
837
|
+
const bucket = index.get(bucketIndex) ?? [];
|
|
838
|
+
bucket.push(segment);
|
|
839
|
+
index.set(bucketIndex, bucket);
|
|
489
840
|
}
|
|
490
841
|
}
|
|
491
842
|
return routes.map((route, index) => bridgedOrthogonalPath(route, crossingsByRoute.get(index) ?? new Map()));
|
|
@@ -497,6 +848,9 @@ function componentHalfExtents(component) {
|
|
|
497
848
|
halfWidth = 48;
|
|
498
849
|
halfHeight = classicalGateHeight(component) / 2;
|
|
499
850
|
}
|
|
851
|
+
else if (isUmlComponent(component)) {
|
|
852
|
+
return umlHalfExtents(component);
|
|
853
|
+
}
|
|
500
854
|
else {
|
|
501
855
|
switch (component.kind) {
|
|
502
856
|
case 'resistor':
|
|
@@ -543,12 +897,27 @@ export function componentTextAnchors(component) {
|
|
|
543
897
|
return {
|
|
544
898
|
designatorY: -halfHeight - DESIGNATOR_BASELINE_GAP,
|
|
545
899
|
labelY: halfHeight + LABEL_BASELINE_GAP,
|
|
546
|
-
designatorWidth:
|
|
547
|
-
labelWidth:
|
|
900
|
+
designatorWidth: mathLabelTextWidth(component.id, TEXT_ADVANCE),
|
|
901
|
+
labelWidth: mathLabelTextWidth(component.label, TEXT_ADVANCE)
|
|
548
902
|
};
|
|
549
903
|
}
|
|
550
904
|
export function componentRectangle(component) {
|
|
551
|
-
const { halfWidth } = componentHalfExtents(component);
|
|
905
|
+
const { halfWidth, halfHeight } = componentHalfExtents(component);
|
|
906
|
+
if (isUmlComponent(component)) {
|
|
907
|
+
const rectangle = {
|
|
908
|
+
minX: component.x - halfWidth,
|
|
909
|
+
minY: component.y - halfHeight,
|
|
910
|
+
maxX: component.x + halfWidth,
|
|
911
|
+
maxY: component.y + halfHeight
|
|
912
|
+
};
|
|
913
|
+
for (const port of enumerateComponentPorts(component)) {
|
|
914
|
+
rectangle.minX = Math.min(rectangle.minX, port.point.x - PORT_HOTSPOT_RADIUS);
|
|
915
|
+
rectangle.minY = Math.min(rectangle.minY, port.point.y - PORT_HOTSPOT_RADIUS);
|
|
916
|
+
rectangle.maxX = Math.max(rectangle.maxX, port.point.x + PORT_HOTSPOT_RADIUS);
|
|
917
|
+
rectangle.maxY = Math.max(rectangle.maxY, port.point.y + PORT_HOTSPOT_RADIUS);
|
|
918
|
+
}
|
|
919
|
+
return rectangle;
|
|
920
|
+
}
|
|
552
921
|
const anchors = componentTextAnchors(component);
|
|
553
922
|
const textHalfWidth = Math.max(anchors.designatorWidth, anchors.labelWidth) / 2 + TEXT_HORIZONTAL_GUTTER;
|
|
554
923
|
const boundedHalfWidth = Math.max(halfWidth, textHalfWidth);
|
|
@@ -575,16 +944,19 @@ function rectangleInsideBounds(rectangle, bounds) {
|
|
|
575
944
|
function pointInsideBounds(point, bounds) {
|
|
576
945
|
return point.x >= 0 && point.y >= 0 && point.x <= bounds.width && point.y <= bounds.height;
|
|
577
946
|
}
|
|
578
|
-
export function validateDocumentGeometry(document, fence) {
|
|
947
|
+
export function validateDocumentGeometry(document, fence, routedConnections) {
|
|
579
948
|
for (const component of document.components) {
|
|
580
949
|
if (!rectangleInsideBounds(componentRectangle(component), fence.bounds)) {
|
|
581
950
|
throw new SchematicSyntaxError(`${component.id} geometry exceeds the declared ${fence.bounds.width}x${fence.bounds.height} bounds.`, component.line);
|
|
582
951
|
}
|
|
583
952
|
}
|
|
584
|
-
const
|
|
585
|
-
|
|
953
|
+
const routes = routedConnections ??
|
|
954
|
+
routeConnections(document.connections, new Map(document.components.map((component) => [component.id, component])), fence.bounds);
|
|
955
|
+
if (routes.length !== document.connections.length) {
|
|
956
|
+
throw new TypeError('Routed connection count does not match the schematic document.');
|
|
957
|
+
}
|
|
586
958
|
for (const [index, connection] of document.connections.entries()) {
|
|
587
|
-
const routed =
|
|
959
|
+
const routed = routes[index];
|
|
588
960
|
if (!routed.points.every((point) => pointInsideBounds(point, fence.bounds))) {
|
|
589
961
|
throw new SchematicSyntaxError('Connection trace exceeds the declared schematic bounds.', connection.line);
|
|
590
962
|
}
|