@schemd/core 0.3.1 → 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 +37 -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 +26 -3
- package/dist/layout.js +621 -82
- package/dist/parser.d.ts +1 -1
- package/dist/parser.js +137 -20
- package/dist/renderer.js +96 -88
- 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,7 +79,7 @@ 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
|
}
|
|
@@ -106,16 +109,16 @@ export function quantumGateDimensions(component) {
|
|
|
106
109
|
const bodyHeight = 50 + details.length * 15;
|
|
107
110
|
return { bodyWidth, bodyHeight, stubExtent: bodyWidth / 2 + 23 };
|
|
108
111
|
}
|
|
109
|
-
function isClassicalGate(component) {
|
|
112
|
+
export function isClassicalGate(component) {
|
|
110
113
|
return CLASSICAL_GATE_KINDS.includes(component.kind);
|
|
111
114
|
}
|
|
112
|
-
function isDigitalComponent(component) {
|
|
115
|
+
export function isDigitalComponent(component) {
|
|
113
116
|
return DIGITAL_COMPONENT_KINDS.includes(component.kind);
|
|
114
117
|
}
|
|
115
|
-
function isQuantumSpecial(component) {
|
|
118
|
+
export function isQuantumSpecial(component) {
|
|
116
119
|
return QUANTUM_SPECIAL_KINDS.includes(component.kind);
|
|
117
120
|
}
|
|
118
|
-
function isUmlComponent(component) {
|
|
121
|
+
export function isUmlComponent(component) {
|
|
119
122
|
return UML_COMPONENT_KINDS.includes(component.kind);
|
|
120
123
|
}
|
|
121
124
|
function componentTurn(component) {
|
|
@@ -794,6 +797,96 @@ export function componentObstacleRectangle(component, clearance = SCHEMATIC_OBST
|
|
|
794
797
|
}
|
|
795
798
|
return expandedComponentRectangle(component, clearance);
|
|
796
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
|
+
}
|
|
797
890
|
function segmentIntersectsRectangle(start, end, rectangle) {
|
|
798
891
|
if (start.y === end.y) {
|
|
799
892
|
return (start.y > rectangle.minY &&
|
|
@@ -801,16 +894,94 @@ function segmentIntersectsRectangle(start, end, rectangle) {
|
|
|
801
894
|
Math.max(start.x, end.x) > rectangle.minX &&
|
|
802
895
|
Math.min(start.x, end.x) < rectangle.maxX);
|
|
803
896
|
}
|
|
804
|
-
|
|
805
|
-
start.x
|
|
806
|
-
|
|
807
|
-
|
|
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;
|
|
808
917
|
}
|
|
809
|
-
function
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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;
|
|
814
985
|
}
|
|
815
986
|
}
|
|
816
987
|
return false;
|
|
@@ -827,6 +998,52 @@ function candidateLength(points) {
|
|
|
827
998
|
function candidateBends(points) {
|
|
828
999
|
return Math.max(0, points.length - 2);
|
|
829
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
|
+
}
|
|
830
1047
|
class RouteHeap {
|
|
831
1048
|
#items = [];
|
|
832
1049
|
get size() {
|
|
@@ -877,7 +1094,7 @@ class RouteHeap {
|
|
|
877
1094
|
function uniqueSorted(values) {
|
|
878
1095
|
return [...new Set(values)].sort((left, right) => left - right);
|
|
879
1096
|
}
|
|
880
|
-
function searchOrthogonalRoute(start, end, obstacles, bounds, line) {
|
|
1097
|
+
function searchOrthogonalRoute(start, end, obstacles, index, fromId, toId, netId, bounds, line) {
|
|
881
1098
|
const withinX = (value) => bounds === undefined || (value >= 0 && value <= bounds.width);
|
|
882
1099
|
const withinY = (value) => bounds === undefined || (value >= 0 && value <= bounds.height);
|
|
883
1100
|
const boundaryXs = bounds === undefined ? [] : [0, bounds.width];
|
|
@@ -938,19 +1155,16 @@ function searchOrthogonalRoute(start, end, obstacles, bounds, line) {
|
|
|
938
1155
|
if (nextX < 0 || nextY < 0 || nextX >= width || nextY >= ys.length)
|
|
939
1156
|
continue;
|
|
940
1157
|
const to = { x: xs[nextX], y: ys[nextY] };
|
|
941
|
-
|
|
942
|
-
for (const obstacle of obstacles) {
|
|
943
|
-
if (segmentIntersectsRectangle(from, to, obstacle)) {
|
|
944
|
-
blocked = true;
|
|
945
|
-
break;
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
if (blocked)
|
|
1158
|
+
if (indexedSegmentCollision(index, from, to, fromId, toId, 'route'))
|
|
949
1159
|
continue;
|
|
950
1160
|
const cell = nextY * width + nextX;
|
|
951
1161
|
const state = stateId(cell, direction);
|
|
952
1162
|
const bend = current.direction !== 0 && current.direction !== direction;
|
|
953
|
-
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);
|
|
954
1168
|
if (g >= (gScore.get(state) ?? Number.POSITIVE_INFINITY))
|
|
955
1169
|
continue;
|
|
956
1170
|
gScore.set(state, g);
|
|
@@ -978,7 +1192,7 @@ function searchOrthogonalRoute(start, end, obstacles, bounds, line) {
|
|
|
978
1192
|
}
|
|
979
1193
|
return compactOrthogonalPoints(reversed.reverse());
|
|
980
1194
|
}
|
|
981
|
-
function routeBetweenEscapes(start, end, obstacles, bounds, line) {
|
|
1195
|
+
function routeBetweenEscapes(start, end, obstacles, index, fromId, toId, netId, bounds, line) {
|
|
982
1196
|
const middleX = (start.x + end.x) / 2;
|
|
983
1197
|
const direct = compactOrthogonalPoints([
|
|
984
1198
|
start,
|
|
@@ -986,9 +1200,26 @@ function routeBetweenEscapes(start, end, obstacles, bounds, line) {
|
|
|
986
1200
|
{ x: middleX, y: end.y },
|
|
987
1201
|
end
|
|
988
1202
|
]);
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
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
|
+
};
|
|
992
1223
|
const yLanes = uniqueSorted([
|
|
993
1224
|
start.y,
|
|
994
1225
|
end.y,
|
|
@@ -998,7 +1229,7 @@ function routeBetweenEscapes(start, end, obstacles, bounds, line) {
|
|
|
998
1229
|
for (const y of yLanes) {
|
|
999
1230
|
if (bounds !== undefined && (y < 0 || y > bounds.height))
|
|
1000
1231
|
continue;
|
|
1001
|
-
|
|
1232
|
+
consider([start, { x: start.x, y }, { x: end.x, y }, end]);
|
|
1002
1233
|
}
|
|
1003
1234
|
const xLanes = uniqueSorted([
|
|
1004
1235
|
start.x,
|
|
@@ -1009,21 +1240,53 @@ function routeBetweenEscapes(start, end, obstacles, bounds, line) {
|
|
|
1009
1240
|
for (const x of xLanes) {
|
|
1010
1241
|
if (bounds !== undefined && (x < 0 || x > bounds.width))
|
|
1011
1242
|
continue;
|
|
1012
|
-
|
|
1243
|
+
consider([start, { x, y: start.y }, { x, y: end.y }, end]);
|
|
1013
1244
|
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
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)
|
|
1019
1272
|
continue;
|
|
1020
|
-
const
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
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);
|
|
1024
1287
|
}
|
|
1025
1288
|
}
|
|
1026
|
-
return
|
|
1289
|
+
return route;
|
|
1027
1290
|
}
|
|
1028
1291
|
function endpointsFaceEachOther(from, to) {
|
|
1029
1292
|
const dx = to.point.x - from.point.x;
|
|
@@ -1038,7 +1301,7 @@ function endpointsFaceEachOther(from, to) {
|
|
|
1038
1301
|
}
|
|
1039
1302
|
return false;
|
|
1040
1303
|
}
|
|
1041
|
-
function routeConnectionInternal(connection, components, bounds,
|
|
1304
|
+
function routeConnectionInternal(connection, components, bounds, spatialIndex) {
|
|
1042
1305
|
const from = endpointGeometry(connection.from, components);
|
|
1043
1306
|
const to = endpointGeometry(connection.to, components);
|
|
1044
1307
|
const start = from.point;
|
|
@@ -1047,29 +1310,29 @@ function routeConnectionInternal(connection, components, bounds, cachedObstacles
|
|
|
1047
1310
|
const sy = formatNumber(start.y);
|
|
1048
1311
|
const ex = formatNumber(end.x);
|
|
1049
1312
|
const ey = formatNumber(end.y);
|
|
1313
|
+
const routingIndex = spatialIndex ?? createRoutingIndex(components);
|
|
1050
1314
|
if (connection.curve === 'bezier') {
|
|
1051
1315
|
const middleX = (start.x + end.x) / 2;
|
|
1052
1316
|
const controlA = { x: middleX, y: start.y };
|
|
1053
1317
|
const controlB = { x: middleX, y: end.y };
|
|
1054
|
-
|
|
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, {
|
|
1055
1325
|
curve: 'bezier',
|
|
1056
1326
|
d: `M ${sx} ${sy} C ${formatNumber(controlA.x)} ${formatNumber(controlA.y)}, ${formatNumber(controlB.x)} ${formatNumber(controlB.y)}, ${ex} ${ey}`,
|
|
1057
1327
|
points: [start, controlA, controlB, end]
|
|
1058
|
-
};
|
|
1328
|
+
}, routingIndex, from.component.id, to.component.id);
|
|
1059
1329
|
}
|
|
1060
1330
|
if (connection.curve === 'ortho') {
|
|
1061
|
-
const obstacleCache = cachedObstacles ??
|
|
1062
|
-
Array.from(components.values(), (component) => ({
|
|
1063
|
-
id: component.id,
|
|
1064
|
-
expanded: componentObstacleRectangle(component),
|
|
1065
|
-
body: componentObstacleRectangle(component, 0)
|
|
1066
|
-
}));
|
|
1067
1331
|
if (from.component.id !== to.component.id &&
|
|
1068
1332
|
endpointsFaceEachOther(from, to) &&
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
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);
|
|
1073
1336
|
}
|
|
1074
1337
|
const fromObstacle = componentObstacleRectangle(from.component);
|
|
1075
1338
|
const toObstacle = componentObstacleRectangle(to.component);
|
|
@@ -1087,9 +1350,8 @@ function routeConnectionInternal(connection, components, bounds, cachedObstacles
|
|
|
1087
1350
|
});
|
|
1088
1351
|
const startEscape = escape(from, fromObstacle);
|
|
1089
1352
|
const endEscape = escape(to, toObstacle);
|
|
1090
|
-
const
|
|
1091
|
-
const
|
|
1092
|
-
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);
|
|
1093
1355
|
if (middle === undefined) {
|
|
1094
1356
|
throw new SchematicSyntaxError('No collision-free orthogonal route exists.', connection.line);
|
|
1095
1357
|
}
|
|
@@ -1097,25 +1359,169 @@ function routeConnectionInternal(connection, components, bounds, cachedObstacles
|
|
|
1097
1359
|
for (let index = 1; index < points.length; index += 1) {
|
|
1098
1360
|
const allowFrom = index === 1;
|
|
1099
1361
|
const allowTo = index === points.length - 1;
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
segmentIntersectsRectangle(points[index - 1], points[index], obstacle.body)) {
|
|
1104
|
-
throw new SchematicSyntaxError(`Orthogonal route intersects ${obstacle.id} after routing.`, connection.line);
|
|
1105
|
-
}
|
|
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);
|
|
1106
1365
|
}
|
|
1107
1366
|
}
|
|
1108
|
-
return {
|
|
1367
|
+
return validateMarkerCollisions(connection, {
|
|
1109
1368
|
curve: 'ortho',
|
|
1110
1369
|
d: orthogonalPath(points),
|
|
1111
1370
|
points: points
|
|
1112
|
-
};
|
|
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);
|
|
1113
1376
|
}
|
|
1114
|
-
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);
|
|
1115
1378
|
}
|
|
1116
1379
|
export function routeConnection(connection, components, bounds) {
|
|
1117
1380
|
return routeConnectionInternal(connection, components, bounds, undefined);
|
|
1118
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
|
+
}
|
|
1119
1525
|
function axisSegments(route, routeIndex) {
|
|
1120
1526
|
const segments = [];
|
|
1121
1527
|
if (route.curve !== 'ortho')
|
|
@@ -1144,12 +1550,29 @@ function segmentCrossing(left, right) {
|
|
|
1144
1550
|
}
|
|
1145
1551
|
return point;
|
|
1146
1552
|
}
|
|
1147
|
-
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) {
|
|
1148
1571
|
if (crossings.size === 0)
|
|
1149
1572
|
return route;
|
|
1150
1573
|
const first = route.points[0];
|
|
1151
1574
|
let path = `M ${formatNumber(first.x)} ${formatNumber(first.y)}`;
|
|
1152
|
-
const boundsPoints = [
|
|
1575
|
+
const boundsPoints = [first];
|
|
1153
1576
|
for (let index = 1; index < route.points.length; index += 1) {
|
|
1154
1577
|
const start = route.points[index - 1];
|
|
1155
1578
|
const end = route.points[index];
|
|
@@ -1174,15 +1597,16 @@ function bridgedOrthogonalPath(route, crossings) {
|
|
|
1174
1597
|
let cursorX = start.x;
|
|
1175
1598
|
for (const [crossingIndex, crossing] of segmentCrossings.entries()) {
|
|
1176
1599
|
const radius = radii[crossingIndex];
|
|
1177
|
-
if (radius < MIN_RENDERABLE_BRIDGE_RADIUS)
|
|
1178
|
-
|
|
1600
|
+
if (radius < MIN_RENDERABLE_BRIDGE_RADIUS) {
|
|
1601
|
+
throw new SchematicSyntaxError('Wire crossings are too close to render distinct bridge arcs.', line);
|
|
1602
|
+
}
|
|
1179
1603
|
const before = crossing.x - direction * radius;
|
|
1180
1604
|
const after = crossing.x + direction * radius;
|
|
1181
1605
|
if (before !== cursorX)
|
|
1182
1606
|
path += ` H ${formatNumber(before)}`;
|
|
1183
1607
|
path += ` A ${formatNumber(radius)} ${formatNumber(radius)} 0 0 ${direction > 0 ? 1 : 0} ${formatNumber(after)} ${formatNumber(crossing.y)}`;
|
|
1184
1608
|
cursorX = after;
|
|
1185
|
-
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 });
|
|
1186
1610
|
}
|
|
1187
1611
|
if (cursorX !== end.x)
|
|
1188
1612
|
path += ` H ${formatNumber(end.x)}`;
|
|
@@ -1191,19 +1615,23 @@ function bridgedOrthogonalPath(route, crossings) {
|
|
|
1191
1615
|
let cursorY = start.y;
|
|
1192
1616
|
for (const [crossingIndex, crossing] of segmentCrossings.entries()) {
|
|
1193
1617
|
const radius = radii[crossingIndex];
|
|
1194
|
-
if (radius < MIN_RENDERABLE_BRIDGE_RADIUS)
|
|
1195
|
-
|
|
1618
|
+
if (radius < MIN_RENDERABLE_BRIDGE_RADIUS) {
|
|
1619
|
+
throw new SchematicSyntaxError('Wire crossings are too close to render distinct bridge arcs.', line);
|
|
1620
|
+
}
|
|
1196
1621
|
const before = crossing.y - direction * radius;
|
|
1197
1622
|
const after = crossing.y + direction * radius;
|
|
1198
1623
|
if (before !== cursorY)
|
|
1199
1624
|
path += ` V ${formatNumber(before)}`;
|
|
1200
1625
|
path += ` A ${formatNumber(radius)} ${formatNumber(radius)} 0 0 ${direction > 0 ? 0 : 1} ${formatNumber(crossing.x)} ${formatNumber(after)}`;
|
|
1201
1626
|
cursorY = after;
|
|
1202
|
-
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 });
|
|
1203
1628
|
}
|
|
1204
1629
|
if (cursorY !== end.y)
|
|
1205
1630
|
path += ` V ${formatNumber(end.y)}`;
|
|
1206
1631
|
}
|
|
1632
|
+
if (boundsPoints.at(-1).x !== end.x || boundsPoints.at(-1).y !== end.y) {
|
|
1633
|
+
boundsPoints.push(end);
|
|
1634
|
+
}
|
|
1207
1635
|
}
|
|
1208
1636
|
return {
|
|
1209
1637
|
curve: route.curve,
|
|
@@ -1212,12 +1640,14 @@ function bridgedOrthogonalPath(route, crossings) {
|
|
|
1212
1640
|
};
|
|
1213
1641
|
}
|
|
1214
1642
|
export function routeConnections(connections, components, bounds) {
|
|
1215
|
-
const
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
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);
|
|
1221
1651
|
const horizontalBuckets = new Map();
|
|
1222
1652
|
const verticalBuckets = new Map();
|
|
1223
1653
|
const crossingsByRoute = new Map();
|
|
@@ -1241,28 +1671,55 @@ export function routeConnections(connections, components, bounds) {
|
|
|
1241
1671
|
}
|
|
1242
1672
|
routeCrossings.set(segmentIndex, points);
|
|
1243
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
|
+
};
|
|
1244
1677
|
for (let routeIndex = 0; routeIndex < routes.length; routeIndex += 1) {
|
|
1245
1678
|
const route = routes[routeIndex];
|
|
1246
1679
|
for (const segment of axisSegments(route, routeIndex)) {
|
|
1247
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
|
+
}
|
|
1248
1687
|
const minBucket = Math.floor(Math.min(segment.start.x, segment.end.x) / CROSSING_BUCKET_SIZE);
|
|
1249
1688
|
const maxBucket = Math.floor(Math.max(segment.start.x, segment.end.x) / CROSSING_BUCKET_SIZE);
|
|
1250
1689
|
for (let bucket = minBucket; bucket <= maxBucket; bucket += 1) {
|
|
1251
1690
|
for (const previous of verticalBuckets.get(bucket) ?? []) {
|
|
1252
1691
|
const crossing = segmentCrossing(segment, previous);
|
|
1253
|
-
|
|
1692
|
+
const shared = connectionsShareNet(connections[routeIndex], connections[previous.routeIndex]);
|
|
1693
|
+
if (crossing !== undefined &&
|
|
1694
|
+
!shared) {
|
|
1254
1695
|
recordCrossing(routeIndex, segment.segmentIndex, crossing);
|
|
1696
|
+
}
|
|
1697
|
+
else if (crossing === undefined && !shared && axisSegmentsTouch(segment, previous)) {
|
|
1698
|
+
rejectUnbridgeableContact(routeIndex);
|
|
1699
|
+
}
|
|
1255
1700
|
}
|
|
1256
1701
|
}
|
|
1257
1702
|
}
|
|
1258
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
|
+
}
|
|
1259
1710
|
const minBucket = Math.floor(Math.min(segment.start.y, segment.end.y) / CROSSING_BUCKET_SIZE);
|
|
1260
1711
|
const maxBucket = Math.floor(Math.max(segment.start.y, segment.end.y) / CROSSING_BUCKET_SIZE);
|
|
1261
1712
|
for (let bucket = minBucket; bucket <= maxBucket; bucket += 1) {
|
|
1262
1713
|
for (const previous of horizontalBuckets.get(bucket) ?? []) {
|
|
1263
1714
|
const crossing = segmentCrossing(segment, previous);
|
|
1264
|
-
|
|
1715
|
+
const shared = connectionsShareNet(connections[routeIndex], connections[previous.routeIndex]);
|
|
1716
|
+
if (crossing !== undefined &&
|
|
1717
|
+
!shared) {
|
|
1265
1718
|
recordCrossing(routeIndex, segment.segmentIndex, crossing);
|
|
1719
|
+
}
|
|
1720
|
+
else if (crossing === undefined && !shared && axisSegmentsTouch(segment, previous)) {
|
|
1721
|
+
rejectUnbridgeableContact(routeIndex);
|
|
1722
|
+
}
|
|
1266
1723
|
}
|
|
1267
1724
|
}
|
|
1268
1725
|
}
|
|
@@ -1274,7 +1731,7 @@ export function routeConnections(connections, components, bounds) {
|
|
|
1274
1731
|
index.set(bucketIndex, bucket);
|
|
1275
1732
|
}
|
|
1276
1733
|
}
|
|
1277
|
-
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));
|
|
1278
1735
|
}
|
|
1279
1736
|
function componentHalfExtents(component) {
|
|
1280
1737
|
let halfWidth;
|
|
@@ -1435,7 +1892,89 @@ function rectangleInsideBounds(rectangle, bounds) {
|
|
|
1435
1892
|
function pointInsideBounds(point, bounds) {
|
|
1436
1893
|
return point.x >= 0 && point.y >= 0 && point.x <= bounds.width && point.y <= bounds.height;
|
|
1437
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
|
+
}
|
|
1438
1976
|
export function validateDocumentGeometry(document, fence, routedConnections) {
|
|
1977
|
+
validateComponentOverlaps(document.components);
|
|
1439
1978
|
for (const component of document.components) {
|
|
1440
1979
|
if (!rectangleInsideBounds(componentRectangle(component), fence.bounds)) {
|
|
1441
1980
|
throw new SchematicSyntaxError(`${component.id} geometry exceeds the declared ${fence.bounds.width}x${fence.bounds.height} bounds.`, component.line);
|