@tscircuit/schematic-trace-solver 0.0.126 → 0.0.128
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/README.md +28 -0
- package/dist/index.d.ts +115 -2
- package/dist/index.js +314 -1
- package/lib/index.ts +2 -0
- package/lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver.ts +426 -0
- package/lib/solvers/InlineNetLabelSolver/getAxisAlignedSegments.ts +73 -0
- package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +17 -0
- package/lib/types/InputProblem.ts +24 -0
- package/package.json +2 -2
- package/site/examples/inline-net-label01.page.tsx +6 -0
- package/tests/assets/inline-net-label01.json +47 -0
- package/tests/functions/getAxisAlignedSegments.test.ts +56 -0
- package/tests/solvers/InlineNetLabelSolver/__snapshots__/inline-net-label01.snap.svg +54 -0
- package/tests/solvers/InlineNetLabelSolver/inline-net-label01.test.ts +44 -0
package/README.md
CHANGED
|
@@ -35,6 +35,34 @@ Finally, the `NetLabelPlacementSolver` places net labels for each net connection
|
|
|
35
35
|
this requires drawing small traces to adapt to the `availableFacingDirections` of the net connection.
|
|
36
36
|
If there is crowding at the pin, we look for an available spot along the trace connected to the pin.
|
|
37
37
|
|
|
38
|
+
### Inline net labels
|
|
39
|
+
|
|
40
|
+
A point-to-point signal trace can be labeled *inline*: the net name is drawn
|
|
41
|
+
parallel to the wire (above a horizontal trace, to the left of a vertical one)
|
|
42
|
+
instead of as an anchored label hanging off the end of it. This keeps the visual
|
|
43
|
+
line between the two pins intact.
|
|
44
|
+
|
|
45
|
+
Inline labels are opt-in per direct connection - set `allowInlineNetLabel: true`
|
|
46
|
+
on the `directConnection`. The caller (usually [@tscircuit/core](https://github.com/tscircuit/core))
|
|
47
|
+
decides which connections deserve one; the solver only honors the request when
|
|
48
|
+
the connection actually got routed, since there is nothing to run parallel to
|
|
49
|
+
otherwise.
|
|
50
|
+
|
|
51
|
+
The `InlineNetLabelSolver` runs last in the pipeline and exposes
|
|
52
|
+
`inlineNetLabelPlacements`. A net that receives an inline label has its anchored
|
|
53
|
+
`NetLabelPlacement` removed from `getOutput().netLabelPlacements`, so a net is
|
|
54
|
+
never labeled twice.
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
directConnections: [
|
|
58
|
+
{
|
|
59
|
+
pinIds: ["U1.1", "D1.1"],
|
|
60
|
+
netId: "USER_LED_ANODE",
|
|
61
|
+
allowInlineNetLabel: true,
|
|
62
|
+
},
|
|
63
|
+
]
|
|
64
|
+
```
|
|
65
|
+
|
|
38
66
|
## Usage
|
|
39
67
|
|
|
40
68
|
```tsx
|
package/dist/index.d.ts
CHANGED
|
@@ -88,6 +88,27 @@ interface InputDirectConnection {
|
|
|
88
88
|
pinIds: [PinId, PinId];
|
|
89
89
|
netId?: string;
|
|
90
90
|
netLabelWidth?: number;
|
|
91
|
+
/**
|
|
92
|
+
* When true, this point-to-point connection may be labeled with an "inline
|
|
93
|
+
* net label": the net name is drawn parallel to (and offset from) the routed
|
|
94
|
+
* trace instead of being placed as a separate anchored net label at the end
|
|
95
|
+
* of the trace.
|
|
96
|
+
*
|
|
97
|
+
* Only set this for connections whose net name is worth showing on the wire -
|
|
98
|
+
* the solver trusts the caller (e.g. @tscircuit/core) to make that decision.
|
|
99
|
+
* An inline label is only emitted when the connection actually got routed.
|
|
100
|
+
*/
|
|
101
|
+
allowInlineNetLabel?: boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Extent of the inline net label along the trace. Falls back to
|
|
104
|
+
* `netLabelWidth`, then to an estimate from the netId text.
|
|
105
|
+
*/
|
|
106
|
+
inlineNetLabelWidth?: number;
|
|
107
|
+
/**
|
|
108
|
+
* Height of the inline net label text. Defaults to
|
|
109
|
+
* DEFAULT_INLINE_NET_LABEL_HEIGHT.
|
|
110
|
+
*/
|
|
111
|
+
inlineNetLabelHeight?: number;
|
|
91
112
|
}
|
|
92
113
|
interface InputNetConnection {
|
|
93
114
|
netId: string;
|
|
@@ -1124,6 +1145,97 @@ declare class TraceElbowTransitionSimplificationSolver extends BaseSolver {
|
|
|
1124
1145
|
visualize(): GraphicsObject;
|
|
1125
1146
|
}
|
|
1126
1147
|
|
|
1148
|
+
declare const DEFAULT_INLINE_NET_LABEL_HEIGHT = 0.18;
|
|
1149
|
+
/**
|
|
1150
|
+
* Gap between the trace and the near edge of the inline label text.
|
|
1151
|
+
*/
|
|
1152
|
+
declare const INLINE_NET_LABEL_TRACE_MARGIN = 0.05;
|
|
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.
|
|
1158
|
+
*/
|
|
1159
|
+
declare const MIN_INLINE_NET_LABEL_SEGMENT_RATIO = 0.5;
|
|
1160
|
+
/**
|
|
1161
|
+
* A net label drawn parallel to the trace it names, rather than anchored to the
|
|
1162
|
+
* end of it. Used for point-to-point signal traces, where an anchored label
|
|
1163
|
+
* would break the visual line between the two pins.
|
|
1164
|
+
*
|
|
1165
|
+
* `center` is the center of the text box in the schematic's own (unrotated)
|
|
1166
|
+
* coordinate space. `width` is the extent of the text along the trace and
|
|
1167
|
+
* `height` is its extent perpendicular to the trace, so a vertical label has
|
|
1168
|
+
* its `width` running along y.
|
|
1169
|
+
*/
|
|
1170
|
+
interface InlineNetLabelPlacement {
|
|
1171
|
+
globalConnNetId: string;
|
|
1172
|
+
netId?: string;
|
|
1173
|
+
mspPairId: string;
|
|
1174
|
+
pinIds: PinId[];
|
|
1175
|
+
/** Axis the text runs along: "x" reads left-to-right, "y" reads bottom-to-top */
|
|
1176
|
+
axis: "x" | "y";
|
|
1177
|
+
/** Midpoint of the trace segment the label is attached to */
|
|
1178
|
+
anchorPoint: Point;
|
|
1179
|
+
/** Center of the label text, offset perpendicular to the trace */
|
|
1180
|
+
center: Point;
|
|
1181
|
+
/** Extent along the trace */
|
|
1182
|
+
width: number;
|
|
1183
|
+
/** Extent perpendicular to the trace */
|
|
1184
|
+
height: number;
|
|
1185
|
+
/**
|
|
1186
|
+
* Which side of the trace the label sits on. For a horizontal trace, "y+" is
|
|
1187
|
+
* above; for a vertical trace, "x-" is the left side (which is "above" once
|
|
1188
|
+
* the text is rotated).
|
|
1189
|
+
*/
|
|
1190
|
+
side: "x+" | "x-" | "y+" | "y-";
|
|
1191
|
+
}
|
|
1192
|
+
interface InlineNetLabelSolverInput {
|
|
1193
|
+
inputProblem: InputProblem;
|
|
1194
|
+
traces: SolvedTracePath[];
|
|
1195
|
+
netLabelPlacements: NetLabelPlacement[];
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Places "inline net labels" - net names drawn alongside the trace they belong
|
|
1199
|
+
* to - for direct connections that opted in via `allowInlineNetLabel`.
|
|
1200
|
+
*
|
|
1201
|
+
* Any regular (anchored) net label placement for the same net is dropped, so a
|
|
1202
|
+
* net is never labeled twice.
|
|
1203
|
+
*/
|
|
1204
|
+
declare class InlineNetLabelSolver extends BaseSolver {
|
|
1205
|
+
inputProblem: InputProblem;
|
|
1206
|
+
traces: SolvedTracePath[];
|
|
1207
|
+
inputNetLabelPlacements: NetLabelPlacement[];
|
|
1208
|
+
inlineNetLabelPlacements: InlineNetLabelPlacement[];
|
|
1209
|
+
/** Direct connections that opted in, still waiting to be processed */
|
|
1210
|
+
queuedDirectConnections: InputDirectConnection[];
|
|
1211
|
+
private tracesByPinPairKey;
|
|
1212
|
+
constructor(input: InlineNetLabelSolverInput);
|
|
1213
|
+
getConstructorParams(): [InlineNetLabelSolverInput];
|
|
1214
|
+
_step(): void;
|
|
1215
|
+
private computeInlinePlacement;
|
|
1216
|
+
/**
|
|
1217
|
+
* An inline label may not sit on top of a chip, a component's text, or a
|
|
1218
|
+
* trace belonging to another net.
|
|
1219
|
+
*/
|
|
1220
|
+
private isObstructed;
|
|
1221
|
+
/**
|
|
1222
|
+
* Net label placements superseded by an inline label. A net gets one label or
|
|
1223
|
+
* the other, never both.
|
|
1224
|
+
*/
|
|
1225
|
+
private getSupersededNetLabelKeys;
|
|
1226
|
+
getOutput(): {
|
|
1227
|
+
traces: SolvedTracePath[];
|
|
1228
|
+
netLabelPlacements: NetLabelPlacement[];
|
|
1229
|
+
inlineNetLabelPlacements: InlineNetLabelPlacement[];
|
|
1230
|
+
};
|
|
1231
|
+
visualize(): GraphicsObject;
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Mirrors the net label text width used by @tscircuit/core so a label that core
|
|
1235
|
+
* did not measure for us still reserves a sane amount of space.
|
|
1236
|
+
*/
|
|
1237
|
+
declare const estimateInlineNetLabelWidth: (text: string, fontSize?: number) => number;
|
|
1238
|
+
|
|
1127
1239
|
/**
|
|
1128
1240
|
* Pipeline solver that runs a series of solvers to find the best schematic layout.
|
|
1129
1241
|
* Coordinates the entire layout process from chip partitioning through final packing.
|
|
@@ -1162,13 +1274,14 @@ declare class SchematicTracePipelineSolver extends BaseSolver {
|
|
|
1162
1274
|
preAlignmentTraceElbowTransitionSimplificationSolver?: TraceElbowTransitionSimplificationSolver;
|
|
1163
1275
|
finalTraceElbowTransitionSimplificationSolver?: TraceElbowTransitionSimplificationSolver;
|
|
1164
1276
|
sameNetJunctionAlignmentSolver?: SameNetJunctionAlignmentSolver;
|
|
1277
|
+
inlineNetLabelSolver?: InlineNetLabelSolver;
|
|
1165
1278
|
startTimeOfPhase: Record<string, number>;
|
|
1166
1279
|
endTimeOfPhase: Record<string, number>;
|
|
1167
1280
|
timeSpentOnPhase: Record<string, number>;
|
|
1168
1281
|
firstIterationOfPhase: Record<string, number>;
|
|
1169
1282
|
inputProblem: InputProblem;
|
|
1170
1283
|
hideRatsNet: boolean;
|
|
1171
|
-
pipelineDef: (PipelineStep<typeof MspConnectionPairSolver> | PipelineStep<typeof SchematicTraceLinesSolver> | PipelineStep<typeof LongDistancePairSolver> | PipelineStep<typeof UnroutedTraceRecoverySolver> | PipelineStep<typeof TraceOverlapShiftSolver> | PipelineStep<typeof NetLabelPlacementSolver> | PipelineStep<typeof TraceLabelOverlapAvoidanceSolver> | PipelineStep<typeof TraceElbowTransitionSimplificationSolver> | PipelineStep<typeof TraceCleanupSolver> | PipelineStep<typeof Example28Solver> | PipelineStep<typeof AvailableNetOrientationSolver> | PipelineStep<typeof RailNetLabelCornerPlacementSolver> | PipelineStep<typeof TraceAnchoredNetLabelOverlapSolver> | PipelineStep<typeof NetLabelTraceCollisionSolver> | PipelineStep<typeof NetLabelNetLabelCollisionSolver> | PipelineStep<typeof SameNetJunctionAlignmentSolver>)[];
|
|
1284
|
+
pipelineDef: (PipelineStep<typeof MspConnectionPairSolver> | PipelineStep<typeof SchematicTraceLinesSolver> | PipelineStep<typeof LongDistancePairSolver> | PipelineStep<typeof UnroutedTraceRecoverySolver> | PipelineStep<typeof TraceOverlapShiftSolver> | PipelineStep<typeof NetLabelPlacementSolver> | PipelineStep<typeof TraceLabelOverlapAvoidanceSolver> | PipelineStep<typeof TraceElbowTransitionSimplificationSolver> | PipelineStep<typeof TraceCleanupSolver> | PipelineStep<typeof Example28Solver> | PipelineStep<typeof AvailableNetOrientationSolver> | PipelineStep<typeof RailNetLabelCornerPlacementSolver> | PipelineStep<typeof TraceAnchoredNetLabelOverlapSolver> | PipelineStep<typeof NetLabelTraceCollisionSolver> | PipelineStep<typeof NetLabelNetLabelCollisionSolver> | PipelineStep<typeof SameNetJunctionAlignmentSolver> | PipelineStep<typeof InlineNetLabelSolver>)[];
|
|
1172
1285
|
constructor(inputProblem: InputProblem, opts?: Options);
|
|
1173
1286
|
getConstructorParams(): ConstructorParameters<typeof SchematicTracePipelineSolver>;
|
|
1174
1287
|
currentPipelineStepIndex: number;
|
|
@@ -1184,4 +1297,4 @@ declare class SchematicTracePipelineSolver extends BaseSolver {
|
|
|
1184
1297
|
preview(): GraphicsObject;
|
|
1185
1298
|
}
|
|
1186
1299
|
|
|
1187
|
-
export { type ChipId, type InputChip, type InputDirectConnection, type InputNetConnection, type InputPin, type InputProblem, type NetId, type PinId, SchematicTracePipelineSolver, SchematicTraceSingleLineSolver2, type SectionId, type TextBoxes };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -11863,6 +11863,299 @@ var TraceElbowTransitionSimplificationSolver = class extends BaseSolver {
|
|
|
11863
11863
|
}
|
|
11864
11864
|
};
|
|
11865
11865
|
|
|
11866
|
+
// lib/solvers/InlineNetLabelSolver/getAxisAlignedSegments.ts
|
|
11867
|
+
var getAxisAlignedSegments = (path, epsilon = 1e-6) => {
|
|
11868
|
+
const segments = [];
|
|
11869
|
+
let runStart = null;
|
|
11870
|
+
let runAxis = null;
|
|
11871
|
+
let runSign = 0;
|
|
11872
|
+
const closeRun = (runEnd) => {
|
|
11873
|
+
if (runStart && runAxis) {
|
|
11874
|
+
const length = runAxis === "x" ? Math.abs(runEnd.x - runStart.x) : Math.abs(runEnd.y - runStart.y);
|
|
11875
|
+
if (length > epsilon) {
|
|
11876
|
+
segments.push({ start: runStart, end: runEnd, axis: runAxis, length });
|
|
11877
|
+
}
|
|
11878
|
+
}
|
|
11879
|
+
runStart = null;
|
|
11880
|
+
runAxis = null;
|
|
11881
|
+
runSign = 0;
|
|
11882
|
+
};
|
|
11883
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
11884
|
+
const a = path[i];
|
|
11885
|
+
const b = path[i + 1];
|
|
11886
|
+
const dx = b.x - a.x;
|
|
11887
|
+
const dy = b.y - a.y;
|
|
11888
|
+
if (Math.abs(dx) <= epsilon && Math.abs(dy) <= epsilon) continue;
|
|
11889
|
+
const axis = Math.abs(dy) <= epsilon ? "x" : Math.abs(dx) <= epsilon ? "y" : null;
|
|
11890
|
+
if (!axis) {
|
|
11891
|
+
closeRun(a);
|
|
11892
|
+
continue;
|
|
11893
|
+
}
|
|
11894
|
+
const sign = Math.sign(axis === "x" ? dx : dy);
|
|
11895
|
+
if (runStart && runAxis === axis && runSign === sign) continue;
|
|
11896
|
+
closeRun(a);
|
|
11897
|
+
runStart = a;
|
|
11898
|
+
runAxis = axis;
|
|
11899
|
+
runSign = sign;
|
|
11900
|
+
}
|
|
11901
|
+
if (path.length >= 2) {
|
|
11902
|
+
closeRun(path[path.length - 1]);
|
|
11903
|
+
}
|
|
11904
|
+
return segments.sort((a, b) => b.length - a.length);
|
|
11905
|
+
};
|
|
11906
|
+
|
|
11907
|
+
// lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver.ts
|
|
11908
|
+
var DEFAULT_INLINE_NET_LABEL_HEIGHT = 0.18;
|
|
11909
|
+
var INLINE_NET_LABEL_TRACE_MARGIN = 0.05;
|
|
11910
|
+
var MIN_INLINE_NET_LABEL_SEGMENT_RATIO = 0.5;
|
|
11911
|
+
var getPinPairKey2 = (pinIds) => [...pinIds].sort().join("::");
|
|
11912
|
+
var InlineNetLabelSolver = class extends BaseSolver {
|
|
11913
|
+
inputProblem;
|
|
11914
|
+
traces;
|
|
11915
|
+
inputNetLabelPlacements;
|
|
11916
|
+
inlineNetLabelPlacements = [];
|
|
11917
|
+
/** Direct connections that opted in, still waiting to be processed */
|
|
11918
|
+
queuedDirectConnections;
|
|
11919
|
+
tracesByPinPairKey;
|
|
11920
|
+
constructor(input) {
|
|
11921
|
+
super();
|
|
11922
|
+
this.inputProblem = input.inputProblem;
|
|
11923
|
+
this.traces = input.traces;
|
|
11924
|
+
this.inputNetLabelPlacements = input.netLabelPlacements;
|
|
11925
|
+
this.queuedDirectConnections = this.inputProblem.directConnections.filter(
|
|
11926
|
+
(dc) => dc.allowInlineNetLabel && dc.netId
|
|
11927
|
+
);
|
|
11928
|
+
this.tracesByPinPairKey = /* @__PURE__ */ new Map();
|
|
11929
|
+
for (const trace of this.traces) {
|
|
11930
|
+
const key = getPinPairKey2(trace.pins.map((p) => p.pinId));
|
|
11931
|
+
const existing = this.tracesByPinPairKey.get(key);
|
|
11932
|
+
if (existing) {
|
|
11933
|
+
existing.push(trace);
|
|
11934
|
+
} else {
|
|
11935
|
+
this.tracesByPinPairKey.set(key, [trace]);
|
|
11936
|
+
}
|
|
11937
|
+
}
|
|
11938
|
+
}
|
|
11939
|
+
getConstructorParams() {
|
|
11940
|
+
return [
|
|
11941
|
+
{
|
|
11942
|
+
inputProblem: this.inputProblem,
|
|
11943
|
+
traces: this.traces,
|
|
11944
|
+
netLabelPlacements: this.inputNetLabelPlacements
|
|
11945
|
+
}
|
|
11946
|
+
];
|
|
11947
|
+
}
|
|
11948
|
+
_step() {
|
|
11949
|
+
const directConnection = this.queuedDirectConnections.shift();
|
|
11950
|
+
if (!directConnection) {
|
|
11951
|
+
this.solved = true;
|
|
11952
|
+
this.stats.inlineNetLabelCount = this.inlineNetLabelPlacements.length;
|
|
11953
|
+
return;
|
|
11954
|
+
}
|
|
11955
|
+
const placement = this.computeInlinePlacement(directConnection);
|
|
11956
|
+
if (placement) {
|
|
11957
|
+
this.inlineNetLabelPlacements.push(placement);
|
|
11958
|
+
}
|
|
11959
|
+
}
|
|
11960
|
+
computeInlinePlacement(directConnection) {
|
|
11961
|
+
const traces = this.tracesByPinPairKey.get(getPinPairKey2(directConnection.pinIds)) ?? [];
|
|
11962
|
+
if (traces.length === 0) return null;
|
|
11963
|
+
const trace = traces[0];
|
|
11964
|
+
const segments = getAxisAlignedSegments(trace.tracePath);
|
|
11965
|
+
if (segments.length === 0) return null;
|
|
11966
|
+
const height = directConnection.inlineNetLabelHeight ?? DEFAULT_INLINE_NET_LABEL_HEIGHT;
|
|
11967
|
+
const width = directConnection.inlineNetLabelWidth ?? directConnection.netLabelWidth ?? estimateInlineNetLabelWidth(directConnection.netId, height);
|
|
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
|
+
});
|
|
11977
|
+
for (const segment of usableSegments) {
|
|
11978
|
+
const sides = segment.axis === "x" ? ["y+", "y-"] : ["x-", "x+"];
|
|
11979
|
+
for (const side of sides) {
|
|
11980
|
+
for (const anchorPoint of getAnchorCandidates(segment, width)) {
|
|
11981
|
+
const center = side === "y+" ? { x: anchorPoint.x, y: anchorPoint.y + offset } : side === "y-" ? { x: anchorPoint.x, y: anchorPoint.y - offset } : side === "x-" ? { x: anchorPoint.x - offset, y: anchorPoint.y } : { x: anchorPoint.x + offset, y: anchorPoint.y };
|
|
11982
|
+
const halfAlong = width / 2;
|
|
11983
|
+
const halfAcross = height / 2;
|
|
11984
|
+
const bounds = segment.axis === "x" ? {
|
|
11985
|
+
minX: center.x - halfAlong,
|
|
11986
|
+
maxX: center.x + halfAlong,
|
|
11987
|
+
minY: center.y - halfAcross,
|
|
11988
|
+
maxY: center.y + halfAcross
|
|
11989
|
+
} : {
|
|
11990
|
+
minX: center.x - halfAcross,
|
|
11991
|
+
maxX: center.x + halfAcross,
|
|
11992
|
+
minY: center.y - halfAlong,
|
|
11993
|
+
maxY: center.y + halfAlong
|
|
11994
|
+
};
|
|
11995
|
+
if (this.isObstructed(bounds, trace)) continue;
|
|
11996
|
+
return {
|
|
11997
|
+
globalConnNetId: trace.globalConnNetId,
|
|
11998
|
+
netId: directConnection.netId,
|
|
11999
|
+
mspPairId: trace.mspPairId,
|
|
12000
|
+
pinIds: [...directConnection.pinIds],
|
|
12001
|
+
axis: segment.axis,
|
|
12002
|
+
anchorPoint,
|
|
12003
|
+
center,
|
|
12004
|
+
width,
|
|
12005
|
+
height,
|
|
12006
|
+
side
|
|
12007
|
+
};
|
|
12008
|
+
}
|
|
12009
|
+
}
|
|
12010
|
+
}
|
|
12011
|
+
return null;
|
|
12012
|
+
}
|
|
12013
|
+
/**
|
|
12014
|
+
* An inline label may not sit on top of a chip, a component's text, or a
|
|
12015
|
+
* trace belonging to another net.
|
|
12016
|
+
*/
|
|
12017
|
+
isObstructed(bounds, ownTrace) {
|
|
12018
|
+
for (const chip of this.inputProblem.chips) {
|
|
12019
|
+
const chipBounds = {
|
|
12020
|
+
minX: chip.center.x - chip.width / 2,
|
|
12021
|
+
maxX: chip.center.x + chip.width / 2,
|
|
12022
|
+
minY: chip.center.y - chip.height / 2,
|
|
12023
|
+
maxY: chip.center.y + chip.height / 2
|
|
12024
|
+
};
|
|
12025
|
+
if (boundsOverlap(bounds, chipBounds)) return true;
|
|
12026
|
+
}
|
|
12027
|
+
for (const textBox of this.inputProblem.textBoxes ?? []) {
|
|
12028
|
+
if (boundsOverlap(bounds, getTextBoxBounds(textBox))) return true;
|
|
12029
|
+
}
|
|
12030
|
+
for (const trace of this.traces) {
|
|
12031
|
+
if (trace.mspPairId === ownTrace.mspPairId) continue;
|
|
12032
|
+
if (trace.globalConnNetId === ownTrace.globalConnNetId) continue;
|
|
12033
|
+
if (doesPathIntersectBounds(trace.tracePath, bounds)) return true;
|
|
12034
|
+
}
|
|
12035
|
+
return false;
|
|
12036
|
+
}
|
|
12037
|
+
/**
|
|
12038
|
+
* Net label placements superseded by an inline label. A net gets one label or
|
|
12039
|
+
* the other, never both.
|
|
12040
|
+
*/
|
|
12041
|
+
getSupersededNetLabelKeys() {
|
|
12042
|
+
const keys = /* @__PURE__ */ new Set();
|
|
12043
|
+
for (const placement of this.inlineNetLabelPlacements) {
|
|
12044
|
+
keys.add(placement.globalConnNetId);
|
|
12045
|
+
}
|
|
12046
|
+
return keys;
|
|
12047
|
+
}
|
|
12048
|
+
getOutput() {
|
|
12049
|
+
const superseded = this.getSupersededNetLabelKeys();
|
|
12050
|
+
return {
|
|
12051
|
+
traces: this.traces,
|
|
12052
|
+
netLabelPlacements: this.inputNetLabelPlacements.filter(
|
|
12053
|
+
(placement) => !superseded.has(placement.globalConnNetId)
|
|
12054
|
+
),
|
|
12055
|
+
inlineNetLabelPlacements: this.inlineNetLabelPlacements
|
|
12056
|
+
};
|
|
12057
|
+
}
|
|
12058
|
+
visualize() {
|
|
12059
|
+
const graphics = visualizeInputProblem(this.inputProblem);
|
|
12060
|
+
graphics.lines ??= [];
|
|
12061
|
+
graphics.rects ??= [];
|
|
12062
|
+
graphics.points ??= [];
|
|
12063
|
+
graphics.texts ??= [];
|
|
12064
|
+
for (const trace of this.traces) {
|
|
12065
|
+
graphics.lines.push({
|
|
12066
|
+
points: trace.tracePath,
|
|
12067
|
+
strokeColor: "purple"
|
|
12068
|
+
});
|
|
12069
|
+
}
|
|
12070
|
+
const { netLabelPlacements } = this.getOutput();
|
|
12071
|
+
for (const label of netLabelPlacements) {
|
|
12072
|
+
graphics.rects.push({
|
|
12073
|
+
center: label.center,
|
|
12074
|
+
width: label.width,
|
|
12075
|
+
height: label.height,
|
|
12076
|
+
fill: getColorFromString(label.globalConnNetId, 0.35),
|
|
12077
|
+
strokeColor: getColorFromString(label.globalConnNetId, 0.9),
|
|
12078
|
+
label: `netId: ${label.netId}
|
|
12079
|
+
globalConnNetId: ${label.globalConnNetId}`
|
|
12080
|
+
});
|
|
12081
|
+
graphics.points.push({
|
|
12082
|
+
x: label.anchorPoint.x,
|
|
12083
|
+
y: label.anchorPoint.y,
|
|
12084
|
+
color: getColorFromString(label.globalConnNetId, 0.9),
|
|
12085
|
+
label: `anchorPoint
|
|
12086
|
+
orientation: ${label.orientation}`
|
|
12087
|
+
});
|
|
12088
|
+
}
|
|
12089
|
+
for (const inlineLabel of this.inlineNetLabelPlacements) {
|
|
12090
|
+
const isHorizontal4 = inlineLabel.axis === "x";
|
|
12091
|
+
graphics.rects.push({
|
|
12092
|
+
center: inlineLabel.center,
|
|
12093
|
+
width: isHorizontal4 ? inlineLabel.width : inlineLabel.height,
|
|
12094
|
+
height: isHorizontal4 ? inlineLabel.height : inlineLabel.width,
|
|
12095
|
+
fill: getColorFromString(inlineLabel.globalConnNetId, 0.35),
|
|
12096
|
+
strokeColor: "green",
|
|
12097
|
+
label: [
|
|
12098
|
+
`INLINE netId: ${inlineLabel.netId}`,
|
|
12099
|
+
`axis: ${inlineLabel.axis}`,
|
|
12100
|
+
`side: ${inlineLabel.side}`
|
|
12101
|
+
].join("\n")
|
|
12102
|
+
});
|
|
12103
|
+
graphics.texts.push({
|
|
12104
|
+
x: inlineLabel.center.x,
|
|
12105
|
+
y: inlineLabel.center.y,
|
|
12106
|
+
text: inlineLabel.netId ?? "",
|
|
12107
|
+
color: "green",
|
|
12108
|
+
fontSize: inlineLabel.height,
|
|
12109
|
+
anchorSide: "center",
|
|
12110
|
+
// Vertical labels read bottom-to-top, alongside the wire they name.
|
|
12111
|
+
rotation: inlineLabel.axis === "y" ? 90 : void 0
|
|
12112
|
+
});
|
|
12113
|
+
graphics.points.push({
|
|
12114
|
+
x: inlineLabel.anchorPoint.x,
|
|
12115
|
+
y: inlineLabel.anchorPoint.y,
|
|
12116
|
+
color: "green",
|
|
12117
|
+
label: `inline anchor
|
|
12118
|
+
${inlineLabel.netId}`
|
|
12119
|
+
});
|
|
12120
|
+
}
|
|
12121
|
+
return graphics;
|
|
12122
|
+
}
|
|
12123
|
+
};
|
|
12124
|
+
var estimateInlineNetLabelWidth = (text, fontSize = DEFAULT_INLINE_NET_LABEL_HEIGHT) => {
|
|
12125
|
+
const fontScale = fontSize / 0.18;
|
|
12126
|
+
return text.length * 0.12 * fontScale + 0.12 * fontScale;
|
|
12127
|
+
};
|
|
12128
|
+
var getAnchorCandidates = (segment, labelWidth, step = 0.1) => {
|
|
12129
|
+
const along = segment.axis === "x" ? "x" : "y";
|
|
12130
|
+
const start = segment.start[along];
|
|
12131
|
+
const end = segment.end[along];
|
|
12132
|
+
const mid = (start + end) / 2;
|
|
12133
|
+
const direction = Math.sign(end - start) || 1;
|
|
12134
|
+
const pointAt = (value) => segment.axis === "x" ? { x: value, y: segment.start.y } : { x: segment.start.x, y: segment.start.y + (value - start) };
|
|
12135
|
+
const slack = segment.length - labelWidth;
|
|
12136
|
+
if (slack <= 0) return [pointAt(mid)];
|
|
12137
|
+
const offsets = [0];
|
|
12138
|
+
for (let offset = step; offset <= slack / 2 + 1e-9; offset += step) {
|
|
12139
|
+
offsets.push(offset, -offset);
|
|
12140
|
+
}
|
|
12141
|
+
offsets.push(slack / 2, -slack / 2);
|
|
12142
|
+
return offsets.map((offset) => pointAt(mid + offset * direction));
|
|
12143
|
+
};
|
|
12144
|
+
var doesPathIntersectBounds = (path, bounds) => {
|
|
12145
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
12146
|
+
const a = path[i];
|
|
12147
|
+
const b = path[i + 1];
|
|
12148
|
+
const segmentBounds = {
|
|
12149
|
+
minX: Math.min(a.x, b.x),
|
|
12150
|
+
maxX: Math.max(a.x, b.x),
|
|
12151
|
+
minY: Math.min(a.y, b.y),
|
|
12152
|
+
maxY: Math.max(a.y, b.y)
|
|
12153
|
+
};
|
|
12154
|
+
if (boundsOverlap(segmentBounds, bounds)) return true;
|
|
12155
|
+
}
|
|
12156
|
+
return false;
|
|
12157
|
+
};
|
|
12158
|
+
|
|
11866
12159
|
// lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts
|
|
11867
12160
|
function definePipelineStep(solverName, solverClass, getConstructorParams, opts = {}) {
|
|
11868
12161
|
return {
|
|
@@ -11897,6 +12190,7 @@ var SchematicTracePipelineSolver = class extends BaseSolver {
|
|
|
11897
12190
|
preAlignmentTraceElbowTransitionSimplificationSolver;
|
|
11898
12191
|
finalTraceElbowTransitionSimplificationSolver;
|
|
11899
12192
|
sameNetJunctionAlignmentSolver;
|
|
12193
|
+
inlineNetLabelSolver;
|
|
11900
12194
|
startTimeOfPhase;
|
|
11901
12195
|
endTimeOfPhase;
|
|
11902
12196
|
timeSpentOnPhase;
|
|
@@ -12252,6 +12546,20 @@ var SchematicTracePipelineSolver = class extends BaseSolver {
|
|
|
12252
12546
|
}
|
|
12253
12547
|
];
|
|
12254
12548
|
}
|
|
12549
|
+
),
|
|
12550
|
+
definePipelineStep(
|
|
12551
|
+
"inlineNetLabelSolver",
|
|
12552
|
+
InlineNetLabelSolver,
|
|
12553
|
+
(instance) => {
|
|
12554
|
+
const junctionOutput = instance.sameNetJunctionAlignmentSolver.getOutput();
|
|
12555
|
+
return [
|
|
12556
|
+
{
|
|
12557
|
+
inputProblem: instance.inputProblem,
|
|
12558
|
+
traces: junctionOutput.traces,
|
|
12559
|
+
netLabelPlacements: junctionOutput.netLabelPlacements
|
|
12560
|
+
}
|
|
12561
|
+
];
|
|
12562
|
+
}
|
|
12255
12563
|
)
|
|
12256
12564
|
];
|
|
12257
12565
|
constructor(inputProblem, opts) {
|
|
@@ -12365,6 +12673,11 @@ var SchematicTracePipelineSolver = class extends BaseSolver {
|
|
|
12365
12673
|
}
|
|
12366
12674
|
};
|
|
12367
12675
|
export {
|
|
12676
|
+
DEFAULT_INLINE_NET_LABEL_HEIGHT,
|
|
12677
|
+
INLINE_NET_LABEL_TRACE_MARGIN,
|
|
12678
|
+
InlineNetLabelSolver,
|
|
12679
|
+
MIN_INLINE_NET_LABEL_SEGMENT_RATIO,
|
|
12368
12680
|
SchematicTracePipelineSolver,
|
|
12369
|
-
SchematicTraceSingleLineSolver2
|
|
12681
|
+
SchematicTraceSingleLineSolver2,
|
|
12682
|
+
estimateInlineNetLabelWidth
|
|
12370
12683
|
};
|
package/lib/index.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export * from "./solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
|
|
2
2
|
export * from "./types/InputProblem"
|
|
3
|
+
export * from "./solvers/InlineNetLabelSolver/InlineNetLabelSolver"
|
|
4
|
+
export type { NetLabelPlacement } from "./solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
3
5
|
export { SchematicTraceSingleLineSolver2 } from "./solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2"
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import type { Bounds, Point } from "@tscircuit/math-utils"
|
|
2
|
+
import type { GraphicsObject, Rect } from "graphics-debug"
|
|
3
|
+
import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
|
|
4
|
+
import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
5
|
+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
6
|
+
import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
|
|
7
|
+
import type {
|
|
8
|
+
InputDirectConnection,
|
|
9
|
+
InputProblem,
|
|
10
|
+
PinId,
|
|
11
|
+
} from "lib/types/InputProblem"
|
|
12
|
+
import { getColorFromString } from "lib/utils/getColorFromString"
|
|
13
|
+
import { boundsOverlap, getTextBoxBounds } from "lib/utils/textBoxBounds"
|
|
14
|
+
import {
|
|
15
|
+
type AxisAlignedSegment,
|
|
16
|
+
getAxisAlignedSegments,
|
|
17
|
+
} from "./getAxisAlignedSegments"
|
|
18
|
+
|
|
19
|
+
export const DEFAULT_INLINE_NET_LABEL_HEIGHT = 0.18
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Gap between the trace and the near edge of the inline label text.
|
|
23
|
+
*/
|
|
24
|
+
export const INLINE_NET_LABEL_TRACE_MARGIN = 0.05
|
|
25
|
+
|
|
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.
|
|
31
|
+
*/
|
|
32
|
+
export const MIN_INLINE_NET_LABEL_SEGMENT_RATIO = 0.5
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A net label drawn parallel to the trace it names, rather than anchored to the
|
|
36
|
+
* end of it. Used for point-to-point signal traces, where an anchored label
|
|
37
|
+
* would break the visual line between the two pins.
|
|
38
|
+
*
|
|
39
|
+
* `center` is the center of the text box in the schematic's own (unrotated)
|
|
40
|
+
* coordinate space. `width` is the extent of the text along the trace and
|
|
41
|
+
* `height` is its extent perpendicular to the trace, so a vertical label has
|
|
42
|
+
* its `width` running along y.
|
|
43
|
+
*/
|
|
44
|
+
export interface InlineNetLabelPlacement {
|
|
45
|
+
globalConnNetId: string
|
|
46
|
+
netId?: string
|
|
47
|
+
mspPairId: string
|
|
48
|
+
pinIds: PinId[]
|
|
49
|
+
|
|
50
|
+
/** Axis the text runs along: "x" reads left-to-right, "y" reads bottom-to-top */
|
|
51
|
+
axis: "x" | "y"
|
|
52
|
+
|
|
53
|
+
/** Midpoint of the trace segment the label is attached to */
|
|
54
|
+
anchorPoint: Point
|
|
55
|
+
|
|
56
|
+
/** Center of the label text, offset perpendicular to the trace */
|
|
57
|
+
center: Point
|
|
58
|
+
|
|
59
|
+
/** Extent along the trace */
|
|
60
|
+
width: number
|
|
61
|
+
/** Extent perpendicular to the trace */
|
|
62
|
+
height: number
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Which side of the trace the label sits on. For a horizontal trace, "y+" is
|
|
66
|
+
* above; for a vertical trace, "x-" is the left side (which is "above" once
|
|
67
|
+
* the text is rotated).
|
|
68
|
+
*/
|
|
69
|
+
side: "x+" | "x-" | "y+" | "y-"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface InlineNetLabelSolverInput {
|
|
73
|
+
inputProblem: InputProblem
|
|
74
|
+
traces: SolvedTracePath[]
|
|
75
|
+
netLabelPlacements: NetLabelPlacement[]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const getPinPairKey = (pinIds: readonly string[]) =>
|
|
79
|
+
[...pinIds].sort().join("::")
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Places "inline net labels" - net names drawn alongside the trace they belong
|
|
83
|
+
* to - for direct connections that opted in via `allowInlineNetLabel`.
|
|
84
|
+
*
|
|
85
|
+
* Any regular (anchored) net label placement for the same net is dropped, so a
|
|
86
|
+
* net is never labeled twice.
|
|
87
|
+
*/
|
|
88
|
+
export class InlineNetLabelSolver extends BaseSolver {
|
|
89
|
+
inputProblem: InputProblem
|
|
90
|
+
traces: SolvedTracePath[]
|
|
91
|
+
inputNetLabelPlacements: NetLabelPlacement[]
|
|
92
|
+
|
|
93
|
+
inlineNetLabelPlacements: InlineNetLabelPlacement[] = []
|
|
94
|
+
|
|
95
|
+
/** Direct connections that opted in, still waiting to be processed */
|
|
96
|
+
queuedDirectConnections: InputDirectConnection[]
|
|
97
|
+
|
|
98
|
+
private tracesByPinPairKey: Map<string, SolvedTracePath[]>
|
|
99
|
+
|
|
100
|
+
constructor(input: InlineNetLabelSolverInput) {
|
|
101
|
+
super()
|
|
102
|
+
this.inputProblem = input.inputProblem
|
|
103
|
+
this.traces = input.traces
|
|
104
|
+
this.inputNetLabelPlacements = input.netLabelPlacements
|
|
105
|
+
|
|
106
|
+
this.queuedDirectConnections = this.inputProblem.directConnections.filter(
|
|
107
|
+
(dc) => dc.allowInlineNetLabel && dc.netId,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
this.tracesByPinPairKey = new Map()
|
|
111
|
+
for (const trace of this.traces) {
|
|
112
|
+
const key = getPinPairKey(trace.pins.map((p) => p.pinId))
|
|
113
|
+
const existing = this.tracesByPinPairKey.get(key)
|
|
114
|
+
if (existing) {
|
|
115
|
+
existing.push(trace)
|
|
116
|
+
} else {
|
|
117
|
+
this.tracesByPinPairKey.set(key, [trace])
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
override getConstructorParams(): [InlineNetLabelSolverInput] {
|
|
123
|
+
return [
|
|
124
|
+
{
|
|
125
|
+
inputProblem: this.inputProblem,
|
|
126
|
+
traces: this.traces,
|
|
127
|
+
netLabelPlacements: this.inputNetLabelPlacements,
|
|
128
|
+
},
|
|
129
|
+
]
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
override _step() {
|
|
133
|
+
const directConnection = this.queuedDirectConnections.shift()
|
|
134
|
+
if (!directConnection) {
|
|
135
|
+
this.solved = true
|
|
136
|
+
this.stats.inlineNetLabelCount = this.inlineNetLabelPlacements.length
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const placement = this.computeInlinePlacement(directConnection)
|
|
141
|
+
if (placement) {
|
|
142
|
+
this.inlineNetLabelPlacements.push(placement)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private computeInlinePlacement(
|
|
147
|
+
directConnection: InputDirectConnection,
|
|
148
|
+
): InlineNetLabelPlacement | null {
|
|
149
|
+
// Only connections the router actually drew a trace for can carry an inline
|
|
150
|
+
// label - there's nothing to run parallel to otherwise.
|
|
151
|
+
const traces =
|
|
152
|
+
this.tracesByPinPairKey.get(getPinPairKey(directConnection.pinIds)) ?? []
|
|
153
|
+
if (traces.length === 0) return null
|
|
154
|
+
|
|
155
|
+
const trace = traces[0]!
|
|
156
|
+
const segments = getAxisAlignedSegments(trace.tracePath)
|
|
157
|
+
if (segments.length === 0) return null
|
|
158
|
+
|
|
159
|
+
const height =
|
|
160
|
+
directConnection.inlineNetLabelHeight ?? DEFAULT_INLINE_NET_LABEL_HEIGHT
|
|
161
|
+
const width =
|
|
162
|
+
directConnection.inlineNetLabelWidth ??
|
|
163
|
+
directConnection.netLabelWidth ??
|
|
164
|
+
estimateInlineNetLabelWidth(directConnection.netId!, height)
|
|
165
|
+
|
|
166
|
+
const offset = height / 2 + INLINE_NET_LABEL_TRACE_MARGIN
|
|
167
|
+
|
|
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.
|
|
171
|
+
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
|
+
})
|
|
182
|
+
|
|
183
|
+
for (const segment of usableSegments) {
|
|
184
|
+
// "Above" the trace: +y for a horizontal trace, and -x for a vertical one
|
|
185
|
+
// (rotating the text 90deg counter-clockwise maps "above" onto the left).
|
|
186
|
+
const sides: InlineNetLabelPlacement["side"][] =
|
|
187
|
+
segment.axis === "x" ? ["y+", "y-"] : ["x-", "x+"]
|
|
188
|
+
|
|
189
|
+
for (const side of sides) {
|
|
190
|
+
for (const anchorPoint of getAnchorCandidates(segment, width)) {
|
|
191
|
+
const center =
|
|
192
|
+
side === "y+"
|
|
193
|
+
? { x: anchorPoint.x, y: anchorPoint.y + offset }
|
|
194
|
+
: side === "y-"
|
|
195
|
+
? { x: anchorPoint.x, y: anchorPoint.y - offset }
|
|
196
|
+
: side === "x-"
|
|
197
|
+
? { x: anchorPoint.x - offset, y: anchorPoint.y }
|
|
198
|
+
: { x: anchorPoint.x + offset, y: anchorPoint.y }
|
|
199
|
+
|
|
200
|
+
const halfAlong = width / 2
|
|
201
|
+
const halfAcross = height / 2
|
|
202
|
+
const bounds: Bounds =
|
|
203
|
+
segment.axis === "x"
|
|
204
|
+
? {
|
|
205
|
+
minX: center.x - halfAlong,
|
|
206
|
+
maxX: center.x + halfAlong,
|
|
207
|
+
minY: center.y - halfAcross,
|
|
208
|
+
maxY: center.y + halfAcross,
|
|
209
|
+
}
|
|
210
|
+
: {
|
|
211
|
+
minX: center.x - halfAcross,
|
|
212
|
+
maxX: center.x + halfAcross,
|
|
213
|
+
minY: center.y - halfAlong,
|
|
214
|
+
maxY: center.y + halfAlong,
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (this.isObstructed(bounds, trace)) continue
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
globalConnNetId: trace.globalConnNetId,
|
|
221
|
+
netId: directConnection.netId,
|
|
222
|
+
mspPairId: trace.mspPairId,
|
|
223
|
+
pinIds: [...directConnection.pinIds],
|
|
224
|
+
axis: segment.axis,
|
|
225
|
+
anchorPoint,
|
|
226
|
+
center,
|
|
227
|
+
width,
|
|
228
|
+
height,
|
|
229
|
+
side,
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// No room anywhere along the trace. Leave the net to the regular anchored
|
|
236
|
+
// net label rather than drawing the name over a chip.
|
|
237
|
+
return null
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* An inline label may not sit on top of a chip, a component's text, or a
|
|
242
|
+
* trace belonging to another net.
|
|
243
|
+
*/
|
|
244
|
+
private isObstructed(bounds: Bounds, ownTrace: SolvedTracePath): boolean {
|
|
245
|
+
for (const chip of this.inputProblem.chips) {
|
|
246
|
+
const chipBounds: Bounds = {
|
|
247
|
+
minX: chip.center.x - chip.width / 2,
|
|
248
|
+
maxX: chip.center.x + chip.width / 2,
|
|
249
|
+
minY: chip.center.y - chip.height / 2,
|
|
250
|
+
maxY: chip.center.y + chip.height / 2,
|
|
251
|
+
}
|
|
252
|
+
if (boundsOverlap(bounds, chipBounds)) return true
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
for (const textBox of this.inputProblem.textBoxes ?? []) {
|
|
256
|
+
if (boundsOverlap(bounds, getTextBoxBounds(textBox))) return true
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
for (const trace of this.traces) {
|
|
260
|
+
if (trace.mspPairId === ownTrace.mspPairId) continue
|
|
261
|
+
if (trace.globalConnNetId === ownTrace.globalConnNetId) continue
|
|
262
|
+
if (doesPathIntersectBounds(trace.tracePath, bounds)) return true
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return false
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Net label placements superseded by an inline label. A net gets one label or
|
|
270
|
+
* the other, never both.
|
|
271
|
+
*/
|
|
272
|
+
private getSupersededNetLabelKeys(): Set<string> {
|
|
273
|
+
const keys = new Set<string>()
|
|
274
|
+
for (const placement of this.inlineNetLabelPlacements) {
|
|
275
|
+
keys.add(placement.globalConnNetId)
|
|
276
|
+
}
|
|
277
|
+
return keys
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
getOutput() {
|
|
281
|
+
const superseded = this.getSupersededNetLabelKeys()
|
|
282
|
+
return {
|
|
283
|
+
traces: this.traces,
|
|
284
|
+
netLabelPlacements: this.inputNetLabelPlacements.filter(
|
|
285
|
+
(placement) => !superseded.has(placement.globalConnNetId),
|
|
286
|
+
),
|
|
287
|
+
inlineNetLabelPlacements: this.inlineNetLabelPlacements,
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
override visualize(): GraphicsObject {
|
|
292
|
+
// Mirrors the previous pipeline stage's visualization so that a problem
|
|
293
|
+
// with no inline labels renders identically, then layers the inline labels
|
|
294
|
+
// on top.
|
|
295
|
+
const graphics = visualizeInputProblem(this.inputProblem)
|
|
296
|
+
graphics.lines ??= []
|
|
297
|
+
graphics.rects ??= []
|
|
298
|
+
graphics.points ??= []
|
|
299
|
+
graphics.texts ??= []
|
|
300
|
+
|
|
301
|
+
for (const trace of this.traces) {
|
|
302
|
+
graphics.lines.push({
|
|
303
|
+
points: trace.tracePath,
|
|
304
|
+
strokeColor: "purple",
|
|
305
|
+
})
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const { netLabelPlacements } = this.getOutput()
|
|
309
|
+
for (const label of netLabelPlacements) {
|
|
310
|
+
graphics.rects.push({
|
|
311
|
+
center: label.center,
|
|
312
|
+
width: label.width,
|
|
313
|
+
height: label.height,
|
|
314
|
+
fill: getColorFromString(label.globalConnNetId, 0.35),
|
|
315
|
+
strokeColor: getColorFromString(label.globalConnNetId, 0.9),
|
|
316
|
+
label: `netId: ${label.netId}\nglobalConnNetId: ${label.globalConnNetId}`,
|
|
317
|
+
} as Rect & { strokeColor: string })
|
|
318
|
+
graphics.points.push({
|
|
319
|
+
x: label.anchorPoint.x,
|
|
320
|
+
y: label.anchorPoint.y,
|
|
321
|
+
color: getColorFromString(label.globalConnNetId, 0.9),
|
|
322
|
+
label: `anchorPoint\norientation: ${label.orientation}`,
|
|
323
|
+
})
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
for (const inlineLabel of this.inlineNetLabelPlacements) {
|
|
327
|
+
const isHorizontal = inlineLabel.axis === "x"
|
|
328
|
+
graphics.rects.push({
|
|
329
|
+
center: inlineLabel.center,
|
|
330
|
+
width: isHorizontal ? inlineLabel.width : inlineLabel.height,
|
|
331
|
+
height: isHorizontal ? inlineLabel.height : inlineLabel.width,
|
|
332
|
+
fill: getColorFromString(inlineLabel.globalConnNetId, 0.35),
|
|
333
|
+
strokeColor: "green",
|
|
334
|
+
label: [
|
|
335
|
+
`INLINE netId: ${inlineLabel.netId}`,
|
|
336
|
+
`axis: ${inlineLabel.axis}`,
|
|
337
|
+
`side: ${inlineLabel.side}`,
|
|
338
|
+
].join("\n"),
|
|
339
|
+
} as Rect & { strokeColor: string })
|
|
340
|
+
graphics.texts.push({
|
|
341
|
+
x: inlineLabel.center.x,
|
|
342
|
+
y: inlineLabel.center.y,
|
|
343
|
+
text: inlineLabel.netId ?? "",
|
|
344
|
+
color: "green",
|
|
345
|
+
fontSize: inlineLabel.height,
|
|
346
|
+
anchorSide: "center",
|
|
347
|
+
// Vertical labels read bottom-to-top, alongside the wire they name.
|
|
348
|
+
rotation: inlineLabel.axis === "y" ? 90 : undefined,
|
|
349
|
+
})
|
|
350
|
+
graphics.points.push({
|
|
351
|
+
x: inlineLabel.anchorPoint.x,
|
|
352
|
+
y: inlineLabel.anchorPoint.y,
|
|
353
|
+
color: "green",
|
|
354
|
+
label: `inline anchor\n${inlineLabel.netId}`,
|
|
355
|
+
})
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return graphics
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Mirrors the net label text width used by @tscircuit/core so a label that core
|
|
364
|
+
* did not measure for us still reserves a sane amount of space.
|
|
365
|
+
*/
|
|
366
|
+
export const estimateInlineNetLabelWidth = (
|
|
367
|
+
text: string,
|
|
368
|
+
fontSize = DEFAULT_INLINE_NET_LABEL_HEIGHT,
|
|
369
|
+
) => {
|
|
370
|
+
const fontScale = fontSize / 0.18
|
|
371
|
+
return text.length * 0.12 * fontScale + 0.12 * fontScale
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Points along a segment where the label's midpoint could sit, ordered from the
|
|
376
|
+
* 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).
|
|
382
|
+
*/
|
|
383
|
+
const getAnchorCandidates = (
|
|
384
|
+
segment: AxisAlignedSegment,
|
|
385
|
+
labelWidth: number,
|
|
386
|
+
step = 0.1,
|
|
387
|
+
): Point[] => {
|
|
388
|
+
const along = segment.axis === "x" ? "x" : "y"
|
|
389
|
+
const start = segment.start[along]
|
|
390
|
+
const end = segment.end[along]
|
|
391
|
+
const mid = (start + end) / 2
|
|
392
|
+
const direction = Math.sign(end - start) || 1
|
|
393
|
+
|
|
394
|
+
const pointAt = (value: number): Point =>
|
|
395
|
+
segment.axis === "x"
|
|
396
|
+
? { x: value, y: segment.start.y }
|
|
397
|
+
: { x: segment.start.x, y: segment.start.y + (value - start) }
|
|
398
|
+
|
|
399
|
+
const slack = segment.length - labelWidth
|
|
400
|
+
if (slack <= 0) return [pointAt(mid)]
|
|
401
|
+
|
|
402
|
+
const offsets: number[] = [0]
|
|
403
|
+
for (let offset = step; offset <= slack / 2 + 1e-9; offset += step) {
|
|
404
|
+
offsets.push(offset, -offset)
|
|
405
|
+
}
|
|
406
|
+
// Always consider both extremes, even when they fall between samples.
|
|
407
|
+
offsets.push(slack / 2, -slack / 2)
|
|
408
|
+
|
|
409
|
+
return offsets.map((offset) => pointAt(mid + offset * direction))
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const doesPathIntersectBounds = (path: Point[], bounds: Bounds): boolean => {
|
|
413
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
414
|
+
const a = path[i]!
|
|
415
|
+
const b = path[i + 1]!
|
|
416
|
+
const segmentBounds: Bounds = {
|
|
417
|
+
minX: Math.min(a.x, b.x),
|
|
418
|
+
maxX: Math.max(a.x, b.x),
|
|
419
|
+
minY: Math.min(a.y, b.y),
|
|
420
|
+
maxY: Math.max(a.y, b.y),
|
|
421
|
+
}
|
|
422
|
+
// Trace segments are axis-aligned, so their bounding box is the segment.
|
|
423
|
+
if (boundsOverlap(segmentBounds, bounds)) return true
|
|
424
|
+
}
|
|
425
|
+
return false
|
|
426
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Point } from "@tscircuit/math-utils"
|
|
2
|
+
|
|
3
|
+
export interface AxisAlignedSegment {
|
|
4
|
+
start: Point
|
|
5
|
+
end: Point
|
|
6
|
+
axis: "x" | "y"
|
|
7
|
+
length: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Splits a trace path into its axis-aligned runs - the straight stretches of
|
|
12
|
+
* wire an inline net label can sit alongside without crossing a corner -
|
|
13
|
+
* ordered longest first.
|
|
14
|
+
*
|
|
15
|
+
* Segments that continue in the same direction are merged, so a straight
|
|
16
|
+
* stretch split into several points counts as one run. A reversal (a trace
|
|
17
|
+
* doubling back on itself) starts a new run, and diagonal segments are dropped
|
|
18
|
+
* since they have no well-defined "parallel" direction.
|
|
19
|
+
*/
|
|
20
|
+
export const getAxisAlignedSegments = (
|
|
21
|
+
path: Point[],
|
|
22
|
+
epsilon = 1e-6,
|
|
23
|
+
): AxisAlignedSegment[] => {
|
|
24
|
+
const segments: AxisAlignedSegment[] = []
|
|
25
|
+
|
|
26
|
+
let runStart: Point | null = null
|
|
27
|
+
let runAxis: "x" | "y" | null = null
|
|
28
|
+
let runSign = 0
|
|
29
|
+
|
|
30
|
+
const closeRun = (runEnd: Point) => {
|
|
31
|
+
if (runStart && runAxis) {
|
|
32
|
+
const length =
|
|
33
|
+
runAxis === "x"
|
|
34
|
+
? Math.abs(runEnd.x - runStart.x)
|
|
35
|
+
: Math.abs(runEnd.y - runStart.y)
|
|
36
|
+
if (length > epsilon) {
|
|
37
|
+
segments.push({ start: runStart, end: runEnd, axis: runAxis, length })
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
runStart = null
|
|
41
|
+
runAxis = null
|
|
42
|
+
runSign = 0
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
46
|
+
const a = path[i]!
|
|
47
|
+
const b = path[i + 1]!
|
|
48
|
+
const dx = b.x - a.x
|
|
49
|
+
const dy = b.y - a.y
|
|
50
|
+
|
|
51
|
+
if (Math.abs(dx) <= epsilon && Math.abs(dy) <= epsilon) continue
|
|
52
|
+
const axis: "x" | "y" | null =
|
|
53
|
+
Math.abs(dy) <= epsilon ? "x" : Math.abs(dx) <= epsilon ? "y" : null
|
|
54
|
+
if (!axis) {
|
|
55
|
+
closeRun(a)
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
const sign = Math.sign(axis === "x" ? dx : dy)
|
|
59
|
+
|
|
60
|
+
if (runStart && runAxis === axis && runSign === sign) continue
|
|
61
|
+
|
|
62
|
+
closeRun(a)
|
|
63
|
+
runStart = a
|
|
64
|
+
runAxis = axis
|
|
65
|
+
runSign = sign
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (path.length >= 2) {
|
|
69
|
+
closeRun(path[path.length - 1]!)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return segments.sort((a, b) => b.length - a.length)
|
|
73
|
+
}
|
|
@@ -31,6 +31,7 @@ import { NetLabelNetLabelCollisionSolver } from "../NetLabelNetLabelCollisionSol
|
|
|
31
31
|
import { UnroutedTraceRecoverySolver } from "../UnroutedTraceRecoverySolver/UnroutedTraceRecoverySolver"
|
|
32
32
|
import { SameNetJunctionAlignmentSolver } from "../SameNetJunctionAlignmentSolver/SameNetJunctionAlignmentSolver"
|
|
33
33
|
import { TraceElbowTransitionSimplificationSolver } from "../TraceElbowTransitionSimplificationSolver/TraceElbowTransitionSimplificationSolver"
|
|
34
|
+
import { InlineNetLabelSolver } from "../InlineNetLabelSolver/InlineNetLabelSolver"
|
|
34
35
|
|
|
35
36
|
type PipelineStep<T extends new (...args: any[]) => BaseSolver> = {
|
|
36
37
|
solverName: string
|
|
@@ -93,6 +94,7 @@ export class SchematicTracePipelineSolver extends BaseSolver {
|
|
|
93
94
|
preAlignmentTraceElbowTransitionSimplificationSolver?: TraceElbowTransitionSimplificationSolver
|
|
94
95
|
finalTraceElbowTransitionSimplificationSolver?: TraceElbowTransitionSimplificationSolver
|
|
95
96
|
sameNetJunctionAlignmentSolver?: SameNetJunctionAlignmentSolver
|
|
97
|
+
inlineNetLabelSolver?: InlineNetLabelSolver
|
|
96
98
|
|
|
97
99
|
startTimeOfPhase: Record<string, number>
|
|
98
100
|
endTimeOfPhase: Record<string, number>
|
|
@@ -502,6 +504,21 @@ export class SchematicTracePipelineSolver extends BaseSolver {
|
|
|
502
504
|
]
|
|
503
505
|
},
|
|
504
506
|
),
|
|
507
|
+
definePipelineStep(
|
|
508
|
+
"inlineNetLabelSolver",
|
|
509
|
+
InlineNetLabelSolver,
|
|
510
|
+
(instance) => {
|
|
511
|
+
const junctionOutput =
|
|
512
|
+
instance.sameNetJunctionAlignmentSolver!.getOutput()
|
|
513
|
+
return [
|
|
514
|
+
{
|
|
515
|
+
inputProblem: instance.inputProblem,
|
|
516
|
+
traces: junctionOutput.traces,
|
|
517
|
+
netLabelPlacements: junctionOutput.netLabelPlacements,
|
|
518
|
+
},
|
|
519
|
+
]
|
|
520
|
+
},
|
|
521
|
+
),
|
|
505
522
|
]
|
|
506
523
|
|
|
507
524
|
constructor(inputProblem: InputProblem, opts?: Options) {
|
|
@@ -34,6 +34,30 @@ export interface InputDirectConnection {
|
|
|
34
34
|
pinIds: [PinId, PinId]
|
|
35
35
|
netId?: string
|
|
36
36
|
netLabelWidth?: number
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* When true, this point-to-point connection may be labeled with an "inline
|
|
40
|
+
* net label": the net name is drawn parallel to (and offset from) the routed
|
|
41
|
+
* trace instead of being placed as a separate anchored net label at the end
|
|
42
|
+
* of the trace.
|
|
43
|
+
*
|
|
44
|
+
* Only set this for connections whose net name is worth showing on the wire -
|
|
45
|
+
* the solver trusts the caller (e.g. @tscircuit/core) to make that decision.
|
|
46
|
+
* An inline label is only emitted when the connection actually got routed.
|
|
47
|
+
*/
|
|
48
|
+
allowInlineNetLabel?: boolean
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Extent of the inline net label along the trace. Falls back to
|
|
52
|
+
* `netLabelWidth`, then to an estimate from the netId text.
|
|
53
|
+
*/
|
|
54
|
+
inlineNetLabelWidth?: number
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Height of the inline net label text. Defaults to
|
|
58
|
+
* DEFAULT_INLINE_NET_LABEL_HEIGHT.
|
|
59
|
+
*/
|
|
60
|
+
inlineNetLabelHeight?: number
|
|
37
61
|
}
|
|
38
62
|
|
|
39
63
|
export interface InputNetConnection {
|
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.
|
|
4
|
+
"version": "0.0.128",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"start": "cosmos",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"calculate-elbow": "^0.0.12",
|
|
20
20
|
"connectivity-map": "^1.0.0",
|
|
21
21
|
"flatbush": "^4.5.0",
|
|
22
|
-
"graphics-debug": "^0.0.
|
|
22
|
+
"graphics-debug": "^0.0.98",
|
|
23
23
|
"react": "^19.1.1",
|
|
24
24
|
"react-cosmos": "^7.0.0",
|
|
25
25
|
"react-cosmos-plugin-vite": "^7.0.0",
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"chips": [
|
|
3
|
+
{
|
|
4
|
+
"chipId": "U1",
|
|
5
|
+
"center": { "x": 0, "y": 0 },
|
|
6
|
+
"width": 1,
|
|
7
|
+
"height": 0.6,
|
|
8
|
+
"pins": [{ "pinId": "U1.1", "x": 0.5, "y": 0 }]
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"chipId": "U2",
|
|
12
|
+
"center": { "x": 3, "y": 0 },
|
|
13
|
+
"width": 1,
|
|
14
|
+
"height": 0.6,
|
|
15
|
+
"pins": [{ "pinId": "U2.1", "x": 2.5, "y": 0 }]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"chipId": "U3",
|
|
19
|
+
"center": { "x": 0, "y": -2 },
|
|
20
|
+
"width": 0.6,
|
|
21
|
+
"height": 1,
|
|
22
|
+
"pins": [{ "pinId": "U3.1", "x": 0, "y": -2.5 }]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"chipId": "U4",
|
|
26
|
+
"center": { "x": 0, "y": -4.5 },
|
|
27
|
+
"width": 0.6,
|
|
28
|
+
"height": 1,
|
|
29
|
+
"pins": [{ "pinId": "U4.1", "x": 0, "y": -4 }]
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"directConnections": [
|
|
33
|
+
{
|
|
34
|
+
"pinIds": ["U1.1", "U2.1"],
|
|
35
|
+
"netId": "USER_LED_ANODE",
|
|
36
|
+
"allowInlineNetLabel": true
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"pinIds": ["U3.1", "U4.1"],
|
|
40
|
+
"netId": "SPI_SCK",
|
|
41
|
+
"allowInlineNetLabel": true
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
"netConnections": [],
|
|
45
|
+
"availableNetLabelOrientations": {},
|
|
46
|
+
"maxMspPairDistance": 2.4
|
|
47
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import { getAxisAlignedSegments } from "lib/solvers/InlineNetLabelSolver/getAxisAlignedSegments"
|
|
3
|
+
|
|
4
|
+
test("getAxisAlignedSegments returns runs longest first", () => {
|
|
5
|
+
const segments = getAxisAlignedSegments([
|
|
6
|
+
{ x: 0, y: 0 },
|
|
7
|
+
{ x: 1, y: 0 },
|
|
8
|
+
{ x: 1, y: 3 },
|
|
9
|
+
{ x: 2, y: 3 },
|
|
10
|
+
])
|
|
11
|
+
|
|
12
|
+
expect(segments.map((s) => [s.axis, s.length])).toEqual([
|
|
13
|
+
["y", 3],
|
|
14
|
+
["x", 1],
|
|
15
|
+
["x", 1],
|
|
16
|
+
])
|
|
17
|
+
expect(segments[0]).toMatchObject({
|
|
18
|
+
start: { x: 1, y: 0 },
|
|
19
|
+
end: { x: 1, y: 3 },
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test("getAxisAlignedSegments merges collinear points", () => {
|
|
24
|
+
const segments = getAxisAlignedSegments([
|
|
25
|
+
{ x: 0, y: 0 },
|
|
26
|
+
{ x: 1, y: 0 },
|
|
27
|
+
{ x: 2, y: 0 },
|
|
28
|
+
{ x: 2, y: 1 },
|
|
29
|
+
])
|
|
30
|
+
|
|
31
|
+
expect(segments.map((s) => [s.axis, s.length])).toEqual([
|
|
32
|
+
["x", 2],
|
|
33
|
+
["y", 1],
|
|
34
|
+
])
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test("getAxisAlignedSegments splits a run that doubles back", () => {
|
|
38
|
+
const segments = getAxisAlignedSegments([
|
|
39
|
+
{ x: 0, y: 0 },
|
|
40
|
+
{ x: 3, y: 0 },
|
|
41
|
+
{ x: 1, y: 0 },
|
|
42
|
+
])
|
|
43
|
+
|
|
44
|
+
expect(segments.map((s) => s.length)).toEqual([3, 2])
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test("getAxisAlignedSegments returns nothing for degenerate paths", () => {
|
|
48
|
+
expect(getAxisAlignedSegments([])).toEqual([])
|
|
49
|
+
expect(getAxisAlignedSegments([{ x: 0, y: 0 }])).toEqual([])
|
|
50
|
+
expect(
|
|
51
|
+
getAxisAlignedSegments([
|
|
52
|
+
{ x: 0, y: 0 },
|
|
53
|
+
{ x: 0, y: 0 },
|
|
54
|
+
]),
|
|
55
|
+
).toEqual([])
|
|
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.5,0 2.5,0" data-type="line" data-label="" points="214.33962264150944,71.69811320754715 425.66037735849056,71.69811320754715" fill="none" stroke="hsl(48, 100%, 50%, 0.8)" stroke-width="1px"/></g><g><polyline data-points="0,-2.5 0,-4" data-type="line" data-label="" points="161.50943396226415,335.84905660377353 161.50943396226415,494.33962264150944" fill="none" stroke="hsl(136, 100%, 50%, 0.8)" stroke-width="1px"/></g><g><polyline data-points="0.5,0 2.5,0" data-type="line" data-label="" points="214.33962264150944,71.69811320754715 425.66037735849056,71.69811320754715" fill="none" stroke="purple" stroke-width="1px"/></g><g><polyline data-points="0,-2.5 0,-4" data-type="line" data-label="" points="161.50943396226415,335.84905660377353 161.50943396226415,494.33962264150944" fill="none" stroke="purple" stroke-width="1px"/></g><g><rect data-type="rect" data-label="U1" data-x="0" data-y="0" x="108.67924528301887" y="39.999999999999986" width="105.66037735849056" height="63.39622641509433" fill="hsl(164, 100%, 50%, 0.8)" stroke="black" stroke-width="0.009464285714285715"/></g><g><rect data-type="rect" data-label="U2" data-x="3" data-y="0" x="425.66037735849056" y="39.999999999999986" width="105.66037735849056" height="63.39622641509433" fill="hsl(165, 100%, 50%, 0.8)" stroke="black" stroke-width="0.009464285714285715"/></g><g><rect data-type="rect" data-label="U3" data-x="0" data-y="-2" x="129.81132075471697" y="230.18867924528303" width="63.39622641509436" height="105.66037735849054" fill="hsl(166, 100%, 50%, 0.8)" stroke="black" stroke-width="0.009464285714285715"/></g><g><rect data-type="rect" data-label="U4" data-x="0" data-y="-4.5" x="129.81132075471697" y="494.3396226415094" width="63.39622641509436" height="105.66037735849056" fill="hsl(167, 100%, 50%, 0.8)" stroke="black" stroke-width="0.009464285714285715"/></g><g><rect data-type="rect" data-label="INLINE netId: USER_LED_ANODE
|
|
2
|
+
axis: x
|
|
3
|
+
side: y+" data-x="1.5" data-y="0.14" x="224.90566037735852" y="47.39622641509432" width="190.18867924528297" height="19.01886792452831" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.009464285714285715"/></g><g><rect data-type="rect" data-label="INLINE netId: SPI_SCK
|
|
4
|
+
axis: y
|
|
5
|
+
side: x-" data-x="-0.14" data-y="-3.25" x="137.20754716981133" y="364.3773584905661" width="19.01886792452828" height="101.43396226415086" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.009464285714285715"/></g><text data-type="text" data-label="USER_LED_ANODE" data-x="1.5" data-y="0.14" x="320" y="56.90566037735847" fill="green" font-size="19.0188679245283" font-family="sans-serif" text-anchor="middle" dominant-baseline="central">USER_LED_ANODE</text><text data-type="text" data-label="SPI_SCK" data-x="-0.14" data-y="-3.25" x="146.7169811320755" y="415.09433962264154" fill="green" font-size="19.0188679245283" font-family="sans-serif" text-anchor="middle" dominant-baseline="central" transform="rotate(-90, 146.7169811320755, 415.09433962264154)">SPI_SCK</text><g><circle data-type="point" data-label="U1.1
|
|
6
|
+
x+" data-x="0.5" data-y="0" cx="214.33962264150944" cy="71.69811320754715" r="3" fill="hsl(319, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="U2.1
|
|
7
|
+
x-" data-x="2.5" data-y="0" cx="425.66037735849056" cy="71.69811320754715" r="3" fill="hsl(200, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="U3.1
|
|
8
|
+
y-" data-x="0" data-y="-2.5" cx="161.50943396226415" cy="335.84905660377353" r="3" fill="hsl(81, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="U4.1
|
|
9
|
+
y+" data-x="0" data-y="-4" cx="161.50943396226415" cy="494.33962264150944" r="3" fill="hsl(322, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="inline anchor
|
|
10
|
+
USER_LED_ANODE" data-x="1.5" data-y="0" cx="320" cy="71.69811320754715" r="3" fill="green"/></g><g><circle data-type="point" data-label="inline anchor
|
|
11
|
+
SPI_SCK" data-x="0" data-y="-3.25" cx="161.50943396226415" cy="415.09433962264154" 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":105.66037735849056,"c":0,"e":161.50943396226415,"b":0,"d":-105.66037735849056,"f":71.69811320754715};
|
|
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,44 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
|
|
3
|
+
import inputProblem from "../../assets/inline-net-label01.json"
|
|
4
|
+
import "tests/fixtures/matcher"
|
|
5
|
+
|
|
6
|
+
test("inline-net-label01 places inline labels alongside point-to-point traces", () => {
|
|
7
|
+
const solver = new SchematicTracePipelineSolver(inputProblem as any)
|
|
8
|
+
|
|
9
|
+
solver.solve()
|
|
10
|
+
|
|
11
|
+
const inlinePlacements = solver
|
|
12
|
+
.inlineNetLabelSolver!.inlineNetLabelPlacements.slice()
|
|
13
|
+
.sort((a, b) => (a.netId ?? "").localeCompare(b.netId ?? ""))
|
|
14
|
+
|
|
15
|
+
expect(inlinePlacements.map((p) => p.netId)).toEqual([
|
|
16
|
+
"SPI_SCK",
|
|
17
|
+
"USER_LED_ANODE",
|
|
18
|
+
])
|
|
19
|
+
|
|
20
|
+
const [spiSck, userLedAnode] = inlinePlacements
|
|
21
|
+
|
|
22
|
+
// The horizontal trace gets a horizontal label sitting above the wire.
|
|
23
|
+
expect(userLedAnode!.axis).toBe("x")
|
|
24
|
+
expect(userLedAnode!.side).toBe("y+")
|
|
25
|
+
expect(userLedAnode!.center.x).toBeCloseTo(userLedAnode!.anchorPoint.x, 6)
|
|
26
|
+
expect(userLedAnode!.center.y).toBeGreaterThan(userLedAnode!.anchorPoint.y)
|
|
27
|
+
|
|
28
|
+
// The vertical trace gets a vertical label on the left of the wire, which is
|
|
29
|
+
// "above" it once the text is rotated.
|
|
30
|
+
expect(spiSck!.axis).toBe("y")
|
|
31
|
+
expect(spiSck!.side).toBe("x-")
|
|
32
|
+
expect(spiSck!.center.y).toBeCloseTo(spiSck!.anchorPoint.y, 6)
|
|
33
|
+
expect(spiSck!.center.x).toBeLessThan(spiSck!.anchorPoint.x)
|
|
34
|
+
|
|
35
|
+
// An inline label replaces the anchored label for that net, it never adds a
|
|
36
|
+
// second label.
|
|
37
|
+
const inlineNetIds = new Set(inlinePlacements.map((p) => p.globalConnNetId))
|
|
38
|
+
const anchoredNetIds = solver
|
|
39
|
+
.inlineNetLabelSolver!.getOutput()
|
|
40
|
+
.netLabelPlacements.map((p) => p.globalConnNetId)
|
|
41
|
+
expect(anchoredNetIds.filter((id) => inlineNetIds.has(id))).toEqual([])
|
|
42
|
+
|
|
43
|
+
expect(solver).toMatchSolverSnapshot(import.meta.path)
|
|
44
|
+
})
|