@tscircuit/schematic-trace-solver 0.0.89 → 0.0.90
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 +4 -0
- package/dist/index.js +82 -7
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts +116 -5
- package/lib/testing/PipelineStageDebugRunner.ts +21 -4
- package/package.json +1 -1
- package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +22 -22
- package/tests/bug-reports/bug-report-20260707T020342Z/__snapshots__/bug-report-20260707T020342Z.snap.svg +20 -22
- package/tests/bug-reports/bug-report-20260707T092615Z/__snapshots__/bug-report-20260707T092615Z.snap.svg +3 -3
- package/tests/bug-reports/bug-report-20260707T141421Z/__snapshots__/bug-report-20260707T141421Z.snap.svg +70 -72
- package/tests/examples/__snapshots__/example42.snap.svg +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -170,6 +170,7 @@ declare class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
170
170
|
chipMap: Record<string, InputChip>;
|
|
171
171
|
obstacles: ObstacleRect[];
|
|
172
172
|
textObstacles: Set<ObstacleRect>;
|
|
173
|
+
endpointTextObstacles: Set<ObstacleRect>;
|
|
173
174
|
aabb: {
|
|
174
175
|
minX: number;
|
|
175
176
|
maxX: number;
|
|
@@ -192,6 +193,9 @@ declare class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
192
193
|
private getNetLabelHeightForConnectionPair;
|
|
193
194
|
private axisOfSegment;
|
|
194
195
|
private pathLength;
|
|
196
|
+
private getPinBandPenalty;
|
|
197
|
+
private pathCost;
|
|
198
|
+
private isSegmentOutsidePinBand;
|
|
195
199
|
_step(): void;
|
|
196
200
|
visualize(): GraphicsObject;
|
|
197
201
|
}
|
package/dist/index.js
CHANGED
|
@@ -854,6 +854,7 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
|
|
|
854
854
|
chipMap;
|
|
855
855
|
obstacles;
|
|
856
856
|
textObstacles;
|
|
857
|
+
endpointTextObstacles;
|
|
857
858
|
aabb;
|
|
858
859
|
baseElbow;
|
|
859
860
|
solvedTracePath = null;
|
|
@@ -877,6 +878,12 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
|
|
|
877
878
|
this.textObstacles = new Set(
|
|
878
879
|
this.obstacles.filter((r) => r.kind === "text_box")
|
|
879
880
|
);
|
|
881
|
+
const endpointChipIds = new Set(this.pins.map((pin) => pin.chipId));
|
|
882
|
+
this.endpointTextObstacles = new Set(
|
|
883
|
+
endpointChipIds.size > 1 ? this.obstacles.filter(
|
|
884
|
+
(r) => r.kind === "text_box" && r.textBox.chipId !== void 0 && endpointChipIds.has(r.textBox.chipId)
|
|
885
|
+
) : []
|
|
886
|
+
);
|
|
880
887
|
const [pin1, pin2] = this.pins;
|
|
881
888
|
this.baseElbow = calculateElbow(
|
|
882
889
|
{
|
|
@@ -985,6 +992,31 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
|
|
|
985
992
|
}
|
|
986
993
|
return sum;
|
|
987
994
|
}
|
|
995
|
+
getPinBandPenalty(path) {
|
|
996
|
+
let penalty = 0;
|
|
997
|
+
for (let i = 1; i < path.length - 2; i++) {
|
|
998
|
+
const a = path[i];
|
|
999
|
+
const b = path[i + 1];
|
|
1000
|
+
if (isHorizontal(a, b) && a.y > this.aabb.minY && a.y < this.aabb.maxY) {
|
|
1001
|
+
penalty += 10;
|
|
1002
|
+
} else if (isVertical(a, b) && a.x > this.aabb.minX && a.x < this.aabb.maxX) {
|
|
1003
|
+
penalty += 10;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
return penalty;
|
|
1007
|
+
}
|
|
1008
|
+
pathCost(path) {
|
|
1009
|
+
return this.pathLength(path) + this.getPinBandPenalty(path);
|
|
1010
|
+
}
|
|
1011
|
+
isSegmentOutsidePinBand(a, b) {
|
|
1012
|
+
if (isHorizontal(a, b)) {
|
|
1013
|
+
return a.y <= this.aabb.minY || a.y >= this.aabb.maxY;
|
|
1014
|
+
}
|
|
1015
|
+
if (isVertical(a, b)) {
|
|
1016
|
+
return a.x <= this.aabb.minX || a.x >= this.aabb.maxX;
|
|
1017
|
+
}
|
|
1018
|
+
return false;
|
|
1019
|
+
}
|
|
988
1020
|
_step() {
|
|
989
1021
|
if (this.solvedTracePath) {
|
|
990
1022
|
this.solved = true;
|
|
@@ -1000,10 +1032,17 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
|
|
|
1000
1032
|
const [PA, PB] = this.pins;
|
|
1001
1033
|
const collision = findFirstCollision(path, this.obstacles, {
|
|
1002
1034
|
excludeRectsForSegment: (segIndex2) => {
|
|
1003
|
-
const
|
|
1004
|
-
if (segIndex2 === 0 || segIndex2 ===
|
|
1035
|
+
const lastSegIndex2 = path.length - 2;
|
|
1036
|
+
if (segIndex2 === 0 || segIndex2 === lastSegIndex2) {
|
|
1005
1037
|
return this.textObstacles;
|
|
1006
1038
|
}
|
|
1039
|
+
const adjacentEndpointSegIndex = segIndex2 === 1 ? 2 : segIndex2 === lastSegIndex2 - 1 ? lastSegIndex2 - 2 : null;
|
|
1040
|
+
if (adjacentEndpointSegIndex !== null && adjacentEndpointSegIndex >= 0 && adjacentEndpointSegIndex < lastSegIndex2 && this.isSegmentOutsidePinBand(
|
|
1041
|
+
path[adjacentEndpointSegIndex],
|
|
1042
|
+
path[adjacentEndpointSegIndex + 1]
|
|
1043
|
+
)) {
|
|
1044
|
+
return this.endpointTextObstacles;
|
|
1045
|
+
}
|
|
1007
1046
|
return /* @__PURE__ */ new Set();
|
|
1008
1047
|
}
|
|
1009
1048
|
});
|
|
@@ -1018,6 +1057,7 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
|
|
|
1018
1057
|
}
|
|
1019
1058
|
return;
|
|
1020
1059
|
}
|
|
1060
|
+
const originalSegIndex = collision.segIndex;
|
|
1021
1061
|
let { segIndex, rect } = collision;
|
|
1022
1062
|
const isFirstSegment = segIndex === 0;
|
|
1023
1063
|
const isLastSegment = segIndex === path.length - 2;
|
|
@@ -1050,16 +1090,51 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
|
|
|
1050
1090
|
candidates.push(...mids);
|
|
1051
1091
|
}
|
|
1052
1092
|
const newStates = [];
|
|
1053
|
-
|
|
1054
|
-
const newPath = shiftSegmentOrth(
|
|
1055
|
-
|
|
1093
|
+
const addShiftedCandidate = (candidateSegIndex, candidateAxis, coord) => {
|
|
1094
|
+
const newPath = shiftSegmentOrth(
|
|
1095
|
+
path,
|
|
1096
|
+
candidateSegIndex,
|
|
1097
|
+
candidateAxis,
|
|
1098
|
+
coord
|
|
1099
|
+
);
|
|
1100
|
+
if (!newPath) return;
|
|
1056
1101
|
const key = pathKey(newPath);
|
|
1057
|
-
if (this.visited.has(key))
|
|
1102
|
+
if (this.visited.has(key)) return;
|
|
1058
1103
|
this.visited.add(key);
|
|
1059
1104
|
const nextSet = new Set(collisionRects);
|
|
1060
1105
|
nextSet.add(rect);
|
|
1061
|
-
const len = this.
|
|
1106
|
+
const len = this.pathCost(newPath);
|
|
1062
1107
|
newStates.push({ path: newPath, collisionRects: nextSet, len });
|
|
1108
|
+
};
|
|
1109
|
+
for (const coord of candidates) {
|
|
1110
|
+
addShiftedCandidate(segIndex, axis, coord);
|
|
1111
|
+
}
|
|
1112
|
+
const lastSegIndex = path.length - 2;
|
|
1113
|
+
const adjacentSegmentIndexes = originalSegIndex === 1 ? [2] : originalSegIndex === lastSegIndex - 1 ? [lastSegIndex - 2] : [];
|
|
1114
|
+
for (const adjacentSegIndex of adjacentSegmentIndexes) {
|
|
1115
|
+
if (adjacentSegIndex < 0 || adjacentSegIndex >= lastSegIndex) continue;
|
|
1116
|
+
const adjacentAxis = this.axisOfSegment(
|
|
1117
|
+
path[adjacentSegIndex],
|
|
1118
|
+
path[adjacentSegIndex + 1]
|
|
1119
|
+
);
|
|
1120
|
+
if (!adjacentAxis || adjacentAxis === axis) continue;
|
|
1121
|
+
const adjacentCandidates = [
|
|
1122
|
+
...midBetweenPointAndRect(adjacentAxis, { x: PA.x, y: PA.y }, rect),
|
|
1123
|
+
...midBetweenPointAndRect(adjacentAxis, { x: PB.x, y: PB.y }, rect)
|
|
1124
|
+
];
|
|
1125
|
+
if (collisionRects.size > 0) {
|
|
1126
|
+
adjacentCandidates.push(
|
|
1127
|
+
...candidateMidsFromSet(
|
|
1128
|
+
adjacentAxis,
|
|
1129
|
+
rect,
|
|
1130
|
+
collisionRects,
|
|
1131
|
+
this.aabb
|
|
1132
|
+
)
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
for (const coord of [...new Set(adjacentCandidates)]) {
|
|
1136
|
+
addShiftedCandidate(adjacentSegIndex, adjacentAxis, coord);
|
|
1137
|
+
}
|
|
1063
1138
|
}
|
|
1064
1139
|
newStates.sort((a2, b2) => a2.len - b2.len);
|
|
1065
1140
|
for (const st of newStates) {
|
|
@@ -29,6 +29,7 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
29
29
|
|
|
30
30
|
obstacles: ObstacleRect[]
|
|
31
31
|
textObstacles: Set<ObstacleRect>
|
|
32
|
+
endpointTextObstacles: Set<ObstacleRect>
|
|
32
33
|
aabb: { minX: number; maxX: number; minY: number; maxY: number }
|
|
33
34
|
|
|
34
35
|
baseElbow: Point[]
|
|
@@ -67,6 +68,17 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
67
68
|
this.textObstacles = new Set(
|
|
68
69
|
this.obstacles.filter((r) => r.kind === "text_box"),
|
|
69
70
|
)
|
|
71
|
+
const endpointChipIds = new Set(this.pins.map((pin) => pin.chipId))
|
|
72
|
+
this.endpointTextObstacles = new Set(
|
|
73
|
+
endpointChipIds.size > 1
|
|
74
|
+
? this.obstacles.filter(
|
|
75
|
+
(r) =>
|
|
76
|
+
r.kind === "text_box" &&
|
|
77
|
+
r.textBox.chipId !== undefined &&
|
|
78
|
+
endpointChipIds.has(r.textBox.chipId),
|
|
79
|
+
)
|
|
80
|
+
: [],
|
|
81
|
+
)
|
|
70
82
|
|
|
71
83
|
// Build initial elbow path
|
|
72
84
|
const [pin1, pin2] = this.pins
|
|
@@ -203,6 +215,38 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
203
215
|
return sum
|
|
204
216
|
}
|
|
205
217
|
|
|
218
|
+
private getPinBandPenalty(path: Point[]): number {
|
|
219
|
+
let penalty = 0
|
|
220
|
+
for (let i = 1; i < path.length - 2; i++) {
|
|
221
|
+
const a = path[i]!
|
|
222
|
+
const b = path[i + 1]!
|
|
223
|
+
if (isHorizontal(a, b) && a.y > this.aabb.minY && a.y < this.aabb.maxY) {
|
|
224
|
+
penalty += 10
|
|
225
|
+
} else if (
|
|
226
|
+
isVertical(a, b) &&
|
|
227
|
+
a.x > this.aabb.minX &&
|
|
228
|
+
a.x < this.aabb.maxX
|
|
229
|
+
) {
|
|
230
|
+
penalty += 10
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return penalty
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private pathCost(path: Point[]): number {
|
|
237
|
+
return this.pathLength(path) + this.getPinBandPenalty(path)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private isSegmentOutsidePinBand(a: Point, b: Point): boolean {
|
|
241
|
+
if (isHorizontal(a, b)) {
|
|
242
|
+
return a.y <= this.aabb.minY || a.y >= this.aabb.maxY
|
|
243
|
+
}
|
|
244
|
+
if (isVertical(a, b)) {
|
|
245
|
+
return a.x <= this.aabb.minX || a.x >= this.aabb.maxX
|
|
246
|
+
}
|
|
247
|
+
return false
|
|
248
|
+
}
|
|
249
|
+
|
|
206
250
|
override _step() {
|
|
207
251
|
if (this.solvedTracePath) {
|
|
208
252
|
this.solved = true
|
|
@@ -225,6 +269,23 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
225
269
|
if (segIndex === 0 || segIndex === lastSegIndex) {
|
|
226
270
|
return this.textObstacles
|
|
227
271
|
}
|
|
272
|
+
const adjacentEndpointSegIndex =
|
|
273
|
+
segIndex === 1
|
|
274
|
+
? 2
|
|
275
|
+
: segIndex === lastSegIndex - 1
|
|
276
|
+
? lastSegIndex - 2
|
|
277
|
+
: null
|
|
278
|
+
if (
|
|
279
|
+
adjacentEndpointSegIndex !== null &&
|
|
280
|
+
adjacentEndpointSegIndex >= 0 &&
|
|
281
|
+
adjacentEndpointSegIndex < lastSegIndex &&
|
|
282
|
+
this.isSegmentOutsidePinBand(
|
|
283
|
+
path[adjacentEndpointSegIndex]!,
|
|
284
|
+
path[adjacentEndpointSegIndex + 1]!,
|
|
285
|
+
)
|
|
286
|
+
) {
|
|
287
|
+
return this.endpointTextObstacles
|
|
288
|
+
}
|
|
228
289
|
return new Set<ObstacleRect>()
|
|
229
290
|
},
|
|
230
291
|
})
|
|
@@ -246,6 +307,7 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
246
307
|
return
|
|
247
308
|
}
|
|
248
309
|
|
|
310
|
+
const originalSegIndex = collision.segIndex
|
|
249
311
|
let { segIndex, rect } = collision
|
|
250
312
|
|
|
251
313
|
// Never move the first or last segments - move adjacent segment instead
|
|
@@ -301,18 +363,67 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
301
363
|
len: number
|
|
302
364
|
}> = []
|
|
303
365
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
366
|
+
const addShiftedCandidate = (
|
|
367
|
+
candidateSegIndex: number,
|
|
368
|
+
candidateAxis: Axis,
|
|
369
|
+
coord: number,
|
|
370
|
+
) => {
|
|
371
|
+
const newPath = shiftSegmentOrth(
|
|
372
|
+
path,
|
|
373
|
+
candidateSegIndex,
|
|
374
|
+
candidateAxis,
|
|
375
|
+
coord,
|
|
376
|
+
)
|
|
377
|
+
if (!newPath) return
|
|
307
378
|
const key = pathKey(newPath)
|
|
308
|
-
if (this.visited.has(key))
|
|
379
|
+
if (this.visited.has(key)) return
|
|
309
380
|
this.visited.add(key)
|
|
310
381
|
const nextSet = new Set(collisionRects)
|
|
311
382
|
nextSet.add(rect)
|
|
312
|
-
const len = this.
|
|
383
|
+
const len = this.pathCost(newPath)
|
|
313
384
|
newStates.push({ path: newPath, collisionRects: nextSet, len })
|
|
314
385
|
}
|
|
315
386
|
|
|
387
|
+
for (const coord of candidates) {
|
|
388
|
+
addShiftedCandidate(segIndex, axis, coord)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const lastSegIndex = path.length - 2
|
|
392
|
+
const adjacentSegmentIndexes =
|
|
393
|
+
originalSegIndex === 1
|
|
394
|
+
? [2]
|
|
395
|
+
: originalSegIndex === lastSegIndex - 1
|
|
396
|
+
? [lastSegIndex - 2]
|
|
397
|
+
: []
|
|
398
|
+
|
|
399
|
+
for (const adjacentSegIndex of adjacentSegmentIndexes) {
|
|
400
|
+
if (adjacentSegIndex < 0 || adjacentSegIndex >= lastSegIndex) continue
|
|
401
|
+
const adjacentAxis = this.axisOfSegment(
|
|
402
|
+
path[adjacentSegIndex]!,
|
|
403
|
+
path[adjacentSegIndex + 1]!,
|
|
404
|
+
)
|
|
405
|
+
if (!adjacentAxis || adjacentAxis === axis) continue
|
|
406
|
+
|
|
407
|
+
const adjacentCandidates = [
|
|
408
|
+
...midBetweenPointAndRect(adjacentAxis, { x: PA.x, y: PA.y }, rect),
|
|
409
|
+
...midBetweenPointAndRect(adjacentAxis, { x: PB.x, y: PB.y }, rect),
|
|
410
|
+
]
|
|
411
|
+
if (collisionRects.size > 0) {
|
|
412
|
+
adjacentCandidates.push(
|
|
413
|
+
...candidateMidsFromSet(
|
|
414
|
+
adjacentAxis,
|
|
415
|
+
rect,
|
|
416
|
+
collisionRects,
|
|
417
|
+
this.aabb,
|
|
418
|
+
),
|
|
419
|
+
)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
for (const coord of [...new Set(adjacentCandidates)]) {
|
|
423
|
+
addShiftedCandidate(adjacentSegIndex, adjacentAxis, coord)
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
316
427
|
newStates.sort((a, b) => a.len - b.len)
|
|
317
428
|
for (const st of newStates) {
|
|
318
429
|
this.queue.push({ path: st.path, collisionRects: st.collisionRects })
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { mkdir, writeFile } from "node:fs/promises"
|
|
2
2
|
import path from "node:path"
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
getSvgFromGraphicsObject,
|
|
6
|
-
} from "graphics-debug"
|
|
3
|
+
import * as graphicsDebug from "graphics-debug"
|
|
4
|
+
import { getSvgFromGraphicsObject } from "graphics-debug"
|
|
7
5
|
import type { GraphicsObject } from "graphics-debug"
|
|
8
6
|
import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
|
|
9
7
|
import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
|
|
@@ -262,6 +260,25 @@ export class PipelineStageDebugRunner {
|
|
|
262
260
|
}
|
|
263
261
|
|
|
264
262
|
private renderGraphicsToPng(graphics: GraphicsObject): Promise<Uint8Array> {
|
|
263
|
+
const getPngBufferFromGraphicsObject = (
|
|
264
|
+
graphicsDebug as unknown as {
|
|
265
|
+
getPngBufferFromGraphicsObject?: (
|
|
266
|
+
graphics: GraphicsObject,
|
|
267
|
+
opts: {
|
|
268
|
+
backgroundColor: string
|
|
269
|
+
pngWidth: number
|
|
270
|
+
pngHeight: number
|
|
271
|
+
},
|
|
272
|
+
) => Promise<Uint8Array>
|
|
273
|
+
}
|
|
274
|
+
).getPngBufferFromGraphicsObject
|
|
275
|
+
|
|
276
|
+
if (!getPngBufferFromGraphicsObject) {
|
|
277
|
+
throw new Error(
|
|
278
|
+
"graphics-debug does not provide getPngBufferFromGraphicsObject; run with --no-png --svg or upgrade graphics-debug",
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
|
|
265
282
|
return getPngBufferFromGraphicsObject(graphics, {
|
|
266
283
|
backgroundColor: "white",
|
|
267
284
|
pngWidth: this.pngWidth,
|