@schemd/core 0.3.0 → 0.3.2

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