@tscircuit/schematic-trace-solver 0.0.145 → 0.0.146
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/index.d.ts +2 -0
- package/dist/index.js +386 -12
- package/lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver.ts +30 -8
- package/lib/solvers/InlineNetLabelSolver/pushAnchoredNetLabelsAwayFromInlineLabels.ts +530 -0
- package/package.json +1 -1
- package/tests/assets/inline-net-label-anchored-label-clearance.json +56 -0
- package/tests/solvers/InlineNetLabelSolver/__snapshots__/inline-net-label-anchored-label-clearance.snap.svg +54 -0
- package/tests/solvers/InlineNetLabelSolver/inline-net-label-anchored-label-clearance.test.ts +28 -0
- package/tests/solvers/InlineNetLabelSolver/push-anchored-net-labels-away.test.ts +227 -0
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import type { Bounds, Point } from "@tscircuit/math-utils"
|
|
2
|
+
import {
|
|
3
|
+
getPinMap,
|
|
4
|
+
getTracePins,
|
|
5
|
+
} from "lib/solvers/AvailableNetOrientationSolver/traces"
|
|
6
|
+
import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
7
|
+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
8
|
+
import type { InputProblem } from "lib/types/InputProblem"
|
|
9
|
+
import { dir, type FacingDirection } from "lib/utils/dir"
|
|
10
|
+
import { boundsOverlap, getTextBoxBounds } from "lib/utils/textBoxBounds"
|
|
11
|
+
import type { InlineNetLabelPlacement } from "./InlineNetLabelSolver"
|
|
12
|
+
|
|
13
|
+
const LABEL_CLEARANCE = 0.05
|
|
14
|
+
const POINT_EPSILON = 1e-6
|
|
15
|
+
const CONTIGUOUS_LABEL_GAP = 0.01
|
|
16
|
+
const MAX_OUTWARD_DISTANCE = 5
|
|
17
|
+
|
|
18
|
+
const getBounds = (placement: {
|
|
19
|
+
center: Point
|
|
20
|
+
width: number
|
|
21
|
+
height: number
|
|
22
|
+
axis?: "x" | "y"
|
|
23
|
+
}): Bounds => {
|
|
24
|
+
const renderedWidth =
|
|
25
|
+
placement.axis === "y" ? placement.height : placement.width
|
|
26
|
+
const renderedHeight =
|
|
27
|
+
placement.axis === "y" ? placement.width : placement.height
|
|
28
|
+
return {
|
|
29
|
+
minX: placement.center.x - renderedWidth / 2,
|
|
30
|
+
maxX: placement.center.x + renderedWidth / 2,
|
|
31
|
+
minY: placement.center.y - renderedHeight / 2,
|
|
32
|
+
maxY: placement.center.y + renderedHeight / 2,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const pointsEqual = (a: Point, b: Point) =>
|
|
37
|
+
Math.abs(a.x - b.x) <= POINT_EPSILON && Math.abs(a.y - b.y) <= POINT_EPSILON
|
|
38
|
+
|
|
39
|
+
const pathIntersectsBounds = (path: Point[], bounds: Bounds) => {
|
|
40
|
+
for (let index = 0; index < path.length - 1; index++) {
|
|
41
|
+
const start = path[index]!
|
|
42
|
+
const end = path[index + 1]!
|
|
43
|
+
const segmentBounds: Bounds = {
|
|
44
|
+
minX: Math.min(start.x, end.x),
|
|
45
|
+
maxX: Math.max(start.x, end.x),
|
|
46
|
+
minY: Math.min(start.y, end.y),
|
|
47
|
+
maxY: Math.max(start.y, end.y),
|
|
48
|
+
}
|
|
49
|
+
if (boundsOverlap(segmentBounds, bounds)) return true
|
|
50
|
+
}
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const isPointOnPath = (point: Point, path: Point[]) => {
|
|
55
|
+
for (let index = 0; index < path.length - 1; index++) {
|
|
56
|
+
const start = path[index]!
|
|
57
|
+
const end = path[index + 1]!
|
|
58
|
+
const minX = Math.min(start.x, end.x) - POINT_EPSILON
|
|
59
|
+
const maxX = Math.max(start.x, end.x) + POINT_EPSILON
|
|
60
|
+
const minY = Math.min(start.y, end.y) - POINT_EPSILON
|
|
61
|
+
const maxY = Math.max(start.y, end.y) + POINT_EPSILON
|
|
62
|
+
const isHorizontal = Math.abs(start.y - end.y) <= POINT_EPSILON
|
|
63
|
+
const isVertical = Math.abs(start.x - end.x) <= POINT_EPSILON
|
|
64
|
+
if (
|
|
65
|
+
((isHorizontal && Math.abs(point.y - start.y) <= POINT_EPSILON) ||
|
|
66
|
+
(isVertical && Math.abs(point.x - start.x) <= POINT_EPSILON)) &&
|
|
67
|
+
point.x >= minX &&
|
|
68
|
+
point.x <= maxX &&
|
|
69
|
+
point.y >= minY &&
|
|
70
|
+
point.y <= maxY
|
|
71
|
+
) {
|
|
72
|
+
return true
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return false
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const getRequiredOutwardDistance = (
|
|
79
|
+
label: NetLabelPlacement,
|
|
80
|
+
inlineBounds: Bounds[],
|
|
81
|
+
) => {
|
|
82
|
+
const labelBounds = getBounds(label)
|
|
83
|
+
|
|
84
|
+
if (label.orientation === "x-" || label.orientation === "x+") {
|
|
85
|
+
const nearby = inlineBounds.filter(
|
|
86
|
+
(bounds) =>
|
|
87
|
+
labelBounds.minY < bounds.maxY && labelBounds.maxY > bounds.minY,
|
|
88
|
+
)
|
|
89
|
+
if (nearby.length === 0) return 0
|
|
90
|
+
if (label.orientation === "x-") {
|
|
91
|
+
const targetMaxX = Math.min(...nearby.map((bounds) => bounds.minX))
|
|
92
|
+
return Math.max(0, labelBounds.maxX - targetMaxX + LABEL_CLEARANCE)
|
|
93
|
+
}
|
|
94
|
+
const targetMinX = Math.max(...nearby.map((bounds) => bounds.maxX))
|
|
95
|
+
return Math.max(0, targetMinX - labelBounds.minX + LABEL_CLEARANCE)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const nearby = inlineBounds.filter(
|
|
99
|
+
(bounds) =>
|
|
100
|
+
labelBounds.minX < bounds.maxX && labelBounds.maxX > bounds.minX,
|
|
101
|
+
)
|
|
102
|
+
if (nearby.length === 0) return 0
|
|
103
|
+
if (label.orientation === "y-") {
|
|
104
|
+
const targetMaxY = Math.min(...nearby.map((bounds) => bounds.minY))
|
|
105
|
+
return Math.max(0, labelBounds.maxY - targetMaxY + LABEL_CLEARANCE)
|
|
106
|
+
}
|
|
107
|
+
const targetMinY = Math.max(...nearby.map((bounds) => bounds.maxY))
|
|
108
|
+
return Math.max(0, targetMinY - labelBounds.minY + LABEL_CLEARANCE)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const moveLabel = (
|
|
112
|
+
label: NetLabelPlacement,
|
|
113
|
+
orientation: FacingDirection,
|
|
114
|
+
distance: number,
|
|
115
|
+
): NetLabelPlacement => {
|
|
116
|
+
const direction = dir(orientation)
|
|
117
|
+
return {
|
|
118
|
+
...label,
|
|
119
|
+
anchorPoint: {
|
|
120
|
+
x: label.anchorPoint.x + direction.x * distance,
|
|
121
|
+
y: label.anchorPoint.y + direction.y * distance,
|
|
122
|
+
},
|
|
123
|
+
center: {
|
|
124
|
+
x: label.center.x + direction.x * distance,
|
|
125
|
+
y: label.center.y + direction.y * distance,
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const getDistanceToShoveBoundsPast = (
|
|
131
|
+
obstacleBounds: Bounds,
|
|
132
|
+
movingBounds: Bounds,
|
|
133
|
+
orientation: FacingDirection,
|
|
134
|
+
) => {
|
|
135
|
+
switch (orientation) {
|
|
136
|
+
case "x-":
|
|
137
|
+
return obstacleBounds.maxX - movingBounds.minX + LABEL_CLEARANCE
|
|
138
|
+
case "x+":
|
|
139
|
+
return movingBounds.maxX - obstacleBounds.minX + LABEL_CLEARANCE
|
|
140
|
+
case "y-":
|
|
141
|
+
return obstacleBounds.maxY - movingBounds.minY + LABEL_CLEARANCE
|
|
142
|
+
case "y+":
|
|
143
|
+
return movingBounds.maxY - obstacleBounds.minY + LABEL_CLEARANCE
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const boundsGapOnPerpendicularAxis = (
|
|
148
|
+
a: Bounds,
|
|
149
|
+
b: Bounds,
|
|
150
|
+
orientation: FacingDirection,
|
|
151
|
+
) => {
|
|
152
|
+
if (orientation === "x-" || orientation === "x+") {
|
|
153
|
+
return Math.max(0, a.minY - b.maxY, b.minY - a.maxY)
|
|
154
|
+
}
|
|
155
|
+
return Math.max(0, a.minX - b.maxX, b.minX - a.maxX)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const sharesOwnerChip = (
|
|
159
|
+
label: NetLabelPlacement,
|
|
160
|
+
ownerChipIds: Set<string>,
|
|
161
|
+
chipIdByPinId: Map<string, string>,
|
|
162
|
+
) =>
|
|
163
|
+
label.pinIds.some((pinId) => ownerChipIds.has(chipIdByPinId.get(pinId) ?? ""))
|
|
164
|
+
|
|
165
|
+
const isGeneratedLabelConnector = (trace: SolvedTracePath) =>
|
|
166
|
+
trace.mspPairId.startsWith("available-net-orientation-") ||
|
|
167
|
+
trace.mspPairId.startsWith("inline-net-label-clearance-")
|
|
168
|
+
|
|
169
|
+
const findConnectorTraceIndex = (
|
|
170
|
+
label: NetLabelPlacement,
|
|
171
|
+
traces: SolvedTracePath[],
|
|
172
|
+
) =>
|
|
173
|
+
traces.findIndex((trace) => {
|
|
174
|
+
if (trace.globalConnNetId !== label.globalConnNetId) return false
|
|
175
|
+
if (!isGeneratedLabelConnector(trace)) return false
|
|
176
|
+
const first = trace.tracePath[0]
|
|
177
|
+
const last = trace.tracePath.at(-1)
|
|
178
|
+
return Boolean(
|
|
179
|
+
(first && pointsEqual(first, label.anchorPoint)) ||
|
|
180
|
+
(last && pointsEqual(last, label.anchorPoint)),
|
|
181
|
+
)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
const canAddConnectorAtAnchor = (
|
|
185
|
+
label: NetLabelPlacement,
|
|
186
|
+
traces: SolvedTracePath[],
|
|
187
|
+
pinMap: ReturnType<typeof getPinMap>,
|
|
188
|
+
) => {
|
|
189
|
+
if (
|
|
190
|
+
label.pinIds.some((pinId) => {
|
|
191
|
+
const pin = pinMap[pinId]
|
|
192
|
+
return pin && pointsEqual(pin, label.anchorPoint)
|
|
193
|
+
})
|
|
194
|
+
) {
|
|
195
|
+
return true
|
|
196
|
+
}
|
|
197
|
+
return traces.some(
|
|
198
|
+
(trace) =>
|
|
199
|
+
trace.globalConnNetId === label.globalConnNetId &&
|
|
200
|
+
isPointOnPath(label.anchorPoint, trace.tracePath),
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const moveConnectorEndpoint = (
|
|
205
|
+
trace: SolvedTracePath,
|
|
206
|
+
oldAnchor: Point,
|
|
207
|
+
newAnchor: Point,
|
|
208
|
+
): SolvedTracePath => {
|
|
209
|
+
const tracePath = trace.tracePath.map((point) => ({ ...point }))
|
|
210
|
+
if (pointsEqual(tracePath[0]!, oldAnchor)) tracePath[0] = newAnchor
|
|
211
|
+
if (pointsEqual(tracePath.at(-1)!, oldAnchor)) {
|
|
212
|
+
tracePath[tracePath.length - 1] = newAnchor
|
|
213
|
+
}
|
|
214
|
+
return { ...trace, tracePath }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const createConnectorTrace = ({
|
|
218
|
+
label,
|
|
219
|
+
labelIndex,
|
|
220
|
+
newAnchor,
|
|
221
|
+
pinMap,
|
|
222
|
+
}: {
|
|
223
|
+
label: NetLabelPlacement
|
|
224
|
+
labelIndex: number
|
|
225
|
+
newAnchor: Point
|
|
226
|
+
pinMap: ReturnType<typeof getPinMap>
|
|
227
|
+
}): SolvedTracePath => {
|
|
228
|
+
const mspPairId = `inline-net-label-clearance-${labelIndex}-${label.netId ?? label.globalConnNetId}`
|
|
229
|
+
return {
|
|
230
|
+
mspPairId,
|
|
231
|
+
dcConnNetId: label.dcConnNetId ?? label.globalConnNetId,
|
|
232
|
+
globalConnNetId: label.globalConnNetId,
|
|
233
|
+
userNetId: label.netId,
|
|
234
|
+
pins: getTracePins(label, pinMap),
|
|
235
|
+
tracePath: [label.anchorPoint, newAnchor],
|
|
236
|
+
mspConnectionPairIds: [mspPairId],
|
|
237
|
+
pinIds: label.pinIds,
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const getContiguousLabelGroup = ({
|
|
242
|
+
triggerIndex,
|
|
243
|
+
labels,
|
|
244
|
+
chipIdByPinId,
|
|
245
|
+
}: {
|
|
246
|
+
triggerIndex: number
|
|
247
|
+
labels: NetLabelPlacement[]
|
|
248
|
+
chipIdByPinId: Map<string, string>
|
|
249
|
+
}) => {
|
|
250
|
+
const trigger = labels[triggerIndex]!
|
|
251
|
+
const ownerChipIds = new Set(
|
|
252
|
+
trigger.pinIds.flatMap((pinId) => {
|
|
253
|
+
const chipId = chipIdByPinId.get(pinId)
|
|
254
|
+
return chipId ? [chipId] : []
|
|
255
|
+
}),
|
|
256
|
+
)
|
|
257
|
+
const candidates = labels
|
|
258
|
+
.map((label, labelIndex) => ({ label, labelIndex }))
|
|
259
|
+
.filter(
|
|
260
|
+
({ label }) =>
|
|
261
|
+
label.orientation === trigger.orientation &&
|
|
262
|
+
label.mspConnectionPairIds.length === 0 &&
|
|
263
|
+
sharesOwnerChip(label, ownerChipIds, chipIdByPinId),
|
|
264
|
+
)
|
|
265
|
+
const group = new Set([triggerIndex])
|
|
266
|
+
let changed = true
|
|
267
|
+
while (changed) {
|
|
268
|
+
changed = false
|
|
269
|
+
for (const { label, labelIndex } of candidates) {
|
|
270
|
+
if (group.has(labelIndex)) continue
|
|
271
|
+
if (
|
|
272
|
+
[...group].some(
|
|
273
|
+
(memberIndex) =>
|
|
274
|
+
boundsGapOnPerpendicularAxis(
|
|
275
|
+
getBounds(labels[memberIndex]!),
|
|
276
|
+
getBounds(label),
|
|
277
|
+
trigger.orientation,
|
|
278
|
+
) <= CONTIGUOUS_LABEL_GAP,
|
|
279
|
+
)
|
|
280
|
+
) {
|
|
281
|
+
group.add(labelIndex)
|
|
282
|
+
changed = true
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return { group, ownerChipIds }
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Pushes conventional endpoint labels past nearby inline label text.
|
|
291
|
+
*
|
|
292
|
+
* Contiguous conventional labels on the same component side move as a group,
|
|
293
|
+
* keeping their connector tips aligned. When that new column encounters
|
|
294
|
+
* another label belonging to the same component, the obstacle is shoved one
|
|
295
|
+
* column farther outward and receives its own short connector. The entire
|
|
296
|
+
* proposal is rejected if a chip, component text, inline label, fixed label,
|
|
297
|
+
* or unrelated trace would still be hit.
|
|
298
|
+
*/
|
|
299
|
+
export const pushAnchoredNetLabelsAwayFromInlineLabels = ({
|
|
300
|
+
inputProblem,
|
|
301
|
+
traces,
|
|
302
|
+
netLabelPlacements,
|
|
303
|
+
inlineNetLabelPlacements,
|
|
304
|
+
}: {
|
|
305
|
+
inputProblem: InputProblem
|
|
306
|
+
traces: SolvedTracePath[]
|
|
307
|
+
netLabelPlacements: NetLabelPlacement[]
|
|
308
|
+
inlineNetLabelPlacements: InlineNetLabelPlacement[]
|
|
309
|
+
}): {
|
|
310
|
+
traces: SolvedTracePath[]
|
|
311
|
+
netLabelPlacements: NetLabelPlacement[]
|
|
312
|
+
movedLabelCount: number
|
|
313
|
+
} => {
|
|
314
|
+
const outputTraces = traces.map((trace) => ({
|
|
315
|
+
...trace,
|
|
316
|
+
tracePath: trace.tracePath.map((point) => ({ ...point })),
|
|
317
|
+
}))
|
|
318
|
+
const outputLabels = netLabelPlacements.map((label) => ({ ...label }))
|
|
319
|
+
const inlineBounds = inlineNetLabelPlacements.map(getBounds)
|
|
320
|
+
const pinMap = getPinMap(inputProblem)
|
|
321
|
+
const chipIdByPinId = new Map<string, string>()
|
|
322
|
+
for (const chip of inputProblem.chips) {
|
|
323
|
+
for (const pin of chip.pins) chipIdByPinId.set(pin.pinId, chip.chipId)
|
|
324
|
+
}
|
|
325
|
+
const movedLabelIndices = new Set<number>()
|
|
326
|
+
|
|
327
|
+
for (
|
|
328
|
+
let triggerIndex = 0;
|
|
329
|
+
triggerIndex < outputLabels.length;
|
|
330
|
+
triggerIndex++
|
|
331
|
+
) {
|
|
332
|
+
const trigger = outputLabels[triggerIndex]!
|
|
333
|
+
const distance = getRequiredOutwardDistance(trigger, inlineBounds)
|
|
334
|
+
if (distance <= POINT_EPSILON || distance > MAX_OUTWARD_DISTANCE) continue
|
|
335
|
+
|
|
336
|
+
const { group, ownerChipIds } = getContiguousLabelGroup({
|
|
337
|
+
triggerIndex,
|
|
338
|
+
labels: outputLabels,
|
|
339
|
+
chipIdByPinId,
|
|
340
|
+
})
|
|
341
|
+
const distances = new Map<number, number>(
|
|
342
|
+
[...group].map((labelIndex) => [labelIndex, distance]),
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
let failed = false
|
|
346
|
+
for (let iteration = 0; iteration < outputLabels.length; iteration++) {
|
|
347
|
+
let adjustedObstacle = false
|
|
348
|
+
for (const [movingIndex, movingDistance] of distances) {
|
|
349
|
+
const movingBounds = getBounds(
|
|
350
|
+
moveLabel(
|
|
351
|
+
outputLabels[movingIndex]!,
|
|
352
|
+
trigger.orientation,
|
|
353
|
+
movingDistance,
|
|
354
|
+
),
|
|
355
|
+
)
|
|
356
|
+
for (
|
|
357
|
+
let obstacleIndex = 0;
|
|
358
|
+
obstacleIndex < outputLabels.length;
|
|
359
|
+
obstacleIndex++
|
|
360
|
+
) {
|
|
361
|
+
if (group.has(obstacleIndex)) continue
|
|
362
|
+
const obstacle = outputLabels[obstacleIndex]!
|
|
363
|
+
const existingObstacleDistance = distances.get(obstacleIndex) ?? 0
|
|
364
|
+
const obstacleBounds = getBounds(
|
|
365
|
+
moveLabel(obstacle, trigger.orientation, existingObstacleDistance),
|
|
366
|
+
)
|
|
367
|
+
if (!boundsOverlap(movingBounds, obstacleBounds)) continue
|
|
368
|
+
if (
|
|
369
|
+
!sharesOwnerChip(obstacle, ownerChipIds, chipIdByPinId) ||
|
|
370
|
+
(findConnectorTraceIndex(obstacle, outputTraces) === -1 &&
|
|
371
|
+
!canAddConnectorAtAnchor(obstacle, outputTraces, pinMap))
|
|
372
|
+
) {
|
|
373
|
+
failed = true
|
|
374
|
+
break
|
|
375
|
+
}
|
|
376
|
+
const shoveDistance = getDistanceToShoveBoundsPast(
|
|
377
|
+
getBounds(obstacle),
|
|
378
|
+
movingBounds,
|
|
379
|
+
trigger.orientation,
|
|
380
|
+
)
|
|
381
|
+
if (
|
|
382
|
+
shoveDistance > MAX_OUTWARD_DISTANCE ||
|
|
383
|
+
shoveDistance <= existingObstacleDistance + POINT_EPSILON
|
|
384
|
+
) {
|
|
385
|
+
failed = true
|
|
386
|
+
break
|
|
387
|
+
}
|
|
388
|
+
distances.set(obstacleIndex, shoveDistance)
|
|
389
|
+
adjustedObstacle = true
|
|
390
|
+
}
|
|
391
|
+
if (failed) break
|
|
392
|
+
}
|
|
393
|
+
if (failed || !adjustedObstacle) break
|
|
394
|
+
}
|
|
395
|
+
if (failed) continue
|
|
396
|
+
|
|
397
|
+
const proposals = new Map<number, NetLabelPlacement>()
|
|
398
|
+
for (const [labelIndex, labelDistance] of distances) {
|
|
399
|
+
proposals.set(
|
|
400
|
+
labelIndex,
|
|
401
|
+
moveLabel(
|
|
402
|
+
outputLabels[labelIndex]!,
|
|
403
|
+
trigger.orientation,
|
|
404
|
+
labelDistance,
|
|
405
|
+
),
|
|
406
|
+
)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const finalLabelAt = (labelIndex: number) =>
|
|
410
|
+
proposals.get(labelIndex) ?? outputLabels[labelIndex]!
|
|
411
|
+
for (const [labelIndex, movedLabel] of proposals) {
|
|
412
|
+
const movedBounds = getBounds(movedLabel)
|
|
413
|
+
if (inlineBounds.some((bounds) => boundsOverlap(movedBounds, bounds))) {
|
|
414
|
+
failed = true
|
|
415
|
+
break
|
|
416
|
+
}
|
|
417
|
+
if (
|
|
418
|
+
inputProblem.chips.some((chip) =>
|
|
419
|
+
boundsOverlap(movedBounds, {
|
|
420
|
+
minX: chip.center.x - chip.width / 2,
|
|
421
|
+
maxX: chip.center.x + chip.width / 2,
|
|
422
|
+
minY: chip.center.y - chip.height / 2,
|
|
423
|
+
maxY: chip.center.y + chip.height / 2,
|
|
424
|
+
}),
|
|
425
|
+
) ||
|
|
426
|
+
(inputProblem.textBoxes ?? []).some((textBox) =>
|
|
427
|
+
boundsOverlap(movedBounds, getTextBoxBounds(textBox)),
|
|
428
|
+
)
|
|
429
|
+
) {
|
|
430
|
+
failed = true
|
|
431
|
+
break
|
|
432
|
+
}
|
|
433
|
+
if (
|
|
434
|
+
outputLabels.some(
|
|
435
|
+
(_, otherIndex) =>
|
|
436
|
+
otherIndex !== labelIndex &&
|
|
437
|
+
boundsOverlap(movedBounds, getBounds(finalLabelAt(otherIndex))),
|
|
438
|
+
)
|
|
439
|
+
) {
|
|
440
|
+
failed = true
|
|
441
|
+
break
|
|
442
|
+
}
|
|
443
|
+
if (
|
|
444
|
+
outputTraces.some(
|
|
445
|
+
(trace) =>
|
|
446
|
+
trace.globalConnNetId !== movedLabel.globalConnNetId &&
|
|
447
|
+
pathIntersectsBounds(trace.tracePath, movedBounds),
|
|
448
|
+
)
|
|
449
|
+
) {
|
|
450
|
+
failed = true
|
|
451
|
+
break
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (failed) continue
|
|
455
|
+
|
|
456
|
+
const connectorUpdates: Array<{
|
|
457
|
+
labelIndex: number
|
|
458
|
+
connectorIndex: number
|
|
459
|
+
trace: SolvedTracePath
|
|
460
|
+
}> = []
|
|
461
|
+
for (const [labelIndex, movedLabel] of proposals) {
|
|
462
|
+
const label = outputLabels[labelIndex]!
|
|
463
|
+
const connectorIndex = findConnectorTraceIndex(label, outputTraces)
|
|
464
|
+
if (
|
|
465
|
+
connectorIndex === -1 &&
|
|
466
|
+
!canAddConnectorAtAnchor(label, outputTraces, pinMap)
|
|
467
|
+
) {
|
|
468
|
+
failed = true
|
|
469
|
+
break
|
|
470
|
+
}
|
|
471
|
+
const connector =
|
|
472
|
+
connectorIndex === -1
|
|
473
|
+
? createConnectorTrace({
|
|
474
|
+
label,
|
|
475
|
+
labelIndex,
|
|
476
|
+
newAnchor: movedLabel.anchorPoint,
|
|
477
|
+
pinMap,
|
|
478
|
+
})
|
|
479
|
+
: moveConnectorEndpoint(
|
|
480
|
+
outputTraces[connectorIndex]!,
|
|
481
|
+
label.anchorPoint,
|
|
482
|
+
movedLabel.anchorPoint,
|
|
483
|
+
)
|
|
484
|
+
const connectorObstructed =
|
|
485
|
+
inlineBounds.some((bounds) =>
|
|
486
|
+
pathIntersectsBounds(connector.tracePath, bounds),
|
|
487
|
+
) ||
|
|
488
|
+
inputProblem.chips.some((chip) =>
|
|
489
|
+
pathIntersectsBounds(connector.tracePath, {
|
|
490
|
+
minX: chip.center.x - chip.width / 2,
|
|
491
|
+
maxX: chip.center.x + chip.width / 2,
|
|
492
|
+
minY: chip.center.y - chip.height / 2,
|
|
493
|
+
maxY: chip.center.y + chip.height / 2,
|
|
494
|
+
}),
|
|
495
|
+
) ||
|
|
496
|
+
(inputProblem.textBoxes ?? []).some((textBox) =>
|
|
497
|
+
pathIntersectsBounds(connector.tracePath, getTextBoxBounds(textBox)),
|
|
498
|
+
) ||
|
|
499
|
+
outputLabels.some(
|
|
500
|
+
(_, otherIndex) =>
|
|
501
|
+
otherIndex !== labelIndex &&
|
|
502
|
+
pathIntersectsBounds(
|
|
503
|
+
connector.tracePath,
|
|
504
|
+
getBounds(finalLabelAt(otherIndex)),
|
|
505
|
+
),
|
|
506
|
+
)
|
|
507
|
+
if (connectorObstructed) {
|
|
508
|
+
failed = true
|
|
509
|
+
break
|
|
510
|
+
}
|
|
511
|
+
connectorUpdates.push({ labelIndex, connectorIndex, trace: connector })
|
|
512
|
+
}
|
|
513
|
+
if (failed) continue
|
|
514
|
+
|
|
515
|
+
for (const [labelIndex, movedLabel] of proposals) {
|
|
516
|
+
outputLabels[labelIndex] = movedLabel
|
|
517
|
+
movedLabelIndices.add(labelIndex)
|
|
518
|
+
}
|
|
519
|
+
for (const update of connectorUpdates) {
|
|
520
|
+
if (update.connectorIndex === -1) outputTraces.push(update.trace)
|
|
521
|
+
else outputTraces[update.connectorIndex] = update.trace
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return {
|
|
526
|
+
traces: outputTraces,
|
|
527
|
+
netLabelPlacements: outputLabels,
|
|
528
|
+
movedLabelCount: movedLabelIndices.size,
|
|
529
|
+
}
|
|
530
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"chips": [
|
|
3
|
+
{
|
|
4
|
+
"chipId": "U1",
|
|
5
|
+
"center": { "x": 0, "y": 0.2 },
|
|
6
|
+
"width": 1.4,
|
|
7
|
+
"height": 1,
|
|
8
|
+
"pins": [
|
|
9
|
+
{
|
|
10
|
+
"pinId": "U1.minus",
|
|
11
|
+
"x": -0.7,
|
|
12
|
+
"y": 0.4,
|
|
13
|
+
"_facingDirection": "x-"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"pinId": "U1.plus",
|
|
17
|
+
"x": -0.7,
|
|
18
|
+
"y": 0.2,
|
|
19
|
+
"_facingDirection": "x-"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"pinId": "U1.swclk",
|
|
23
|
+
"x": -0.7,
|
|
24
|
+
"y": 0,
|
|
25
|
+
"_facingDirection": "x-"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"directConnections": [],
|
|
31
|
+
"netConnections": [
|
|
32
|
+
{
|
|
33
|
+
"netId": "D_MINUS",
|
|
34
|
+
"pinIds": ["U1.minus"],
|
|
35
|
+
"netLabelWidth": 0.96
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"netId": "D_PLUS",
|
|
39
|
+
"pinIds": ["U1.plus"],
|
|
40
|
+
"netLabelWidth": 0.84
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"netId": "SWCLK",
|
|
44
|
+
"pinIds": ["U1.swclk"],
|
|
45
|
+
"netLabelWidth": 0.48,
|
|
46
|
+
"allowInlineNetLabel": true,
|
|
47
|
+
"inlineNetLabelWidth": 0.48,
|
|
48
|
+
"inlineNetLabelHeight": 0.12
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
"availableNetLabelOrientations": {
|
|
52
|
+
"D_MINUS": ["x-"],
|
|
53
|
+
"D_PLUS": ["x-"],
|
|
54
|
+
"SWCLK": ["x-"]
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<svg width="640" height="640" viewBox="0 0 640 640" xmlns="http://www.w3.org/2000/svg"><rect width="100%" height="100%" fill="white"/><g><polyline data-points="-0.7,0.2 -1.3290000000000002,0.2" data-type="line" data-label="" points="337.7926421404682,320 219.98662207357856,320" fill="none" stroke="purple" stroke-width="1px"/></g><g><polyline data-points="-0.7,0.4 -1.3290000000000002,0.4" data-type="line" data-label="" points="337.7926421404682,282.54180602006693 219.98662207357856,282.54180602006693" fill="none" stroke="purple" stroke-width="1px"/></g><g><polyline data-points="-0.7,0 -1.38,0" data-type="line" data-label="" points="337.7926421404682,357.4581939799331 210.43478260869568,357.4581939799331" fill="none" stroke="purple" stroke-width="1px"/></g><g><rect data-type="rect" data-label="U1" data-x="0" data-y="0.2" x="337.79264214046816" y="226.35451505016724" width="262.2073578595318" height="187.29096989966553" fill="hsl(164, 100%, 50%, 0.8)" stroke="black" stroke-width="0.005339285714285715"/></g><g><rect data-type="rect" data-label="netId: D_MINUS
|
|
2
|
+
globalConnNetId: connectivity_net0" data-x="-1.81" data-y="0.4" x="40" y="263.81270903010034" width="179.79933110367892" height="37.45819397993313" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.005339285714285715"/></g><g><rect data-type="rect" data-label="netId: D_PLUS
|
|
3
|
+
globalConnNetId: connectivity_net1" data-x="-1.75" data-y="0.2" x="62.474916387959894" y="301.2709030100334" width="157.324414715719" height="37.45819397993313" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.005339285714285715"/></g><g><rect data-type="rect" data-label="INLINE netId: SWCLK
|
|
4
|
+
axis: x
|
|
5
|
+
side: y+" data-x="-1.04" data-y="0.11" x="229.16387959866222" y="325.61872909698997" width="89.89966555183949" height="22.474916387959865" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.005339285714285715"/></g><text data-type="text" data-label="SWCLK" data-x="-0.8" data-y="0.11" x="319.0635451505017" y="336.8561872909699" fill="green" font-size="22.474916387959862" font-family="sans-serif" text-anchor="end" dominant-baseline="central">SWCLK</text><g><circle data-type="point" data-label="U1.minus
|
|
6
|
+
x-" data-x="-0.7" data-y="0.4" cx="337.7926421404682" cy="282.54180602006693" r="3" fill="hsl(166, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="U1.plus
|
|
7
|
+
x-" data-x="-0.7" data-y="0.2" cx="337.7926421404682" cy="320" r="3" fill="hsl(92, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="U1.swclk
|
|
8
|
+
x-" data-x="-0.7" data-y="0" cx="337.7926421404682" cy="357.4581939799331" r="3" fill="hsl(308, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="anchorPoint
|
|
9
|
+
orientation: x-" data-x="-1.3290000000000002" data-y="0.4" cx="219.98662207357856" cy="282.54180602006693" r="3" fill="hsl(40, 100%, 50%, 0.9)"/></g><g><circle data-type="point" data-label="anchorPoint
|
|
10
|
+
orientation: x-" data-x="-1.3290000000000002" data-y="0.2" cx="219.98662207357856" cy="320" r="3" fill="hsl(40, 100%, 50%, 0.9)"/></g><g><circle data-type="point" data-label="inline anchor
|
|
11
|
+
SWCLK" data-x="-1.04" data-y="0" cx="274.11371237458195" cy="357.4581939799331" r="3" fill="green"/></g><g id="crosshair" style="display: none"><line id="crosshair-h" y1="0" y2="640" stroke="#666" stroke-width="0.5"/><line id="crosshair-v" x1="0" x2="640" stroke="#666" stroke-width="0.5"/><text id="coordinates" font-family="monospace" font-size="12" fill="#666"></text></g><script><![CDATA[
|
|
12
|
+
document.currentScript.parentElement.addEventListener('mousemove', (e) => {
|
|
13
|
+
const svg = e.currentTarget;
|
|
14
|
+
const rect = svg.getBoundingClientRect();
|
|
15
|
+
const x = e.clientX - rect.left;
|
|
16
|
+
const y = e.clientY - rect.top;
|
|
17
|
+
const crosshair = svg.getElementById('crosshair');
|
|
18
|
+
const h = svg.getElementById('crosshair-h');
|
|
19
|
+
const v = svg.getElementById('crosshair-v');
|
|
20
|
+
const coords = svg.getElementById('coordinates');
|
|
21
|
+
|
|
22
|
+
crosshair.style.display = 'block';
|
|
23
|
+
h.setAttribute('x1', '0');
|
|
24
|
+
h.setAttribute('x2', '640');
|
|
25
|
+
h.setAttribute('y1', y);
|
|
26
|
+
h.setAttribute('y2', y);
|
|
27
|
+
v.setAttribute('x1', x);
|
|
28
|
+
v.setAttribute('x2', x);
|
|
29
|
+
v.setAttribute('y1', '0');
|
|
30
|
+
v.setAttribute('y2', '640');
|
|
31
|
+
|
|
32
|
+
// Calculate real coordinates using inverse transformation
|
|
33
|
+
const matrix = {"a":187.29096989966553,"c":0,"e":468.8963210702341,"b":0,"d":-187.29096989966553,"f":357.4581939799331};
|
|
34
|
+
// Manually invert and apply the affine transform
|
|
35
|
+
// Since we only use translate and scale, we can directly compute:
|
|
36
|
+
// x' = (x - tx) / sx
|
|
37
|
+
// y' = (y - ty) / sy
|
|
38
|
+
const sx = matrix.a;
|
|
39
|
+
const sy = matrix.d;
|
|
40
|
+
const tx = matrix.e;
|
|
41
|
+
const ty = matrix.f;
|
|
42
|
+
const realPoint = {
|
|
43
|
+
x: (x - tx) / sx,
|
|
44
|
+
y: (y - ty) / sy // Flip y back since we used negative scale
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
coords.textContent = `(${realPoint.x.toFixed(2)}, ${realPoint.y.toFixed(2)})`;
|
|
48
|
+
coords.setAttribute('x', (x + 5).toString());
|
|
49
|
+
coords.setAttribute('y', (y - 5).toString());
|
|
50
|
+
});
|
|
51
|
+
document.currentScript.parentElement.addEventListener('mouseleave', () => {
|
|
52
|
+
document.currentScript.parentElement.getElementById('crosshair').style.display = 'none';
|
|
53
|
+
});
|
|
54
|
+
]]></script></svg>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
|
|
3
|
+
import inputProblem from "../../assets/inline-net-label-anchored-label-clearance.json"
|
|
4
|
+
import "tests/fixtures/matcher"
|
|
5
|
+
|
|
6
|
+
test("regular labels are drawn beyond adjacent inline labels", () => {
|
|
7
|
+
const solver = new SchematicTracePipelineSolver(inputProblem as any)
|
|
8
|
+
|
|
9
|
+
solver.solve()
|
|
10
|
+
|
|
11
|
+
const output = solver.inlineNetLabelSolver!.getOutput()
|
|
12
|
+
const regularLabels = output.netLabelPlacements.filter((label) =>
|
|
13
|
+
["D_MINUS", "D_PLUS"].includes(label.netId ?? ""),
|
|
14
|
+
)
|
|
15
|
+
expect(solver.inlineNetLabelSolver!.stats.pushedAnchoredNetLabelCount).toBe(2)
|
|
16
|
+
expect(regularLabels).toHaveLength(2)
|
|
17
|
+
expect(regularLabels[0]!.anchorPoint.x).toBeCloseTo(
|
|
18
|
+
regularLabels[1]!.anchorPoint.x,
|
|
19
|
+
)
|
|
20
|
+
expect(regularLabels[0]!.anchorPoint.x).toBeLessThan(-0.7)
|
|
21
|
+
expect(
|
|
22
|
+
output.traces.filter((trace) =>
|
|
23
|
+
trace.mspPairId.startsWith("inline-net-label-clearance-"),
|
|
24
|
+
),
|
|
25
|
+
).toHaveLength(2)
|
|
26
|
+
|
|
27
|
+
expect(solver).toMatchSolverSnapshot(import.meta.path)
|
|
28
|
+
})
|