@tscircuit/schematic-trace-solver 0.0.128 → 0.0.129
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 +14 -6
- package/dist/index.js +88 -10
- package/lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver.ts +142 -23
- package/package.json +1 -1
- package/tests/assets/inline-net-label02.json +112 -0
- package/tests/solvers/InlineNetLabelSolver/__snapshots__/inline-net-label02.snap.svg +57 -0
- package/tests/solvers/InlineNetLabelSolver/inline-net-label02.test.ts +59 -0
package/dist/index.d.ts
CHANGED
|
@@ -1151,12 +1151,13 @@ declare const DEFAULT_INLINE_NET_LABEL_HEIGHT = 0.18;
|
|
|
1151
1151
|
*/
|
|
1152
1152
|
declare const INLINE_NET_LABEL_TRACE_MARGIN = 0.05;
|
|
1153
1153
|
/**
|
|
1154
|
-
*
|
|
1155
|
-
*
|
|
1156
|
-
*
|
|
1157
|
-
*
|
|
1154
|
+
* Largest perpendicular deviation a route may have and still be labeled as one
|
|
1155
|
+
* span. A route that is straight except for elbow jogs up to this size reads
|
|
1156
|
+
* as a single wire, so its name may be centered over the whole route,
|
|
1157
|
+
* bridging the jogs. Anything more meandering falls back to an anchored net
|
|
1158
|
+
* label.
|
|
1158
1159
|
*/
|
|
1159
|
-
declare const
|
|
1160
|
+
declare const INLINE_NET_LABEL_MAX_SPAN_JOG = 0.4;
|
|
1160
1161
|
/**
|
|
1161
1162
|
* A net label drawn parallel to the trace it names, rather than anchored to the
|
|
1162
1163
|
* end of it. Used for point-to-point signal traces, where an anchored label
|
|
@@ -1213,6 +1214,13 @@ declare class InlineNetLabelSolver extends BaseSolver {
|
|
|
1213
1214
|
getConstructorParams(): [InlineNetLabelSolverInput];
|
|
1214
1215
|
_step(): void;
|
|
1215
1216
|
private computeInlinePlacement;
|
|
1217
|
+
/**
|
|
1218
|
+
* Places the label over the whole route when the route is straight except
|
|
1219
|
+
* for perpendicular jogs of at most INLINE_NET_LABEL_MAX_SPAN_JOG. The label
|
|
1220
|
+
* sits clear of the route's full perpendicular extent and stays within its
|
|
1221
|
+
* endpoints along the dominant axis.
|
|
1222
|
+
*/
|
|
1223
|
+
private computeSpanPlacement;
|
|
1216
1224
|
/**
|
|
1217
1225
|
* An inline label may not sit on top of a chip, a component's text, or a
|
|
1218
1226
|
* trace belonging to another net.
|
|
@@ -1297,4 +1305,4 @@ declare class SchematicTracePipelineSolver extends BaseSolver {
|
|
|
1297
1305
|
preview(): GraphicsObject;
|
|
1298
1306
|
}
|
|
1299
1307
|
|
|
1300
|
-
export { type ChipId, DEFAULT_INLINE_NET_LABEL_HEIGHT, INLINE_NET_LABEL_TRACE_MARGIN, type InlineNetLabelPlacement, InlineNetLabelSolver, type InputChip, type InputDirectConnection, type InputNetConnection, type InputPin, type InputProblem,
|
|
1308
|
+
export { type ChipId, DEFAULT_INLINE_NET_LABEL_HEIGHT, INLINE_NET_LABEL_MAX_SPAN_JOG, INLINE_NET_LABEL_TRACE_MARGIN, type InlineNetLabelPlacement, InlineNetLabelSolver, type InputChip, type InputDirectConnection, type InputNetConnection, type InputPin, type InputProblem, type NetId, type NetLabelPlacement, type PinId, SchematicTracePipelineSolver, SchematicTraceSingleLineSolver2, type SectionId, type TextBoxes, estimateInlineNetLabelWidth };
|
package/dist/index.js
CHANGED
|
@@ -11907,7 +11907,7 @@ var getAxisAlignedSegments = (path, epsilon = 1e-6) => {
|
|
|
11907
11907
|
// lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver.ts
|
|
11908
11908
|
var DEFAULT_INLINE_NET_LABEL_HEIGHT = 0.18;
|
|
11909
11909
|
var INLINE_NET_LABEL_TRACE_MARGIN = 0.05;
|
|
11910
|
-
var
|
|
11910
|
+
var INLINE_NET_LABEL_MAX_SPAN_JOG = 0.4;
|
|
11911
11911
|
var getPinPairKey2 = (pinIds) => [...pinIds].sort().join("::");
|
|
11912
11912
|
var InlineNetLabelSolver = class extends BaseSolver {
|
|
11913
11913
|
inputProblem;
|
|
@@ -11966,14 +11966,7 @@ var InlineNetLabelSolver = class extends BaseSolver {
|
|
|
11966
11966
|
const height = directConnection.inlineNetLabelHeight ?? DEFAULT_INLINE_NET_LABEL_HEIGHT;
|
|
11967
11967
|
const width = directConnection.inlineNetLabelWidth ?? directConnection.netLabelWidth ?? estimateInlineNetLabelWidth(directConnection.netId, height);
|
|
11968
11968
|
const offset = height / 2 + INLINE_NET_LABEL_TRACE_MARGIN;
|
|
11969
|
-
const usableSegments = segments.filter(
|
|
11970
|
-
(segment) => segment.length >= width * MIN_INLINE_NET_LABEL_SEGMENT_RATIO
|
|
11971
|
-
).sort((a, b) => {
|
|
11972
|
-
const aFits = a.length >= width;
|
|
11973
|
-
const bFits = b.length >= width;
|
|
11974
|
-
if (aFits !== bFits) return aFits ? -1 : 1;
|
|
11975
|
-
return b.length - a.length;
|
|
11976
|
-
});
|
|
11969
|
+
const usableSegments = segments.filter((segment) => segment.length >= width).sort((a, b) => b.length - a.length);
|
|
11977
11970
|
for (const segment of usableSegments) {
|
|
11978
11971
|
const sides = segment.axis === "x" ? ["y+", "y-"] : ["x-", "x+"];
|
|
11979
11972
|
for (const side of sides) {
|
|
@@ -12008,6 +12001,91 @@ var InlineNetLabelSolver = class extends BaseSolver {
|
|
|
12008
12001
|
}
|
|
12009
12002
|
}
|
|
12010
12003
|
}
|
|
12004
|
+
const spanPlacement = this.computeSpanPlacement({
|
|
12005
|
+
trace,
|
|
12006
|
+
directConnection,
|
|
12007
|
+
width,
|
|
12008
|
+
height,
|
|
12009
|
+
offset
|
|
12010
|
+
});
|
|
12011
|
+
if (spanPlacement) return spanPlacement;
|
|
12012
|
+
return null;
|
|
12013
|
+
}
|
|
12014
|
+
/**
|
|
12015
|
+
* Places the label over the whole route when the route is straight except
|
|
12016
|
+
* for perpendicular jogs of at most INLINE_NET_LABEL_MAX_SPAN_JOG. The label
|
|
12017
|
+
* sits clear of the route's full perpendicular extent and stays within its
|
|
12018
|
+
* endpoints along the dominant axis.
|
|
12019
|
+
*/
|
|
12020
|
+
computeSpanPlacement({
|
|
12021
|
+
trace,
|
|
12022
|
+
directConnection,
|
|
12023
|
+
width,
|
|
12024
|
+
height,
|
|
12025
|
+
offset
|
|
12026
|
+
}) {
|
|
12027
|
+
const path = trace.tracePath;
|
|
12028
|
+
if (path.length < 2) return null;
|
|
12029
|
+
let minX = Infinity;
|
|
12030
|
+
let maxX = -Infinity;
|
|
12031
|
+
let minY = Infinity;
|
|
12032
|
+
let maxY = -Infinity;
|
|
12033
|
+
for (const point of path) {
|
|
12034
|
+
minX = Math.min(minX, point.x);
|
|
12035
|
+
maxX = Math.max(maxX, point.x);
|
|
12036
|
+
minY = Math.min(minY, point.y);
|
|
12037
|
+
maxY = Math.max(maxY, point.y);
|
|
12038
|
+
}
|
|
12039
|
+
const extentX = maxX - minX;
|
|
12040
|
+
const extentY = maxY - minY;
|
|
12041
|
+
const axis = extentX >= extentY ? "x" : "y";
|
|
12042
|
+
const spanLength = axis === "x" ? extentX : extentY;
|
|
12043
|
+
const jog = axis === "x" ? extentY : extentX;
|
|
12044
|
+
if (jog > INLINE_NET_LABEL_MAX_SPAN_JOG) return null;
|
|
12045
|
+
if (spanLength < width) return null;
|
|
12046
|
+
const spanStart = axis === "x" ? minX : minY;
|
|
12047
|
+
const spanEnd = axis === "x" ? maxX : maxY;
|
|
12048
|
+
const mid = (spanStart + spanEnd) / 2;
|
|
12049
|
+
const halfRange = (spanLength - width) / 2;
|
|
12050
|
+
const offsets = [0];
|
|
12051
|
+
for (let value = 0.1; value <= halfRange + 1e-9; value += 0.1) {
|
|
12052
|
+
offsets.push(value, -value);
|
|
12053
|
+
}
|
|
12054
|
+
offsets.push(halfRange, -halfRange);
|
|
12055
|
+
const sides = axis === "x" ? ["y+", "y-"] : ["x-", "x+"];
|
|
12056
|
+
for (const side of sides) {
|
|
12057
|
+
for (const alongOffset of offsets) {
|
|
12058
|
+
const along = mid + alongOffset;
|
|
12059
|
+
const center = side === "y+" ? { x: along, y: maxY + offset } : side === "y-" ? { x: along, y: minY - offset } : side === "x-" ? { x: minX - offset, y: along } : { x: maxX + offset, y: along };
|
|
12060
|
+
const halfAlong = width / 2;
|
|
12061
|
+
const halfAcross = height / 2;
|
|
12062
|
+
const bounds = axis === "x" ? {
|
|
12063
|
+
minX: center.x - halfAlong,
|
|
12064
|
+
maxX: center.x + halfAlong,
|
|
12065
|
+
minY: center.y - halfAcross,
|
|
12066
|
+
maxY: center.y + halfAcross
|
|
12067
|
+
} : {
|
|
12068
|
+
minX: center.x - halfAcross,
|
|
12069
|
+
maxX: center.x + halfAcross,
|
|
12070
|
+
minY: center.y - halfAlong,
|
|
12071
|
+
maxY: center.y + halfAlong
|
|
12072
|
+
};
|
|
12073
|
+
if (this.isObstructed(bounds, trace)) continue;
|
|
12074
|
+
const anchorPoint = axis === "x" ? { x: along, y: side === "y+" ? maxY : minY } : { x: side === "x-" ? minX : maxX, y: along };
|
|
12075
|
+
return {
|
|
12076
|
+
globalConnNetId: trace.globalConnNetId,
|
|
12077
|
+
netId: directConnection.netId,
|
|
12078
|
+
mspPairId: trace.mspPairId,
|
|
12079
|
+
pinIds: [...directConnection.pinIds],
|
|
12080
|
+
axis,
|
|
12081
|
+
anchorPoint,
|
|
12082
|
+
center,
|
|
12083
|
+
width,
|
|
12084
|
+
height,
|
|
12085
|
+
side
|
|
12086
|
+
};
|
|
12087
|
+
}
|
|
12088
|
+
}
|
|
12011
12089
|
return null;
|
|
12012
12090
|
}
|
|
12013
12091
|
/**
|
|
@@ -12674,9 +12752,9 @@ var SchematicTracePipelineSolver = class extends BaseSolver {
|
|
|
12674
12752
|
};
|
|
12675
12753
|
export {
|
|
12676
12754
|
DEFAULT_INLINE_NET_LABEL_HEIGHT,
|
|
12755
|
+
INLINE_NET_LABEL_MAX_SPAN_JOG,
|
|
12677
12756
|
INLINE_NET_LABEL_TRACE_MARGIN,
|
|
12678
12757
|
InlineNetLabelSolver,
|
|
12679
|
-
MIN_INLINE_NET_LABEL_SEGMENT_RATIO,
|
|
12680
12758
|
SchematicTracePipelineSolver,
|
|
12681
12759
|
SchematicTraceSingleLineSolver2,
|
|
12682
12760
|
estimateInlineNetLabelWidth
|
|
@@ -24,12 +24,13 @@ export const DEFAULT_INLINE_NET_LABEL_HEIGHT = 0.18
|
|
|
24
24
|
export const INLINE_NET_LABEL_TRACE_MARGIN = 0.05
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
27
|
+
* Largest perpendicular deviation a route may have and still be labeled as one
|
|
28
|
+
* span. A route that is straight except for elbow jogs up to this size reads
|
|
29
|
+
* as a single wire, so its name may be centered over the whole route,
|
|
30
|
+
* bridging the jogs. Anything more meandering falls back to an anchored net
|
|
31
|
+
* label.
|
|
31
32
|
*/
|
|
32
|
-
export const
|
|
33
|
+
export const INLINE_NET_LABEL_MAX_SPAN_JOG = 0.4
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
36
|
* A net label drawn parallel to the trace it names, rather than anchored to the
|
|
@@ -165,20 +166,14 @@ export class InlineNetLabelSolver extends BaseSolver {
|
|
|
165
166
|
|
|
166
167
|
const offset = height / 2 + INLINE_NET_LABEL_TRACE_MARGIN
|
|
167
168
|
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
// side, and positions near the middle before
|
|
169
|
+
// First choice: a straight run the label fully fits on, longest first, so
|
|
170
|
+
// the text hugs the wire it names. Within a run, try the preferred side
|
|
171
|
+
// before the far side, and positions near the middle before the ends. The
|
|
172
|
+
// label never extends past the end of a run - a name hanging off its wire
|
|
173
|
+
// reads as floating text.
|
|
171
174
|
const usableSegments = segments
|
|
172
|
-
.filter(
|
|
173
|
-
|
|
174
|
-
segment.length >= width * MIN_INLINE_NET_LABEL_SEGMENT_RATIO,
|
|
175
|
-
)
|
|
176
|
-
.sort((a, b) => {
|
|
177
|
-
const aFits = a.length >= width
|
|
178
|
-
const bFits = b.length >= width
|
|
179
|
-
if (aFits !== bFits) return aFits ? -1 : 1
|
|
180
|
-
return b.length - a.length
|
|
181
|
-
})
|
|
175
|
+
.filter((segment) => segment.length >= width)
|
|
176
|
+
.sort((a, b) => b.length - a.length)
|
|
182
177
|
|
|
183
178
|
for (const segment of usableSegments) {
|
|
184
179
|
// "Above" the trace: +y for a horizontal trace, and -x for a vertical one
|
|
@@ -232,11 +227,138 @@ export class InlineNetLabelSolver extends BaseSolver {
|
|
|
232
227
|
}
|
|
233
228
|
}
|
|
234
229
|
|
|
230
|
+
// Second choice: no single run fits, but a route that is straight except
|
|
231
|
+
// for small jogs still reads as one wire - center the label over the
|
|
232
|
+
// route's overall span, bridging the jogs, without extending past its
|
|
233
|
+
// endpoints.
|
|
234
|
+
const spanPlacement = this.computeSpanPlacement({
|
|
235
|
+
trace,
|
|
236
|
+
directConnection,
|
|
237
|
+
width,
|
|
238
|
+
height,
|
|
239
|
+
offset,
|
|
240
|
+
})
|
|
241
|
+
if (spanPlacement) return spanPlacement
|
|
242
|
+
|
|
235
243
|
// No room anywhere along the trace. Leave the net to the regular anchored
|
|
236
244
|
// net label rather than drawing the name over a chip.
|
|
237
245
|
return null
|
|
238
246
|
}
|
|
239
247
|
|
|
248
|
+
/**
|
|
249
|
+
* Places the label over the whole route when the route is straight except
|
|
250
|
+
* for perpendicular jogs of at most INLINE_NET_LABEL_MAX_SPAN_JOG. The label
|
|
251
|
+
* sits clear of the route's full perpendicular extent and stays within its
|
|
252
|
+
* endpoints along the dominant axis.
|
|
253
|
+
*/
|
|
254
|
+
private computeSpanPlacement({
|
|
255
|
+
trace,
|
|
256
|
+
directConnection,
|
|
257
|
+
width,
|
|
258
|
+
height,
|
|
259
|
+
offset,
|
|
260
|
+
}: {
|
|
261
|
+
trace: SolvedTracePath
|
|
262
|
+
directConnection: InputDirectConnection
|
|
263
|
+
width: number
|
|
264
|
+
height: number
|
|
265
|
+
offset: number
|
|
266
|
+
}): InlineNetLabelPlacement | null {
|
|
267
|
+
const path = trace.tracePath
|
|
268
|
+
if (path.length < 2) return null
|
|
269
|
+
|
|
270
|
+
let minX = Infinity
|
|
271
|
+
let maxX = -Infinity
|
|
272
|
+
let minY = Infinity
|
|
273
|
+
let maxY = -Infinity
|
|
274
|
+
for (const point of path) {
|
|
275
|
+
minX = Math.min(minX, point.x)
|
|
276
|
+
maxX = Math.max(maxX, point.x)
|
|
277
|
+
minY = Math.min(minY, point.y)
|
|
278
|
+
maxY = Math.max(maxY, point.y)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const extentX = maxX - minX
|
|
282
|
+
const extentY = maxY - minY
|
|
283
|
+
const axis: InlineNetLabelPlacement["axis"] = extentX >= extentY ? "x" : "y"
|
|
284
|
+
const spanLength = axis === "x" ? extentX : extentY
|
|
285
|
+
const jog = axis === "x" ? extentY : extentX
|
|
286
|
+
|
|
287
|
+
if (jog > INLINE_NET_LABEL_MAX_SPAN_JOG) return null
|
|
288
|
+
if (spanLength < width) return null
|
|
289
|
+
|
|
290
|
+
const spanStart = axis === "x" ? minX : minY
|
|
291
|
+
const spanEnd = axis === "x" ? maxX : maxY
|
|
292
|
+
const mid = (spanStart + spanEnd) / 2
|
|
293
|
+
|
|
294
|
+
// Slide candidates within the span, centered first (same scheme as the
|
|
295
|
+
// per-segment stage).
|
|
296
|
+
const halfRange = (spanLength - width) / 2
|
|
297
|
+
const offsets: number[] = [0]
|
|
298
|
+
for (let value = 0.1; value <= halfRange + 1e-9; value += 0.1) {
|
|
299
|
+
offsets.push(value, -value)
|
|
300
|
+
}
|
|
301
|
+
offsets.push(halfRange, -halfRange)
|
|
302
|
+
|
|
303
|
+
const sides: InlineNetLabelPlacement["side"][] =
|
|
304
|
+
axis === "x" ? ["y+", "y-"] : ["x-", "x+"]
|
|
305
|
+
|
|
306
|
+
for (const side of sides) {
|
|
307
|
+
for (const alongOffset of offsets) {
|
|
308
|
+
const along = mid + alongOffset
|
|
309
|
+
// The label clears the far side of every jog, not just the run it is
|
|
310
|
+
// nearest to.
|
|
311
|
+
const center: Point =
|
|
312
|
+
side === "y+"
|
|
313
|
+
? { x: along, y: maxY + offset }
|
|
314
|
+
: side === "y-"
|
|
315
|
+
? { x: along, y: minY - offset }
|
|
316
|
+
: side === "x-"
|
|
317
|
+
? { x: minX - offset, y: along }
|
|
318
|
+
: { x: maxX + offset, y: along }
|
|
319
|
+
|
|
320
|
+
const halfAlong = width / 2
|
|
321
|
+
const halfAcross = height / 2
|
|
322
|
+
const bounds: Bounds =
|
|
323
|
+
axis === "x"
|
|
324
|
+
? {
|
|
325
|
+
minX: center.x - halfAlong,
|
|
326
|
+
maxX: center.x + halfAlong,
|
|
327
|
+
minY: center.y - halfAcross,
|
|
328
|
+
maxY: center.y + halfAcross,
|
|
329
|
+
}
|
|
330
|
+
: {
|
|
331
|
+
minX: center.x - halfAcross,
|
|
332
|
+
maxX: center.x + halfAcross,
|
|
333
|
+
minY: center.y - halfAlong,
|
|
334
|
+
maxY: center.y + halfAlong,
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (this.isObstructed(bounds, trace)) continue
|
|
338
|
+
|
|
339
|
+
const anchorPoint: Point =
|
|
340
|
+
axis === "x"
|
|
341
|
+
? { x: along, y: side === "y+" ? maxY : minY }
|
|
342
|
+
: { x: side === "x-" ? minX : maxX, y: along }
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
globalConnNetId: trace.globalConnNetId,
|
|
346
|
+
netId: directConnection.netId,
|
|
347
|
+
mspPairId: trace.mspPairId,
|
|
348
|
+
pinIds: [...directConnection.pinIds],
|
|
349
|
+
axis,
|
|
350
|
+
anchorPoint,
|
|
351
|
+
center,
|
|
352
|
+
width,
|
|
353
|
+
height,
|
|
354
|
+
side,
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return null
|
|
360
|
+
}
|
|
361
|
+
|
|
240
362
|
/**
|
|
241
363
|
* An inline label may not sit on top of a chip, a component's text, or a
|
|
242
364
|
* trace belonging to another net.
|
|
@@ -374,11 +496,8 @@ export const estimateInlineNetLabelWidth = (
|
|
|
374
496
|
/**
|
|
375
497
|
* Points along a segment where the label's midpoint could sit, ordered from the
|
|
376
498
|
* middle of the segment outwards. Sliding the label along the wire lets it
|
|
377
|
-
* dodge a chip that only crowds one end.
|
|
378
|
-
*
|
|
379
|
-
* When the label is shorter than the segment, candidates are limited to
|
|
380
|
-
* positions that keep it fully on the wire; when it is longer, only the
|
|
381
|
-
* midpoint is offered (it will overhang either way).
|
|
499
|
+
* dodge a chip that only crowds one end. Candidates always keep the label
|
|
500
|
+
* fully on the wire; callers only pass segments the label fits on.
|
|
382
501
|
*/
|
|
383
502
|
const getAnchorCandidates = (
|
|
384
503
|
segment: AxisAlignedSegment,
|
package/package.json
CHANGED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
{
|
|
2
|
+
"chips": [
|
|
3
|
+
{
|
|
4
|
+
"chipId": "schematic_component_0",
|
|
5
|
+
"center": {
|
|
6
|
+
"x": 0,
|
|
7
|
+
"y": 0
|
|
8
|
+
},
|
|
9
|
+
"width": 0.4,
|
|
10
|
+
"height": 1,
|
|
11
|
+
"pins": [
|
|
12
|
+
{
|
|
13
|
+
"pinId": "schematic_port_0",
|
|
14
|
+
"x": -0.6000000000000001,
|
|
15
|
+
"y": 0.30000000000000004,
|
|
16
|
+
"_facingDirection": "x-"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"pinId": "schematic_port_1",
|
|
20
|
+
"x": -0.6000000000000001,
|
|
21
|
+
"y": 0.10000000000000003,
|
|
22
|
+
"_facingDirection": "x-"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"pinId": "schematic_port_2",
|
|
26
|
+
"x": -0.6000000000000001,
|
|
27
|
+
"y": -0.09999999999999998,
|
|
28
|
+
"_facingDirection": "x-"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"pinId": "schematic_port_3",
|
|
32
|
+
"x": -0.6000000000000001,
|
|
33
|
+
"y": -0.30000000000000004,
|
|
34
|
+
"_facingDirection": "x-"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"pinId": "schematic_port_4",
|
|
38
|
+
"x": 0.6000000000000001,
|
|
39
|
+
"y": -0.30000000000000004,
|
|
40
|
+
"_facingDirection": "x+"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"pinId": "schematic_port_5",
|
|
44
|
+
"x": 0.6000000000000001,
|
|
45
|
+
"y": -0.10000000000000003,
|
|
46
|
+
"_facingDirection": "x+"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"pinId": "schematic_port_6",
|
|
50
|
+
"x": 0.6000000000000001,
|
|
51
|
+
"y": 0.09999999999999998,
|
|
52
|
+
"_facingDirection": "x+"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"pinId": "schematic_port_7",
|
|
56
|
+
"x": 0.6000000000000001,
|
|
57
|
+
"y": 0.30000000000000004,
|
|
58
|
+
"_facingDirection": "x+"
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"chipId": "schematic_component_1",
|
|
64
|
+
"center": {
|
|
65
|
+
"x": 3,
|
|
66
|
+
"y": 0.13249999999999998
|
|
67
|
+
},
|
|
68
|
+
"width": 1.13,
|
|
69
|
+
"height": 0.915,
|
|
70
|
+
"pins": [
|
|
71
|
+
{
|
|
72
|
+
"pinId": "schematic_port_8",
|
|
73
|
+
"x": 2.46,
|
|
74
|
+
"y": 0,
|
|
75
|
+
"_facingDirection": "x-"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"pinId": "schematic_port_9",
|
|
79
|
+
"x": 3.54,
|
|
80
|
+
"y": 0,
|
|
81
|
+
"_facingDirection": "x+"
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
],
|
|
86
|
+
"directConnections": [
|
|
87
|
+
{
|
|
88
|
+
"netId": "USER_LED_ANODE",
|
|
89
|
+
"allowInlineNetLabel": true,
|
|
90
|
+
"inlineNetLabelWidth": 1.8,
|
|
91
|
+
"pinIds": [
|
|
92
|
+
"schematic_port_4",
|
|
93
|
+
"schematic_port_8"
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
],
|
|
97
|
+
"netConnections": [],
|
|
98
|
+
"textBoxes": [
|
|
99
|
+
{
|
|
100
|
+
"chipId": "schematic_component_0",
|
|
101
|
+
"center": {
|
|
102
|
+
"x": -0.08000000000000002,
|
|
103
|
+
"y": 0.615
|
|
104
|
+
},
|
|
105
|
+
"width": 0.36,
|
|
106
|
+
"height": 0.25,
|
|
107
|
+
"text": "U1"
|
|
108
|
+
}
|
|
109
|
+
],
|
|
110
|
+
"availableNetLabelOrientations": {},
|
|
111
|
+
"maxMspPairDistance": 2.4
|
|
112
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
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.6000000000000001,-0.30000000000000004 2.435,0" data-type="line" data-label="" points="201.34453781512605,376.47058823529414 448.06722689075633,336.1344537815126" fill="none" stroke="hsl(48, 100%, 50%, 0.8)" stroke-width="1px"/></g><g><polyline data-points="0.6000000000000001,-0.30000000000000004 1.5175,-0.30000000000000004 1.5175,0 2.435,0" data-type="line" data-label="" points="201.34453781512605,376.47058823529414 324.7058823529412,376.47058823529414 324.7058823529412,336.1344537815126 448.06722689075633,336.1344537815126" fill="none" stroke="purple" stroke-width="1px"/></g><g><rect data-type="rect" data-label="schematic_component_0" data-x="0" data-y="0" x="40" y="268.9075630252101" width="161.34453781512605" height="134.45378151260502" fill="hsl(24, 100%, 50%, 0.8)" stroke="black" stroke-width="0.0074375"/></g><g><rect data-type="rect" data-label="schematic_component_1" data-x="3" data-y="0.13249999999999998" x="448.0672268907564" y="256.8067226890756" width="151.93277310924367" height="123.02521008403363" fill="hsl(24, 100%, 50%, 0.8)" stroke="black" stroke-width="0.0074375"/></g><g><rect data-type="rect" data-label="U1" data-x="-0.08000000000000002" data-y="0.615" x="85.7142857142857" y="236.6386554621849" width="48.40336134453783" height="33.613445378151255" fill="rgba(160, 0, 220, 0.14)" stroke="black" stroke-width="0.0074375"/></g><g><rect data-type="rect" data-label="INLINE netId: USER_LED_ANODE
|
|
2
|
+
axis: x
|
|
3
|
+
side: y+" data-x="1.5175" data-y="0.14" x="203.69747899159668" y="305.21008403361344" width="242.01680672268907" height="24.201680672268935" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.0074375"/></g><text data-type="text" data-label="USER_LED_ANODE" data-x="1.5175" data-y="0.14" x="324.7058823529412" y="317.3109243697479" fill="green" font-size="24.201680672268907" font-family="sans-serif" text-anchor="middle" dominant-baseline="central">USER_LED_ANODE</text><g><circle data-type="point" data-label="schematic_port_0
|
|
4
|
+
x-" data-x="-0.6000000000000001" data-y="0.30000000000000004" cx="39.999999999999986" cy="295.79831932773106" r="3" fill="hsl(248, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="schematic_port_1
|
|
5
|
+
x-" data-x="-0.6000000000000001" data-y="0.10000000000000003" cx="39.999999999999986" cy="322.6890756302521" r="3" fill="hsl(248, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="schematic_port_2
|
|
6
|
+
x-" data-x="-0.6000000000000001" data-y="-0.09999999999999998" cx="39.999999999999986" cy="349.5798319327731" r="3" fill="hsl(248, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="schematic_port_3
|
|
7
|
+
x-" data-x="-0.6000000000000001" data-y="-0.30000000000000004" cx="39.999999999999986" cy="376.47058823529414" r="3" fill="hsl(248, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="schematic_port_4
|
|
8
|
+
x+" data-x="0.6000000000000001" data-y="-0.30000000000000004" cx="201.34453781512605" cy="376.47058823529414" r="3" fill="hsl(248, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="schematic_port_5
|
|
9
|
+
x+" data-x="0.6000000000000001" data-y="-0.10000000000000003" cx="201.34453781512605" cy="349.5798319327731" r="3" fill="hsl(248, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="schematic_port_6
|
|
10
|
+
x+" data-x="0.6000000000000001" data-y="0.09999999999999998" cx="201.34453781512605" cy="322.6890756302521" r="3" fill="hsl(248, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="schematic_port_7
|
|
11
|
+
x+" data-x="0.6000000000000001" data-y="0.30000000000000004" cx="201.34453781512605" cy="295.79831932773106" r="3" fill="hsl(248, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="schematic_port_8
|
|
12
|
+
x-" data-x="2.435" data-y="0" cx="448.06722689075633" cy="336.1344537815126" r="3" fill="hsl(248, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="schematic_port_9
|
|
13
|
+
x+" data-x="3.565" data-y="0" cx="600" cy="336.1344537815126" r="3" fill="hsl(248, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="inline anchor
|
|
14
|
+
USER_LED_ANODE" data-x="1.5175" data-y="0" cx="324.7058823529412" cy="336.1344537815126" 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[
|
|
15
|
+
document.currentScript.parentElement.addEventListener('mousemove', (e) => {
|
|
16
|
+
const svg = e.currentTarget;
|
|
17
|
+
const rect = svg.getBoundingClientRect();
|
|
18
|
+
const x = e.clientX - rect.left;
|
|
19
|
+
const y = e.clientY - rect.top;
|
|
20
|
+
const crosshair = svg.getElementById('crosshair');
|
|
21
|
+
const h = svg.getElementById('crosshair-h');
|
|
22
|
+
const v = svg.getElementById('crosshair-v');
|
|
23
|
+
const coords = svg.getElementById('coordinates');
|
|
24
|
+
|
|
25
|
+
crosshair.style.display = 'block';
|
|
26
|
+
h.setAttribute('x1', '0');
|
|
27
|
+
h.setAttribute('x2', '640');
|
|
28
|
+
h.setAttribute('y1', y);
|
|
29
|
+
h.setAttribute('y2', y);
|
|
30
|
+
v.setAttribute('x1', x);
|
|
31
|
+
v.setAttribute('x2', x);
|
|
32
|
+
v.setAttribute('y1', '0');
|
|
33
|
+
v.setAttribute('y2', '640');
|
|
34
|
+
|
|
35
|
+
// Calculate real coordinates using inverse transformation
|
|
36
|
+
const matrix = {"a":134.45378151260505,"c":0,"e":120.67226890756302,"b":0,"d":-134.45378151260505,"f":336.1344537815126};
|
|
37
|
+
// Manually invert and apply the affine transform
|
|
38
|
+
// Since we only use translate and scale, we can directly compute:
|
|
39
|
+
// x' = (x - tx) / sx
|
|
40
|
+
// y' = (y - ty) / sy
|
|
41
|
+
const sx = matrix.a;
|
|
42
|
+
const sy = matrix.d;
|
|
43
|
+
const tx = matrix.e;
|
|
44
|
+
const ty = matrix.f;
|
|
45
|
+
const realPoint = {
|
|
46
|
+
x: (x - tx) / sx,
|
|
47
|
+
y: (y - ty) / sy // Flip y back since we used negative scale
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
coords.textContent = `(${realPoint.x.toFixed(2)}, ${realPoint.y.toFixed(2)})`;
|
|
51
|
+
coords.setAttribute('x', (x + 5).toString());
|
|
52
|
+
coords.setAttribute('y', (y - 5).toString());
|
|
53
|
+
});
|
|
54
|
+
document.currentScript.parentElement.addEventListener('mouseleave', () => {
|
|
55
|
+
document.currentScript.parentElement.getElementById('crosshair').style.display = 'none';
|
|
56
|
+
});
|
|
57
|
+
]]></script></svg>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
|
|
3
|
+
import inputProblem from "../../assets/inline-net-label02.json"
|
|
4
|
+
import "tests/fixtures/matcher"
|
|
5
|
+
|
|
6
|
+
// Real geometry from @tscircuit/core: a soic8 and an 0603 LED 3 units apart.
|
|
7
|
+
// The label (width 1.8) is longer than every straight run of the routed trace
|
|
8
|
+
// (~0.95), but the route is straight except for a 0.3-unit elbow jog, so the
|
|
9
|
+
// name is centered over the route's overall span - bridging the jog - instead
|
|
10
|
+
// of falling back to an anchored label.
|
|
11
|
+
test("inline-net-label02 spans a route with a small jog", () => {
|
|
12
|
+
const solver = new SchematicTracePipelineSolver(inputProblem as any)
|
|
13
|
+
|
|
14
|
+
solver.solve()
|
|
15
|
+
|
|
16
|
+
const placements = solver.inlineNetLabelSolver!.inlineNetLabelPlacements
|
|
17
|
+
expect(placements.map((p) => p.netId)).toEqual(["USER_LED_ANODE"])
|
|
18
|
+
|
|
19
|
+
const [placement] = placements
|
|
20
|
+
expect(placement!.axis).toBe("x")
|
|
21
|
+
|
|
22
|
+
const labelBounds = {
|
|
23
|
+
minX: placement!.center.x - placement!.width / 2,
|
|
24
|
+
maxX: placement!.center.x + placement!.width / 2,
|
|
25
|
+
minY: placement!.center.y - placement!.height / 2,
|
|
26
|
+
maxY: placement!.center.y + placement!.height / 2,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// The label stays within the route's endpoints - no floating overhang.
|
|
30
|
+
const trace = solver.inlineNetLabelSolver!.traces.find(
|
|
31
|
+
(t) => t.mspPairId === placement!.mspPairId,
|
|
32
|
+
)!
|
|
33
|
+
const routeMinX = Math.min(...trace.tracePath.map((p) => p.x))
|
|
34
|
+
const routeMaxX = Math.max(...trace.tracePath.map((p) => p.x))
|
|
35
|
+
const routeMaxY = Math.max(...trace.tracePath.map((p) => p.y))
|
|
36
|
+
expect(labelBounds.minX).toBeGreaterThanOrEqual(routeMinX - 1e-9)
|
|
37
|
+
expect(labelBounds.maxX).toBeLessThanOrEqual(routeMaxX + 1e-9)
|
|
38
|
+
|
|
39
|
+
// It sits clear of the route's full perpendicular extent (above every jog).
|
|
40
|
+
expect(labelBounds.minY).toBeGreaterThan(routeMaxY)
|
|
41
|
+
|
|
42
|
+
// And never on top of a chip.
|
|
43
|
+
for (const chip of (inputProblem as any).chips) {
|
|
44
|
+
const chipBounds = {
|
|
45
|
+
minX: chip.center.x - chip.width / 2,
|
|
46
|
+
maxX: chip.center.x + chip.width / 2,
|
|
47
|
+
minY: chip.center.y - chip.height / 2,
|
|
48
|
+
maxY: chip.center.y + chip.height / 2,
|
|
49
|
+
}
|
|
50
|
+
const overlaps =
|
|
51
|
+
labelBounds.minX < chipBounds.maxX &&
|
|
52
|
+
labelBounds.maxX > chipBounds.minX &&
|
|
53
|
+
labelBounds.minY < chipBounds.maxY &&
|
|
54
|
+
labelBounds.maxY > chipBounds.minY
|
|
55
|
+
expect(overlaps).toBe(false)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
expect(solver).toMatchSolverSnapshot(import.meta.path)
|
|
59
|
+
})
|