Skip to content

Commit 8f57f12

Browse files
authored
Add interactive DRC visualization tooling with selectable error markers and layer-aware graphics (#18)
1 parent 3e09ba6 commit 8f57f12

5 files changed

Lines changed: 746 additions & 210 deletions

File tree

Lines changed: 381 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,381 @@
1+
import type { Circle, GraphicsObject, Line, Point, Rect } from "graphics-debug"
2+
import {
3+
GlobalDrcForceImproveSolver,
4+
setGlobalDrcForceImproveSolverVisualizer,
5+
} from "../lib/solvers/GlobalDrcForceImproveSolver"
6+
import { getDrcSnapshot } from "../lib/solvers/GlobalDrcForceImproveSolver/drc-snapshot"
7+
import type {
8+
GlobalDrcForceImproveSolverParams,
9+
HighDensityRoute,
10+
SimpleRouteJson,
11+
} from "../lib"
12+
import type { DrcError } from "../lib/solvers/GlobalDrcForceImproveSolver"
13+
import { mapZToLayerName } from "../lib/utils/mapZToLayerName"
14+
15+
export type VisualizedGlobalDrcForceImproveSolverParams =
16+
GlobalDrcForceImproveSolverParams & {
17+
visibleLayer?: "all" | string
18+
}
19+
20+
export type VisualizedDrcMarker = {
21+
id: string
22+
center: { x: number; y: number }
23+
message: string
24+
status: "fixed" | "unfixed" | "created"
25+
}
26+
27+
const routeColors = [
28+
"rgba(37, 99, 235, 0.42)",
29+
"rgba(220, 38, 38, 0.42)",
30+
"rgba(5, 150, 105, 0.42)",
31+
"rgba(124, 58, 237, 0.42)",
32+
"rgba(217, 119, 6, 0.42)",
33+
"rgba(8, 145, 178, 0.42)",
34+
]
35+
36+
const getVisibleZ = (
37+
srj: SimpleRouteJson,
38+
visibleLayer: "all" | string,
39+
): number | null => {
40+
if (visibleLayer === "all") return null
41+
const z = Array.from({ length: srj.layerCount }, (_, index) => index).find(
42+
(index) => mapZToLayerName(index, srj.layerCount) === visibleLayer,
43+
)
44+
return z ?? null
45+
}
46+
47+
const getLayerColor = (z: number) => routeColors[z % routeColors.length]!
48+
49+
const routeSegmentIsVisible = (
50+
startZ: number,
51+
endZ: number,
52+
visibleZ: number | null,
53+
) => visibleZ === null || (startZ === visibleZ && endZ === visibleZ)
54+
55+
const obstacleIsVisible = (
56+
obstacle: SimpleRouteJson["obstacles"][number],
57+
visibleLayer: "all" | string,
58+
visibleZ: number | null,
59+
) => {
60+
if (visibleZ === null) return true
61+
if (obstacle.zLayers?.includes(visibleZ)) return true
62+
if (obstacle.layers.includes(visibleLayer)) return true
63+
if (visibleZ === 0 && obstacle.layers.includes("top")) return true
64+
if (visibleZ === 1 && obstacle.layers.includes("bottom")) return true
65+
return obstacle.layers.length === 0
66+
}
67+
68+
const connectionPointIsVisible = (
69+
point: SimpleRouteJson["connections"][number]["pointsToConnect"][number],
70+
visibleLayer: "all" | string,
71+
) => {
72+
if (visibleLayer === "all") return true
73+
if ("layer" in point) return point.layer === visibleLayer
74+
return point.layers.includes(visibleLayer)
75+
}
76+
77+
const connectionPointHasCoordinates = (
78+
point: SimpleRouteJson["connections"][number]["pointsToConnect"][number],
79+
): point is SimpleRouteJson["connections"][number]["pointsToConnect"][number] & {
80+
x: number
81+
y: number
82+
} =>
83+
typeof (point as { x?: unknown }).x === "number" &&
84+
Number.isFinite((point as { x: number }).x) &&
85+
typeof (point as { y?: unknown }).y === "number" &&
86+
Number.isFinite((point as { y: number }).y)
87+
88+
const getRouteLines = (
89+
routes: HighDensityRoute[],
90+
visibleZ: number | null,
91+
): Line[] =>
92+
routes.flatMap((route, routeIndex) => {
93+
const lines: Line[] = []
94+
for (let index = 0; index < route.route.length - 1; index += 1) {
95+
const start = route.route[index]
96+
const end = route.route[index + 1]
97+
if (!start || !end) continue
98+
if (start.z !== end.z) continue
99+
if (!routeSegmentIsVisible(start.z, end.z, visibleZ)) continue
100+
lines.push({
101+
points: [
102+
{ x: start.x, y: start.y },
103+
{ x: end.x, y: end.y },
104+
],
105+
strokeColor: getLayerColor(start.z),
106+
strokeWidth: Math.max(route.traceThickness, 0.04),
107+
label: `${route.connectionName} z${start.z}`,
108+
zIndex: -100 + routeIndex,
109+
})
110+
}
111+
return lines
112+
})
113+
114+
const getViaCircles = (
115+
routes: HighDensityRoute[],
116+
visibleZ: number | null,
117+
): Circle[] =>
118+
routes.flatMap((route) => {
119+
const diameter = route.viaDiameter || 0.3
120+
const explicitVias = route.vias.map((via) => ({
121+
center: { x: via.x, y: via.y },
122+
radius: diameter / 2,
123+
fill: "rgba(17, 24, 39, 0.5)",
124+
stroke: "rgba(248, 250, 252, 0.5)",
125+
label: `${route.connectionName} via`,
126+
}))
127+
128+
if (explicitVias.length > 0 || visibleZ !== null) return explicitVias
129+
130+
const derivedVias: Circle[] = []
131+
for (let index = 0; index < route.route.length - 1; index += 1) {
132+
const start = route.route[index]
133+
const end = route.route[index + 1]
134+
if (!start || !end || start.z === end.z) continue
135+
derivedVias.push({
136+
center: { x: start.x, y: start.y },
137+
radius: diameter / 2,
138+
fill: "rgba(17, 24, 39, 0.5)",
139+
stroke: "rgba(248, 250, 252, 0.5)",
140+
label: `${route.connectionName} via`,
141+
})
142+
}
143+
return derivedVias
144+
})
145+
146+
const getObstacleRects = (
147+
srj: SimpleRouteJson,
148+
visibleLayer: "all" | string,
149+
visibleZ: number | null,
150+
): Rect[] =>
151+
srj.obstacles
152+
.filter((obstacle) => obstacleIsVisible(obstacle, visibleLayer, visibleZ))
153+
.map((obstacle) => ({
154+
center: obstacle.center,
155+
width: obstacle.width,
156+
height: obstacle.height,
157+
ccwRotationDegrees: obstacle.ccwRotationDegrees,
158+
fill: obstacle.isCopperPour
159+
? "rgba(20, 184, 166, 0.34)"
160+
: "rgba(148, 163, 184, 0.58)",
161+
stroke: obstacle.isCopperPour ? "#0f766e" : "#64748b",
162+
label: obstacle.obstacleId,
163+
}))
164+
165+
const getConnectionPoints = (
166+
srj: SimpleRouteJson,
167+
visibleLayer: "all" | string,
168+
): Point[] =>
169+
srj.connections.flatMap((connection) =>
170+
connection.pointsToConnect
171+
.filter((point) => connectionPointIsVisible(point, visibleLayer))
172+
.filter(connectionPointHasCoordinates)
173+
.map((point) => ({
174+
x: point.x,
175+
y: point.y,
176+
color: "#111827",
177+
label: connection.name,
178+
})),
179+
)
180+
181+
const getBoardRect = (srj: SimpleRouteJson): Rect => ({
182+
center: {
183+
x: (srj.bounds.minX + srj.bounds.maxX) / 2,
184+
y: (srj.bounds.minY + srj.bounds.maxY) / 2,
185+
},
186+
width: srj.bounds.maxX - srj.bounds.minX,
187+
height: srj.bounds.maxY - srj.bounds.minY,
188+
fill: "rgba(255, 255, 255, 0)",
189+
stroke: "#0f172a",
190+
label: "board bounds",
191+
})
192+
193+
const DRC_MARKER_MATCH_TOLERANCE = 0.08
194+
195+
const getDrcErrorCenter = (
196+
error: DrcError,
197+
): { x: number; y: number } | null => {
198+
const center = error.center ?? error.pcb_center
199+
if (!center || typeof center !== "object") return null
200+
const maybeCenter = center as Record<string, unknown>
201+
return typeof maybeCenter.x === "number" && typeof maybeCenter.y === "number"
202+
? { x: maybeCenter.x, y: maybeCenter.y }
203+
: null
204+
}
205+
206+
const getDrcErrorMessage = (error: DrcError) =>
207+
typeof error.message === "string" ? error.message : "DRC error"
208+
209+
const getDrcErrorKey = (error: DrcError, center: { x: number; y: number }) => {
210+
const stableId =
211+
typeof error.pcb_error_id === "string"
212+
? error.pcb_error_id
213+
: typeof error.pcb_trace_id === "string"
214+
? error.pcb_trace_id
215+
: getDrcErrorMessage(error)
216+
return `${stableId}:${center.x.toFixed(2)}:${center.y.toFixed(2)}`
217+
}
218+
219+
const centersAreClose = (
220+
left: { x: number; y: number },
221+
right: { x: number; y: number },
222+
) =>
223+
Math.hypot(left.x - right.x, left.y - right.y) <= DRC_MARKER_MATCH_TOLERANCE
224+
225+
const isDrcCenter = (
226+
center: { x: number; y: number } | null,
227+
): center is { x: number; y: number } => center !== null
228+
229+
export const getDrcMarkersForSolver = (
230+
solver: GlobalDrcForceImproveSolver,
231+
): VisualizedDrcMarker[] => {
232+
const initialErrors = getDrcSnapshot(
233+
solver.srj,
234+
solver.inputHdRoutes,
235+
solver.drcEvaluator,
236+
).errors
237+
const currentErrors = getDrcSnapshot(
238+
solver.srj,
239+
solver.outputHdRoutes,
240+
solver.drcEvaluator,
241+
).errors
242+
const currentCenters = currentErrors
243+
.map(getDrcErrorCenter)
244+
.filter(isDrcCenter)
245+
const initialCenters = initialErrors
246+
.map(getDrcErrorCenter)
247+
.filter(isDrcCenter)
248+
const seenKeys = new Set<string>()
249+
250+
const initialMarkers = initialErrors.flatMap(
251+
(error): VisualizedDrcMarker[] => {
252+
const center = getDrcErrorCenter(error)
253+
if (!center) return []
254+
255+
const key = getDrcErrorKey(error, center)
256+
if (seenKeys.has(key)) return []
257+
seenKeys.add(key)
258+
259+
const isStillPresent = currentCenters.some((currentCenter) =>
260+
centersAreClose(center, currentCenter),
261+
)
262+
263+
return [
264+
{
265+
id: key,
266+
center,
267+
message: getDrcErrorMessage(error),
268+
status: isStillPresent ? "unfixed" : "fixed",
269+
},
270+
]
271+
},
272+
)
273+
274+
const createdMarkers = currentErrors.flatMap(
275+
(error): VisualizedDrcMarker[] => {
276+
const center = getDrcErrorCenter(error)
277+
if (!center) return []
278+
const matchesInitial = initialCenters.some((initialCenter) =>
279+
centersAreClose(center, initialCenter),
280+
)
281+
if (matchesInitial) return []
282+
283+
const key = `created:${getDrcErrorKey(error, center)}`
284+
if (seenKeys.has(key)) return []
285+
seenKeys.add(key)
286+
287+
return [
288+
{
289+
id: key,
290+
center,
291+
message: getDrcErrorMessage(error),
292+
status: "created",
293+
},
294+
]
295+
},
296+
)
297+
298+
return [...initialMarkers, ...createdMarkers]
299+
}
300+
301+
const getDrcMarkerCircles = (
302+
solver: GlobalDrcForceImproveSolver,
303+
selectedDrcMarkerId?: string,
304+
): Circle[] =>
305+
getDrcMarkersForSolver(solver).map((marker) => {
306+
const isSelected = marker.id === selectedDrcMarkerId
307+
const isFixed = marker.status === "fixed"
308+
return {
309+
center: marker.center,
310+
radius: isSelected ? 0.32 : 0.22,
311+
fill: isFixed ? "rgba(34, 197, 94, 0.42)" : "rgba(147, 51, 234, 0.38)",
312+
stroke: isSelected ? "#111827" : isFixed ? "#16a34a" : "#7e22ce",
313+
label: `${marker.status}: ${marker.message}`,
314+
}
315+
})
316+
317+
const getDrcMarkerById = (
318+
solver: GlobalDrcForceImproveSolver,
319+
markerId: string | undefined,
320+
) =>
321+
markerId
322+
? getDrcMarkersForSolver(solver).find((marker) => marker.id === markerId)
323+
: undefined
324+
325+
export const visualizeGlobalDrcForceImproveSolver = (
326+
solver: GlobalDrcForceImproveSolver,
327+
visibleLayer: "all" | string,
328+
selectedDrcMarkerId?: string,
329+
): GraphicsObject => {
330+
const visibleZ = getVisibleZ(solver.srj, visibleLayer)
331+
const routes = solver.outputHdRoutes
332+
333+
const graphics: GraphicsObject = {
334+
title: `Global DRC Force Improve (${visibleLayer})`,
335+
coordinateSystem: "cartesian",
336+
rects: [
337+
getBoardRect(solver.srj),
338+
...getObstacleRects(solver.srj, visibleLayer, visibleZ),
339+
],
340+
lines: getRouteLines(routes, visibleZ),
341+
circles: [
342+
...getViaCircles(routes, visibleZ),
343+
...getDrcMarkerCircles(solver, selectedDrcMarkerId),
344+
],
345+
points: getConnectionPoints(solver.srj, visibleLayer),
346+
}
347+
348+
getDrcMarkerById(solver, selectedDrcMarkerId)
349+
return graphics
350+
}
351+
352+
setGlobalDrcForceImproveSolverVisualizer((solver) =>
353+
visualizeGlobalDrcForceImproveSolver(solver, "all"),
354+
)
355+
356+
export class VisualizedGlobalDrcForceImproveSolver extends GlobalDrcForceImproveSolver {
357+
private readonly visibleLayer: "all" | string
358+
private selectedDrcMarkerId: string | undefined
359+
360+
constructor(params: VisualizedGlobalDrcForceImproveSolverParams) {
361+
const { visibleLayer = "all", ...solverParams } = params
362+
super(solverParams)
363+
this.visibleLayer = visibleLayer
364+
}
365+
366+
setSelectedDrcMarkerId(markerId: string | undefined) {
367+
this.selectedDrcMarkerId = markerId
368+
}
369+
370+
override getSolverName() {
371+
return "GlobalDrcForceImproveSolver"
372+
}
373+
374+
override visualize(): GraphicsObject {
375+
return visualizeGlobalDrcForceImproveSolver(
376+
this,
377+
this.visibleLayer,
378+
this.selectedDrcMarkerId,
379+
)
380+
}
381+
}

0 commit comments

Comments
 (0)