@tscircuit/schematic-trace-solver 0.0.149 → 0.0.150
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/.agents/skills/schematic-trace-visualization/SKILL.md +7 -2
- package/dist/index.d.ts +28 -1
- package/dist/index.js +26 -4
- package/lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver.ts +14 -4
- package/lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver.ts +17 -1
- package/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/SingleNetLabelPlacementSolver.ts +2 -0
- package/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/solvePortOnlyPin.ts +2 -0
- package/lib/types/InputProblem.ts +27 -1
- package/package.json +8 -1
- package/tests/bug-reports/bug-report-20260706T213649Z/__snapshots__/bug-report-20260706T213649Z.snap.svg +751 -5
- package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +660 -5
- package/tests/bug-reports/bug-report-20260707T020342Z/__snapshots__/bug-report-20260707T020342Z.snap.svg +44 -5
- package/tests/bug-reports/bug-report-20260707T092615Z/__snapshots__/bug-report-20260707T092615Z.snap.svg +394 -5
- package/tests/bug-reports/bug-report-20260707T134549Z/__snapshots__/bug-report-20260707T134549Z.snap.svg +170 -5
- package/tests/bug-reports/bug-report-20260707T134722Z/__snapshots__/bug-report-20260707T134722Z.snap.svg +79 -5
- package/tests/bug-reports/bug-report-20260707T140410Z/__snapshots__/bug-report-20260707T140410Z.snap.svg +219 -5
- package/tests/bug-reports/bug-report-20260707T141025Z/__snapshots__/bug-report-20260707T141025Z.snap.svg +65 -5
- package/tests/bug-reports/bug-report-20260707T141421Z/__snapshots__/bug-report-20260707T141421Z.snap.svg +142 -5
- package/tests/bug-reports/bug-report-20260707T230831Z/__snapshots__/bug-report-20260707T230831Z.snap.svg +100 -5
- package/tests/bug-reports/bug-report-20260708T053736Z/__snapshots__/bug-report-20260708T053736Z.snap.svg +23 -5
- package/tests/bug-reports/bug-report-20260708T055430Z/__snapshots__/bug-report-20260708T055430Z.snap.svg +58 -5
- package/tests/bug-reports/bug-report-20260708T095725Z/__snapshots__/bug-report-20260708T095725Z.snap.svg +86 -5
- package/tests/bug-reports/bug-report-20260716T144856Z/__snapshots__/bug-report-20260716T144856Z.snap.svg +226 -5
- package/tests/bug-reports/bug-report-20260717T022934Z/__snapshots__/bug-report-20260717T022934Z.snap.svg +191 -5
- package/tests/bug-reports/bug-report-20260717T031704Z/__snapshots__/bug-report-20260717T031704Z.snap.svg +30 -5
- package/tests/bug-reports/bug-report-20260717T042845Z/__snapshots__/bug-report-20260717T042845Z.snap.svg +37 -5
- package/tests/bug-reports/bug-report-20260721T221026Z/__snapshots__/bug-report-20260721T221026Z.snap.svg +580 -2
- package/tests/bug-reports/bug-report-20260724T175257Z/__snapshots__/bug-report-20260724T175257Z.snap.svg +44 -5
- package/tests/bug-reports/bug-report-20260728T144234Z/__snapshots__/bug-report-20260728T144234Z.snap.svg +107 -5
- package/tests/bug-reports/bug-report-20260728T225606Z/__snapshots__/bug-report-20260728T225606Z.snap.svg +114 -5
- package/tests/bug-reports/bug-report-20260730T061837Z/__snapshots__/bug-report-20260730T061837Z.snap.svg +121 -5
- package/tests/bug-reports/bug-report-20260731T052229Z/__snapshots__/bug-report-20260731T052229Z.snap.svg +51 -5
- package/tests/bug-reports/bug-report-20260804T095800Z/__snapshots__/bug-report-20260804T095800Z.snap.svg +212 -5
- package/tests/bug-reports/bug-report-20260804T171919Z/__snapshots__/bug-report-20260804T171919Z.snap.svg +569 -5
- package/tests/bug-reports/bug-report-20260804T225912Z/__snapshots__/bug-report-20260804T225912Z.snap.svg +37 -5
- package/tests/bug-reports/bug-report-20260805T061316Z/__snapshots__/bug-report-20260805T061316Z.snap.svg +289 -5
- package/tests/bug-reports/bug-report-20260806T061548Z/__snapshots__/bug-report-20260806T061548Z.snap.svg +289 -5
- package/tests/bug-reports/bug-report-20260806T093501Z/__snapshots__/bug-report-20260806T093501Z.snap.svg +149 -5
- package/tests/bug-reports/bug-report-20260811T075142Z/__snapshots__/bug-report-20260811T075142Z.snap.svg +93 -5
- package/tests/bug-reports/bug-report-20260815T073240Z/__snapshots__/bug-report-20260815T073240Z.snap.svg +79 -5
- package/tests/bug-reports/bug-report-20260819T091818Z/__snapshots__/bug-report-20260819T091818Z.snap.svg +244 -2
- package/tests/examples/__snapshots__/example01.snap.svg +44 -5
- package/tests/examples/__snapshots__/example02.snap.svg +51 -5
- package/tests/examples/__snapshots__/example03.snap.svg +86 -5
- package/tests/examples/__snapshots__/example04.snap.svg +30 -5
- package/tests/examples/__snapshots__/example05.snap.svg +30 -5
- package/tests/examples/__snapshots__/example06.snap.svg +30 -5
- package/tests/examples/__snapshots__/example07.snap.svg +58 -5
- package/tests/examples/__snapshots__/example08.snap.svg +37 -5
- package/tests/examples/__snapshots__/example09.snap.svg +149 -5
- package/tests/examples/__snapshots__/example10.snap.svg +65 -5
- package/tests/examples/__snapshots__/example11.snap.svg +51 -5
- package/tests/examples/__snapshots__/example12.snap.svg +51 -5
- package/tests/examples/__snapshots__/example13.snap.svg +72 -5
- package/tests/examples/__snapshots__/example14.snap.svg +79 -5
- package/tests/examples/__snapshots__/example15.snap.svg +86 -5
- package/tests/examples/__snapshots__/example16.snap.svg +51 -5
- package/tests/examples/__snapshots__/example17.snap.svg +51 -5
- package/tests/examples/__snapshots__/example18.snap.svg +58 -5
- package/tests/examples/__snapshots__/example19.snap.svg +44 -5
- package/tests/examples/__snapshots__/example20.snap.svg +44 -5
- package/tests/examples/__snapshots__/example21.snap.svg +72 -5
- package/tests/examples/__snapshots__/example22.snap.svg +51 -5
- package/tests/examples/__snapshots__/example23.snap.svg +51 -5
- package/tests/examples/__snapshots__/example24.snap.svg +51 -5
- package/tests/examples/__snapshots__/example25.snap.svg +86 -5
- package/tests/examples/__snapshots__/example26.snap.svg +51 -5
- package/tests/examples/__snapshots__/example27.snap.svg +72 -5
- package/tests/examples/__snapshots__/example28.snap.svg +44 -5
- package/tests/examples/__snapshots__/example29.snap.svg +324 -5
- package/tests/examples/__snapshots__/example30.snap.svg +107 -5
- package/tests/examples/__snapshots__/example31.snap.svg +37 -5
- package/tests/examples/__snapshots__/example32.snap.svg +149 -5
- package/tests/examples/__snapshots__/example33.snap.svg +93 -5
- package/tests/examples/__snapshots__/example34.snap.svg +51 -5
- package/tests/examples/__snapshots__/example35.snap.svg +58 -5
- package/tests/examples/__snapshots__/example36.snap.svg +44 -5
- package/tests/examples/__snapshots__/example37.snap.svg +72 -5
- package/tests/examples/__snapshots__/example38.snap.svg +30 -5
- package/tests/examples/__snapshots__/example39.snap.svg +37 -5
- package/tests/examples/__snapshots__/example40.snap.svg +100 -5
- package/tests/examples/__snapshots__/example41.snap.svg +44 -5
- package/tests/examples/__snapshots__/example42.snap.svg +65 -5
- package/tests/examples/__snapshots__/example43.snap.svg +58 -5
- package/tests/examples/__snapshots__/example44.snap.svg +107 -5
- package/tests/examples/__snapshots__/example45.snap.svg +51 -5
- package/tests/examples/__snapshots__/example46.snap.svg +93 -5
- package/tests/examples/__snapshots__/example47.snap.svg +93 -5
- package/tests/examples/__snapshots__/example48.snap.svg +65 -5
- package/tests/examples/__snapshots__/example49.snap.svg +177 -5
- package/tests/examples/__snapshots__/example50.snap.svg +93 -5
- package/tests/examples/__snapshots__/example51.snap.svg +590 -5
- package/tests/fixtures/convertSolverOutputToCircuitJson.test.ts +219 -0
- package/tests/fixtures/convertSolverOutputToCircuitJson.ts +1058 -0
- package/tests/fixtures/matcher.ts +45 -8
- package/tests/repros/__snapshots__/board-1273-trace-overlap-cycle.snap.svg +762 -2
- package/tests/repros/__snapshots__/board-15984-same-net-junction.snap.svg +111 -2
- package/tests/repros/__snapshots__/board1096-usb-label-overlap-iteration.snap.svg +167 -2
- package/tests/repros/__snapshots__/bugreport-001-gnd-overlap.snap.svg +65 -5
- package/tests/repros/__snapshots__/manufacturePartNumber-text-box.snap.svg +30 -5
- package/tests/repros/__snapshots__/netlabel-connector-through-rail-label.snap.svg +79 -5
- package/tests/repros/__snapshots__/repro-atmega328p-fault-pullup.snap.svg +65 -5
- package/tests/repros/__snapshots__/repro-atmega328p-missing-gnd-netlabel.snap.svg +149 -5
- package/tests/repros/__snapshots__/repro-board-589-regulator-section.snap.svg +55 -2
- package/tests/repros/__snapshots__/repro-board-648-esp12f-section.snap.svg +160 -2
- package/tests/repros/__snapshots__/repro-bq24074-battery-charger.snap.svg +135 -5
- package/tests/repros/__snapshots__/repro-bq25895-cross-net-trace-overlap.snap.svg +69 -2
- package/tests/repros/__snapshots__/repro-cc2340r5.snap.svg +86 -5
- package/tests/repros/__snapshots__/repro-core-555-power-cross-net-overlap.snap.svg +44 -5
- package/tests/repros/__snapshots__/repro-core-subcircuit-missing-ground.snap.svg +44 -5
- package/tests/repros/__snapshots__/repro-core-tb67s579ftg-inline-label-routing.snap.svg +30 -5
- package/tests/repros/__snapshots__/repro-example35-minimize-trace-crossing.snap.svg +335 -2
- package/tests/repros/__snapshots__/repro-ground-label-on-lower-rail-end.snap.svg +48 -2
- package/tests/repros/__snapshots__/repro-ina237-current-monitor.snap.svg +149 -5
- package/tests/repros/__snapshots__/repro-missing-trace-netlabel.snap.svg +72 -5
- package/tests/repros/__snapshots__/repro-netlabel-collision-687.snap.svg +100 -5
- package/tests/repros/__snapshots__/repro-netlabel-overlap-trace.snap.svg +51 -5
- package/tests/repros/__snapshots__/repro-nrf52810-clock-routing.snap.svg +90 -2
- package/tests/repros/__snapshots__/repro-power-group-rail-labels.snap.svg +37 -5
- package/tests/repros/__snapshots__/repro-rectifier-trace-overlap.snap.svg +44 -5
- package/tests/repros/__snapshots__/repro-rp2040-gamepad-trace-alignment.snap.svg +237 -2
- package/tests/repros/__snapshots__/repro-rp2040-usb-cc2-ground-overlap.snap.svg +72 -5
- package/tests/repros/__snapshots__/repro-rp2040-v3v3-label-placement.snap.svg +51 -5
- package/tests/repros/__snapshots__/repro-rp2040-zero-crystal-fallback-netlabels.snap.svg +30 -5
- package/tests/repros/__snapshots__/repro-ti-power-output-section.snap.svg +79 -5
- package/tests/repros/__snapshots__/repro-tida010076-j4-ground-bus.snap.svg +27 -2
- package/tests/repros/__snapshots__/repro-tida010076-page02.snap.svg +1150 -5
- package/tests/repros/__snapshots__/repro-tps61222-trace-intersection.snap.svg +51 -5
- package/tests/repros/__snapshots__/repro-type-c-16pin-self-connection.snap.svg +30 -5
- package/tests/repros/__snapshots__/repro-type-c-16pin-to-resistor.snap.svg +30 -5
- package/tests/repros/__snapshots__/repro-type-c-vbus-led-ground-label.snap.svg +44 -5
- package/tests/repros/__snapshots__/repro-usb-power-vbus-label-detour.snap.svg +685 -2
- package/tests/repros/__snapshots__/repro-vcc-pin1-detour.snap.svg +27 -2
- package/tests/repros/__snapshots__/repro129-host-custom-symbol-passives.snap.svg +51 -5
- package/tests/repros/__snapshots__/repro130-bq27441-fuel-gauge-trace-through-c1.snap.svg +100 -5
- package/tests/repros/__snapshots__/repro161-power-section-autolayout.snap.svg +107 -5
- package/tests/repros/__snapshots__/repro47-endpoint-obstacle-detour.snap.svg +51 -5
- package/tests/repros/__snapshots__/repro48-555-timer-vcc-rail-label.snap.svg +104 -2
- package/tests/repros/__snapshots__/repro5-escape-padded-text-obstacles.snap.svg +30 -5
- package/tests/repros/__snapshots__/repro51-overlap-junction-crossing.snap.svg +65 -5
- package/tests/repros/__snapshots__/repro84-stacked-gnd-label-orientation.snap.svg +27 -2
- package/tests/repros/__snapshots__/small-variant-resistor-facing-direction.snap.svg +58 -5
- package/tests/repros/__snapshots__/trace-overlap-box-resistor.snap.svg +37 -5
- package/tests/solvers/InlineNetLabelSolver/__snapshots__/fallback-net-label-width.snap.svg +25 -7
- package/tests/solvers/InlineNetLabelSolver/__snapshots__/inline-net-label-anchored-label-clearance.snap.svg +34 -2
- package/tests/solvers/InlineNetLabelSolver/__snapshots__/inline-net-label-port-stub.snap.svg +23 -5
- package/tests/solvers/InlineNetLabelSolver/__snapshots__/inline-net-label-two-pin-net.snap.svg +23 -5
- package/tests/solvers/InlineNetLabelSolver/__snapshots__/inline-net-label01.snap.svg +23 -5
- package/tests/solvers/InlineNetLabelSolver/__snapshots__/inline-net-label02.snap.svg +23 -5
- package/tests/solvers/InlineNetLabelSolver/__snapshots__/terminal-stub-trace-clearance.snap.svg +23 -5
- package/tests/solvers/InlineNetLabelSolver/fallback-net-label-width.test.ts +10 -1
- package/tests/solvers/SchematicTraceSingleLineSolver2/__snapshots__/SchematicTraceSingleLineSolver2_01-example17-d1_1-u1_1.snap.svg +23 -5
- package/tests/solvers/SchematicTraceSingleLineSolver2/__snapshots__/SchematicTraceSingleLineSolver2_repro47-short-opposing-pins.snap.svg +20 -2
- package/tests/solvers/TraceCleanupSolver/__snapshots__/TraceCleanupSolver.snap.svg +44 -5
- package/tests/solvers/TraceLabelOverlapAvoidanceSolver/__snapshots__/TraceLabelOverlapAvoidanceSolver.snap.svg +79 -5
- package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/__snapshots__/MergedNetLabelObstacles.snap.svg +44 -5
- package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/__snapshots__/OverlapAvoidanceStepSolver.snap.svg +23 -5
- package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/__snapshots__/SingleOverlapSolver.snap.svg +30 -5
|
@@ -0,0 +1,1058 @@
|
|
|
1
|
+
import type { Point } from "@tscircuit/math-utils"
|
|
2
|
+
import type {
|
|
3
|
+
AnyCircuitElement,
|
|
4
|
+
SchematicBox,
|
|
5
|
+
SchematicComponent,
|
|
6
|
+
SchematicNetLabel,
|
|
7
|
+
SchematicPort,
|
|
8
|
+
SchematicText,
|
|
9
|
+
SchematicTrace,
|
|
10
|
+
SourceNet,
|
|
11
|
+
SourcePort,
|
|
12
|
+
SourceSimpleChip,
|
|
13
|
+
SourceTrace,
|
|
14
|
+
} from "circuit-json"
|
|
15
|
+
import type { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
|
|
16
|
+
import type { InlineNetLabelPlacement } from "lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver"
|
|
17
|
+
import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
18
|
+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
19
|
+
import { getPinDirection } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/getPinDirection"
|
|
20
|
+
import type { InputChip, InputPin, InputProblem } from "lib/types/InputProblem"
|
|
21
|
+
import type { FacingDirection } from "lib/utils/dir"
|
|
22
|
+
import { type SchSymbol, symbols } from "schematic-symbols"
|
|
23
|
+
|
|
24
|
+
type SnapshotTrace = Partial<SolvedTracePath> & {
|
|
25
|
+
tracePath: Point[]
|
|
26
|
+
mspPairId?: string
|
|
27
|
+
netId?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type SolverOutput = {
|
|
31
|
+
traces?: SnapshotTrace[]
|
|
32
|
+
allTraces?: SnapshotTrace[]
|
|
33
|
+
allTracesMerged?: SnapshotTrace[]
|
|
34
|
+
netLabelPlacements?: NetLabelPlacement[]
|
|
35
|
+
inlineNetLabelPlacements?: InlineNetLabelPlacement[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type SolverLike = BaseSolver & {
|
|
39
|
+
inputProblem?: InputProblem
|
|
40
|
+
problem?: InputProblem
|
|
41
|
+
input?: {
|
|
42
|
+
inputProblem?: InputProblem
|
|
43
|
+
problem?: InputProblem
|
|
44
|
+
traces?: SnapshotTrace[]
|
|
45
|
+
allTraces?: SnapshotTrace[]
|
|
46
|
+
allLabelPlacements?: NetLabelPlacement[]
|
|
47
|
+
netLabelPlacements?: NetLabelPlacement[]
|
|
48
|
+
}
|
|
49
|
+
params?: { inputProblem?: InputProblem }
|
|
50
|
+
traces?: SnapshotTrace[]
|
|
51
|
+
allTraces?: SnapshotTrace[]
|
|
52
|
+
outputTraces?: SnapshotTrace[]
|
|
53
|
+
solvedTracePaths?: SnapshotTrace[]
|
|
54
|
+
solvedTracePath?: Point[] | null
|
|
55
|
+
initialTrace?: SnapshotTrace
|
|
56
|
+
connectionPair?: Partial<SnapshotTrace>
|
|
57
|
+
pins?: Array<InputPin & { chipId?: string }>
|
|
58
|
+
netLabelPlacements?: NetLabelPlacement[]
|
|
59
|
+
outputNetLabelPlacements?: NetLabelPlacement[]
|
|
60
|
+
inputNetLabelPlacements?: NetLabelPlacement[]
|
|
61
|
+
initialNetLabelPlacements?: NetLabelPlacement[]
|
|
62
|
+
inlineNetLabelPlacements?: InlineNetLabelPlacement[]
|
|
63
|
+
label?: NetLabelPlacement
|
|
64
|
+
getOutput?: () => SolverOutput
|
|
65
|
+
inlineNetLabelSolver?: { getOutput: () => SolverOutput }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const isInputProblem = (value: unknown): value is InputProblem => {
|
|
69
|
+
if (!value || typeof value !== "object") return false
|
|
70
|
+
const candidate = value as Partial<InputProblem>
|
|
71
|
+
return (
|
|
72
|
+
Array.isArray(candidate.chips) &&
|
|
73
|
+
Array.isArray(candidate.directConnections) &&
|
|
74
|
+
Array.isArray(candidate.netConnections)
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const getInputProblemFromSolver = (
|
|
79
|
+
solver: BaseSolver,
|
|
80
|
+
): InputProblem | undefined => {
|
|
81
|
+
const candidate = solver as SolverLike
|
|
82
|
+
const possibleInputs = [
|
|
83
|
+
candidate.inputProblem,
|
|
84
|
+
candidate.problem,
|
|
85
|
+
candidate.input?.inputProblem,
|
|
86
|
+
candidate.input?.problem,
|
|
87
|
+
candidate.params?.inputProblem,
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
return possibleInputs.find(isInputProblem)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const safelyGetOutput = (solver: SolverLike): SolverOutput | undefined => {
|
|
94
|
+
try {
|
|
95
|
+
if (solver.inlineNetLabelSolver?.getOutput) {
|
|
96
|
+
return solver.inlineNetLabelSolver.getOutput()
|
|
97
|
+
}
|
|
98
|
+
return solver.getOutput?.()
|
|
99
|
+
} catch {
|
|
100
|
+
// A partially solved solver can expose getOutput before it is ready. The
|
|
101
|
+
// property fallbacks below still produce a useful snapshot in that case.
|
|
102
|
+
return undefined
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const isTraceArray = (value: unknown): value is SnapshotTrace[] =>
|
|
107
|
+
Array.isArray(value) &&
|
|
108
|
+
value.every(
|
|
109
|
+
(trace) =>
|
|
110
|
+
trace &&
|
|
111
|
+
typeof trace === "object" &&
|
|
112
|
+
Array.isArray((trace as SnapshotTrace).tracePath),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
const firstTraceArray = (candidates: unknown[]): SnapshotTrace[] | undefined =>
|
|
116
|
+
candidates.find(isTraceArray)
|
|
117
|
+
|
|
118
|
+
const firstArray = <T>(candidates: unknown[]): T[] | undefined =>
|
|
119
|
+
candidates.find(Array.isArray) as T[] | undefined
|
|
120
|
+
|
|
121
|
+
const getSnapshotData = (solver: BaseSolver) => {
|
|
122
|
+
const candidate = solver as SolverLike
|
|
123
|
+
const output = safelyGetOutput(candidate)
|
|
124
|
+
|
|
125
|
+
let traces = firstTraceArray([
|
|
126
|
+
output?.traces,
|
|
127
|
+
output?.allTraces,
|
|
128
|
+
output?.allTracesMerged,
|
|
129
|
+
candidate.outputTraces,
|
|
130
|
+
candidate.traces,
|
|
131
|
+
candidate.solvedTracePaths,
|
|
132
|
+
candidate.allTraces,
|
|
133
|
+
candidate.input?.traces,
|
|
134
|
+
candidate.input?.allTraces,
|
|
135
|
+
])
|
|
136
|
+
|
|
137
|
+
if (!traces && candidate.solvedTracePath) {
|
|
138
|
+
traces = [
|
|
139
|
+
{
|
|
140
|
+
...(candidate.initialTrace ?? candidate.connectionPair),
|
|
141
|
+
tracePath: candidate.solvedTracePath,
|
|
142
|
+
pinIds:
|
|
143
|
+
candidate.connectionPair?.pinIds ??
|
|
144
|
+
candidate.pins?.map((p) => p.pinId),
|
|
145
|
+
},
|
|
146
|
+
]
|
|
147
|
+
} else if (!traces && candidate.initialTrace) {
|
|
148
|
+
traces = [candidate.initialTrace]
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const netLabelPlacements =
|
|
152
|
+
firstArray<NetLabelPlacement>([
|
|
153
|
+
output?.netLabelPlacements,
|
|
154
|
+
candidate.outputNetLabelPlacements,
|
|
155
|
+
candidate.netLabelPlacements,
|
|
156
|
+
candidate.inputNetLabelPlacements,
|
|
157
|
+
candidate.initialNetLabelPlacements,
|
|
158
|
+
candidate.input?.allLabelPlacements,
|
|
159
|
+
candidate.input?.netLabelPlacements,
|
|
160
|
+
]) ?? (candidate.label ? [candidate.label] : [])
|
|
161
|
+
|
|
162
|
+
const inlineNetLabelPlacements =
|
|
163
|
+
firstArray<InlineNetLabelPlacement>([
|
|
164
|
+
output?.inlineNetLabelPlacements,
|
|
165
|
+
candidate.inlineNetLabelPlacements,
|
|
166
|
+
]) ?? []
|
|
167
|
+
|
|
168
|
+
const tracesWithInlineStubs = [...(traces ?? [])]
|
|
169
|
+
for (const [inlineIndex, placement] of inlineNetLabelPlacements.entries()) {
|
|
170
|
+
if (!placement.stubTracePath) continue
|
|
171
|
+
tracesWithInlineStubs.push({
|
|
172
|
+
tracePath: placement.stubTracePath,
|
|
173
|
+
mspPairId: `inline-stub-${inlineIndex}`,
|
|
174
|
+
globalConnNetId: placement.globalConnNetId,
|
|
175
|
+
userNetId: placement.netId,
|
|
176
|
+
pinIds: placement.pinIds,
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
traces: tracesWithInlineStubs,
|
|
182
|
+
netLabelPlacements,
|
|
183
|
+
inlineNetLabelPlacements,
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const getRefdes = (chip: InputChip) => {
|
|
188
|
+
const pinPrefixes = new Set(
|
|
189
|
+
chip.pins
|
|
190
|
+
.map((pin) => pin.pinId.split(".")[0])
|
|
191
|
+
.filter((prefix): prefix is string => Boolean(prefix)),
|
|
192
|
+
)
|
|
193
|
+
if (pinPrefixes.size === 1) return [...pinPrefixes][0]!
|
|
194
|
+
return chip.chipId
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const getPinLabel = (pinId: string) =>
|
|
198
|
+
pinId.includes(".") ? pinId.slice(pinId.indexOf(".") + 1) : pinId
|
|
199
|
+
|
|
200
|
+
const getPinNumber = (pinId: string) => {
|
|
201
|
+
const label = getPinLabel(pinId)
|
|
202
|
+
return /^\d+$/.test(label) ? Number(label) : undefined
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const getFacingDirection = (pin: InputPin, chip: InputChip): FacingDirection =>
|
|
206
|
+
pin._facingDirection ?? getPinDirection(pin, chip)
|
|
207
|
+
|
|
208
|
+
const facingDirectionToSide = (
|
|
209
|
+
facingDirection: FacingDirection,
|
|
210
|
+
): NonNullable<SchematicPort["side_of_component"]> => {
|
|
211
|
+
switch (facingDirection) {
|
|
212
|
+
case "x+":
|
|
213
|
+
return "right"
|
|
214
|
+
case "x-":
|
|
215
|
+
return "left"
|
|
216
|
+
case "y+":
|
|
217
|
+
return "top"
|
|
218
|
+
case "y-":
|
|
219
|
+
return "bottom"
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const facingDirectionToCircuitJson = (
|
|
224
|
+
facingDirection: FacingDirection,
|
|
225
|
+
): NonNullable<SchematicPort["facing_direction"]> => {
|
|
226
|
+
switch (facingDirection) {
|
|
227
|
+
case "x+":
|
|
228
|
+
return "right"
|
|
229
|
+
case "x-":
|
|
230
|
+
return "left"
|
|
231
|
+
case "y+":
|
|
232
|
+
return "up"
|
|
233
|
+
case "y-":
|
|
234
|
+
return "down"
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const getDefaultSymbolBaseName = (refdes: string) => {
|
|
239
|
+
const prefix = refdes.match(/^[A-Za-z]+/)?.[0]?.toUpperCase()
|
|
240
|
+
if (!prefix) return undefined
|
|
241
|
+
|
|
242
|
+
// Match the defaults used by tscircuit/core's normal components. The more
|
|
243
|
+
// specific LED prefix must be checked before the generic inductor prefix.
|
|
244
|
+
if (prefix === "LED") return "led"
|
|
245
|
+
if (prefix.startsWith("R")) return "boxresistor"
|
|
246
|
+
if (["C", "CIN", "COUT", "CCOMP"].includes(prefix)) return "capacitor"
|
|
247
|
+
if (prefix === "L") return "inductor"
|
|
248
|
+
if (prefix.startsWith("D")) return "diode"
|
|
249
|
+
if (prefix === "F") return "fuse"
|
|
250
|
+
if (["B", "BT", "BAT"].includes(prefix)) return "battery"
|
|
251
|
+
if (prefix === "X" || prefix === "Y") return "crystal"
|
|
252
|
+
if (prefix === "SW") return "spst_switch"
|
|
253
|
+
if (prefix === "I") return "current_source"
|
|
254
|
+
if (prefix === "V" || prefix === "VS") return "ac_voltmeter"
|
|
255
|
+
|
|
256
|
+
return undefined
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const getPinsInNumberOrder = (chip: InputChip) =>
|
|
260
|
+
[...chip.pins].sort((pinA, pinB) => {
|
|
261
|
+
const pinNumberA = getPinNumber(pinA.pinId)
|
|
262
|
+
const pinNumberB = getPinNumber(pinB.pinId)
|
|
263
|
+
if (pinNumberA === undefined || pinNumberB === undefined) return 0
|
|
264
|
+
return pinNumberA - pinNumberB
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
const getDefaultSymbolName = (chip: InputChip, refdes: string) => {
|
|
268
|
+
if (chip.pins.length !== 2) return undefined
|
|
269
|
+
if (chip.symbolName && chip.symbolName in symbols) return chip.symbolName
|
|
270
|
+
const baseSymbolName = getDefaultSymbolBaseName(refdes)
|
|
271
|
+
if (!baseSymbolName) return undefined
|
|
272
|
+
|
|
273
|
+
const [firstPin, secondPin] = getPinsInNumberOrder(chip)
|
|
274
|
+
const deltaX = secondPin!.x - firstPin!.x
|
|
275
|
+
const deltaY = secondPin!.y - firstPin!.y
|
|
276
|
+
const isHorizontal = Math.abs(deltaX) >= Math.abs(deltaY)
|
|
277
|
+
|
|
278
|
+
const suffix =
|
|
279
|
+
baseSymbolName === "fuse"
|
|
280
|
+
? isHorizontal
|
|
281
|
+
? "horz"
|
|
282
|
+
: "vert"
|
|
283
|
+
: isHorizontal
|
|
284
|
+
? deltaX >= 0
|
|
285
|
+
? "right"
|
|
286
|
+
: "left"
|
|
287
|
+
: deltaY >= 0
|
|
288
|
+
? "up"
|
|
289
|
+
: "down"
|
|
290
|
+
const symbolName = `${baseSymbolName}_${suffix}`
|
|
291
|
+
|
|
292
|
+
return symbolName in symbols ? symbolName : undefined
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const getAngularDifference = (angle1: number, angle2: number) => {
|
|
296
|
+
const normalizedAngle1 = angle1 < 0 ? angle1 + 2 * Math.PI : angle1
|
|
297
|
+
const normalizedAngle2 = angle2 < 0 ? angle2 + 2 * Math.PI : angle2
|
|
298
|
+
const difference = Math.abs(normalizedAngle1 - normalizedAngle2)
|
|
299
|
+
return difference > Math.PI ? 2 * Math.PI - difference : difference
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* circuit-to-svg@0.0.407 derives a symbol transform from two schematic ports.
|
|
304
|
+
* Its scale is correct, but its translation is calculated in unscaled symbol
|
|
305
|
+
* coordinates. Offsetting the component and both ports here makes the rendered
|
|
306
|
+
* symbol terminals land on the original solver pin coordinates.
|
|
307
|
+
*/
|
|
308
|
+
const getCircuitToSvgSymbolOffset = ({
|
|
309
|
+
chip,
|
|
310
|
+
componentCenter,
|
|
311
|
+
symbol,
|
|
312
|
+
scale,
|
|
313
|
+
}: {
|
|
314
|
+
chip: InputChip
|
|
315
|
+
componentCenter: Point
|
|
316
|
+
symbol: SchSymbol
|
|
317
|
+
scale: number
|
|
318
|
+
}): Point => {
|
|
319
|
+
const pinsByAngle = chip.pins
|
|
320
|
+
.map((pin) => ({
|
|
321
|
+
pin,
|
|
322
|
+
angle: Math.atan2(pin.y - componentCenter.y, pin.x - componentCenter.x),
|
|
323
|
+
}))
|
|
324
|
+
.sort((a, b) => a.angle - b.angle)
|
|
325
|
+
const symbolPortsByAngle = symbol.ports
|
|
326
|
+
.map((port) => ({
|
|
327
|
+
port,
|
|
328
|
+
angle: Math.atan2(port.y - symbol.center.y, port.x - symbol.center.x),
|
|
329
|
+
}))
|
|
330
|
+
.sort((a, b) => a.angle - b.angle)
|
|
331
|
+
|
|
332
|
+
const matches: Array<{
|
|
333
|
+
pin: InputPin
|
|
334
|
+
symbolPort: SchSymbol["ports"][number]
|
|
335
|
+
}> = []
|
|
336
|
+
const usedSymbolPorts = new Set<SchSymbol["ports"][number]>()
|
|
337
|
+
for (const pinWithAngle of pinsByAngle) {
|
|
338
|
+
const match = symbolPortsByAngle
|
|
339
|
+
.filter(({ port }) => !usedSymbolPorts.has(port))
|
|
340
|
+
.map(({ port, angle }) => ({
|
|
341
|
+
port,
|
|
342
|
+
difference: getAngularDifference(pinWithAngle.angle, angle),
|
|
343
|
+
}))
|
|
344
|
+
.sort((a, b) => a.difference - b.difference)[0]
|
|
345
|
+
if (!match || match.difference >= Math.PI / 4) continue
|
|
346
|
+
matches.push({ pin: pinWithAngle.pin, symbolPort: match.port })
|
|
347
|
+
usedSymbolPorts.add(match.port)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (matches.length !== 2) return { x: 0, y: 0 }
|
|
351
|
+
|
|
352
|
+
// Match circuit-to-svg's point-pair order: the second match is the
|
|
353
|
+
// translation anchor and the first match determines the scale.
|
|
354
|
+
const anchorMatch = matches[1]!
|
|
355
|
+
const otherMatch = matches[0]!
|
|
356
|
+
const symbolPortDistance = Math.hypot(
|
|
357
|
+
otherMatch.symbolPort.x - anchorMatch.symbolPort.x,
|
|
358
|
+
otherMatch.symbolPort.y - anchorMatch.symbolPort.y,
|
|
359
|
+
)
|
|
360
|
+
if (symbolPortDistance === 0) return { x: 0, y: 0 }
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
x: (1 - scale) * anchorMatch.symbolPort.x,
|
|
364
|
+
y: (1 - scale) * anchorMatch.symbolPort.y,
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
type SnapshotSymbolGeometry = {
|
|
369
|
+
symbolName: string
|
|
370
|
+
center: Point
|
|
371
|
+
size: { width: number; height: number }
|
|
372
|
+
circuitToSvgOffset: Point
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const GENERIC_BOX_STEM_LENGTH = 0.4
|
|
376
|
+
const CHIP_EDGE_TOLERANCE = 1e-6
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Legacy solver inputs describe generic box obstacles from stem tip to stem
|
|
380
|
+
* tip. Circuit JSON's schematic_component size describes only the box body,
|
|
381
|
+
* while each schematic_port's distance_from_component_edge draws its stem.
|
|
382
|
+
* Newer inputs already place pin tips outside the box, so only contract an
|
|
383
|
+
* axis when a pin tip lies on that obstacle's edge.
|
|
384
|
+
*/
|
|
385
|
+
const getGenericBoxSize = (chip: InputChip) => {
|
|
386
|
+
const halfWidth = chip.width / 2
|
|
387
|
+
const halfHeight = chip.height / 2
|
|
388
|
+
const hasHorizontalPinOnEdge = chip.pins.some((pin) => {
|
|
389
|
+
const direction = getFacingDirection(pin, chip)
|
|
390
|
+
return (
|
|
391
|
+
(direction === "x+" || direction === "x-") &&
|
|
392
|
+
Math.abs(Math.abs(pin.x - chip.center.x) - halfWidth) <=
|
|
393
|
+
CHIP_EDGE_TOLERANCE
|
|
394
|
+
)
|
|
395
|
+
})
|
|
396
|
+
const hasVerticalPinOnEdge = chip.pins.some((pin) => {
|
|
397
|
+
const direction = getFacingDirection(pin, chip)
|
|
398
|
+
return (
|
|
399
|
+
(direction === "y+" || direction === "y-") &&
|
|
400
|
+
Math.abs(Math.abs(pin.y - chip.center.y) - halfHeight) <=
|
|
401
|
+
CHIP_EDGE_TOLERANCE
|
|
402
|
+
)
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
width: hasHorizontalPinOnEdge
|
|
407
|
+
? Math.max(0.1, chip.width - 2 * GENERIC_BOX_STEM_LENGTH)
|
|
408
|
+
: chip.width,
|
|
409
|
+
height: hasVerticalPinOnEdge
|
|
410
|
+
? Math.max(0.1, chip.height - 2 * GENERIC_BOX_STEM_LENGTH)
|
|
411
|
+
: chip.height,
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const getGenericBoxPortStemLength = ({
|
|
416
|
+
pin,
|
|
417
|
+
chip,
|
|
418
|
+
componentSize,
|
|
419
|
+
sideOfComponent,
|
|
420
|
+
}: {
|
|
421
|
+
pin: InputPin
|
|
422
|
+
chip: InputChip
|
|
423
|
+
componentSize: { width: number; height: number }
|
|
424
|
+
sideOfComponent: "left" | "right" | "top" | "bottom"
|
|
425
|
+
}) => {
|
|
426
|
+
const distanceFromBody =
|
|
427
|
+
sideOfComponent === "left" || sideOfComponent === "right"
|
|
428
|
+
? Math.abs(pin.x - chip.center.x) - componentSize.width / 2
|
|
429
|
+
: Math.abs(pin.y - chip.center.y) - componentSize.height / 2
|
|
430
|
+
|
|
431
|
+
return Math.max(0, Math.min(GENERIC_BOX_STEM_LENGTH, distanceFromBody))
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Core passes the solver a text-inclusive chip obstacle. Its center and size
|
|
436
|
+
* can therefore include {REF}, {VAL}, or explicit MPN text and must not be used
|
|
437
|
+
* as the drawable symbol bounds. Reconstruct the original component geometry
|
|
438
|
+
* from the two schematic pin positions and the selected symbol instead.
|
|
439
|
+
*/
|
|
440
|
+
const getSnapshotSymbolGeometry = (
|
|
441
|
+
chip: InputChip,
|
|
442
|
+
refdes: string,
|
|
443
|
+
): SnapshotSymbolGeometry | undefined => {
|
|
444
|
+
const symbolName = getDefaultSymbolName(chip, refdes)
|
|
445
|
+
if (!symbolName) return undefined
|
|
446
|
+
const symbol = symbols[symbolName as keyof typeof symbols] as
|
|
447
|
+
| SchSymbol
|
|
448
|
+
| undefined
|
|
449
|
+
if (!symbol || chip.pins.length !== 2 || symbol.ports.length !== 2) {
|
|
450
|
+
return undefined
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const unusedSymbolPorts = new Set(symbol.ports)
|
|
454
|
+
const matches = chip.pins.map((pin) => {
|
|
455
|
+
const pinLabel = getPinLabel(pin.pinId)
|
|
456
|
+
const symbolPort = [...unusedSymbolPorts].find((port) =>
|
|
457
|
+
port.labels.includes(pinLabel),
|
|
458
|
+
)
|
|
459
|
+
if (symbolPort) unusedSymbolPorts.delete(symbolPort)
|
|
460
|
+
return symbolPort ? { pin, symbolPort } : undefined
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
const matchedByLabel = matches.every(
|
|
464
|
+
(match): match is NonNullable<typeof match> => Boolean(match),
|
|
465
|
+
)
|
|
466
|
+
const pinPortMatches = matchedByLabel
|
|
467
|
+
? matches
|
|
468
|
+
: getPinsInNumberOrder(chip).map((pin, index) => ({
|
|
469
|
+
pin,
|
|
470
|
+
symbolPort: symbol.ports[index]!,
|
|
471
|
+
}))
|
|
472
|
+
|
|
473
|
+
const [firstMatch, secondMatch] = pinPortMatches
|
|
474
|
+
const symbolPortDistance = Math.hypot(
|
|
475
|
+
secondMatch!.symbolPort.x - firstMatch!.symbolPort.x,
|
|
476
|
+
secondMatch!.symbolPort.y - firstMatch!.symbolPort.y,
|
|
477
|
+
)
|
|
478
|
+
const schematicPortDistance = Math.hypot(
|
|
479
|
+
secondMatch!.pin.x - firstMatch!.pin.x,
|
|
480
|
+
secondMatch!.pin.y - firstMatch!.pin.y,
|
|
481
|
+
)
|
|
482
|
+
if (symbolPortDistance === 0 || schematicPortDistance === 0) return undefined
|
|
483
|
+
|
|
484
|
+
const scale = schematicPortDistance / symbolPortDistance
|
|
485
|
+
const centers = pinPortMatches.map(({ pin, symbolPort }) => ({
|
|
486
|
+
x: pin.x - (symbolPort.x - symbol.center.x) * scale,
|
|
487
|
+
y: pin.y - (symbolPort.y - symbol.center.y) * scale,
|
|
488
|
+
}))
|
|
489
|
+
const center = {
|
|
490
|
+
x: (centers[0]!.x + centers[1]!.x) / 2,
|
|
491
|
+
y: (centers[0]!.y + centers[1]!.y) / 2,
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return {
|
|
495
|
+
symbolName,
|
|
496
|
+
center,
|
|
497
|
+
size: {
|
|
498
|
+
width: symbol.size.width * scale,
|
|
499
|
+
height: symbol.size.height * scale,
|
|
500
|
+
},
|
|
501
|
+
circuitToSvgOffset: getCircuitToSvgSymbolOffset({
|
|
502
|
+
chip,
|
|
503
|
+
componentCenter: center,
|
|
504
|
+
symbol,
|
|
505
|
+
scale,
|
|
506
|
+
}),
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const pointKey = (point: Point) => `${point.x.toFixed(9)},${point.y.toFixed(9)}`
|
|
511
|
+
|
|
512
|
+
const traceKey = (trace: SnapshotTrace) =>
|
|
513
|
+
trace.mspPairId ?? trace.tracePath.map(pointKey).join(";")
|
|
514
|
+
|
|
515
|
+
const dedupeTraces = (traces: SnapshotTrace[]) => {
|
|
516
|
+
const uniqueTraces = new Map<string, SnapshotTrace>()
|
|
517
|
+
for (const trace of traces) {
|
|
518
|
+
if (trace.tracePath.length < 2) continue
|
|
519
|
+
uniqueTraces.set(traceKey(trace), trace)
|
|
520
|
+
}
|
|
521
|
+
return [...uniqueTraces.values()]
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const getTracePinIds = (trace: SnapshotTrace) =>
|
|
525
|
+
trace.pinIds ?? trace.pins?.map((pin) => pin.pinId) ?? []
|
|
526
|
+
|
|
527
|
+
const nonEmptyNetName = (name: string | undefined) => {
|
|
528
|
+
const trimmedName = name?.trim()
|
|
529
|
+
return trimmedName ? trimmedName : undefined
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* circuit-to-svg sizes conventional net labels from their text. Approximate a
|
|
534
|
+
* missing label with X characters so the rendered outline stays close to the
|
|
535
|
+
* width that @tscircuit/core measured and supplied to the solver.
|
|
536
|
+
*/
|
|
537
|
+
const getAnchoredNetLabelPlaceholder = (label: NetLabelPlacement) => {
|
|
538
|
+
const labelWidth =
|
|
539
|
+
label.orientation === "x+" || label.orientation === "x-"
|
|
540
|
+
? label.width
|
|
541
|
+
: label.height
|
|
542
|
+
const fontSize = 0.18
|
|
543
|
+
const fixedWidthInFontSizes = 0.9
|
|
544
|
+
const xWidthInFontSizes = 16 / 27 + 0.06
|
|
545
|
+
const xCount = Math.max(
|
|
546
|
+
1,
|
|
547
|
+
Math.round(
|
|
548
|
+
(labelWidth / fontSize - fixedWidthInFontSizes) / xWidthInFontSizes,
|
|
549
|
+
),
|
|
550
|
+
)
|
|
551
|
+
return "X".repeat(xCount)
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const getInlineNetLabelPlaceholder = (label: InlineNetLabelPlacement) => {
|
|
555
|
+
const fontScale = label.height / 0.18
|
|
556
|
+
const characterWidth = 0.12 * fontScale
|
|
557
|
+
const xCount = Math.max(1, Math.round(label.width / characterWidth - 1))
|
|
558
|
+
return "X".repeat(xCount)
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const getNetNameResolvers = (
|
|
562
|
+
inputProblem: InputProblem,
|
|
563
|
+
traces: SnapshotTrace[],
|
|
564
|
+
netLabelPlacements: NetLabelPlacement[],
|
|
565
|
+
inlineNetLabelPlacements: InlineNetLabelPlacement[],
|
|
566
|
+
) => {
|
|
567
|
+
const netNameByPinId = new Map<string, string>()
|
|
568
|
+
const netLabelTextByPinId = new Map<string, string>()
|
|
569
|
+
for (const connection of inputProblem.directConnections) {
|
|
570
|
+
for (const pinId of connection.pinIds) {
|
|
571
|
+
if (connection.netId) netNameByPinId.set(pinId, connection.netId)
|
|
572
|
+
const netLabelText = nonEmptyNetName(connection.netLabelText)
|
|
573
|
+
if (netLabelText) netLabelTextByPinId.set(pinId, netLabelText)
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
for (const connection of inputProblem.netConnections) {
|
|
577
|
+
for (const pinId of connection.pinIds) {
|
|
578
|
+
netNameByPinId.set(pinId, connection.netId)
|
|
579
|
+
const netLabelText = nonEmptyNetName(connection.netLabelText)
|
|
580
|
+
if (netLabelText) netLabelTextByPinId.set(pinId, netLabelText)
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const inferUniqueFromPinIds = (
|
|
585
|
+
pinIds: readonly string[],
|
|
586
|
+
valuesByPinId: Map<string, string>,
|
|
587
|
+
) => {
|
|
588
|
+
const names = [
|
|
589
|
+
...new Set(
|
|
590
|
+
pinIds
|
|
591
|
+
.map((pinId) => valuesByPinId.get(pinId))
|
|
592
|
+
.filter((name): name is string => Boolean(name)),
|
|
593
|
+
),
|
|
594
|
+
]
|
|
595
|
+
return names.length === 1 ? names[0] : undefined
|
|
596
|
+
}
|
|
597
|
+
const inferNetNameFromPinIds = (pinIds: readonly string[]) =>
|
|
598
|
+
inferUniqueFromPinIds(pinIds, netNameByPinId)
|
|
599
|
+
const inferNetLabelTextFromPinIds = (pinIds: readonly string[]) =>
|
|
600
|
+
inferUniqueFromPinIds(pinIds, netLabelTextByPinId)
|
|
601
|
+
|
|
602
|
+
const userNetNameByGlobalConnNetId = new Map<string, string>()
|
|
603
|
+
const userNetNameByMspPairId = new Map<string, string>()
|
|
604
|
+
const netLabelTextByGlobalConnNetId = new Map<string, string>()
|
|
605
|
+
const netLabelTextByMspPairId = new Map<string, string>()
|
|
606
|
+
for (const trace of traces) {
|
|
607
|
+
const userNetName =
|
|
608
|
+
nonEmptyNetName(trace.userNetId) ??
|
|
609
|
+
inferNetNameFromPinIds(getTracePinIds(trace)) ??
|
|
610
|
+
nonEmptyNetName(trace.netId)
|
|
611
|
+
if (!userNetName) continue
|
|
612
|
+
if (trace.globalConnNetId) {
|
|
613
|
+
userNetNameByGlobalConnNetId.set(trace.globalConnNetId, userNetName)
|
|
614
|
+
}
|
|
615
|
+
for (const mspPairId of [
|
|
616
|
+
trace.mspPairId,
|
|
617
|
+
...(trace.mspConnectionPairIds ?? []),
|
|
618
|
+
]) {
|
|
619
|
+
if (mspPairId) userNetNameByMspPairId.set(mspPairId, userNetName)
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
for (const label of [...netLabelPlacements, ...inlineNetLabelPlacements]) {
|
|
624
|
+
const userNetName =
|
|
625
|
+
nonEmptyNetName(label.netId) ?? inferNetNameFromPinIds(label.pinIds)
|
|
626
|
+
if (userNetName) {
|
|
627
|
+
userNetNameByGlobalConnNetId.set(label.globalConnNetId, userNetName)
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const netLabelText =
|
|
631
|
+
nonEmptyNetName(label.netLabelText) ??
|
|
632
|
+
inferNetLabelTextFromPinIds(label.pinIds)
|
|
633
|
+
if (!netLabelText) continue
|
|
634
|
+
netLabelTextByGlobalConnNetId.set(label.globalConnNetId, netLabelText)
|
|
635
|
+
if ("mspConnectionPairIds" in label) {
|
|
636
|
+
for (const mspPairId of label.mspConnectionPairIds) {
|
|
637
|
+
netLabelTextByMspPairId.set(mspPairId, netLabelText)
|
|
638
|
+
}
|
|
639
|
+
} else if (label.mspPairId) {
|
|
640
|
+
netLabelTextByMspPairId.set(label.mspPairId, netLabelText)
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const getTraceNetName = (trace: SnapshotTrace) =>
|
|
645
|
+
nonEmptyNetName(trace.userNetId) ??
|
|
646
|
+
inferNetNameFromPinIds(getTracePinIds(trace)) ??
|
|
647
|
+
(trace.globalConnNetId
|
|
648
|
+
? userNetNameByGlobalConnNetId.get(trace.globalConnNetId)
|
|
649
|
+
: undefined) ??
|
|
650
|
+
nonEmptyNetName(trace.netId) ??
|
|
651
|
+
nonEmptyNetName(trace.globalConnNetId)
|
|
652
|
+
|
|
653
|
+
const getNetLabelNetName = (label: NetLabelPlacement) =>
|
|
654
|
+
nonEmptyNetName(label.netId) ??
|
|
655
|
+
inferNetNameFromPinIds(label.pinIds) ??
|
|
656
|
+
label.mspConnectionPairIds
|
|
657
|
+
.map((mspPairId) => userNetNameByMspPairId.get(mspPairId))
|
|
658
|
+
.find((name): name is string => Boolean(name)) ??
|
|
659
|
+
userNetNameByGlobalConnNetId.get(label.globalConnNetId) ??
|
|
660
|
+
nonEmptyNetName(label.dcConnNetId) ??
|
|
661
|
+
nonEmptyNetName(label.globalConnNetId) ??
|
|
662
|
+
"unnamed net"
|
|
663
|
+
|
|
664
|
+
const getInlineNetLabelNetName = (label: InlineNetLabelPlacement) =>
|
|
665
|
+
nonEmptyNetName(label.netId) ??
|
|
666
|
+
inferNetNameFromPinIds(label.pinIds) ??
|
|
667
|
+
(label.mspPairId
|
|
668
|
+
? userNetNameByMspPairId.get(label.mspPairId)
|
|
669
|
+
: undefined) ??
|
|
670
|
+
userNetNameByGlobalConnNetId.get(label.globalConnNetId) ??
|
|
671
|
+
nonEmptyNetName(label.globalConnNetId) ??
|
|
672
|
+
"unnamed net"
|
|
673
|
+
|
|
674
|
+
const getNetLabelText = (label: NetLabelPlacement) =>
|
|
675
|
+
nonEmptyNetName(label.netLabelText) ??
|
|
676
|
+
inferNetLabelTextFromPinIds(label.pinIds) ??
|
|
677
|
+
label.mspConnectionPairIds
|
|
678
|
+
.map((mspPairId) => netLabelTextByMspPairId.get(mspPairId))
|
|
679
|
+
.find((text): text is string => Boolean(text)) ??
|
|
680
|
+
netLabelTextByGlobalConnNetId.get(label.globalConnNetId) ??
|
|
681
|
+
getAnchoredNetLabelPlaceholder(label)
|
|
682
|
+
|
|
683
|
+
const getInlineNetLabelText = (label: InlineNetLabelPlacement) =>
|
|
684
|
+
nonEmptyNetName(label.netLabelText) ??
|
|
685
|
+
inferNetLabelTextFromPinIds(label.pinIds) ??
|
|
686
|
+
(label.mspPairId
|
|
687
|
+
? netLabelTextByMspPairId.get(label.mspPairId)
|
|
688
|
+
: undefined) ??
|
|
689
|
+
netLabelTextByGlobalConnNetId.get(label.globalConnNetId) ??
|
|
690
|
+
getInlineNetLabelPlaceholder(label)
|
|
691
|
+
|
|
692
|
+
return {
|
|
693
|
+
getTraceNetName,
|
|
694
|
+
getNetLabelNetName,
|
|
695
|
+
getNetLabelText,
|
|
696
|
+
getInlineNetLabelNetName,
|
|
697
|
+
getInlineNetLabelText,
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const getJunctionsByTraceIndex = (
|
|
702
|
+
traces: SnapshotTrace[],
|
|
703
|
+
getTraceNetName: (trace: SnapshotTrace) => string | undefined,
|
|
704
|
+
) => {
|
|
705
|
+
const traceIndexesByNetPoint = new Map<
|
|
706
|
+
string,
|
|
707
|
+
{ point: Point; traceIndexes: Set<number> }
|
|
708
|
+
>()
|
|
709
|
+
|
|
710
|
+
for (const [traceIndex, trace] of traces.entries()) {
|
|
711
|
+
const netKey = trace.globalConnNetId ?? getTraceNetName(trace) ?? "unknown"
|
|
712
|
+
for (const key of new Set(trace.tracePath.map(pointKey))) {
|
|
713
|
+
const netPointKey = `${netKey}:${key}`
|
|
714
|
+
const entry = traceIndexesByNetPoint.get(netPointKey) ?? {
|
|
715
|
+
point: trace.tracePath.find((point) => pointKey(point) === key)!,
|
|
716
|
+
traceIndexes: new Set<number>(),
|
|
717
|
+
}
|
|
718
|
+
entry.traceIndexes.add(traceIndex)
|
|
719
|
+
traceIndexesByNetPoint.set(netPointKey, entry)
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const junctionsByTraceIndex = new Map<number, Point[]>()
|
|
724
|
+
for (const {
|
|
725
|
+
point: sharedPoint,
|
|
726
|
+
traceIndexes,
|
|
727
|
+
} of traceIndexesByNetPoint.values()) {
|
|
728
|
+
if (traceIndexes.size < 2) continue
|
|
729
|
+
const traceIndex = Math.min(...traceIndexes)
|
|
730
|
+
const junctions = junctionsByTraceIndex.get(traceIndex) ?? []
|
|
731
|
+
if (!junctions.some((point) => pointKey(point) === pointKey(sharedPoint))) {
|
|
732
|
+
junctions.push(sharedPoint)
|
|
733
|
+
}
|
|
734
|
+
junctionsByTraceIndex.set(traceIndex, junctions)
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
return junctionsByTraceIndex
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const orientationToAnchorSide = (
|
|
741
|
+
orientation: FacingDirection,
|
|
742
|
+
): SchematicNetLabel["anchor_side"] => {
|
|
743
|
+
switch (orientation) {
|
|
744
|
+
case "x+":
|
|
745
|
+
return "left"
|
|
746
|
+
case "x-":
|
|
747
|
+
return "right"
|
|
748
|
+
case "y+":
|
|
749
|
+
return "bottom"
|
|
750
|
+
case "y-":
|
|
751
|
+
return "top"
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Builds a semantic, renderable Circuit JSON schematic from a solver snapshot.
|
|
757
|
+
* Only solved/output traces are emitted, so input connection lines (the rats
|
|
758
|
+
* nest) are intentionally absent.
|
|
759
|
+
*/
|
|
760
|
+
export const convertSolverOutputToCircuitJson = (
|
|
761
|
+
solver: BaseSolver,
|
|
762
|
+
): AnyCircuitElement[] => {
|
|
763
|
+
const inputProblem = getInputProblemFromSolver(solver)
|
|
764
|
+
if (!inputProblem) {
|
|
765
|
+
throw new Error(
|
|
766
|
+
`Unable to find an InputProblem on ${solver.constructor.name}`,
|
|
767
|
+
)
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
const snapshotData = getSnapshotData(solver)
|
|
771
|
+
const traces = dedupeTraces(snapshotData.traces)
|
|
772
|
+
const {
|
|
773
|
+
getTraceNetName,
|
|
774
|
+
getNetLabelNetName,
|
|
775
|
+
getNetLabelText,
|
|
776
|
+
getInlineNetLabelNetName,
|
|
777
|
+
getInlineNetLabelText,
|
|
778
|
+
} = getNetNameResolvers(
|
|
779
|
+
inputProblem,
|
|
780
|
+
traces,
|
|
781
|
+
snapshotData.netLabelPlacements,
|
|
782
|
+
snapshotData.inlineNetLabelPlacements,
|
|
783
|
+
)
|
|
784
|
+
const circuitJson: AnyCircuitElement[] = []
|
|
785
|
+
const sourcePortIdByPinId = new Map<string, string>()
|
|
786
|
+
const schematicPortIdByPinId = new Map<string, string>()
|
|
787
|
+
const sourcePortIdsByPoint = new Map<string, string[]>()
|
|
788
|
+
const schematicPortIdsByPoint = new Map<string, string[]>()
|
|
789
|
+
|
|
790
|
+
for (const [chipIndex, chip] of inputProblem.chips.entries()) {
|
|
791
|
+
const sourceComponentId = `source_component_${chipIndex}`
|
|
792
|
+
const schematicComponentId = `schematic_component_${chipIndex}`
|
|
793
|
+
const refdes = getRefdes(chip)
|
|
794
|
+
const symbolGeometry = getSnapshotSymbolGeometry(chip, refdes)
|
|
795
|
+
const symbolName = symbolGeometry?.symbolName
|
|
796
|
+
const symbolOffset = symbolGeometry?.circuitToSvgOffset ?? { x: 0, y: 0 }
|
|
797
|
+
const componentCenter = symbolGeometry?.center ?? chip.center
|
|
798
|
+
const componentSize = symbolGeometry?.size ?? getGenericBoxSize(chip)
|
|
799
|
+
|
|
800
|
+
circuitJson.push({
|
|
801
|
+
type: "source_component",
|
|
802
|
+
ftype: "simple_chip",
|
|
803
|
+
source_component_id: sourceComponentId,
|
|
804
|
+
name: refdes,
|
|
805
|
+
} satisfies SourceSimpleChip)
|
|
806
|
+
|
|
807
|
+
circuitJson.push({
|
|
808
|
+
type: "schematic_component",
|
|
809
|
+
schematic_component_id: schematicComponentId,
|
|
810
|
+
source_component_id: sourceComponentId,
|
|
811
|
+
center: {
|
|
812
|
+
x: componentCenter.x + symbolOffset.x,
|
|
813
|
+
y: componentCenter.y + symbolOffset.y,
|
|
814
|
+
},
|
|
815
|
+
size: componentSize,
|
|
816
|
+
is_box_with_pins: true,
|
|
817
|
+
symbol_name: symbolName,
|
|
818
|
+
} satisfies SchematicComponent)
|
|
819
|
+
|
|
820
|
+
if (!symbolName) {
|
|
821
|
+
circuitJson.push({
|
|
822
|
+
type: "schematic_text",
|
|
823
|
+
schematic_text_id: `schematic_component_label_${chipIndex}`,
|
|
824
|
+
schematic_component_id: schematicComponentId,
|
|
825
|
+
text: refdes,
|
|
826
|
+
font_size: Math.max(0.1, Math.min(0.18, chip.height / 3)),
|
|
827
|
+
position: chip.center,
|
|
828
|
+
rotation: 0,
|
|
829
|
+
anchor: "center",
|
|
830
|
+
color: "#0f172a",
|
|
831
|
+
} satisfies SchematicText)
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
for (const [pinIndex, pin] of chip.pins.entries()) {
|
|
835
|
+
const sourcePortId = `source_port_${chipIndex}_${pinIndex}`
|
|
836
|
+
const schematicPortId = `schematic_port_${chipIndex}_${pinIndex}`
|
|
837
|
+
const facingDirection = getFacingDirection(pin, chip)
|
|
838
|
+
const sideOfComponent = facingDirectionToSide(facingDirection)
|
|
839
|
+
const pinLabel = getPinLabel(pin.pinId)
|
|
840
|
+
const pinNumber = getPinNumber(pin.pinId)
|
|
841
|
+
const distanceFromComponentEdge = symbolName
|
|
842
|
+
? Math.min(
|
|
843
|
+
0.2,
|
|
844
|
+
(sideOfComponent === "left" || sideOfComponent === "right"
|
|
845
|
+
? componentSize.width
|
|
846
|
+
: componentSize.height) / 3,
|
|
847
|
+
)
|
|
848
|
+
: getGenericBoxPortStemLength({
|
|
849
|
+
pin,
|
|
850
|
+
chip,
|
|
851
|
+
componentSize,
|
|
852
|
+
sideOfComponent,
|
|
853
|
+
})
|
|
854
|
+
|
|
855
|
+
circuitJson.push({
|
|
856
|
+
type: "source_port",
|
|
857
|
+
source_port_id: sourcePortId,
|
|
858
|
+
source_component_id: sourceComponentId,
|
|
859
|
+
name: pinLabel,
|
|
860
|
+
pin_number: pinNumber,
|
|
861
|
+
} satisfies SourcePort)
|
|
862
|
+
|
|
863
|
+
circuitJson.push({
|
|
864
|
+
type: "schematic_port",
|
|
865
|
+
schematic_port_id: schematicPortId,
|
|
866
|
+
source_port_id: sourcePortId,
|
|
867
|
+
schematic_component_id: schematicComponentId,
|
|
868
|
+
center: {
|
|
869
|
+
x: pin.x + symbolOffset.x,
|
|
870
|
+
y: pin.y + symbolOffset.y,
|
|
871
|
+
},
|
|
872
|
+
facing_direction: facingDirectionToCircuitJson(facingDirection),
|
|
873
|
+
side_of_component: sideOfComponent,
|
|
874
|
+
distance_from_component_edge: distanceFromComponentEdge,
|
|
875
|
+
display_pin_label: pinNumber === undefined ? pinLabel : undefined,
|
|
876
|
+
pin_number: pinNumber,
|
|
877
|
+
} satisfies SchematicPort)
|
|
878
|
+
|
|
879
|
+
sourcePortIdByPinId.set(pin.pinId, sourcePortId)
|
|
880
|
+
schematicPortIdByPinId.set(pin.pinId, schematicPortId)
|
|
881
|
+
const key = pointKey(pin)
|
|
882
|
+
sourcePortIdsByPoint.set(key, [
|
|
883
|
+
...(sourcePortIdsByPoint.get(key) ?? []),
|
|
884
|
+
sourcePortId,
|
|
885
|
+
])
|
|
886
|
+
schematicPortIdsByPoint.set(key, [
|
|
887
|
+
...(schematicPortIdsByPoint.get(key) ?? []),
|
|
888
|
+
schematicPortId,
|
|
889
|
+
])
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
for (const [textBoxIndex, textBox] of (
|
|
894
|
+
inputProblem.textBoxes ?? []
|
|
895
|
+
).entries()) {
|
|
896
|
+
circuitJson.push({
|
|
897
|
+
type: "schematic_box",
|
|
898
|
+
schematic_component_id: textBox.chipId,
|
|
899
|
+
x: textBox.center.x - textBox.width / 2,
|
|
900
|
+
y: textBox.center.y - textBox.height / 2,
|
|
901
|
+
width: textBox.width,
|
|
902
|
+
height: textBox.height,
|
|
903
|
+
is_dashed: true,
|
|
904
|
+
} satisfies SchematicBox)
|
|
905
|
+
|
|
906
|
+
if (textBox.text) {
|
|
907
|
+
circuitJson.push({
|
|
908
|
+
type: "schematic_text",
|
|
909
|
+
schematic_text_id: `schematic_text_box_${textBoxIndex}`,
|
|
910
|
+
text: textBox.text,
|
|
911
|
+
font_size: Math.max(0.08, Math.min(0.18, textBox.height)),
|
|
912
|
+
position: textBox.center,
|
|
913
|
+
rotation: 0,
|
|
914
|
+
anchor: "center",
|
|
915
|
+
color: "#7e22ce",
|
|
916
|
+
} satisfies SchematicText)
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
const netNames = new Set<string>()
|
|
921
|
+
for (const connection of [
|
|
922
|
+
...inputProblem.directConnections,
|
|
923
|
+
...inputProblem.netConnections,
|
|
924
|
+
]) {
|
|
925
|
+
if (connection.netId) netNames.add(connection.netId)
|
|
926
|
+
}
|
|
927
|
+
for (const trace of traces) {
|
|
928
|
+
const netName = getTraceNetName(trace)
|
|
929
|
+
if (netName) netNames.add(netName)
|
|
930
|
+
}
|
|
931
|
+
for (const label of snapshotData.netLabelPlacements) {
|
|
932
|
+
netNames.add(getNetLabelNetName(label))
|
|
933
|
+
}
|
|
934
|
+
for (const label of snapshotData.inlineNetLabelPlacements) {
|
|
935
|
+
netNames.add(getInlineNetLabelNetName(label))
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const sourceNetIdByName = new Map<string, string>()
|
|
939
|
+
for (const [netIndex, netName] of [...netNames].sort().entries()) {
|
|
940
|
+
const sourceNetId = `source_net_${netIndex}`
|
|
941
|
+
sourceNetIdByName.set(netName, sourceNetId)
|
|
942
|
+
circuitJson.push({
|
|
943
|
+
type: "source_net",
|
|
944
|
+
source_net_id: sourceNetId,
|
|
945
|
+
name: netName,
|
|
946
|
+
member_source_group_ids: [],
|
|
947
|
+
is_ground: /(^|[._-])gnd($|[._-])/i.test(netName),
|
|
948
|
+
is_power: /(^|[._-])(vcc|vdd|vss|gnd)($|[._-])/i.test(netName),
|
|
949
|
+
} satisfies SourceNet)
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const junctionsByTraceIndex = getJunctionsByTraceIndex(
|
|
953
|
+
traces,
|
|
954
|
+
getTraceNetName,
|
|
955
|
+
)
|
|
956
|
+
for (const [traceIndex, trace] of traces.entries()) {
|
|
957
|
+
const sourceTraceId = `source_trace_${traceIndex}`
|
|
958
|
+
const schematicTraceId = `schematic_trace_${traceIndex}`
|
|
959
|
+
const tracePinIds = getTracePinIds(trace)
|
|
960
|
+
const startKey = pointKey(trace.tracePath[0]!)
|
|
961
|
+
const endKey = pointKey(trace.tracePath.at(-1)!)
|
|
962
|
+
const sourcePortIds = [
|
|
963
|
+
...tracePinIds
|
|
964
|
+
.map((pinId) => sourcePortIdByPinId.get(pinId))
|
|
965
|
+
.filter((id): id is string => Boolean(id)),
|
|
966
|
+
]
|
|
967
|
+
if (sourcePortIds.length === 0) {
|
|
968
|
+
sourcePortIds.push(
|
|
969
|
+
...(sourcePortIdsByPoint.get(startKey)?.slice(0, 1) ?? []),
|
|
970
|
+
...(sourcePortIdsByPoint.get(endKey)?.slice(0, 1) ?? []),
|
|
971
|
+
)
|
|
972
|
+
}
|
|
973
|
+
const netName = getTraceNetName(trace)
|
|
974
|
+
|
|
975
|
+
circuitJson.push({
|
|
976
|
+
type: "source_trace",
|
|
977
|
+
source_trace_id: sourceTraceId,
|
|
978
|
+
connected_source_port_ids: [...new Set(sourcePortIds)],
|
|
979
|
+
connected_source_net_ids: netName
|
|
980
|
+
? [sourceNetIdByName.get(netName)!]
|
|
981
|
+
: [],
|
|
982
|
+
name: netName,
|
|
983
|
+
} satisfies SourceTrace)
|
|
984
|
+
|
|
985
|
+
circuitJson.push({
|
|
986
|
+
type: "schematic_trace",
|
|
987
|
+
schematic_trace_id: schematicTraceId,
|
|
988
|
+
source_trace_id: sourceTraceId,
|
|
989
|
+
junctions: junctionsByTraceIndex.get(traceIndex) ?? [],
|
|
990
|
+
edges: trace.tracePath.slice(0, -1).map((point, pointIndex) => {
|
|
991
|
+
const nextPoint = trace.tracePath[pointIndex + 1]!
|
|
992
|
+
const fromSchematicPortId =
|
|
993
|
+
pointIndex === 0
|
|
994
|
+
? (tracePinIds
|
|
995
|
+
.map((pinId) => schematicPortIdByPinId.get(pinId))
|
|
996
|
+
.find(
|
|
997
|
+
(portId) =>
|
|
998
|
+
portId &&
|
|
999
|
+
schematicPortIdsByPoint
|
|
1000
|
+
.get(pointKey(point))
|
|
1001
|
+
?.includes(portId),
|
|
1002
|
+
) ?? schematicPortIdsByPoint.get(pointKey(point))?.[0])
|
|
1003
|
+
: undefined
|
|
1004
|
+
const toSchematicPortId =
|
|
1005
|
+
pointIndex === trace.tracePath.length - 2
|
|
1006
|
+
? (tracePinIds
|
|
1007
|
+
.map((pinId) => schematicPortIdByPinId.get(pinId))
|
|
1008
|
+
.find(
|
|
1009
|
+
(portId) =>
|
|
1010
|
+
portId &&
|
|
1011
|
+
schematicPortIdsByPoint
|
|
1012
|
+
.get(pointKey(nextPoint))
|
|
1013
|
+
?.includes(portId),
|
|
1014
|
+
) ?? schematicPortIdsByPoint.get(pointKey(nextPoint))?.[0])
|
|
1015
|
+
: undefined
|
|
1016
|
+
|
|
1017
|
+
return {
|
|
1018
|
+
from: point,
|
|
1019
|
+
to: nextPoint,
|
|
1020
|
+
from_schematic_port_id: fromSchematicPortId,
|
|
1021
|
+
to_schematic_port_id: toSchematicPortId,
|
|
1022
|
+
}
|
|
1023
|
+
}),
|
|
1024
|
+
} satisfies SchematicTrace)
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
for (const [labelIndex, label] of snapshotData.netLabelPlacements.entries()) {
|
|
1028
|
+
const netName = getNetLabelNetName(label)
|
|
1029
|
+
circuitJson.push({
|
|
1030
|
+
type: "schematic_net_label",
|
|
1031
|
+
schematic_net_label_id: `schematic_net_label_${labelIndex}`,
|
|
1032
|
+
source_net_id: sourceNetIdByName.get(netName)!,
|
|
1033
|
+
center: label.center,
|
|
1034
|
+
anchor_position: label.anchorPoint,
|
|
1035
|
+
anchor_side: orientationToAnchorSide(label.orientation),
|
|
1036
|
+
text: getNetLabelText(label),
|
|
1037
|
+
} satisfies SchematicNetLabel)
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
for (const [
|
|
1041
|
+
labelIndex,
|
|
1042
|
+
label,
|
|
1043
|
+
] of snapshotData.inlineNetLabelPlacements.entries()) {
|
|
1044
|
+
circuitJson.push({
|
|
1045
|
+
type: "schematic_text",
|
|
1046
|
+
schematic_text_id: `schematic_inline_net_label_${labelIndex}`,
|
|
1047
|
+
text: getInlineNetLabelText(label),
|
|
1048
|
+
font_size: label.height,
|
|
1049
|
+
position: label.center,
|
|
1050
|
+
// Match core's schematic convention: vertical labels read bottom-to-top.
|
|
1051
|
+
rotation: label.axis === "y" ? -90 : 0,
|
|
1052
|
+
anchor: "center",
|
|
1053
|
+
color: "#047857",
|
|
1054
|
+
} satisfies SchematicText)
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
return circuitJson
|
|
1058
|
+
}
|