@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/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
- return (start.x > rectangle.minX &&
805
- start.x < rectangle.maxX &&
806
- Math.max(start.y, end.y) > rectangle.minY &&
807
- Math.min(start.y, end.y) < rectangle.maxY);
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 routeIntersectsObstacles(points, obstacles) {
810
- for (let index = 1; index < points.length; index += 1) {
811
- for (const obstacle of obstacles) {
812
- if (segmentIntersectsRectangle(points[index - 1], points[index], obstacle))
813
- return true;
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
- let blocked = false;
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 + Math.abs(from.x - to.x) + Math.abs(from.y - to.y) + (bend ? ROUTER_BEND_PENALTY : 0);
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
- if (!routeIntersectsObstacles(direct, obstacles))
990
- return direct;
991
- const candidates = [];
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
- candidates.push([start, { x: start.x, y }, { x: end.x, y }, end]);
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
- candidates.push([start, { x, y: start.y }, { x, y: end.y }, end]);
1243
+ consider([start, { x, y: start.y }, { x, y: end.y }, end]);
1013
1244
  }
1014
- let best;
1015
- let bestScore = Number.POSITIVE_INFINITY;
1016
- for (const raw of candidates) {
1017
- const candidate = compactOrthogonalPoints(raw);
1018
- if (routeIntersectsObstacles(candidate, obstacles))
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 score = candidateLength(candidate) + candidateBends(candidate) * ROUTER_BEND_PENALTY;
1021
- if (score < bestScore) {
1022
- best = candidate;
1023
- bestScore = score;
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 best ?? searchOrthogonalRoute(start, end, obstacles, bounds, line);
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, cachedObstacles) {
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
- return {
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
- !obstacleCache.some((entry) => entry.id !== from.component.id &&
1070
- entry.id !== to.component.id &&
1071
- segmentIntersectsRectangle(start, end, entry.expanded))) {
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 obstacleRectangle = (entry) => entry.id === from.component.id || entry.id === to.component.id ? entry.body : entry.expanded;
1091
- const obstacleRectangles = obstacleCache.map(obstacleRectangle);
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
- for (const obstacle of obstacleCache) {
1101
- if (!(allowFrom && obstacle.id === from.component.id) &&
1102
- !(allowTo && obstacle.id === to.component.id) &&
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 bridgedOrthogonalPath(route, crossings) {
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 = [...route.points];
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
- continue;
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: after, y: crossing.y }, { x: crossing.x, y: crossing.y - radius });
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
- continue;
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: after }, { x: crossing.x - radius, y: crossing.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 cachedObstacles = Array.from(components.values(), (component) => ({
1216
- id: component.id,
1217
- expanded: componentObstacleRectangle(component),
1218
- body: componentObstacleRectangle(component, 0)
1219
- }));
1220
- const routes = connections.map((connection) => routeConnectionInternal(connection, components, bounds, cachedObstacles));
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
- if (crossing !== undefined)
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
- if (crossing !== undefined)
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);