@schemd/core 0.3.0 → 0.3.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/CHANGELOG.md +55 -0
- package/README.md +19 -11
- package/dist/compiler.d.ts +2 -0
- package/dist/compiler.js +4 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/layout.d.ts +38 -3
- package/dist/layout.js +633 -85
- package/dist/math-label.d.ts +5 -3
- package/dist/math-label.js +14 -28
- package/dist/parser.d.ts +1 -1
- package/dist/parser.js +137 -20
- package/dist/renderer.js +105 -99
- package/dist/types.d.ts +4 -0
- package/package.json +8 -2
package/dist/layout.js
CHANGED
|
@@ -61,6 +61,8 @@ export function rotateQuarterExtents(extents, turn) {
|
|
|
61
61
|
}
|
|
62
62
|
const MAX_ROUTER_STATES = 40_000;
|
|
63
63
|
const ROUTER_BEND_PENALTY = 0.25;
|
|
64
|
+
const ROUTER_CROSSING_PENALTY = 8;
|
|
65
|
+
const ROUTER_CHANNEL_REUSE_PENALTY = 16_384;
|
|
64
66
|
const CROSSING_BUCKET_SIZE = 64;
|
|
65
67
|
const MIN_RENDERABLE_BRIDGE_RADIUS = 0.001;
|
|
66
68
|
const DESIGNATOR_BASELINE_GAP = 10;
|
|
@@ -68,6 +70,7 @@ const LABEL_BASELINE_GAP = 19;
|
|
|
68
70
|
const TEXT_ASCENT_GUTTER = 12;
|
|
69
71
|
const TEXT_DESCENT_GUTTER = 5;
|
|
70
72
|
const TEXT_ADVANCE = 7;
|
|
73
|
+
const CONNECTION_TEXT_ADVANCE = 6;
|
|
71
74
|
const TEXT_HORIZONTAL_GUTTER = 4;
|
|
72
75
|
const PASSIVE_INPUT_PORTS = new Set(['in', 'left', 'l']);
|
|
73
76
|
const PASSIVE_OUTPUT_PORTS = new Set(['out', 'right', 'r']);
|
|
@@ -76,18 +79,27 @@ const DIODE_CATHODE_PORTS = new Set(['cathode', 'k', 'c']);
|
|
|
76
79
|
const TRANSISTOR_CONTROL_PORTS = new Set(['base', 'gate', 'b', 'g']);
|
|
77
80
|
const TRANSISTOR_UPPER_PORTS = new Set(['collector', 'drain', 'c', 'd']);
|
|
78
81
|
const TRANSISTOR_LOWER_PORTS = new Set(['emitter', 'source', 'e', 's']);
|
|
79
|
-
function formatNumber(value) {
|
|
82
|
+
export function formatNumber(value) {
|
|
80
83
|
const rounded = Math.abs(value) < 0.0005 ? 0 : Number(value.toFixed(3));
|
|
81
84
|
return String(rounded);
|
|
82
85
|
}
|
|
83
86
|
export function distributedCoordinate(index, count, span) {
|
|
84
87
|
return ((index + 1) * span) / (count + 1) - span / 2;
|
|
85
88
|
}
|
|
89
|
+
export function quantumTrackSpan(tracks) {
|
|
90
|
+
return Math.max(16, (tracks - 1) * 18);
|
|
91
|
+
}
|
|
92
|
+
export function quantumTrackCoordinate(index, tracks) {
|
|
93
|
+
if (tracks <= 1)
|
|
94
|
+
return 0;
|
|
95
|
+
const span = quantumTrackSpan(tracks);
|
|
96
|
+
return -span / 2 + (index * span) / (tracks - 1);
|
|
97
|
+
}
|
|
86
98
|
export function classicalGateHeight(component) {
|
|
87
99
|
return Math.max(48, Math.max(component.inputs, component.outputs) * 16);
|
|
88
100
|
}
|
|
89
101
|
export function quantumGateDimensions(component) {
|
|
90
|
-
const details = [component.parameter, component.phase, component.matrix].filter((value) => value !== undefined);
|
|
102
|
+
const details = [component.parameter, component.phase, component.matrix].filter((value) => value !== undefined && value !== '');
|
|
91
103
|
let widest = 16;
|
|
92
104
|
if (details.length > 0 && component.kind === 'qgate')
|
|
93
105
|
widest = mathLabelTextWidth(component.label, 8);
|
|
@@ -97,16 +109,16 @@ export function quantumGateDimensions(component) {
|
|
|
97
109
|
const bodyHeight = 50 + details.length * 15;
|
|
98
110
|
return { bodyWidth, bodyHeight, stubExtent: bodyWidth / 2 + 23 };
|
|
99
111
|
}
|
|
100
|
-
function isClassicalGate(component) {
|
|
112
|
+
export function isClassicalGate(component) {
|
|
101
113
|
return CLASSICAL_GATE_KINDS.includes(component.kind);
|
|
102
114
|
}
|
|
103
|
-
function isDigitalComponent(component) {
|
|
115
|
+
export function isDigitalComponent(component) {
|
|
104
116
|
return DIGITAL_COMPONENT_KINDS.includes(component.kind);
|
|
105
117
|
}
|
|
106
|
-
function isQuantumSpecial(component) {
|
|
118
|
+
export function isQuantumSpecial(component) {
|
|
107
119
|
return QUANTUM_SPECIAL_KINDS.includes(component.kind);
|
|
108
120
|
}
|
|
109
|
-
function isUmlComponent(component) {
|
|
121
|
+
export function isUmlComponent(component) {
|
|
110
122
|
return UML_COMPONENT_KINDS.includes(component.kind);
|
|
111
123
|
}
|
|
112
124
|
function componentTurn(component) {
|
|
@@ -454,12 +466,12 @@ export function resolvePortPoint(component, port) {
|
|
|
454
466
|
index += component.controls;
|
|
455
467
|
const tracks = Math.max(component.wires, component.controls + component.targets);
|
|
456
468
|
if (port.startsWith('control') || port.startsWith('target')) {
|
|
457
|
-
local = { x: 0, y:
|
|
469
|
+
local = { x: 0, y: quantumTrackCoordinate(index, tracks) };
|
|
458
470
|
}
|
|
459
471
|
else {
|
|
460
472
|
local = {
|
|
461
473
|
x: port.startsWith('out') ? 42 : -42,
|
|
462
|
-
y:
|
|
474
|
+
y: quantumTrackCoordinate(index, tracks)
|
|
463
475
|
};
|
|
464
476
|
}
|
|
465
477
|
break;
|
|
@@ -785,6 +797,96 @@ export function componentObstacleRectangle(component, clearance = SCHEMATIC_OBST
|
|
|
785
797
|
}
|
|
786
798
|
return expandedComponentRectangle(component, clearance);
|
|
787
799
|
}
|
|
800
|
+
export function connectionLabelPoint(route) {
|
|
801
|
+
if (route.curve === 'bezier' && route.points.length >= 4) {
|
|
802
|
+
const a = route.points[0];
|
|
803
|
+
const b = route.points[1];
|
|
804
|
+
const c = route.points[2];
|
|
805
|
+
const d = route.points[3];
|
|
806
|
+
return {
|
|
807
|
+
x: (a.x + 3 * b.x + 3 * c.x + d.x) / 8,
|
|
808
|
+
y: (a.y + 3 * b.y + 3 * c.y + d.y) / 8
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
let total = 0;
|
|
812
|
+
for (let index = 1; index < route.points.length; index += 1) {
|
|
813
|
+
total +=
|
|
814
|
+
Math.abs(route.points[index].x - route.points[index - 1].x) +
|
|
815
|
+
Math.abs(route.points[index].y - route.points[index - 1].y);
|
|
816
|
+
}
|
|
817
|
+
let remaining = total / 2;
|
|
818
|
+
for (let index = 1; index < route.points.length; index += 1) {
|
|
819
|
+
const start = route.points[index - 1];
|
|
820
|
+
const end = route.points[index];
|
|
821
|
+
const length = Math.abs(end.x - start.x) + Math.abs(end.y - start.y);
|
|
822
|
+
if (remaining <= length) {
|
|
823
|
+
const ratio = length === 0 ? 0 : remaining / length;
|
|
824
|
+
return { x: start.x + (end.x - start.x) * ratio, y: start.y + (end.y - start.y) * ratio };
|
|
825
|
+
}
|
|
826
|
+
remaining -= length;
|
|
827
|
+
}
|
|
828
|
+
return route.points.at(-1);
|
|
829
|
+
}
|
|
830
|
+
export function connectionLabelRectangle(connection, route) {
|
|
831
|
+
if (connection.label === undefined)
|
|
832
|
+
return undefined;
|
|
833
|
+
const point = connectionLabelPoint(route);
|
|
834
|
+
const halfWidth = mathLabelTextWidth(connection.label, CONNECTION_TEXT_ADVANCE) / 2 + 3;
|
|
835
|
+
return {
|
|
836
|
+
minX: point.x - halfWidth,
|
|
837
|
+
minY: point.y - 20,
|
|
838
|
+
maxX: point.x + halfWidth,
|
|
839
|
+
maxY: point.y
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
function addRoutingRectangle(index, owner, kind, rectangle) {
|
|
843
|
+
const entry = { owner, kind, rectangle, seen: 0 };
|
|
844
|
+
index.rectangles.push(entry);
|
|
845
|
+
const minimum = Math.floor(rectangle.minX / CROSSING_BUCKET_SIZE);
|
|
846
|
+
const maximum = Math.floor(rectangle.maxX / CROSSING_BUCKET_SIZE);
|
|
847
|
+
for (let bucket = minimum; bucket <= maximum; bucket += 1) {
|
|
848
|
+
const entries = index.rectangleBuckets.get(bucket) ?? [];
|
|
849
|
+
entries.push(entry);
|
|
850
|
+
index.rectangleBuckets.set(bucket, entries);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
function componentLabelRectangles(component) {
|
|
854
|
+
if (isUmlComponent(component))
|
|
855
|
+
return [];
|
|
856
|
+
const anchors = componentTextAnchors(component);
|
|
857
|
+
return [
|
|
858
|
+
{
|
|
859
|
+
minX: component.x - anchors.designatorWidth / 2 - TEXT_HORIZONTAL_GUTTER,
|
|
860
|
+
minY: component.y + anchors.designatorY - TEXT_ASCENT_GUTTER,
|
|
861
|
+
maxX: component.x + anchors.designatorWidth / 2 + TEXT_HORIZONTAL_GUTTER,
|
|
862
|
+
maxY: component.y + anchors.designatorY + TEXT_DESCENT_GUTTER
|
|
863
|
+
},
|
|
864
|
+
{
|
|
865
|
+
minX: component.x - anchors.labelWidth / 2 - TEXT_HORIZONTAL_GUTTER,
|
|
866
|
+
minY: component.y + anchors.labelY - TEXT_ASCENT_GUTTER,
|
|
867
|
+
maxX: component.x + anchors.labelWidth / 2 + TEXT_HORIZONTAL_GUTTER,
|
|
868
|
+
maxY: component.y + anchors.labelY + TEXT_DESCENT_GUTTER
|
|
869
|
+
}
|
|
870
|
+
];
|
|
871
|
+
}
|
|
872
|
+
function createRoutingIndex(components) {
|
|
873
|
+
const index = {
|
|
874
|
+
rectangles: [],
|
|
875
|
+
rectangleBuckets: new Map(),
|
|
876
|
+
wireBuckets: new Map(),
|
|
877
|
+
nextWireKey: 0,
|
|
878
|
+
rectangleQuery: 0,
|
|
879
|
+
wireQuery: 0
|
|
880
|
+
};
|
|
881
|
+
for (const component of components.values()) {
|
|
882
|
+
addRoutingRectangle(index, component.id, 'body', componentObstacleRectangle(component, 0));
|
|
883
|
+
addRoutingRectangle(index, component.id, 'expanded', componentObstacleRectangle(component));
|
|
884
|
+
for (const label of componentLabelRectangles(component)) {
|
|
885
|
+
addRoutingRectangle(index, component.id, 'label', label);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
return index;
|
|
889
|
+
}
|
|
788
890
|
function segmentIntersectsRectangle(start, end, rectangle) {
|
|
789
891
|
if (start.y === end.y) {
|
|
790
892
|
return (start.y > rectangle.minY &&
|
|
@@ -792,16 +894,94 @@ function segmentIntersectsRectangle(start, end, rectangle) {
|
|
|
792
894
|
Math.max(start.x, end.x) > rectangle.minX &&
|
|
793
895
|
Math.min(start.x, end.x) < rectangle.maxX);
|
|
794
896
|
}
|
|
795
|
-
|
|
796
|
-
start.x
|
|
797
|
-
|
|
798
|
-
|
|
897
|
+
if (start.x === end.x) {
|
|
898
|
+
return (start.x > rectangle.minX &&
|
|
899
|
+
start.x < rectangle.maxX &&
|
|
900
|
+
Math.max(start.y, end.y) > rectangle.minY &&
|
|
901
|
+
Math.min(start.y, end.y) < rectangle.maxY);
|
|
902
|
+
}
|
|
903
|
+
let minimum = 0;
|
|
904
|
+
let maximum = 1;
|
|
905
|
+
for (const [origin, delta, low, high] of [
|
|
906
|
+
[start.x, end.x - start.x, rectangle.minX, rectangle.maxX],
|
|
907
|
+
[start.y, end.y - start.y, rectangle.minY, rectangle.maxY]
|
|
908
|
+
]) {
|
|
909
|
+
const first = (low - origin) / delta;
|
|
910
|
+
const second = (high - origin) / delta;
|
|
911
|
+
minimum = Math.max(minimum, Math.min(first, second));
|
|
912
|
+
maximum = Math.min(maximum, Math.max(first, second));
|
|
913
|
+
if (minimum >= maximum)
|
|
914
|
+
return false;
|
|
915
|
+
}
|
|
916
|
+
return maximum > 0 && minimum < 1;
|
|
799
917
|
}
|
|
800
|
-
function
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
918
|
+
function rectangleParticipates(entry, mode, fromId, toId) {
|
|
919
|
+
const endpoint = entry.owner === fromId || entry.owner === toId;
|
|
920
|
+
if (mode === 'body')
|
|
921
|
+
return entry.kind === 'body' && !endpoint;
|
|
922
|
+
if (mode === 'manual')
|
|
923
|
+
return !endpoint && entry.kind !== 'expanded';
|
|
924
|
+
return endpoint ? entry.kind === 'body' : entry.kind !== 'body';
|
|
925
|
+
}
|
|
926
|
+
function indexedSegmentCollision(index, start, end, fromId, toId, mode, margin = 0) {
|
|
927
|
+
const minimum = Math.floor((Math.min(start.x, end.x) - margin) / CROSSING_BUCKET_SIZE);
|
|
928
|
+
const maximum = Math.floor((Math.max(start.x, end.x) + margin) / CROSSING_BUCKET_SIZE);
|
|
929
|
+
const query = ++index.rectangleQuery;
|
|
930
|
+
for (let bucket = minimum; bucket <= maximum; bucket += 1) {
|
|
931
|
+
for (const entry of index.rectangleBuckets.get(bucket) ?? []) {
|
|
932
|
+
if (entry.seen === query || !rectangleParticipates(entry, mode, fromId, toId))
|
|
933
|
+
continue;
|
|
934
|
+
entry.seen = query;
|
|
935
|
+
const rectangle = margin === 0
|
|
936
|
+
? entry.rectangle
|
|
937
|
+
: {
|
|
938
|
+
minX: entry.rectangle.minX - margin,
|
|
939
|
+
minY: entry.rectangle.minY - margin,
|
|
940
|
+
maxX: entry.rectangle.maxX + margin,
|
|
941
|
+
maxY: entry.rectangle.maxY + margin
|
|
942
|
+
};
|
|
943
|
+
if (segmentIntersectsRectangle(start, end, rectangle))
|
|
944
|
+
return entry;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
return undefined;
|
|
948
|
+
}
|
|
949
|
+
function routingRectangles(index, fromId, toId) {
|
|
950
|
+
return index.rectangles
|
|
951
|
+
.filter((entry) => rectangleParticipates(entry, 'route', fromId, toId))
|
|
952
|
+
.map((entry) => entry.rectangle);
|
|
953
|
+
}
|
|
954
|
+
function cubicIntersectsRectangle(points, rectangle) {
|
|
955
|
+
const minX = Math.min(...points.map((point) => point.x));
|
|
956
|
+
const minY = Math.min(...points.map((point) => point.y));
|
|
957
|
+
const maxX = Math.max(...points.map((point) => point.x));
|
|
958
|
+
const maxY = Math.max(...points.map((point) => point.y));
|
|
959
|
+
if (maxX <= rectangle.minX ||
|
|
960
|
+
minX >= rectangle.maxX ||
|
|
961
|
+
maxY <= rectangle.minY ||
|
|
962
|
+
minY >= rectangle.maxY) {
|
|
963
|
+
return false;
|
|
964
|
+
}
|
|
965
|
+
if (maxX - minX <= 0.25 && maxY - minY <= 0.25)
|
|
966
|
+
return true;
|
|
967
|
+
const midpoint = (left, right) => ({
|
|
968
|
+
x: (left.x + right.x) / 2,
|
|
969
|
+
y: (left.y + right.y) / 2
|
|
970
|
+
});
|
|
971
|
+
const [a, b, c, d] = points;
|
|
972
|
+
const ab = midpoint(a, b);
|
|
973
|
+
const bc = midpoint(b, c);
|
|
974
|
+
const cd = midpoint(c, d);
|
|
975
|
+
const abc = midpoint(ab, bc);
|
|
976
|
+
const bcd = midpoint(bc, cd);
|
|
977
|
+
const center = midpoint(abc, bcd);
|
|
978
|
+
return (cubicIntersectsRectangle([a, ab, abc, center], rectangle) ||
|
|
979
|
+
cubicIntersectsRectangle([center, bcd, cd, d], rectangle));
|
|
980
|
+
}
|
|
981
|
+
function routeIntersectsObstacles(points, spatialIndex, fromId, toId) {
|
|
982
|
+
for (let pointIndex = 1; pointIndex < points.length; pointIndex += 1) {
|
|
983
|
+
if (indexedSegmentCollision(spatialIndex, points[pointIndex - 1], points[pointIndex], fromId, toId, 'route')) {
|
|
984
|
+
return true;
|
|
805
985
|
}
|
|
806
986
|
}
|
|
807
987
|
return false;
|
|
@@ -818,6 +998,52 @@ function candidateLength(points) {
|
|
|
818
998
|
function candidateBends(points) {
|
|
819
999
|
return Math.max(0, points.length - 2);
|
|
820
1000
|
}
|
|
1001
|
+
function wireSegmentCost(index, start, end, netId) {
|
|
1002
|
+
const minimum = Math.floor(Math.min(start.x, end.x) / CROSSING_BUCKET_SIZE);
|
|
1003
|
+
const maximum = Math.floor(Math.max(start.x, end.x) / CROSSING_BUCKET_SIZE);
|
|
1004
|
+
const query = ++index.wireQuery;
|
|
1005
|
+
const candidate = { id: -1, routeIndex: -1, start, end, orthogonal: true, seen: 0 };
|
|
1006
|
+
let cost = 0;
|
|
1007
|
+
for (let bucket = minimum; bucket <= maximum; bucket += 1) {
|
|
1008
|
+
for (const previous of index.wireBuckets.get(bucket) ?? []) {
|
|
1009
|
+
if (previous.seen === query)
|
|
1010
|
+
continue;
|
|
1011
|
+
previous.seen = query;
|
|
1012
|
+
if (netId !== undefined && previous.netId === netId)
|
|
1013
|
+
continue;
|
|
1014
|
+
const contact = segmentContact(candidate, previous);
|
|
1015
|
+
if (contact !== undefined) {
|
|
1016
|
+
cost += contact.strict && !contact.overlap && previous.orthogonal
|
|
1017
|
+
? ROUTER_CROSSING_PENALTY
|
|
1018
|
+
: ROUTER_CHANNEL_REUSE_PENALTY;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
return cost;
|
|
1023
|
+
}
|
|
1024
|
+
function routeOccupancyCost(points, index, netId) {
|
|
1025
|
+
let cost = 0;
|
|
1026
|
+
for (let pointIndex = 1; pointIndex < points.length; pointIndex += 1) {
|
|
1027
|
+
cost += wireSegmentCost(index, points[pointIndex - 1], points[pointIndex], netId);
|
|
1028
|
+
}
|
|
1029
|
+
return cost;
|
|
1030
|
+
}
|
|
1031
|
+
function indexCompletedRoute(index, route, connection, routeIndex) {
|
|
1032
|
+
const segments = traceSegments(route, routeIndex, index.nextWireKey, connection.netId);
|
|
1033
|
+
index.nextWireKey += segments.length;
|
|
1034
|
+
for (const segment of segments) {
|
|
1035
|
+
const minimum = Math.floor(Math.min(segment.start.x, segment.end.x) / CROSSING_BUCKET_SIZE);
|
|
1036
|
+
const maximum = Math.floor(Math.max(segment.start.x, segment.end.x) / CROSSING_BUCKET_SIZE);
|
|
1037
|
+
for (let bucket = minimum; bucket <= maximum; bucket += 1) {
|
|
1038
|
+
const entries = index.wireBuckets.get(bucket) ?? [];
|
|
1039
|
+
entries.push(segment);
|
|
1040
|
+
index.wireBuckets.set(bucket, entries);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
const label = connectionLabelRectangle(connection, route);
|
|
1044
|
+
if (label !== undefined)
|
|
1045
|
+
addRoutingRectangle(index, `wire-${routeIndex}`, 'label', label);
|
|
1046
|
+
}
|
|
821
1047
|
class RouteHeap {
|
|
822
1048
|
#items = [];
|
|
823
1049
|
get size() {
|
|
@@ -868,7 +1094,7 @@ class RouteHeap {
|
|
|
868
1094
|
function uniqueSorted(values) {
|
|
869
1095
|
return [...new Set(values)].sort((left, right) => left - right);
|
|
870
1096
|
}
|
|
871
|
-
function searchOrthogonalRoute(start, end, obstacles, bounds, line) {
|
|
1097
|
+
function searchOrthogonalRoute(start, end, obstacles, index, fromId, toId, netId, bounds, line) {
|
|
872
1098
|
const withinX = (value) => bounds === undefined || (value >= 0 && value <= bounds.width);
|
|
873
1099
|
const withinY = (value) => bounds === undefined || (value >= 0 && value <= bounds.height);
|
|
874
1100
|
const boundaryXs = bounds === undefined ? [] : [0, bounds.width];
|
|
@@ -929,19 +1155,16 @@ function searchOrthogonalRoute(start, end, obstacles, bounds, line) {
|
|
|
929
1155
|
if (nextX < 0 || nextY < 0 || nextX >= width || nextY >= ys.length)
|
|
930
1156
|
continue;
|
|
931
1157
|
const to = { x: xs[nextX], y: ys[nextY] };
|
|
932
|
-
|
|
933
|
-
for (const obstacle of obstacles) {
|
|
934
|
-
if (segmentIntersectsRectangle(from, to, obstacle)) {
|
|
935
|
-
blocked = true;
|
|
936
|
-
break;
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
if (blocked)
|
|
1158
|
+
if (indexedSegmentCollision(index, from, to, fromId, toId, 'route'))
|
|
940
1159
|
continue;
|
|
941
1160
|
const cell = nextY * width + nextX;
|
|
942
1161
|
const state = stateId(cell, direction);
|
|
943
1162
|
const bend = current.direction !== 0 && current.direction !== direction;
|
|
944
|
-
const g = current.g +
|
|
1163
|
+
const g = current.g +
|
|
1164
|
+
Math.abs(from.x - to.x) +
|
|
1165
|
+
Math.abs(from.y - to.y) +
|
|
1166
|
+
(bend ? ROUTER_BEND_PENALTY : 0) +
|
|
1167
|
+
wireSegmentCost(index, from, to, netId);
|
|
945
1168
|
if (g >= (gScore.get(state) ?? Number.POSITIVE_INFINITY))
|
|
946
1169
|
continue;
|
|
947
1170
|
gScore.set(state, g);
|
|
@@ -969,7 +1192,7 @@ function searchOrthogonalRoute(start, end, obstacles, bounds, line) {
|
|
|
969
1192
|
}
|
|
970
1193
|
return compactOrthogonalPoints(reversed.reverse());
|
|
971
1194
|
}
|
|
972
|
-
function routeBetweenEscapes(start, end, obstacles, bounds, line) {
|
|
1195
|
+
function routeBetweenEscapes(start, end, obstacles, index, fromId, toId, netId, bounds, line) {
|
|
973
1196
|
const middleX = (start.x + end.x) / 2;
|
|
974
1197
|
const direct = compactOrthogonalPoints([
|
|
975
1198
|
start,
|
|
@@ -977,9 +1200,26 @@ function routeBetweenEscapes(start, end, obstacles, bounds, line) {
|
|
|
977
1200
|
{ x: middleX, y: end.y },
|
|
978
1201
|
end
|
|
979
1202
|
]);
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1203
|
+
let best;
|
|
1204
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
1205
|
+
if (!routeIntersectsObstacles(direct, index, fromId, toId)) {
|
|
1206
|
+
const occupancy = routeOccupancyCost(direct, index, netId);
|
|
1207
|
+
if (occupancy === 0)
|
|
1208
|
+
return direct;
|
|
1209
|
+
best = direct;
|
|
1210
|
+
bestScore = candidateLength(direct) + candidateBends(direct) * ROUTER_BEND_PENALTY + occupancy;
|
|
1211
|
+
}
|
|
1212
|
+
const consider = (raw) => {
|
|
1213
|
+
const candidate = compactOrthogonalPoints(raw);
|
|
1214
|
+
const baseScore = candidateLength(candidate) + candidateBends(candidate) * ROUTER_BEND_PENALTY;
|
|
1215
|
+
if (baseScore >= bestScore || routeIntersectsObstacles(candidate, index, fromId, toId))
|
|
1216
|
+
return;
|
|
1217
|
+
const score = baseScore + routeOccupancyCost(candidate, index, netId);
|
|
1218
|
+
if (score < bestScore) {
|
|
1219
|
+
best = candidate;
|
|
1220
|
+
bestScore = score;
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
983
1223
|
const yLanes = uniqueSorted([
|
|
984
1224
|
start.y,
|
|
985
1225
|
end.y,
|
|
@@ -989,7 +1229,7 @@ function routeBetweenEscapes(start, end, obstacles, bounds, line) {
|
|
|
989
1229
|
for (const y of yLanes) {
|
|
990
1230
|
if (bounds !== undefined && (y < 0 || y > bounds.height))
|
|
991
1231
|
continue;
|
|
992
|
-
|
|
1232
|
+
consider([start, { x: start.x, y }, { x: end.x, y }, end]);
|
|
993
1233
|
}
|
|
994
1234
|
const xLanes = uniqueSorted([
|
|
995
1235
|
start.x,
|
|
@@ -1000,21 +1240,53 @@ function routeBetweenEscapes(start, end, obstacles, bounds, line) {
|
|
|
1000
1240
|
for (const x of xLanes) {
|
|
1001
1241
|
if (bounds !== undefined && (x < 0 || x > bounds.width))
|
|
1002
1242
|
continue;
|
|
1003
|
-
|
|
1243
|
+
consider([start, { x, y: start.y }, { x, y: end.y }, end]);
|
|
1004
1244
|
}
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1245
|
+
return best ?? searchOrthogonalRoute(start, end, obstacles, index, fromId, toId, netId, bounds, line);
|
|
1246
|
+
}
|
|
1247
|
+
function markerCollisionSize(marker) {
|
|
1248
|
+
switch (marker) {
|
|
1249
|
+
case 'dot':
|
|
1250
|
+
return { length: 4, radius: 4 };
|
|
1251
|
+
case 'arrow':
|
|
1252
|
+
return { length: 8, radius: 4 };
|
|
1253
|
+
case 'open-arrow':
|
|
1254
|
+
return { length: 9, radius: 5 };
|
|
1255
|
+
case 'triangle':
|
|
1256
|
+
case 'diamond':
|
|
1257
|
+
case 'diamond-filled':
|
|
1258
|
+
return { length: 12, radius: 6 };
|
|
1259
|
+
default:
|
|
1260
|
+
return { length: 0, radius: 0 };
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
function validateMarkerCollisions(connection, route, index, fromId, toId) {
|
|
1264
|
+
if (connection.markerStart === 'none' && connection.markerEnd === 'none')
|
|
1265
|
+
return route;
|
|
1266
|
+
for (const [atStart, marker] of [
|
|
1267
|
+
[true, connection.markerStart],
|
|
1268
|
+
[false, connection.markerEnd]
|
|
1269
|
+
]) {
|
|
1270
|
+
const size = markerCollisionSize(marker);
|
|
1271
|
+
if (size.length === 0)
|
|
1010
1272
|
continue;
|
|
1011
|
-
const
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1273
|
+
const endpoint = atStart ? route.points[0] : route.points.at(-1);
|
|
1274
|
+
const adjacent = atStart ? route.points[1] : route.points.at(-2);
|
|
1275
|
+
const dx = adjacent.x - endpoint.x;
|
|
1276
|
+
const dy = adjacent.y - endpoint.y;
|
|
1277
|
+
const magnitude = Math.hypot(dx, dy);
|
|
1278
|
+
if (magnitude === 0)
|
|
1279
|
+
continue;
|
|
1280
|
+
const inward = {
|
|
1281
|
+
x: endpoint.x + (dx / magnitude) * size.length,
|
|
1282
|
+
y: endpoint.y + (dy / magnitude) * size.length
|
|
1283
|
+
};
|
|
1284
|
+
const collision = indexedSegmentCollision(index, endpoint, inward, fromId, toId, 'body', size.radius);
|
|
1285
|
+
if (collision !== undefined) {
|
|
1286
|
+
throw new SchematicSyntaxError(`${marker} marker intersects ${collision.owner}; use ortho or move the obstacle.`, connection.line);
|
|
1015
1287
|
}
|
|
1016
1288
|
}
|
|
1017
|
-
return
|
|
1289
|
+
return route;
|
|
1018
1290
|
}
|
|
1019
1291
|
function endpointsFaceEachOther(from, to) {
|
|
1020
1292
|
const dx = to.point.x - from.point.x;
|
|
@@ -1029,7 +1301,7 @@ function endpointsFaceEachOther(from, to) {
|
|
|
1029
1301
|
}
|
|
1030
1302
|
return false;
|
|
1031
1303
|
}
|
|
1032
|
-
function routeConnectionInternal(connection, components, bounds,
|
|
1304
|
+
function routeConnectionInternal(connection, components, bounds, spatialIndex) {
|
|
1033
1305
|
const from = endpointGeometry(connection.from, components);
|
|
1034
1306
|
const to = endpointGeometry(connection.to, components);
|
|
1035
1307
|
const start = from.point;
|
|
@@ -1038,29 +1310,29 @@ function routeConnectionInternal(connection, components, bounds, cachedObstacles
|
|
|
1038
1310
|
const sy = formatNumber(start.y);
|
|
1039
1311
|
const ex = formatNumber(end.x);
|
|
1040
1312
|
const ey = formatNumber(end.y);
|
|
1313
|
+
const routingIndex = spatialIndex ?? createRoutingIndex(components);
|
|
1041
1314
|
if (connection.curve === 'bezier') {
|
|
1042
1315
|
const middleX = (start.x + end.x) / 2;
|
|
1043
1316
|
const controlA = { x: middleX, y: start.y };
|
|
1044
1317
|
const controlB = { x: middleX, y: end.y };
|
|
1045
|
-
|
|
1318
|
+
for (const obstacle of routingIndex.rectangles) {
|
|
1319
|
+
if (rectangleParticipates(obstacle, 'manual', from.component.id, to.component.id) &&
|
|
1320
|
+
cubicIntersectsRectangle([start, controlA, controlB, end], obstacle.rectangle)) {
|
|
1321
|
+
throw new SchematicSyntaxError(`Bézier route intersects ${obstacle.owner}; use an orthogonal route or move the obstacle.`, connection.line);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
return validateMarkerCollisions(connection, {
|
|
1046
1325
|
curve: 'bezier',
|
|
1047
1326
|
d: `M ${sx} ${sy} C ${formatNumber(controlA.x)} ${formatNumber(controlA.y)}, ${formatNumber(controlB.x)} ${formatNumber(controlB.y)}, ${ex} ${ey}`,
|
|
1048
1327
|
points: [start, controlA, controlB, end]
|
|
1049
|
-
};
|
|
1328
|
+
}, routingIndex, from.component.id, to.component.id);
|
|
1050
1329
|
}
|
|
1051
1330
|
if (connection.curve === 'ortho') {
|
|
1052
|
-
const obstacleCache = cachedObstacles ??
|
|
1053
|
-
Array.from(components.values(), (component) => ({
|
|
1054
|
-
id: component.id,
|
|
1055
|
-
expanded: componentObstacleRectangle(component),
|
|
1056
|
-
body: componentObstacleRectangle(component, 0)
|
|
1057
|
-
}));
|
|
1058
1331
|
if (from.component.id !== to.component.id &&
|
|
1059
1332
|
endpointsFaceEachOther(from, to) &&
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
return { curve: 'ortho', d: orthogonalPath([start, end]), points: [start, end] };
|
|
1333
|
+
indexedSegmentCollision(routingIndex, start, end, from.component.id, to.component.id, 'route') === undefined &&
|
|
1334
|
+
wireSegmentCost(routingIndex, start, end, connection.netId) === 0) {
|
|
1335
|
+
return validateMarkerCollisions(connection, { curve: 'ortho', d: orthogonalPath([start, end]), points: [start, end] }, routingIndex, from.component.id, to.component.id);
|
|
1064
1336
|
}
|
|
1065
1337
|
const fromObstacle = componentObstacleRectangle(from.component);
|
|
1066
1338
|
const toObstacle = componentObstacleRectangle(to.component);
|
|
@@ -1078,9 +1350,8 @@ function routeConnectionInternal(connection, components, bounds, cachedObstacles
|
|
|
1078
1350
|
});
|
|
1079
1351
|
const startEscape = escape(from, fromObstacle);
|
|
1080
1352
|
const endEscape = escape(to, toObstacle);
|
|
1081
|
-
const
|
|
1082
|
-
const
|
|
1083
|
-
const middle = routeBetweenEscapes(startEscape, endEscape, obstacleRectangles, bounds, connection.line);
|
|
1353
|
+
const obstacleRectangles = routingRectangles(routingIndex, from.component.id, to.component.id);
|
|
1354
|
+
const middle = routeBetweenEscapes(startEscape, endEscape, obstacleRectangles, routingIndex, from.component.id, to.component.id, connection.netId, bounds, connection.line);
|
|
1084
1355
|
if (middle === undefined) {
|
|
1085
1356
|
throw new SchematicSyntaxError('No collision-free orthogonal route exists.', connection.line);
|
|
1086
1357
|
}
|
|
@@ -1088,25 +1359,169 @@ function routeConnectionInternal(connection, components, bounds, cachedObstacles
|
|
|
1088
1359
|
for (let index = 1; index < points.length; index += 1) {
|
|
1089
1360
|
const allowFrom = index === 1;
|
|
1090
1361
|
const allowTo = index === points.length - 1;
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
segmentIntersectsRectangle(points[index - 1], points[index], obstacleRectangle(obstacle))) {
|
|
1095
|
-
throw new SchematicSyntaxError(`Orthogonal route intersects ${obstacle.id} after routing.`, connection.line);
|
|
1096
|
-
}
|
|
1362
|
+
const collision = indexedSegmentCollision(routingIndex, points[index - 1], points[index], allowFrom ? from.component.id : '', allowTo ? to.component.id : '', 'body');
|
|
1363
|
+
if (collision !== undefined) {
|
|
1364
|
+
throw new SchematicSyntaxError(`Orthogonal route intersects ${collision.owner} after routing.`, connection.line);
|
|
1097
1365
|
}
|
|
1098
1366
|
}
|
|
1099
|
-
return {
|
|
1367
|
+
return validateMarkerCollisions(connection, {
|
|
1100
1368
|
curve: 'ortho',
|
|
1101
1369
|
d: orthogonalPath(points),
|
|
1102
1370
|
points: points
|
|
1103
|
-
};
|
|
1371
|
+
}, routingIndex, from.component.id, to.component.id);
|
|
1372
|
+
}
|
|
1373
|
+
const collision = indexedSegmentCollision(routingIndex, start, end, from.component.id, to.component.id, 'manual');
|
|
1374
|
+
if (collision !== undefined) {
|
|
1375
|
+
throw new SchematicSyntaxError(`Line route intersects ${collision.owner}; use an orthogonal route or move the obstacle.`, connection.line);
|
|
1104
1376
|
}
|
|
1105
|
-
return { curve: 'line', d: `M ${sx} ${sy} L ${ex} ${ey}`, points: [start, end] };
|
|
1377
|
+
return validateMarkerCollisions(connection, { curve: 'line', d: `M ${sx} ${sy} L ${ex} ${ey}`, points: [start, end] }, routingIndex, from.component.id, to.component.id);
|
|
1106
1378
|
}
|
|
1107
1379
|
export function routeConnection(connection, components, bounds) {
|
|
1108
1380
|
return routeConnectionInternal(connection, components, bounds, undefined);
|
|
1109
1381
|
}
|
|
1382
|
+
function connectionsShareNet(left, right) {
|
|
1383
|
+
if (left.netId !== undefined && right.netId !== undefined)
|
|
1384
|
+
return left.netId === right.netId;
|
|
1385
|
+
if (left.net !== undefined && right.net !== undefined && left.net === right.net)
|
|
1386
|
+
return true;
|
|
1387
|
+
const leftEndpoints = [
|
|
1388
|
+
`${left.from.componentId}.${left.from.port}`,
|
|
1389
|
+
`${left.to.componentId}.${left.to.port}`
|
|
1390
|
+
];
|
|
1391
|
+
return leftEndpoints.includes(`${right.from.componentId}.${right.from.port}`) ||
|
|
1392
|
+
leftEndpoints.includes(`${right.to.componentId}.${right.to.port}`);
|
|
1393
|
+
}
|
|
1394
|
+
function connectionsAreInternalToSameComponent(left, right) {
|
|
1395
|
+
return (left.from.componentId === left.to.componentId &&
|
|
1396
|
+
right.from.componentId === right.to.componentId &&
|
|
1397
|
+
left.from.componentId === right.from.componentId);
|
|
1398
|
+
}
|
|
1399
|
+
function traceSegments(route, routeIndex, firstId, netId) {
|
|
1400
|
+
let points = route.points;
|
|
1401
|
+
if (route.curve === 'bezier') {
|
|
1402
|
+
const a = route.points[0];
|
|
1403
|
+
const b = route.points[1];
|
|
1404
|
+
const c = route.points[2];
|
|
1405
|
+
const d = route.points[3];
|
|
1406
|
+
const flattened = [];
|
|
1407
|
+
for (let step = 0; step <= 24; step += 1) {
|
|
1408
|
+
const t = step / 24;
|
|
1409
|
+
const inverse = 1 - t;
|
|
1410
|
+
flattened.push({
|
|
1411
|
+
x: inverse ** 3 * a.x +
|
|
1412
|
+
3 * inverse ** 2 * t * b.x +
|
|
1413
|
+
3 * inverse * t ** 2 * c.x +
|
|
1414
|
+
t ** 3 * d.x,
|
|
1415
|
+
y: inverse ** 3 * a.y +
|
|
1416
|
+
3 * inverse ** 2 * t * b.y +
|
|
1417
|
+
3 * inverse * t ** 2 * c.y +
|
|
1418
|
+
t ** 3 * d.y
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
points = flattened;
|
|
1422
|
+
}
|
|
1423
|
+
const segments = [];
|
|
1424
|
+
for (let index = 1; index < points.length; index += 1) {
|
|
1425
|
+
const start = points[index - 1];
|
|
1426
|
+
const end = points[index];
|
|
1427
|
+
if (start.x === end.x && start.y === end.y)
|
|
1428
|
+
continue;
|
|
1429
|
+
segments.push({
|
|
1430
|
+
id: firstId + segments.length,
|
|
1431
|
+
routeIndex,
|
|
1432
|
+
...(netId === undefined ? {} : { netId }),
|
|
1433
|
+
seen: 0,
|
|
1434
|
+
start,
|
|
1435
|
+
end,
|
|
1436
|
+
orthogonal: route.curve === 'ortho'
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
return segments;
|
|
1440
|
+
}
|
|
1441
|
+
function segmentContact(left, right) {
|
|
1442
|
+
const rx = left.end.x - left.start.x;
|
|
1443
|
+
const ry = left.end.y - left.start.y;
|
|
1444
|
+
const sx = right.end.x - right.start.x;
|
|
1445
|
+
const sy = right.end.y - right.start.y;
|
|
1446
|
+
const qx = right.start.x - left.start.x;
|
|
1447
|
+
const qy = right.start.y - left.start.y;
|
|
1448
|
+
const denominator = rx * sy - ry * sx;
|
|
1449
|
+
const epsilon = 1e-9;
|
|
1450
|
+
if (Math.abs(denominator) <= epsilon) {
|
|
1451
|
+
if (Math.abs(qx * ry - qy * rx) > epsilon)
|
|
1452
|
+
return undefined;
|
|
1453
|
+
const useX = Math.abs(rx) >= Math.abs(ry);
|
|
1454
|
+
const leftStart = useX ? left.start.x : left.start.y;
|
|
1455
|
+
const leftEnd = useX ? left.end.x : left.end.y;
|
|
1456
|
+
const rightStart = useX ? right.start.x : right.start.y;
|
|
1457
|
+
const rightEnd = useX ? right.end.x : right.end.y;
|
|
1458
|
+
const low = Math.max(Math.min(leftStart, leftEnd), Math.min(rightStart, rightEnd));
|
|
1459
|
+
const high = Math.min(Math.max(leftStart, leftEnd), Math.max(rightStart, rightEnd));
|
|
1460
|
+
if (high < low - epsilon)
|
|
1461
|
+
return undefined;
|
|
1462
|
+
return { overlap: high > low + epsilon, strict: false };
|
|
1463
|
+
}
|
|
1464
|
+
const t = (qx * sy - qy * sx) / denominator;
|
|
1465
|
+
const u = (qx * ry - qy * rx) / denominator;
|
|
1466
|
+
if (t < -epsilon || t > 1 + epsilon || u < -epsilon || u > 1 + epsilon)
|
|
1467
|
+
return undefined;
|
|
1468
|
+
return {
|
|
1469
|
+
overlap: false,
|
|
1470
|
+
strict: t > epsilon && t < 1 - epsilon && u > epsilon && u < 1 - epsilon
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
function validateUniversalWireContacts(connections, routes) {
|
|
1474
|
+
if (routes.every((route) => route.curve === 'ortho'))
|
|
1475
|
+
return;
|
|
1476
|
+
const buckets = new Map();
|
|
1477
|
+
let nextSegmentId = 0;
|
|
1478
|
+
for (let routeIndex = 0; routeIndex < routes.length; routeIndex += 1) {
|
|
1479
|
+
const segments = traceSegments(routes[routeIndex], routeIndex, nextSegmentId);
|
|
1480
|
+
nextSegmentId += segments.length;
|
|
1481
|
+
for (const segment of segments) {
|
|
1482
|
+
const checked = new Set();
|
|
1483
|
+
const minimumBucket = Math.floor(Math.min(segment.start.x, segment.end.x) / CROSSING_BUCKET_SIZE);
|
|
1484
|
+
const maximumBucket = Math.floor(Math.max(segment.start.x, segment.end.x) / CROSSING_BUCKET_SIZE);
|
|
1485
|
+
for (let bucket = minimumBucket; bucket <= maximumBucket; bucket += 1) {
|
|
1486
|
+
for (const previous of buckets.get(bucket) ?? []) {
|
|
1487
|
+
if (checked.has(previous.id))
|
|
1488
|
+
continue;
|
|
1489
|
+
checked.add(previous.id);
|
|
1490
|
+
if (Math.max(segment.start.y, segment.end.y) <
|
|
1491
|
+
Math.min(previous.start.y, previous.end.y) ||
|
|
1492
|
+
Math.min(segment.start.y, segment.end.y) >
|
|
1493
|
+
Math.max(previous.start.y, previous.end.y)) {
|
|
1494
|
+
continue;
|
|
1495
|
+
}
|
|
1496
|
+
const contact = segmentContact(segment, previous);
|
|
1497
|
+
if (contact === undefined ||
|
|
1498
|
+
connectionsAreInternalToSameComponent(connections[routeIndex], connections[previous.routeIndex]) ||
|
|
1499
|
+
connectionsShareNet(connections[routeIndex], connections[previous.routeIndex])) {
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
const bridgeable = contact.strict &&
|
|
1503
|
+
!contact.overlap &&
|
|
1504
|
+
segment.orthogonal &&
|
|
1505
|
+
previous.orthogonal &&
|
|
1506
|
+
(segment.start.x === segment.end.x) !==
|
|
1507
|
+
(previous.start.x === previous.end.x);
|
|
1508
|
+
if (!bridgeable) {
|
|
1509
|
+
throw new SchematicSyntaxError('Separate nets touch or cross without an orthogonal bridge; use ortho, a shared net, or an explicit junction.', connections[routeIndex].line);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
for (const segment of segments) {
|
|
1515
|
+
const minimumBucket = Math.floor(Math.min(segment.start.x, segment.end.x) / CROSSING_BUCKET_SIZE);
|
|
1516
|
+
const maximumBucket = Math.floor(Math.max(segment.start.x, segment.end.x) / CROSSING_BUCKET_SIZE);
|
|
1517
|
+
for (let bucket = minimumBucket; bucket <= maximumBucket; bucket += 1) {
|
|
1518
|
+
const entries = buckets.get(bucket) ?? [];
|
|
1519
|
+
entries.push(segment);
|
|
1520
|
+
buckets.set(bucket, entries);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1110
1525
|
function axisSegments(route, routeIndex) {
|
|
1111
1526
|
const segments = [];
|
|
1112
1527
|
if (route.curve !== 'ortho')
|
|
@@ -1135,12 +1550,29 @@ function segmentCrossing(left, right) {
|
|
|
1135
1550
|
}
|
|
1136
1551
|
return point;
|
|
1137
1552
|
}
|
|
1138
|
-
function
|
|
1553
|
+
function axisSegmentsTouch(left, right) {
|
|
1554
|
+
if (left.orientation === right.orientation) {
|
|
1555
|
+
if ((left.orientation === 'horizontal' && left.start.y !== right.start.y) ||
|
|
1556
|
+
(left.orientation === 'vertical' && left.start.x !== right.start.x)) {
|
|
1557
|
+
return false;
|
|
1558
|
+
}
|
|
1559
|
+
const coordinate = left.orientation === 'horizontal' ? 'x' : 'y';
|
|
1560
|
+
return (Math.max(Math.min(left.start[coordinate], left.end[coordinate]), Math.min(right.start[coordinate], right.end[coordinate])) <=
|
|
1561
|
+
Math.min(Math.max(left.start[coordinate], left.end[coordinate]), Math.max(right.start[coordinate], right.end[coordinate])));
|
|
1562
|
+
}
|
|
1563
|
+
const horizontal = left.orientation === 'horizontal' ? left : right;
|
|
1564
|
+
const vertical = left.orientation === 'vertical' ? left : right;
|
|
1565
|
+
return (vertical.start.x >= Math.min(horizontal.start.x, horizontal.end.x) &&
|
|
1566
|
+
vertical.start.x <= Math.max(horizontal.start.x, horizontal.end.x) &&
|
|
1567
|
+
horizontal.start.y >= Math.min(vertical.start.y, vertical.end.y) &&
|
|
1568
|
+
horizontal.start.y <= Math.max(vertical.start.y, vertical.end.y));
|
|
1569
|
+
}
|
|
1570
|
+
function bridgedOrthogonalPath(route, crossings, line) {
|
|
1139
1571
|
if (crossings.size === 0)
|
|
1140
1572
|
return route;
|
|
1141
1573
|
const first = route.points[0];
|
|
1142
1574
|
let path = `M ${formatNumber(first.x)} ${formatNumber(first.y)}`;
|
|
1143
|
-
const boundsPoints = [
|
|
1575
|
+
const boundsPoints = [first];
|
|
1144
1576
|
for (let index = 1; index < route.points.length; index += 1) {
|
|
1145
1577
|
const start = route.points[index - 1];
|
|
1146
1578
|
const end = route.points[index];
|
|
@@ -1165,15 +1597,16 @@ function bridgedOrthogonalPath(route, crossings) {
|
|
|
1165
1597
|
let cursorX = start.x;
|
|
1166
1598
|
for (const [crossingIndex, crossing] of segmentCrossings.entries()) {
|
|
1167
1599
|
const radius = radii[crossingIndex];
|
|
1168
|
-
if (radius < MIN_RENDERABLE_BRIDGE_RADIUS)
|
|
1169
|
-
|
|
1600
|
+
if (radius < MIN_RENDERABLE_BRIDGE_RADIUS) {
|
|
1601
|
+
throw new SchematicSyntaxError('Wire crossings are too close to render distinct bridge arcs.', line);
|
|
1602
|
+
}
|
|
1170
1603
|
const before = crossing.x - direction * radius;
|
|
1171
1604
|
const after = crossing.x + direction * radius;
|
|
1172
1605
|
if (before !== cursorX)
|
|
1173
1606
|
path += ` H ${formatNumber(before)}`;
|
|
1174
1607
|
path += ` A ${formatNumber(radius)} ${formatNumber(radius)} 0 0 ${direction > 0 ? 1 : 0} ${formatNumber(after)} ${formatNumber(crossing.y)}`;
|
|
1175
1608
|
cursorX = after;
|
|
1176
|
-
boundsPoints.push({ x: before, y: crossing.y }, { x:
|
|
1609
|
+
boundsPoints.push({ x: before, y: crossing.y }, { x: crossing.x, y: crossing.y - radius }, { x: after, y: crossing.y });
|
|
1177
1610
|
}
|
|
1178
1611
|
if (cursorX !== end.x)
|
|
1179
1612
|
path += ` H ${formatNumber(end.x)}`;
|
|
@@ -1182,19 +1615,23 @@ function bridgedOrthogonalPath(route, crossings) {
|
|
|
1182
1615
|
let cursorY = start.y;
|
|
1183
1616
|
for (const [crossingIndex, crossing] of segmentCrossings.entries()) {
|
|
1184
1617
|
const radius = radii[crossingIndex];
|
|
1185
|
-
if (radius < MIN_RENDERABLE_BRIDGE_RADIUS)
|
|
1186
|
-
|
|
1618
|
+
if (radius < MIN_RENDERABLE_BRIDGE_RADIUS) {
|
|
1619
|
+
throw new SchematicSyntaxError('Wire crossings are too close to render distinct bridge arcs.', line);
|
|
1620
|
+
}
|
|
1187
1621
|
const before = crossing.y - direction * radius;
|
|
1188
1622
|
const after = crossing.y + direction * radius;
|
|
1189
1623
|
if (before !== cursorY)
|
|
1190
1624
|
path += ` V ${formatNumber(before)}`;
|
|
1191
1625
|
path += ` A ${formatNumber(radius)} ${formatNumber(radius)} 0 0 ${direction > 0 ? 0 : 1} ${formatNumber(crossing.x)} ${formatNumber(after)}`;
|
|
1192
1626
|
cursorY = after;
|
|
1193
|
-
boundsPoints.push({ x: crossing.x, y: before }, { x: crossing.x, y:
|
|
1627
|
+
boundsPoints.push({ x: crossing.x, y: before }, { x: crossing.x - radius, y: crossing.y }, { x: crossing.x, y: after });
|
|
1194
1628
|
}
|
|
1195
1629
|
if (cursorY !== end.y)
|
|
1196
1630
|
path += ` V ${formatNumber(end.y)}`;
|
|
1197
1631
|
}
|
|
1632
|
+
if (boundsPoints.at(-1).x !== end.x || boundsPoints.at(-1).y !== end.y) {
|
|
1633
|
+
boundsPoints.push(end);
|
|
1634
|
+
}
|
|
1198
1635
|
}
|
|
1199
1636
|
return {
|
|
1200
1637
|
curve: route.curve,
|
|
@@ -1203,12 +1640,14 @@ function bridgedOrthogonalPath(route, crossings) {
|
|
|
1203
1640
|
};
|
|
1204
1641
|
}
|
|
1205
1642
|
export function routeConnections(connections, components, bounds) {
|
|
1206
|
-
const
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1643
|
+
const spatialIndex = createRoutingIndex(components);
|
|
1644
|
+
const routes = [];
|
|
1645
|
+
for (const [routeIndex, connection] of connections.entries()) {
|
|
1646
|
+
const route = routeConnectionInternal(connection, components, bounds, spatialIndex);
|
|
1647
|
+
routes.push(route);
|
|
1648
|
+
indexCompletedRoute(spatialIndex, route, connection, routeIndex);
|
|
1649
|
+
}
|
|
1650
|
+
validateUniversalWireContacts(connections, routes);
|
|
1212
1651
|
const horizontalBuckets = new Map();
|
|
1213
1652
|
const verticalBuckets = new Map();
|
|
1214
1653
|
const crossingsByRoute = new Map();
|
|
@@ -1232,28 +1671,55 @@ export function routeConnections(connections, components, bounds) {
|
|
|
1232
1671
|
}
|
|
1233
1672
|
routeCrossings.set(segmentIndex, points);
|
|
1234
1673
|
};
|
|
1674
|
+
const rejectUnbridgeableContact = (routeIndex) => {
|
|
1675
|
+
throw new SchematicSyntaxError('Separate nets touch or overlap without a bridgeable crossing; use a shared net or an explicit junction.', connections[routeIndex]?.line);
|
|
1676
|
+
};
|
|
1235
1677
|
for (let routeIndex = 0; routeIndex < routes.length; routeIndex += 1) {
|
|
1236
1678
|
const route = routes[routeIndex];
|
|
1237
1679
|
for (const segment of axisSegments(route, routeIndex)) {
|
|
1238
1680
|
if (segment.orientation === 'horizontal') {
|
|
1681
|
+
for (const previous of horizontalBuckets.get(Math.floor(segment.start.y / CROSSING_BUCKET_SIZE)) ?? []) {
|
|
1682
|
+
if (axisSegmentsTouch(segment, previous) &&
|
|
1683
|
+
!connectionsShareNet(connections[routeIndex], connections[previous.routeIndex])) {
|
|
1684
|
+
rejectUnbridgeableContact(routeIndex);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1239
1687
|
const minBucket = Math.floor(Math.min(segment.start.x, segment.end.x) / CROSSING_BUCKET_SIZE);
|
|
1240
1688
|
const maxBucket = Math.floor(Math.max(segment.start.x, segment.end.x) / CROSSING_BUCKET_SIZE);
|
|
1241
1689
|
for (let bucket = minBucket; bucket <= maxBucket; bucket += 1) {
|
|
1242
1690
|
for (const previous of verticalBuckets.get(bucket) ?? []) {
|
|
1243
1691
|
const crossing = segmentCrossing(segment, previous);
|
|
1244
|
-
|
|
1692
|
+
const shared = connectionsShareNet(connections[routeIndex], connections[previous.routeIndex]);
|
|
1693
|
+
if (crossing !== undefined &&
|
|
1694
|
+
!shared) {
|
|
1245
1695
|
recordCrossing(routeIndex, segment.segmentIndex, crossing);
|
|
1696
|
+
}
|
|
1697
|
+
else if (crossing === undefined && !shared && axisSegmentsTouch(segment, previous)) {
|
|
1698
|
+
rejectUnbridgeableContact(routeIndex);
|
|
1699
|
+
}
|
|
1246
1700
|
}
|
|
1247
1701
|
}
|
|
1248
1702
|
}
|
|
1249
1703
|
else {
|
|
1704
|
+
for (const previous of verticalBuckets.get(Math.floor(segment.start.x / CROSSING_BUCKET_SIZE)) ?? []) {
|
|
1705
|
+
if (axisSegmentsTouch(segment, previous) &&
|
|
1706
|
+
!connectionsShareNet(connections[routeIndex], connections[previous.routeIndex])) {
|
|
1707
|
+
rejectUnbridgeableContact(routeIndex);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1250
1710
|
const minBucket = Math.floor(Math.min(segment.start.y, segment.end.y) / CROSSING_BUCKET_SIZE);
|
|
1251
1711
|
const maxBucket = Math.floor(Math.max(segment.start.y, segment.end.y) / CROSSING_BUCKET_SIZE);
|
|
1252
1712
|
for (let bucket = minBucket; bucket <= maxBucket; bucket += 1) {
|
|
1253
1713
|
for (const previous of horizontalBuckets.get(bucket) ?? []) {
|
|
1254
1714
|
const crossing = segmentCrossing(segment, previous);
|
|
1255
|
-
|
|
1715
|
+
const shared = connectionsShareNet(connections[routeIndex], connections[previous.routeIndex]);
|
|
1716
|
+
if (crossing !== undefined &&
|
|
1717
|
+
!shared) {
|
|
1256
1718
|
recordCrossing(routeIndex, segment.segmentIndex, crossing);
|
|
1719
|
+
}
|
|
1720
|
+
else if (crossing === undefined && !shared && axisSegmentsTouch(segment, previous)) {
|
|
1721
|
+
rejectUnbridgeableContact(routeIndex);
|
|
1722
|
+
}
|
|
1257
1723
|
}
|
|
1258
1724
|
}
|
|
1259
1725
|
}
|
|
@@ -1265,7 +1731,7 @@ export function routeConnections(connections, components, bounds) {
|
|
|
1265
1731
|
index.set(bucketIndex, bucket);
|
|
1266
1732
|
}
|
|
1267
1733
|
}
|
|
1268
|
-
return routes.map((route, index) => bridgedOrthogonalPath(route, crossingsByRoute.get(index) ?? new Map()));
|
|
1734
|
+
return routes.map((route, index) => bridgedOrthogonalPath(route, crossingsByRoute.get(index) ?? new Map(), connections[index]?.line));
|
|
1269
1735
|
}
|
|
1270
1736
|
function componentHalfExtents(component) {
|
|
1271
1737
|
let halfWidth;
|
|
@@ -1426,7 +1892,89 @@ function rectangleInsideBounds(rectangle, bounds) {
|
|
|
1426
1892
|
function pointInsideBounds(point, bounds) {
|
|
1427
1893
|
return point.x >= 0 && point.y >= 0 && point.x <= bounds.width && point.y <= bounds.height;
|
|
1428
1894
|
}
|
|
1895
|
+
const OVERLAP_CONTAINER_KINDS = new Set([
|
|
1896
|
+
'package',
|
|
1897
|
+
'component',
|
|
1898
|
+
'node',
|
|
1899
|
+
'device',
|
|
1900
|
+
'system',
|
|
1901
|
+
'partition',
|
|
1902
|
+
'fragment',
|
|
1903
|
+
'interaction',
|
|
1904
|
+
'region'
|
|
1905
|
+
]);
|
|
1906
|
+
function rectangleContains(outer, inner) {
|
|
1907
|
+
return (outer.minX <= inner.minX &&
|
|
1908
|
+
outer.minY <= inner.minY &&
|
|
1909
|
+
outer.maxX >= inner.maxX &&
|
|
1910
|
+
outer.maxY >= inner.maxY);
|
|
1911
|
+
}
|
|
1912
|
+
function rectanglesOverlap(left, right) {
|
|
1913
|
+
return (left.minX < right.maxX &&
|
|
1914
|
+
left.maxX > right.minX &&
|
|
1915
|
+
left.minY < right.maxY &&
|
|
1916
|
+
left.maxY > right.minY);
|
|
1917
|
+
}
|
|
1918
|
+
function componentOverlapAllowed(left, right) {
|
|
1919
|
+
for (const [container, child] of [
|
|
1920
|
+
[left, right],
|
|
1921
|
+
[right, left]
|
|
1922
|
+
]) {
|
|
1923
|
+
if (OVERLAP_CONTAINER_KINDS.has(container.component.kind) &&
|
|
1924
|
+
(rectangleContains(container.rectangle, child.rectangle) ||
|
|
1925
|
+
(['component-port', 'gate'].includes(child.component.kind) &&
|
|
1926
|
+
child.component.x >= container.rectangle.minX &&
|
|
1927
|
+
child.component.x <= container.rectangle.maxX &&
|
|
1928
|
+
child.component.y >= container.rectangle.minY &&
|
|
1929
|
+
child.component.y <= container.rectangle.maxY))) {
|
|
1930
|
+
return true;
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
return ((left.component.kind === 'lifeline' &&
|
|
1934
|
+
['activation', 'execution', 'destruction'].includes(right.component.kind)) ||
|
|
1935
|
+
(right.component.kind === 'lifeline' &&
|
|
1936
|
+
['activation', 'execution', 'destruction'].includes(left.component.kind)));
|
|
1937
|
+
}
|
|
1938
|
+
function validateComponentOverlaps(components) {
|
|
1939
|
+
const ordered = components
|
|
1940
|
+
.map((component) => ({
|
|
1941
|
+
component,
|
|
1942
|
+
rectangle: componentObstacleRectangle(component, 0)
|
|
1943
|
+
}))
|
|
1944
|
+
.sort((left, right) => left.rectangle.minX - right.rectangle.minX ||
|
|
1945
|
+
left.rectangle.minY - right.rectangle.minY ||
|
|
1946
|
+
left.component.line - right.component.line);
|
|
1947
|
+
const buckets = [];
|
|
1948
|
+
for (const current of ordered) {
|
|
1949
|
+
const minimumBucket = Math.floor(current.rectangle.minY / CROSSING_BUCKET_SIZE);
|
|
1950
|
+
const maximumBucket = Math.floor(current.rectangle.maxY / CROSSING_BUCKET_SIZE);
|
|
1951
|
+
const candidates = new Set();
|
|
1952
|
+
for (let bucket = minimumBucket; bucket <= maximumBucket; bucket += 1) {
|
|
1953
|
+
const entries = buckets[bucket];
|
|
1954
|
+
if (entries === undefined)
|
|
1955
|
+
continue;
|
|
1956
|
+
let retained = 0;
|
|
1957
|
+
for (const entry of entries) {
|
|
1958
|
+
if (entry.rectangle.maxX <= current.rectangle.minX)
|
|
1959
|
+
continue;
|
|
1960
|
+
entries[retained++] = entry;
|
|
1961
|
+
candidates.add(entry);
|
|
1962
|
+
}
|
|
1963
|
+
entries.length = retained;
|
|
1964
|
+
}
|
|
1965
|
+
for (const previous of candidates) {
|
|
1966
|
+
if (rectanglesOverlap(previous.rectangle, current.rectangle) &&
|
|
1967
|
+
!componentOverlapAllowed(previous, current)) {
|
|
1968
|
+
throw new SchematicSyntaxError(`${current.component.id} overlaps ${previous.component.id}; move one component or use a UML container.`, current.component.line);
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
for (let bucket = minimumBucket; bucket <= maximumBucket; bucket += 1) {
|
|
1972
|
+
(buckets[bucket] ??= []).push(current);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1429
1976
|
export function validateDocumentGeometry(document, fence, routedConnections) {
|
|
1977
|
+
validateComponentOverlaps(document.components);
|
|
1430
1978
|
for (const component of document.components) {
|
|
1431
1979
|
if (!rectangleInsideBounds(componentRectangle(component), fence.bounds)) {
|
|
1432
1980
|
throw new SchematicSyntaxError(`${component.id} geometry exceeds the declared ${fence.bounds.width}x${fence.bounds.height} bounds.`, component.line);
|