11import { BaseSolver } from "@tscircuit/solver-utils"
22import type { GraphicsObject } from "graphics-debug"
3- import type { PcbTrace , PcbVia , LayerRef } from "circuit-json"
3+ import type {
4+ PcbTrace ,
5+ PcbVia ,
6+ LayerRef ,
7+ PcbSmtPad ,
8+ PcbPlatedHole ,
9+ } from "circuit-json"
410import { hslToHex } from "./utils/hslToHex"
511
612/**
@@ -14,6 +20,8 @@ export type ColorMode = "layer" | "trace"
1420export interface TraceViewerInput {
1521 traces : PcbTrace [ ]
1622 vias : PcbVia [ ]
23+ smtpads ?: PcbSmtPad [ ]
24+ platedHoles ?: PcbPlatedHole [ ]
1725 boardBounds ?: {
1826 minX : number
1927 minY : number
@@ -30,6 +38,8 @@ export interface TraceViewerOutput {
3038 stats : {
3139 traceCount : number
3240 viaCount : number
41+ smtpadCount : number
42+ platedHoleCount : number
3343 topLayerSegments : number
3444 bottomLayerSegments : number
3545 totalRoutePoints : number
@@ -80,6 +90,26 @@ export class TraceViewer extends BaseSolver {
8090 hole_diameter : number
8191 } > = [ ]
8292
93+ // Scaled SMT pads for visualization
94+ private scaledSmtpads : Array < {
95+ x : number
96+ y : number
97+ shape : "rect" | "circle" | "pill" | "rotated_rect" | "polygon" | "rotated_pill"
98+ width ?: number
99+ height ?: number
100+ radius ?: number
101+ layer : LayerRef
102+ } > = [ ]
103+
104+ // Scaled plated holes for visualization
105+ private scaledPlatedHoles : Array < {
106+ x : number
107+ y : number
108+ outer_diameter : number
109+ hole_diameter : number
110+ shape : "circle" | "oval" | "pill"
111+ } > = [ ]
112+
83113 // Map of trace IDs to their assigned colors
84114 private traceColors : Map < string , string > = new Map ( )
85115
@@ -129,6 +159,12 @@ export class TraceViewer extends BaseSolver {
129159 // Scale vias for visualization
130160 this . scaleVias ( )
131161
162+ // Scale SMT pads for visualization
163+ this . scaleSmtpads ( )
164+
165+ // Scale plated holes for visualization
166+ this . scalePlatedHoles ( )
167+
132168 // Assign colors to each unique trace ID
133169 this . assignTraceColors ( )
134170
@@ -170,6 +206,68 @@ export class TraceViewer extends BaseSolver {
170206 }
171207 }
172208
209+ /**
210+ * Scale SMT pads for visualization
211+ */
212+ private scaleSmtpads ( ) : void {
213+ const smtpads = this . input . smtpads || [ ]
214+ for ( const pad of smtpads ) {
215+ // Skip polygon pads for now as they don't have simple x/y coordinates
216+ if ( pad . shape === "polygon" ) {
217+ continue
218+ }
219+
220+ const scaledPad : ( typeof this . scaledSmtpads ) [ 0 ] = {
221+ x : pad . x * this . scale ,
222+ y : pad . y * this . scale ,
223+ shape : pad . shape ,
224+ layer : pad . layer ,
225+ }
226+
227+ if ( pad . shape === "rect" || pad . shape === "rotated_rect" ) {
228+ scaledPad . width = ( pad . width ?? 0 ) * this . scale
229+ scaledPad . height = ( pad . height ?? 0 ) * this . scale
230+ } else if ( pad . shape === "circle" ) {
231+ scaledPad . radius = ( pad . radius ?? 0 ) * this . scale
232+ } else if ( pad . shape === "pill" || pad . shape === "rotated_pill" ) {
233+ scaledPad . width = ( pad . width ?? 0 ) * this . scale
234+ scaledPad . height = ( pad . height ?? 0 ) * this . scale
235+ scaledPad . radius = ( pad . radius ?? 0 ) * this . scale
236+ }
237+
238+ this . scaledSmtpads . push ( scaledPad )
239+ }
240+ }
241+
242+ /**
243+ * Scale plated holes for visualization
244+ */
245+ private scalePlatedHoles ( ) : void {
246+ const platedHoles = this . input . platedHoles || [ ]
247+ for ( const hole of platedHoles ) {
248+ // Handle different plated hole shapes
249+ if ( hole . shape === "circle" ) {
250+ this . scaledPlatedHoles . push ( {
251+ x : hole . x * this . scale ,
252+ y : hole . y * this . scale ,
253+ outer_diameter : hole . outer_diameter * this . scale ,
254+ hole_diameter : hole . hole_diameter * this . scale ,
255+ shape : "circle" ,
256+ } )
257+ } else if ( hole . shape === "oval" || hole . shape === "pill" ) {
258+ // Oval and pill shaped holes use outer_width/height instead of diameter
259+ this . scaledPlatedHoles . push ( {
260+ x : hole . x * this . scale ,
261+ y : hole . y * this . scale ,
262+ outer_diameter : Math . max ( hole . outer_width , hole . outer_height ) * this . scale ,
263+ hole_diameter : Math . max ( hole . hole_width , hole . hole_height ) * this . scale ,
264+ shape : hole . shape ,
265+ } )
266+ }
267+ // Skip complex shapes like circular_hole_with_rect_pad, etc. for now
268+ }
269+ }
270+
173271 /**
174272 * Parse traces into segments for easier visualization
175273 * Applies scaling to all coordinates
@@ -225,7 +323,7 @@ export class TraceViewer extends BaseSolver {
225323 }
226324
227325 /**
228- * Calculate board bounds from scaled trace segments and vias
326+ * Calculate board bounds from scaled trace segments, vias, pads, and plated holes
229327 */
230328 private calculateBoardBounds ( ) : void {
231329 let minX = Infinity
@@ -251,6 +349,25 @@ export class TraceViewer extends BaseSolver {
251349 maxY = Math . max ( maxY , via . y )
252350 }
253351
352+ // Use already-scaled SMT pads
353+ for ( const pad of this . scaledSmtpads ) {
354+ const halfWidth = ( pad . width ?? pad . radius ?? 0 ) / 2
355+ const halfHeight = ( pad . height ?? pad . radius ?? 0 ) / 2
356+ minX = Math . min ( minX , pad . x - halfWidth )
357+ minY = Math . min ( minY , pad . y - halfHeight )
358+ maxX = Math . max ( maxX , pad . x + halfWidth )
359+ maxY = Math . max ( maxY , pad . y + halfHeight )
360+ }
361+
362+ // Use already-scaled plated holes
363+ for ( const hole of this . scaledPlatedHoles ) {
364+ const halfDiameter = hole . outer_diameter / 2
365+ minX = Math . min ( minX , hole . x - halfDiameter )
366+ minY = Math . min ( minY , hole . y - halfDiameter )
367+ maxX = Math . max ( maxX , hole . x + halfDiameter )
368+ maxY = Math . max ( maxY , hole . y + halfDiameter )
369+ }
370+
254371 if ( minX === Infinity ) {
255372 // No data, use defaults
256373 this . input . boardBounds = { minX : - 50 , minY : - 50 , maxX : 50 , maxY : 50 }
@@ -392,9 +509,79 @@ export class TraceViewer extends BaseSolver {
392509 } )
393510 }
394511
512+ // Draw SMT pads (always visible, underneath traces)
513+ for ( const pad of this . scaledSmtpads ) {
514+ const padColor = layerColors [ pad . layer ] || "#888"
515+ // Lighter version for pads (50% lighter)
516+ const lightPadColor = this . lightenColor ( padColor , 0.5 )
517+
518+ if ( pad . shape === "circle" ) {
519+ graphics . circles ! . push ( {
520+ center : { x : pad . x , y : pad . y } ,
521+ radius : pad . radius ?? 1 ,
522+ fill : lightPadColor ,
523+ stroke : padColor ,
524+ } )
525+ } else if (
526+ pad . shape === "rect" ||
527+ pad . shape === "rotated_rect" ||
528+ pad . shape === "pill" ||
529+ pad . shape === "rotated_pill" ||
530+ pad . shape === "polygon"
531+ ) {
532+ graphics . rects ! . push ( {
533+ center : { x : pad . x , y : pad . y } ,
534+ width : pad . width ?? 1 ,
535+ height : pad . height ?? 1 ,
536+ fill : lightPadColor ,
537+ stroke : padColor ,
538+ } )
539+ }
540+ }
541+
542+ // Draw plated holes (always visible)
543+ for ( const hole of this . scaledPlatedHoles ) {
544+ // Outer copper ring
545+ graphics . circles ! . push ( {
546+ center : { x : hole . x , y : hole . y } ,
547+ radius : hole . outer_diameter / 2 ,
548+ fill : "#d4af37" , // Gold color for plated holes
549+ stroke : "#b8960c" ,
550+ } )
551+
552+ // Inner hole
553+ graphics . circles ! . push ( {
554+ center : { x : hole . x , y : hole . y } ,
555+ radius : hole . hole_diameter / 2 ,
556+ fill : "#1a1a2e" ,
557+ stroke : "#333" ,
558+ } )
559+ }
560+
395561 return graphics
396562 }
397563
564+ /**
565+ * Lighten a hex color by a factor
566+ */
567+ private lightenColor ( hexColor : string , factor : number ) : string {
568+ const r = parseInt ( hexColor . slice ( 1 , 3 ) , 16 )
569+ const g = parseInt ( hexColor . slice ( 3 , 5 ) , 16 )
570+ const b = parseInt ( hexColor . slice ( 5 , 7 ) , 16 )
571+
572+ const newR = Math . min ( 255 , Math . round ( r + ( 255 - r ) * factor ) )
573+ . toString ( 16 )
574+ . padStart ( 2 , "0" )
575+ const newG = Math . min ( 255 , Math . round ( g + ( 255 - g ) * factor ) )
576+ . toString ( 16 )
577+ . padStart ( 2 , "0" )
578+ const newB = Math . min ( 255 , Math . round ( b + ( 255 - b ) * factor ) )
579+ . toString ( 16 )
580+ . padStart ( 2 , "0" )
581+
582+ return `#${ newR } ${ newG } ${ newB } `
583+ }
584+
398585 /**
399586 * Adjust color opacity by modifying the hex color
400587 */
@@ -451,6 +638,8 @@ export class TraceViewer extends BaseSolver {
451638 stats : {
452639 traceCount : this . input . traces . length ,
453640 viaCount : this . input . vias . length ,
641+ smtpadCount : this . input . smtpads ?. length ?? 0 ,
642+ platedHoleCount : this . input . platedHoles ?. length ?? 0 ,
454643 topLayerSegments,
455644 bottomLayerSegments,
456645 totalRoutePoints,
0 commit comments