@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 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
- * How much of the label is allowed to hang off the end of the wire it names,
1155
- * as a fraction of the label's length. A short elbow stub is technically clear
1156
- * of every obstacle but reads as a label floating in space, so runs shorter
1157
- * than this are not considered.
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 MIN_INLINE_NET_LABEL_SEGMENT_RATIO = 0.5;
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, MIN_INLINE_NET_LABEL_SEGMENT_RATIO, type NetId, type NetLabelPlacement, type PinId, SchematicTracePipelineSolver, SchematicTraceSingleLineSolver2, type SectionId, type TextBoxes, estimateInlineNetLabelWidth };
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 MIN_INLINE_NET_LABEL_SEGMENT_RATIO = 0.5;
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
- * How much of the label is allowed to hang off the end of the wire it names,
28
- * as a fraction of the label's length. A short elbow stub is technically clear
29
- * of every obstacle but reads as a label floating in space, so runs shorter
30
- * than this are not considered.
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 MIN_INLINE_NET_LABEL_SEGMENT_RATIO = 0.5
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
- // Runs the label fully fits on come first, then longer-to-shorter among the
169
- // runs it may overhang. Within a run, try the preferred side before the far
170
- // side, and positions near the middle before ones near the ends.
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
- (segment) =>
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tscircuit/schematic-trace-solver",
3
3
  "main": "dist/index.js",
4
- "version": "0.0.128",
4
+ "version": "0.0.129",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "cosmos",
@@ -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
+ })