@tscircuit/schematic-trace-solver 0.0.86 → 0.0.88
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 +115 -0
- package/.github/scripts/import-json-bug-report.ts +15 -1
- package/.github/workflows/json-bug-report.yml +3 -2
- package/AGENTS.md +22 -0
- package/dist/index.js +86 -10
- package/lib/solvers/Example28Solver/Example28Solver.ts +46 -10
- package/lib/solvers/Example28Solver/countCrossingsWithOtherTraces.ts +19 -0
- package/lib/solvers/Example28Solver/scoreSolutionTracePath.ts +26 -0
- package/lib/testing/PipelineStageDebugRunner.ts +344 -0
- package/package.json +3 -2
- package/scripts/debug-pipeline-stages.ts +146 -0
- package/site/bug-reports/bug-report-20260706T220324Z.page.tsx +4 -0
- package/site/bug-reports/bug-report-20260707T020342Z.page.tsx +4 -0
- package/site/bug-reports/bug-report-20260707T092615Z.page.tsx +4 -0
- package/site/bug-reports/bug-report-20260707T134549Z.page.tsx +4 -0
- package/site/bug-reports/bug-report-20260707T134722Z.page.tsx +4 -0
- package/site/bug-reports/bug-report-20260707T140410Z.page.tsx +4 -0
- package/site/bug-reports/bug-report-20260707T141025Z.page.tsx +4 -0
- package/site/bug-reports/bug-report-20260707T141421Z.page.tsx +4 -0
- package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +357 -3448
- package/tests/bug-reports/bug-report-20260707T020342Z/__snapshots__/bug-report-20260707T020342Z.snap.svg +64 -0
- package/tests/bug-reports/bug-report-20260707T020342Z/bug-report-20260707T020342Z.json +233 -0
- package/tests/bug-reports/bug-report-20260707T020342Z/bug-report-20260707T020342Z.test.ts +12 -0
- package/tests/bug-reports/bug-report-20260707T092615Z/__snapshots__/bug-report-20260707T092615Z.snap.svg +2098 -0
- package/tests/bug-reports/bug-report-20260707T092615Z/bug-report-20260707T092615Z.json +1392 -0
- package/tests/bug-reports/bug-report-20260707T092615Z/bug-report-20260707T092615Z.test.ts +12 -0
- package/tests/bug-reports/bug-report-20260707T134549Z/__snapshots__/bug-report-20260707T134549Z.snap.svg +857 -0
- package/tests/bug-reports/bug-report-20260707T134549Z/bug-report-20260707T134549Z.json +652 -0
- package/tests/bug-reports/bug-report-20260707T134549Z/bug-report-20260707T134549Z.test.ts +12 -0
- package/tests/bug-reports/bug-report-20260707T134722Z/__snapshots__/bug-report-20260707T134722Z.snap.svg +275 -0
- package/tests/bug-reports/bug-report-20260707T134722Z/bug-report-20260707T134722Z.json +265 -0
- package/tests/bug-reports/bug-report-20260707T134722Z/bug-report-20260707T134722Z.test.ts +12 -0
- package/tests/bug-reports/bug-report-20260707T140410Z/__snapshots__/bug-report-20260707T140410Z.snap.svg +950 -0
- package/tests/bug-reports/bug-report-20260707T140410Z/bug-report-20260707T140410Z.json +610 -0
- package/tests/bug-reports/bug-report-20260707T140410Z/bug-report-20260707T140410Z.test.ts +12 -0
- package/tests/bug-reports/bug-report-20260707T141025Z/__snapshots__/bug-report-20260707T141025Z.snap.svg +218 -0
- package/tests/bug-reports/bug-report-20260707T141025Z/bug-report-20260707T141025Z.json +185 -0
- package/tests/bug-reports/bug-report-20260707T141025Z/bug-report-20260707T141025Z.test.ts +12 -0
- package/tests/bug-reports/bug-report-20260707T141421Z/__snapshots__/bug-report-20260707T141421Z.snap.svg +613 -0
- package/tests/bug-reports/bug-report-20260707T141421Z/bug-report-20260707T141421Z.json +499 -0
- package/tests/bug-reports/bug-report-20260707T141421Z/bug-report-20260707T141421Z.test.ts +12 -0
- package/tests/examples/__snapshots__/example01.snap.svg +59 -162
- package/tests/examples/__snapshots__/example02.snap.svg +64 -273
- package/tests/examples/__snapshots__/example03.snap.svg +85 -290
- package/tests/examples/__snapshots__/example04.snap.svg +50 -94
- package/tests/examples/__snapshots__/example05.snap.svg +73 -191
- package/tests/examples/__snapshots__/example06.snap.svg +49 -95
- package/tests/examples/__snapshots__/example07.snap.svg +65 -218
- package/tests/examples/__snapshots__/example08.snap.svg +61 -143
- package/tests/examples/__snapshots__/example09.snap.svg +123 -493
- package/tests/examples/__snapshots__/example10.snap.svg +57 -136
- package/tests/examples/__snapshots__/example11.snap.svg +64 -173
- package/tests/examples/__snapshots__/example12.snap.svg +58 -159
- package/tests/examples/__snapshots__/example13.snap.svg +78 -348
- package/tests/examples/__snapshots__/example14.snap.svg +77 -293
- package/tests/examples/__snapshots__/example15.snap.svg +140 -984
- package/tests/examples/__snapshots__/example16.snap.svg +62 -162
- package/tests/examples/__snapshots__/example17.snap.svg +67 -198
- package/tests/examples/__snapshots__/example18.snap.svg +69 -249
- package/tests/examples/__snapshots__/example19.snap.svg +63 -202
- package/tests/examples/__snapshots__/example20.snap.svg +60 -148
- package/tests/examples/__snapshots__/example21.snap.svg +73 -298
- package/tests/examples/__snapshots__/example22.snap.svg +62 -156
- package/tests/examples/__snapshots__/example23.snap.svg +66 -172
- package/tests/examples/__snapshots__/example24.snap.svg +64 -164
- package/tests/examples/__snapshots__/example25.snap.svg +76 -299
- package/tests/examples/__snapshots__/example26.snap.svg +64 -164
- package/tests/examples/__snapshots__/example27.snap.svg +83 -300
- package/tests/examples/__snapshots__/example28.snap.svg +54 -115
- package/tests/examples/__snapshots__/example29.snap.svg +257 -1299
- package/tests/examples/__snapshots__/example30.snap.svg +91 -293
- package/tests/examples/__snapshots__/example31.snap.svg +47 -88
- package/tests/examples/__snapshots__/example32.snap.svg +107 -405
- package/tests/examples/__snapshots__/example33.snap.svg +71 -180
- package/tests/examples/__snapshots__/example34.snap.svg +55 -116
- package/tests/examples/__snapshots__/example35.snap.svg +69 -176
- package/tests/examples/__snapshots__/example36.snap.svg +65 -159
- package/tests/examples/__snapshots__/example37.snap.svg +75 -223
- package/tests/examples/__snapshots__/example38.snap.svg +46 -76
- package/tests/examples/__snapshots__/example39.snap.svg +51 -106
- package/tests/examples/__snapshots__/example40.snap.svg +83 -270
- package/tests/examples/__snapshots__/example41.snap.svg +54 -118
- package/tests/examples/__snapshots__/example42.snap.svg +67 -194
- package/tests/examples/__snapshots__/example43.snap.svg +62 -168
- package/tests/examples/__snapshots__/example44.snap.svg +86 -369
- package/tests/examples/__snapshots__/example45.snap.svg +60 -169
- package/tests/examples/__snapshots__/example46.snap.svg +80 -342
- package/tests/examples/__snapshots__/example47.snap.svg +82 -323
- package/tests/examples/__snapshots__/example48.snap.svg +64 -167
- package/tests/examples/__snapshots__/example49.snap.svg +120 -565
- package/tests/examples/__snapshots__/example50.snap.svg +71 -195
- package/tests/fixtures/getLastStepGraphicsObject.ts +38 -0
- package/tests/fixtures/getLastStepSvg.ts +9 -0
- package/tests/repros/__snapshots__/bugreport-001-gnd-overlap.snap.svg +67 -218
- package/tests/repros/__snapshots__/manufacturePartNumber-text-box.snap.svg +47 -87
- package/tests/repros/__snapshots__/netlabel-connector-through-rail-label.snap.svg +73 -272
- package/tests/repros/__snapshots__/repro-atmega328p-fault-pullup.snap.svg +284 -0
- package/tests/repros/__snapshots__/repro-atmega328p-missing-gnd-netlabel.snap.svg +470 -0
- package/tests/repros/__snapshots__/repro-bq24074-battery-charger.snap.svg +94 -395
- package/tests/repros/__snapshots__/repro-cc2340r5.snap.svg +88 -467
- package/tests/repros/__snapshots__/repro-ina237-current-monitor.snap.svg +109 -569
- package/tests/repros/__snapshots__/repro130-bq27441-fuel-gauge-trace-through-c1.snap.svg +95 -459
- package/tests/repros/__snapshots__/repro51-overlap-junction-crossing.snap.svg +64 -176
- package/tests/repros/__snapshots__/trace-overlap-box-resistor.snap.svg +51 -112
- package/tests/repros/assets/repro-atmega328p-fault-pullup.input.json +239 -0
- package/tests/repros/assets/repro-atmega328p-missing-gnd-netlabel.input.json +432 -0
- package/tests/repros/repro-atmega328p-fault-pullup.test.ts +11 -0
- package/tests/repros/repro-atmega328p-missing-gnd-netlabel.test.ts +16 -0
- package/tests/solvers/SchematicTraceSingleLineSolver2/__snapshots__/SchematicTraceSingleLineSolver2_01-example17-d1_1-u1_1.snap.svg +59 -159
- package/tests/solvers/TraceCleanupSolver/__snapshots__/TraceCleanupSolver.snap.svg +54 -118
- package/tests/solvers/TraceLabelOverlapAvoidanceSolver/__snapshots__/TraceLabelOverlapAvoidanceSolver.snap.svg +58 -245
- package/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/__snapshots__/renderComparisonView01.snap.svg +73 -222
- package/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/__snapshots__/renderComparisonView02.snap.svg +65 -204
- package/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/__snapshots__/renderComparisonView03.snap.svg +65 -183
- package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/__snapshots__/MergedNetLabelObstacles.snap.svg +54 -139
- package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/__snapshots__/OverlapAvoidanceStepSolver.snap.svg +61 -215
- package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/__snapshots__/SingleOverlapSolver.snap.svg +54 -124
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import {
|
|
4
|
+
getPngBufferFromGraphicsObject,
|
|
5
|
+
getSvgFromGraphicsObject,
|
|
6
|
+
} from "graphics-debug"
|
|
7
|
+
import type { GraphicsObject } from "graphics-debug"
|
|
8
|
+
import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
|
|
9
|
+
import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
|
|
10
|
+
|
|
11
|
+
type PipelineStepLike = {
|
|
12
|
+
solverName: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type VisualizingSolver = {
|
|
16
|
+
visualize: () => GraphicsObject
|
|
17
|
+
iterations?: number
|
|
18
|
+
error?: string | null
|
|
19
|
+
solved?: boolean
|
|
20
|
+
failed?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type PipelineStageArtifact = {
|
|
24
|
+
stageName: string
|
|
25
|
+
stageNumber: number
|
|
26
|
+
pngPath?: string
|
|
27
|
+
svgPath?: string
|
|
28
|
+
graphicsJsonPath?: string
|
|
29
|
+
stepPngPaths: string[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type PipelineStageDebugRunnerResult = {
|
|
33
|
+
outputDir: string
|
|
34
|
+
logsPath: string
|
|
35
|
+
stageArtifacts: PipelineStageArtifact[]
|
|
36
|
+
solved: boolean
|
|
37
|
+
failed: boolean
|
|
38
|
+
error: string | null
|
|
39
|
+
stoppedAfterStage: string | null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class PipelineStageDebugRunner {
|
|
43
|
+
readonly pipelineSolver: SchematicTracePipelineSolver
|
|
44
|
+
readonly outputDir: string
|
|
45
|
+
readonly logsPath: string
|
|
46
|
+
readonly pngWidth: number
|
|
47
|
+
readonly pngHeight: number
|
|
48
|
+
readonly writePng: boolean
|
|
49
|
+
readonly writeSvg: boolean
|
|
50
|
+
readonly writeGraphicsJson: boolean
|
|
51
|
+
readonly writeStepPngs: boolean
|
|
52
|
+
readonly stopAfterStage?: string
|
|
53
|
+
readonly context: Record<string, string | number | boolean | null | undefined>
|
|
54
|
+
readonly onLog?: (line: string) => void
|
|
55
|
+
|
|
56
|
+
private readonly stageArtifacts: PipelineStageArtifact[] = []
|
|
57
|
+
private readonly capturedStageNumbers = new Set<number>()
|
|
58
|
+
|
|
59
|
+
constructor(opts: {
|
|
60
|
+
pipelineSolver: SchematicTracePipelineSolver
|
|
61
|
+
outputDir: string
|
|
62
|
+
pngWidth?: number
|
|
63
|
+
pngHeight?: number
|
|
64
|
+
writePng?: boolean
|
|
65
|
+
writeSvg?: boolean
|
|
66
|
+
writeGraphicsJson?: boolean
|
|
67
|
+
writeStepPngs?: boolean
|
|
68
|
+
stopAfterStage?: string
|
|
69
|
+
context?: Record<string, string | number | boolean | null | undefined>
|
|
70
|
+
onLog?: (line: string) => void
|
|
71
|
+
}) {
|
|
72
|
+
this.pipelineSolver = opts.pipelineSolver
|
|
73
|
+
this.outputDir = opts.outputDir
|
|
74
|
+
this.logsPath = path.join(this.outputDir, "logs.txt")
|
|
75
|
+
this.pngWidth = opts.pngWidth ?? 1536
|
|
76
|
+
this.pngHeight = opts.pngHeight ?? 1536
|
|
77
|
+
this.writePng = opts.writePng ?? true
|
|
78
|
+
this.writeSvg = opts.writeSvg ?? false
|
|
79
|
+
this.writeGraphicsJson = opts.writeGraphicsJson ?? true
|
|
80
|
+
this.writeStepPngs = opts.writeStepPngs ?? false
|
|
81
|
+
this.stopAfterStage = opts.stopAfterStage
|
|
82
|
+
this.context = opts.context ?? {}
|
|
83
|
+
this.onLog = opts.onLog
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async run(): Promise<PipelineStageDebugRunnerResult> {
|
|
87
|
+
await mkdir(this.outputDir, { recursive: true })
|
|
88
|
+
await writeFile(this.logsPath, "")
|
|
89
|
+
|
|
90
|
+
await this.log(`startedAt=${new Date().toISOString()}`)
|
|
91
|
+
await this.log(`outputDir=${this.toDisplayPath(this.outputDir)}`)
|
|
92
|
+
await this.log(
|
|
93
|
+
`artifacts png=${this.writePng} svg=${this.writeSvg} graphicsJson=${this.writeGraphicsJson} stepPngs=${this.writeStepPngs} pngSize=${this.pngWidth}x${this.pngHeight}`,
|
|
94
|
+
)
|
|
95
|
+
for (const [key, value] of Object.entries(this.context)) {
|
|
96
|
+
await this.log(`${key}=${value ?? ""}`)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let stoppedAfterStage: string | null = null
|
|
100
|
+
let thrownError: unknown = null
|
|
101
|
+
|
|
102
|
+
await this.log(
|
|
103
|
+
this.getStageEnterLogLine(this.pipelineSolver.getCurrentPhase(), 0),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
while (!this.pipelineSolver.solved && !this.pipelineSolver.failed) {
|
|
107
|
+
const previousStageNumber = this.pipelineSolver.currentPipelineStepIndex
|
|
108
|
+
const previousStageName = this.pipelineSolver.getCurrentPhase()
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
this.pipelineSolver.step()
|
|
112
|
+
} catch (error) {
|
|
113
|
+
thrownError = error
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const currentStageNumber = this.pipelineSolver.currentPipelineStepIndex
|
|
117
|
+
const currentStageName = this.pipelineSolver.getCurrentPhase()
|
|
118
|
+
|
|
119
|
+
if (
|
|
120
|
+
currentStageNumber !== previousStageNumber &&
|
|
121
|
+
previousStageName !== "none"
|
|
122
|
+
) {
|
|
123
|
+
await this.captureStage(previousStageNumber)
|
|
124
|
+
if (
|
|
125
|
+
previousStageName === this.stopAfterStage ||
|
|
126
|
+
String(previousStageNumber + 1) === this.stopAfterStage
|
|
127
|
+
) {
|
|
128
|
+
stoppedAfterStage = previousStageName
|
|
129
|
+
break
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (
|
|
134
|
+
currentStageNumber !== previousStageNumber &&
|
|
135
|
+
currentStageName !== "none"
|
|
136
|
+
) {
|
|
137
|
+
await this.log(
|
|
138
|
+
this.getStageEnterLogLine(currentStageName, currentStageNumber),
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (thrownError) break
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (this.pipelineSolver.failed) {
|
|
146
|
+
await this.captureActiveStage()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (thrownError) {
|
|
150
|
+
await this.log(`thrownError=${this.formatError(thrownError)}`)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const status = stoppedAfterStage
|
|
154
|
+
? "stopped"
|
|
155
|
+
: this.pipelineSolver.solved
|
|
156
|
+
? "solved"
|
|
157
|
+
: this.pipelineSolver.failed
|
|
158
|
+
? "failed"
|
|
159
|
+
: "incomplete"
|
|
160
|
+
|
|
161
|
+
await this.log(
|
|
162
|
+
`completed status=${status} iterations=${this.pipelineSolver.iterations} error=${this.pipelineSolver.error ?? ""}`,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
outputDir: this.outputDir,
|
|
167
|
+
logsPath: this.logsPath,
|
|
168
|
+
stageArtifacts: [...this.stageArtifacts],
|
|
169
|
+
solved: this.pipelineSolver.solved,
|
|
170
|
+
failed: this.pipelineSolver.failed,
|
|
171
|
+
error:
|
|
172
|
+
this.pipelineSolver.error ??
|
|
173
|
+
(thrownError ? this.formatError(thrownError) : null),
|
|
174
|
+
stoppedAfterStage,
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private async captureActiveStage() {
|
|
179
|
+
const stageNumber = this.pipelineSolver.currentPipelineStepIndex
|
|
180
|
+
if (this.pipelineSolver.getCurrentPhase() === "none") return
|
|
181
|
+
if (this.capturedStageNumbers.has(stageNumber)) return
|
|
182
|
+
|
|
183
|
+
await this.captureStage(stageNumber, this.pipelineSolver.activeSubSolver)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private async captureStage(stageNumber: number, solver?: BaseSolver | null) {
|
|
187
|
+
if (this.capturedStageNumbers.has(stageNumber)) return
|
|
188
|
+
|
|
189
|
+
const stageDef = this.getStageDef(stageNumber)
|
|
190
|
+
if (!stageDef) return
|
|
191
|
+
|
|
192
|
+
const stageSolver = solver ?? this.getStageSolver(stageDef.solverName)
|
|
193
|
+
if (!stageSolver) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
`Unable to resolve solver for stage ${stageNumber + 1} "${stageDef.solverName}"`,
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const basePath = path.join(
|
|
200
|
+
this.outputDir,
|
|
201
|
+
`stage${String(stageNumber + 1).padStart(2, "0")}-${this.getSafeStageName(stageDef.solverName)}`,
|
|
202
|
+
)
|
|
203
|
+
const pngPath = this.writePng ? `${basePath}.png` : undefined
|
|
204
|
+
const svgPath = this.writeSvg ? `${basePath}.svg` : undefined
|
|
205
|
+
const graphicsJsonPath = this.writeGraphicsJson
|
|
206
|
+
? `${basePath}.graphics.json`
|
|
207
|
+
: undefined
|
|
208
|
+
const graphics = stageSolver.visualize()
|
|
209
|
+
const svg = getSvgFromGraphicsObject(graphics, { backgroundColor: "white" })
|
|
210
|
+
|
|
211
|
+
if (pngPath) {
|
|
212
|
+
await writeFile(pngPath, await this.renderGraphicsToPng(graphics))
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const stepPngPaths =
|
|
216
|
+
this.writeStepPngs && this.writePng
|
|
217
|
+
? await this.writeStepPngsForGraphics(basePath, graphics)
|
|
218
|
+
: []
|
|
219
|
+
|
|
220
|
+
if (svgPath) {
|
|
221
|
+
await writeFile(svgPath, svg)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (graphicsJsonPath) {
|
|
225
|
+
await writeFile(graphicsJsonPath, JSON.stringify(graphics, null, 2))
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
this.capturedStageNumbers.add(stageNumber)
|
|
229
|
+
const artifact = {
|
|
230
|
+
stageName: stageDef.solverName,
|
|
231
|
+
stageNumber: stageNumber + 1,
|
|
232
|
+
pngPath,
|
|
233
|
+
svgPath,
|
|
234
|
+
graphicsJsonPath,
|
|
235
|
+
stepPngPaths,
|
|
236
|
+
} satisfies PipelineStageArtifact
|
|
237
|
+
this.stageArtifacts.push(artifact)
|
|
238
|
+
|
|
239
|
+
await this.log(
|
|
240
|
+
`captured stage=${artifact.stageNumber} name=${artifact.stageName} iterations=${stageSolver.iterations ?? 0} png=${pngPath ? this.toDisplayPath(pngPath) : ""} svg=${svgPath ? this.toDisplayPath(svgPath) : ""} steps=${stepPngPaths.length}`,
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private async writeStepPngsForGraphics(
|
|
245
|
+
basePath: string,
|
|
246
|
+
graphics: GraphicsObject,
|
|
247
|
+
): Promise<string[]> {
|
|
248
|
+
const stepPngPaths: string[] = []
|
|
249
|
+
|
|
250
|
+
for (const step of this.getObjectSteps(graphics)) {
|
|
251
|
+
const stepPngPath = `${basePath}.step-${step}.png`
|
|
252
|
+
await writeFile(
|
|
253
|
+
stepPngPath,
|
|
254
|
+
await this.renderGraphicsToPng(
|
|
255
|
+
this.filterGraphicsByStep(graphics, step),
|
|
256
|
+
),
|
|
257
|
+
)
|
|
258
|
+
stepPngPaths.push(stepPngPath)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return stepPngPaths
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private renderGraphicsToPng(graphics: GraphicsObject): Promise<Uint8Array> {
|
|
265
|
+
return getPngBufferFromGraphicsObject(graphics, {
|
|
266
|
+
backgroundColor: "white",
|
|
267
|
+
pngWidth: this.pngWidth,
|
|
268
|
+
pngHeight: this.pngHeight,
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private getObjectSteps(graphics: GraphicsObject): number[] {
|
|
273
|
+
const steps = new Set<number>()
|
|
274
|
+
for (const objects of this.getGraphicsObjectArrays(graphics)) {
|
|
275
|
+
for (const object of objects) {
|
|
276
|
+
if (typeof object.step === "number") steps.add(object.step)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return [...steps].sort((a, b) => a - b)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private filterGraphicsByStep(
|
|
284
|
+
graphics: GraphicsObject,
|
|
285
|
+
step: number,
|
|
286
|
+
): GraphicsObject {
|
|
287
|
+
const filtered: Record<string, unknown[]> = {}
|
|
288
|
+
|
|
289
|
+
for (const key of Object.keys(graphics)) {
|
|
290
|
+
const value = (graphics as Record<string, unknown>)[key]
|
|
291
|
+
filtered[key] = Array.isArray(value)
|
|
292
|
+
? value.filter((object) => {
|
|
293
|
+
return (
|
|
294
|
+
typeof object === "object" &&
|
|
295
|
+
object !== null &&
|
|
296
|
+
(object as { step?: number }).step === step
|
|
297
|
+
)
|
|
298
|
+
})
|
|
299
|
+
: []
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return filtered as GraphicsObject
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private getGraphicsObjectArrays(
|
|
306
|
+
graphics: GraphicsObject,
|
|
307
|
+
): Array<Array<{ step?: number }>> {
|
|
308
|
+
return Object.values(graphics).filter(Array.isArray) as Array<
|
|
309
|
+
Array<{ step?: number }>
|
|
310
|
+
>
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private getStageDef(stageNumber: number): PipelineStepLike | undefined {
|
|
314
|
+
return this.pipelineSolver.pipelineDef[stageNumber]
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private getStageSolver(stageName: string): VisualizingSolver | undefined {
|
|
318
|
+
return (
|
|
319
|
+
this.pipelineSolver as unknown as Record<string, VisualizingSolver>
|
|
320
|
+
)[stageName]
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private getSafeStageName(stageName: string): string {
|
|
324
|
+
return stageName.replace(/[^a-zA-Z0-9._-]/g, "_")
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private getStageEnterLogLine(stageName: string, stageNumber: number) {
|
|
328
|
+
return `enter stage=${stageNumber + 1} name=${stageName}`
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private async log(line: string) {
|
|
332
|
+
await writeFile(this.logsPath, `${line}\n`, { flag: "a" })
|
|
333
|
+
this.onLog?.(line)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private toDisplayPath(filePath: string): string {
|
|
337
|
+
return path.relative(process.cwd(), filePath) || "."
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private formatError(error: unknown) {
|
|
341
|
+
if (error instanceof Error) return `${error.name}: ${error.message}`
|
|
342
|
+
return String(error)
|
|
343
|
+
}
|
|
344
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tscircuit/schematic-trace-solver",
|
|
3
3
|
"main": "dist/index.js",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.88",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"start": "cosmos",
|
|
8
|
+
"debug:pipeline": "bun scripts/debug-pipeline-stages.ts",
|
|
8
9
|
"build": "tsup-node lib/index.ts --format esm --dts",
|
|
9
10
|
"format": "biome format --write .",
|
|
10
11
|
"format:check": "biome format ."
|
|
@@ -18,7 +19,7 @@
|
|
|
18
19
|
"calculate-elbow": "^0.0.12",
|
|
19
20
|
"connectivity-map": "^1.0.0",
|
|
20
21
|
"flatbush": "^4.5.0",
|
|
21
|
-
"graphics-debug": "^0.0.
|
|
22
|
+
"graphics-debug": "^0.0.96",
|
|
22
23
|
"react": "^19.1.1",
|
|
23
24
|
"react-cosmos": "^7.0.0",
|
|
24
25
|
"react-cosmos-plugin-vite": "^7.0.0",
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import { readFile } from "node:fs/promises"
|
|
3
|
+
import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
|
|
4
|
+
import { PipelineStageDebugRunner } from "lib/testing/PipelineStageDebugRunner"
|
|
5
|
+
import type { InputProblem } from "lib/types/InputProblem"
|
|
6
|
+
|
|
7
|
+
type Options = {
|
|
8
|
+
inputPath: string
|
|
9
|
+
outDir?: string
|
|
10
|
+
stopAfterStage?: string
|
|
11
|
+
pngSize: number
|
|
12
|
+
writePng: boolean
|
|
13
|
+
writeSvg: boolean
|
|
14
|
+
writeGraphicsJson: boolean
|
|
15
|
+
writeStepPngs: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function printUsage() {
|
|
19
|
+
console.log(`Usage: bun run debug:pipeline <input.json> [options]
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
--out <dir> Output directory
|
|
23
|
+
--stop-after <stage|n> Stop after a stage name or 1-based stage number
|
|
24
|
+
--png-size <px> PNG width and height, default 1536
|
|
25
|
+
--no-png Do not write .png files
|
|
26
|
+
--svg Also write .svg files
|
|
27
|
+
--no-json Do not write .graphics.json files
|
|
28
|
+
--step-pngs Also write one PNG per graphics step when step values exist
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
bun run debug:pipeline tests/assets/example01.json
|
|
32
|
+
`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseArgs(argv: string[]): Options {
|
|
36
|
+
const args = [...argv]
|
|
37
|
+
const inputPath = args.shift()
|
|
38
|
+
if (!inputPath || inputPath === "--help" || inputPath === "-h") {
|
|
39
|
+
printUsage()
|
|
40
|
+
process.exit(inputPath ? 0 : 1)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const options: Options = {
|
|
44
|
+
inputPath,
|
|
45
|
+
pngSize: 1536,
|
|
46
|
+
writePng: true,
|
|
47
|
+
writeSvg: false,
|
|
48
|
+
writeGraphicsJson: true,
|
|
49
|
+
writeStepPngs: false,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
while (args.length > 0) {
|
|
53
|
+
const arg = args.shift()
|
|
54
|
+
if (arg === "--out") {
|
|
55
|
+
options.outDir = args.shift()
|
|
56
|
+
if (!options.outDir) throw new Error("--out requires a directory")
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
if (arg === "--stop-after") {
|
|
60
|
+
options.stopAfterStage = args.shift()
|
|
61
|
+
if (!options.stopAfterStage) {
|
|
62
|
+
throw new Error("--stop-after requires a stage name or number")
|
|
63
|
+
}
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
if (arg === "--png-size") {
|
|
67
|
+
const pngSize = Number(args.shift())
|
|
68
|
+
if (!Number.isFinite(pngSize) || pngSize <= 0) {
|
|
69
|
+
throw new Error("--png-size requires a positive number")
|
|
70
|
+
}
|
|
71
|
+
options.pngSize = pngSize
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
if (arg === "--no-png") {
|
|
75
|
+
options.writePng = false
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
if (arg === "--svg") {
|
|
79
|
+
options.writeSvg = true
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
if (arg === "--no-json") {
|
|
83
|
+
options.writeGraphicsJson = false
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
if (arg === "--step-pngs") {
|
|
87
|
+
options.writeStepPngs = true
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
throw new Error(`Unknown argument: ${arg}`)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return options
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getDefaultOutputDir(inputPath: string) {
|
|
97
|
+
const basename = path.basename(inputPath).replace(/\.json$/i, "")
|
|
98
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
|
|
99
|
+
return path.resolve(
|
|
100
|
+
process.cwd(),
|
|
101
|
+
"tmp",
|
|
102
|
+
"schematic-trace-debug",
|
|
103
|
+
`${basename}-${timestamp}`,
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const options = parseArgs(process.argv.slice(2))
|
|
108
|
+
const inputPath = path.resolve(process.cwd(), options.inputPath)
|
|
109
|
+
const inputProblem = JSON.parse(
|
|
110
|
+
await readFile(inputPath, "utf8"),
|
|
111
|
+
) as InputProblem
|
|
112
|
+
const outputDir = options.outDir
|
|
113
|
+
? path.resolve(process.cwd(), options.outDir)
|
|
114
|
+
: getDefaultOutputDir(inputPath)
|
|
115
|
+
const solver = new SchematicTracePipelineSolver(inputProblem)
|
|
116
|
+
|
|
117
|
+
const runner = new PipelineStageDebugRunner({
|
|
118
|
+
pipelineSolver: solver,
|
|
119
|
+
outputDir,
|
|
120
|
+
writeGraphicsJson: options.writeGraphicsJson,
|
|
121
|
+
writePng: options.writePng,
|
|
122
|
+
writeSvg: options.writeSvg,
|
|
123
|
+
pngWidth: options.pngSize,
|
|
124
|
+
pngHeight: options.pngSize,
|
|
125
|
+
writeStepPngs: options.writeStepPngs,
|
|
126
|
+
stopAfterStage: options.stopAfterStage,
|
|
127
|
+
context: {
|
|
128
|
+
inputPath: path.relative(process.cwd(), inputPath),
|
|
129
|
+
stopAfterStage: options.stopAfterStage ?? "",
|
|
130
|
+
writePng: options.writePng,
|
|
131
|
+
writeSvg: options.writeSvg,
|
|
132
|
+
writeGraphicsJson: options.writeGraphicsJson,
|
|
133
|
+
writeStepPngs: options.writeStepPngs,
|
|
134
|
+
pngSize: options.pngSize,
|
|
135
|
+
},
|
|
136
|
+
onLog: (line) => console.log(line),
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
const result = await runner.run()
|
|
140
|
+
|
|
141
|
+
console.log(`\nWrote ${result.stageArtifacts.length} stage artifact(s).`)
|
|
142
|
+
console.log(`Output: ${path.relative(process.cwd(), result.outputDir)}`)
|
|
143
|
+
|
|
144
|
+
if (result.error) {
|
|
145
|
+
process.exitCode = 1
|
|
146
|
+
}
|