circuit-to-canvas 0.0.59 → 0.0.61

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/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AnyCircuitElement, PcbRenderLayer, NinePointAnchor, PcbPlatedHole, PCBVia, PcbHole, PcbSmtPad, PCBTrace, PcbBoard, PcbPanel, PcbSilkscreenText, PcbSilkscreenRect, PcbSilkscreenCircle, PcbSilkscreenLine, PcbSilkscreenPath, PcbSilkscreenPill, PcbSilkscreenOval, PcbCutout, PCBKeepout, PcbCopperPour, PcbCopperText, PcbFabricationNoteText, PcbFabricationNoteRect, PcbNoteRect, PcbFabricationNotePath, PcbNotePath, PcbNoteText, PcbNoteDimension, PcbFabricationNoteDimension } from 'circuit-json';
1
+ import { AnyCircuitElement, PcbRenderLayer, NinePointAnchor, PcbPlatedHole, PCBVia, PcbHole, PcbSmtPad, PcbTrace, PcbBoard, PcbPanel, PcbSilkscreenText, PcbSilkscreenRect, PcbSilkscreenCircle, PcbSilkscreenLine, PcbSilkscreenPath, PcbSilkscreenPill, PcbSilkscreenOval, PcbCutout, PCBKeepout, PcbCopperPour, PcbCopperText, PcbFabricationNoteText, PcbFabricationNoteRect, PcbNoteRect, PcbFabricationNotePath, PcbNotePath, PcbNoteText, PcbNoteDimension, PcbFabricationNoteDimension } from 'circuit-json';
2
2
  import { Matrix } from 'transformation-matrix';
3
3
 
4
4
  /**
@@ -372,7 +372,7 @@ declare function drawSoldermaskRingForOval(ctx: CanvasContext, center: {
372
372
 
373
373
  interface DrawPcbTraceParams {
374
374
  ctx: CanvasContext;
375
- trace: PCBTrace;
375
+ trace: PcbTrace;
376
376
  realToCanvasMat: Matrix;
377
377
  colorMap: PcbColorMap;
378
378
  }
package/dist/index.js CHANGED
@@ -1108,9 +1108,6 @@ function getRotation(hole) {
1108
1108
  }
1109
1109
  function drawPcbHole(params) {
1110
1110
  const { ctx, hole, realToCanvasMat, colorMap, soldermaskMargin = 0 } = params;
1111
- if (hole.is_covered_with_solder_mask === true) {
1112
- return;
1113
- }
1114
1111
  const holeInset = soldermaskMargin < 0 ? Math.abs(soldermaskMargin) : 0;
1115
1112
  if (hole.hole_shape === "circle") {
1116
1113
  drawCircle({
@@ -3101,32 +3098,165 @@ function processElementSoldermask(params) {
3101
3098
  }
3102
3099
  }
3103
3100
 
3104
- // lib/drawer/elements/pcb-trace.ts
3101
+ // lib/drawer/elements/pcb-trace/normalize-trace-direction.ts
3102
+ function normalizeTraceDirection(x, y) {
3103
+ const length = Math.hypot(x, y);
3104
+ if (length <= Number.EPSILON) return { x: 0, y: 0 };
3105
+ return { x: x / length, y: y / length };
3106
+ }
3107
+
3108
+ // lib/drawer/elements/pcb-trace/get-trace-direction-at.ts
3109
+ function getTraceDirectionAt(points, index) {
3110
+ const count = points.length;
3111
+ if (count < 2) return { x: 1, y: 0 };
3112
+ if (index <= 0) {
3113
+ const next2 = points[1];
3114
+ const current2 = points[0];
3115
+ if (!next2 || !current2) return { x: 1, y: 0 };
3116
+ const dir = normalizeTraceDirection(next2.x - current2.x, next2.y - current2.y);
3117
+ return dir.x === 0 && dir.y === 0 ? { x: 1, y: 0 } : dir;
3118
+ }
3119
+ if (index >= count - 1) {
3120
+ const prev2 = points[count - 2];
3121
+ const current2 = points[count - 1];
3122
+ if (!prev2 || !current2) return { x: 1, y: 0 };
3123
+ const dir = normalizeTraceDirection(current2.x - prev2.x, current2.y - prev2.y);
3124
+ return dir.x === 0 && dir.y === 0 ? { x: 1, y: 0 } : dir;
3125
+ }
3126
+ const prev = points[index - 1];
3127
+ const current = points[index];
3128
+ const next = points[index + 1];
3129
+ if (!prev || !current || !next) return { x: 1, y: 0 };
3130
+ const prevDir = normalizeTraceDirection(
3131
+ current.x - prev.x,
3132
+ current.y - prev.y
3133
+ );
3134
+ const nextDir = normalizeTraceDirection(
3135
+ next.x - current.x,
3136
+ next.y - current.y
3137
+ );
3138
+ const sum = normalizeTraceDirection(
3139
+ prevDir.x + nextDir.x,
3140
+ prevDir.y + nextDir.y
3141
+ );
3142
+ if (sum.x === 0 && sum.y === 0) {
3143
+ if (prevDir.x !== 0 || prevDir.y !== 0) return prevDir;
3144
+ if (nextDir.x !== 0 || nextDir.y !== 0) return nextDir;
3145
+ return { x: 1, y: 0 };
3146
+ }
3147
+ return sum;
3148
+ }
3149
+
3150
+ // lib/drawer/elements/pcb-trace/build-trace-polygon.ts
3151
+ function buildTracePolygon(points) {
3152
+ const left = [];
3153
+ const right = [];
3154
+ for (let i = 0; i < points.length; i++) {
3155
+ const point = points[i];
3156
+ if (!point) continue;
3157
+ const dir = getTraceDirectionAt(points, i);
3158
+ const normal = normalizeTraceDirection(-dir.y, dir.x);
3159
+ const offset = point.width / 2;
3160
+ left.push({
3161
+ x: point.x + normal.x * offset,
3162
+ y: point.y + normal.y * offset
3163
+ });
3164
+ right.push({
3165
+ x: point.x - normal.x * offset,
3166
+ y: point.y - normal.y * offset
3167
+ });
3168
+ }
3169
+ return left.concat(right.reverse());
3170
+ }
3171
+
3172
+ // lib/drawer/elements/pcb-trace/collect-trace-segments.ts
3173
+ function collectTraceSegments(route) {
3174
+ const segments = [];
3175
+ let current = [];
3176
+ let currentLayer = null;
3177
+ for (const routePoint of route ?? []) {
3178
+ if (!routePoint || routePoint.route_type !== "wire") {
3179
+ if (current.length >= 2) segments.push(current);
3180
+ current = [];
3181
+ currentLayer = null;
3182
+ continue;
3183
+ }
3184
+ const layer = routePoint.layer ?? currentLayer;
3185
+ if (!layer) continue;
3186
+ const x = routePoint.x;
3187
+ const y = routePoint.y;
3188
+ if (typeof x !== "number" || typeof y !== "number") continue;
3189
+ if (typeof routePoint.width !== "number") continue;
3190
+ const width = routePoint.width;
3191
+ if (currentLayer && layer !== currentLayer) {
3192
+ if (current.length >= 2) segments.push(current);
3193
+ current = [];
3194
+ }
3195
+ currentLayer = layer;
3196
+ current.push({
3197
+ route_type: "wire",
3198
+ x,
3199
+ y,
3200
+ width,
3201
+ layer,
3202
+ start_pcb_port_id: "start_pcb_port_id" in routePoint ? routePoint.start_pcb_port_id : void 0,
3203
+ end_pcb_port_id: "end_pcb_port_id" in routePoint ? routePoint.end_pcb_port_id : void 0
3204
+ });
3205
+ }
3206
+ if (current.length >= 2) segments.push(current);
3207
+ return segments;
3208
+ }
3209
+
3210
+ // lib/drawer/elements/pcb-trace/has-variable-width.ts
3211
+ function hasVariableWidth(points) {
3212
+ if (points.length < 2) return false;
3213
+ const baseWidth = points[0]?.width;
3214
+ if (typeof baseWidth !== "number") return false;
3215
+ return points.some(
3216
+ (point) => Math.abs(point.width - baseWidth) > Number.EPSILON
3217
+ );
3218
+ }
3219
+
3220
+ // lib/drawer/elements/pcb-trace/layer-to-color.ts
3105
3221
  function layerToColor4(layer, colorMap) {
3106
3222
  return colorMap.copper[layer] ?? colorMap.copper.top;
3107
3223
  }
3224
+
3225
+ // lib/drawer/elements/pcb-trace/pcb-trace.ts
3108
3226
  function drawPcbTrace(params) {
3109
3227
  const { ctx, trace, realToCanvasMat, colorMap } = params;
3110
3228
  if (!trace.route || !Array.isArray(trace.route) || trace.route.length < 2) {
3111
3229
  return;
3112
3230
  }
3113
- for (let i = 0; i < trace.route.length - 1; i++) {
3114
- const start = trace.route[i];
3115
- const end = trace.route[i + 1];
3116
- if (!start || !end) continue;
3117
- const layer = "layer" in start ? start.layer : "layer" in end ? end.layer : null;
3231
+ const segments = collectTraceSegments(trace.route);
3232
+ for (const segment of segments) {
3233
+ const layer = segment[0]?.layer;
3118
3234
  if (!layer) continue;
3119
3235
  const color = layerToColor4(layer, colorMap);
3120
- const traceWidth = "width" in start ? start.width : "width" in end ? end.width : 0.1;
3121
- drawLine({
3122
- ctx,
3123
- start: { x: start.x, y: start.y },
3124
- end: { x: end.x, y: end.y },
3125
- strokeWidth: traceWidth,
3126
- stroke: color,
3127
- realToCanvasMat,
3128
- lineCap: "round"
3129
- });
3236
+ if (hasVariableWidth(segment)) {
3237
+ const polygonPoints = buildTracePolygon(segment);
3238
+ drawPolygon({
3239
+ ctx,
3240
+ points: polygonPoints,
3241
+ fill: color,
3242
+ realToCanvasMat
3243
+ });
3244
+ continue;
3245
+ }
3246
+ for (let i = 0; i < segment.length - 1; i++) {
3247
+ const start = segment[i];
3248
+ const end = segment[i + 1];
3249
+ if (!start || !end) continue;
3250
+ drawLine({
3251
+ ctx,
3252
+ start: { x: start.x, y: start.y },
3253
+ end: { x: end.x, y: end.y },
3254
+ strokeWidth: start.width,
3255
+ stroke: color,
3256
+ realToCanvasMat,
3257
+ lineCap: "round"
3258
+ });
3259
+ }
3130
3260
  }
3131
3261
  }
3132
3262
 
@@ -3302,6 +3432,18 @@ var CircuitToCanvasDrawer = class {
3302
3432
  }
3303
3433
  }
3304
3434
  const drawSoldermask = options.drawSoldermask ?? false;
3435
+ for (const element of elements) {
3436
+ if (!shouldDrawElement(element, options)) continue;
3437
+ if (element.type === "pcb_hole") {
3438
+ drawPcbHole({
3439
+ ctx: this.ctx,
3440
+ hole: element,
3441
+ realToCanvasMat: this.realToCanvasMat,
3442
+ colorMap: this.colorMap,
3443
+ soldermaskMargin: drawSoldermask ? element.soldermask_margin : void 0
3444
+ });
3445
+ }
3446
+ }
3305
3447
  if (board) {
3306
3448
  drawPcbSoldermask({
3307
3449
  ctx: this.ctx,
@@ -3414,15 +3556,6 @@ var CircuitToCanvasDrawer = class {
3414
3556
  }
3415
3557
  for (const element of elements) {
3416
3558
  if (!shouldDrawElement(element, options)) continue;
3417
- if (element.type === "pcb_hole") {
3418
- drawPcbHole({
3419
- ctx: this.ctx,
3420
- hole: element,
3421
- realToCanvasMat: this.realToCanvasMat,
3422
- colorMap: this.colorMap,
3423
- soldermaskMargin: drawSoldermask ? element.soldermask_margin : void 0
3424
- });
3425
- }
3426
3559
  if (element.type === "pcb_cutout") {
3427
3560
  drawPcbCutout({
3428
3561
  ctx: this.ctx,
@@ -63,7 +63,7 @@ import { drawPcbSilkscreenRect } from "./elements/pcb-silkscreen-rect"
63
63
  import { drawPcbSilkscreenText } from "./elements/pcb-silkscreen-text"
64
64
  import { drawPcbSmtPad } from "./elements/pcb-smtpad"
65
65
  import { drawPcbSoldermask } from "./elements/pcb-soldermask"
66
- import { drawPcbTrace } from "./elements/pcb-trace"
66
+ import { drawPcbTrace } from "./elements/pcb-trace/pcb-trace"
67
67
  import { drawPcbVia } from "./elements/pcb-via"
68
68
  import { shouldDrawElement } from "./pcb-render-layer-filter"
69
69
  import {
@@ -179,12 +179,13 @@ export class CircuitToCanvasDrawer {
179
179
  // 1. Panel outline (outer boundary)
180
180
  // 2. Board outline (inner board)
181
181
  // 3. Copper elements underneath soldermask (pads, copper text)
182
- // 3. Soldermask (covers everything except openings)
183
- // 4. Silkscreen (on soldermask, under top copper layers)
184
- // 5. Copper pour and traces (drawn on top of soldermask and silkscreen)
185
- // 6. Plated holes, vias (copper ring + drill hole on top of soldermask)
186
- // 7. Holes and cutouts (punch through everything)
187
- // 8. Other annotations
182
+ // 4. Holes (under soldermask so mask can cover them)
183
+ // 5. Soldermask (covers everything except openings)
184
+ // 6. Silkscreen (on soldermask, under top copper layers)
185
+ // 7. Copper pour and traces (drawn on top of soldermask and silkscreen)
186
+ // 8. Plated holes, vias (copper ring + drill hole on top of soldermask)
187
+ // 9. Cutouts (punch through everything)
188
+ // 10. Other annotations
188
189
 
189
190
  // Step 1: Draw panel outline (outer boundary)
190
191
  if (panel) {
@@ -231,8 +232,26 @@ export class CircuitToCanvasDrawer {
231
232
  }
232
233
  }
233
234
 
234
- // Step 3: Draw soldermask layer (only if showSoldermask is true)
235
235
  const drawSoldermask = options.drawSoldermask ?? false
236
+
237
+ // Step 3: Draw holes under soldermask
238
+ for (const element of elements) {
239
+ if (!shouldDrawElement(element, options)) continue
240
+
241
+ if (element.type === "pcb_hole") {
242
+ drawPcbHole({
243
+ ctx: this.ctx,
244
+ hole: element as PcbHole,
245
+ realToCanvasMat: this.realToCanvasMat,
246
+ colorMap: this.colorMap,
247
+ soldermaskMargin: drawSoldermask
248
+ ? element.soldermask_margin
249
+ : undefined,
250
+ })
251
+ }
252
+ }
253
+
254
+ // Step 4: Draw soldermask layer (only if showSoldermask is true)
236
255
  if (board) {
237
256
  drawPcbSoldermask({
238
257
  ctx: this.ctx,
@@ -245,7 +264,7 @@ export class CircuitToCanvasDrawer {
245
264
  })
246
265
  }
247
266
 
248
- // Step 4: Draw silkscreen (on soldermask, under top copper layers)
267
+ // Step 5: Draw silkscreen (on soldermask, under top copper layers)
249
268
  for (const element of elements) {
250
269
  if (!shouldDrawElement(element, options)) continue
251
270
 
@@ -313,7 +332,7 @@ export class CircuitToCanvasDrawer {
313
332
  }
314
333
  }
315
334
 
316
- // Step 5: Draw copper pour and traces (on top of soldermask and silkscreen)
335
+ // Step 6: Draw copper pour and traces (on top of soldermask and silkscreen)
317
336
  for (const element of elements) {
318
337
  if (!shouldDrawElement(element, options)) continue
319
338
 
@@ -336,7 +355,7 @@ export class CircuitToCanvasDrawer {
336
355
  }
337
356
  }
338
357
 
339
- // Step 6: Draw plated holes, vias (copper ring + drill hole on top of soldermask)
358
+ // Step 7: Draw plated holes, vias (copper ring + drill hole on top of soldermask)
340
359
  for (const element of elements) {
341
360
  if (!shouldDrawElement(element, options)) continue
342
361
 
@@ -363,22 +382,10 @@ export class CircuitToCanvasDrawer {
363
382
  }
364
383
  }
365
384
 
366
- // Step 7: Draw holes and cutouts (these punch through everything)
385
+ // Step 8: Draw cutouts (these punch through everything)
367
386
  for (const element of elements) {
368
387
  if (!shouldDrawElement(element, options)) continue
369
388
 
370
- if (element.type === "pcb_hole") {
371
- drawPcbHole({
372
- ctx: this.ctx,
373
- hole: element as PcbHole,
374
- realToCanvasMat: this.realToCanvasMat,
375
- colorMap: this.colorMap,
376
- soldermaskMargin: drawSoldermask
377
- ? element.soldermask_margin
378
- : undefined,
379
- })
380
- }
381
-
382
389
  if (element.type === "pcb_cutout") {
383
390
  drawPcbCutout({
384
391
  ctx: this.ctx,
@@ -15,7 +15,7 @@ export {
15
15
  drawSoldermaskRingForOval,
16
16
  } from "./soldermask-margin"
17
17
 
18
- export { drawPcbTrace, type DrawPcbTraceParams } from "./pcb-trace"
18
+ export { drawPcbTrace, type DrawPcbTraceParams } from "./pcb-trace/pcb-trace"
19
19
 
20
20
  export { drawPcbBoard, type DrawPcbBoardParams } from "./pcb-board"
21
21
 
@@ -25,11 +25,6 @@ function getRotation(hole: PcbHole): number {
25
25
  export function drawPcbHole(params: DrawPcbHoleParams): void {
26
26
  const { ctx, hole, realToCanvasMat, colorMap, soldermaskMargin = 0 } = params
27
27
 
28
- // Skip drawing if the hole is fully covered with soldermask
29
- if (hole.is_covered_with_solder_mask === true) {
30
- return
31
- }
32
-
33
28
  // For negative margins, draw smaller hole (inset by margin amount)
34
29
  const holeInset = soldermaskMargin < 0 ? Math.abs(soldermaskMargin) : 0
35
30
 
@@ -0,0 +1,30 @@
1
+ import type { PcbTraceRoutePointWire } from "circuit-json"
2
+ import { getTraceDirectionAt } from "./get-trace-direction-at"
3
+ import { normalizeTraceDirection } from "./normalize-trace-direction"
4
+
5
+ // Builds a filled polygon representing a variable-width trace segment.
6
+ export function buildTracePolygon(
7
+ points: PcbTraceRoutePointWire[],
8
+ ): Array<{ x: number; y: number }> {
9
+ const left: Array<{ x: number; y: number }> = []
10
+ const right: Array<{ x: number; y: number }> = []
11
+
12
+ for (let i = 0; i < points.length; i++) {
13
+ const point = points[i]
14
+ if (!point) continue
15
+ const dir = getTraceDirectionAt(points, i)
16
+ const normal = normalizeTraceDirection(-dir.y, dir.x)
17
+ const offset = point.width / 2
18
+
19
+ left.push({
20
+ x: point.x + normal.x * offset,
21
+ y: point.y + normal.y * offset,
22
+ })
23
+ right.push({
24
+ x: point.x - normal.x * offset,
25
+ y: point.y - normal.y * offset,
26
+ })
27
+ }
28
+
29
+ return left.concat(right.reverse())
30
+ }
@@ -0,0 +1,55 @@
1
+ import type { LayerRef, PcbTrace, PcbTraceRoutePointWire } from "circuit-json"
2
+
3
+ // Splits a trace route into contiguous wire segments by layer.
4
+ export function collectTraceSegments(
5
+ route: PcbTrace["route"],
6
+ ): PcbTraceRoutePointWire[][] {
7
+ const segments: PcbTraceRoutePointWire[][] = []
8
+ let current: PcbTraceRoutePointWire[] = []
9
+ let currentLayer: LayerRef | null = null
10
+
11
+ for (const routePoint of route ?? []) {
12
+ if (!routePoint || routePoint.route_type !== "wire") {
13
+ if (current.length >= 2) segments.push(current)
14
+ current = []
15
+ currentLayer = null
16
+ continue
17
+ }
18
+
19
+ const layer: LayerRef | null = routePoint.layer ?? currentLayer
20
+
21
+ if (!layer) continue
22
+
23
+ const x = routePoint.x
24
+ const y = routePoint.y
25
+ if (typeof x !== "number" || typeof y !== "number") continue
26
+
27
+ if (typeof routePoint.width !== "number") continue
28
+ const width = routePoint.width
29
+
30
+ if (currentLayer && layer !== currentLayer) {
31
+ if (current.length >= 2) segments.push(current)
32
+ current = []
33
+ }
34
+
35
+ currentLayer = layer
36
+ current.push({
37
+ route_type: "wire",
38
+ x,
39
+ y,
40
+ width,
41
+ layer,
42
+ start_pcb_port_id:
43
+ "start_pcb_port_id" in routePoint
44
+ ? routePoint.start_pcb_port_id
45
+ : undefined,
46
+ end_pcb_port_id:
47
+ "end_pcb_port_id" in routePoint
48
+ ? routePoint.end_pcb_port_id
49
+ : undefined,
50
+ })
51
+ }
52
+
53
+ if (current.length >= 2) segments.push(current)
54
+ return segments
55
+ }
@@ -0,0 +1,56 @@
1
+ import type { PcbTraceRoutePointWire } from "circuit-json"
2
+ import { normalizeTraceDirection } from "./normalize-trace-direction"
3
+
4
+ // Returns a unit direction for a trace at a point index.
5
+ export function getTraceDirectionAt(
6
+ points: PcbTraceRoutePointWire[],
7
+ index: number,
8
+ ): {
9
+ x: number
10
+ y: number
11
+ } {
12
+ const count = points.length
13
+ if (count < 2) return { x: 1, y: 0 }
14
+
15
+ if (index <= 0) {
16
+ const next = points[1]
17
+ const current = points[0]
18
+ if (!next || !current) return { x: 1, y: 0 }
19
+ const dir = normalizeTraceDirection(next.x - current.x, next.y - current.y)
20
+ return dir.x === 0 && dir.y === 0 ? { x: 1, y: 0 } : dir
21
+ }
22
+
23
+ if (index >= count - 1) {
24
+ const prev = points[count - 2]
25
+ const current = points[count - 1]
26
+ if (!prev || !current) return { x: 1, y: 0 }
27
+ const dir = normalizeTraceDirection(current.x - prev.x, current.y - prev.y)
28
+ return dir.x === 0 && dir.y === 0 ? { x: 1, y: 0 } : dir
29
+ }
30
+
31
+ const prev = points[index - 1]
32
+ const current = points[index]
33
+ const next = points[index + 1]
34
+ if (!prev || !current || !next) return { x: 1, y: 0 }
35
+
36
+ const prevDir = normalizeTraceDirection(
37
+ current.x - prev.x,
38
+ current.y - prev.y,
39
+ )
40
+ const nextDir = normalizeTraceDirection(
41
+ next.x - current.x,
42
+ next.y - current.y,
43
+ )
44
+ const sum = normalizeTraceDirection(
45
+ prevDir.x + nextDir.x,
46
+ prevDir.y + nextDir.y,
47
+ )
48
+
49
+ if (sum.x === 0 && sum.y === 0) {
50
+ if (prevDir.x !== 0 || prevDir.y !== 0) return prevDir
51
+ if (nextDir.x !== 0 || nextDir.y !== 0) return nextDir
52
+ return { x: 1, y: 0 }
53
+ }
54
+
55
+ return sum
56
+ }
@@ -0,0 +1,11 @@
1
+ import type { PcbTraceRoutePointWire } from "circuit-json"
2
+
3
+ // Returns true when any wire point width differs from the first.
4
+ export function hasVariableWidth(points: PcbTraceRoutePointWire[]): boolean {
5
+ if (points.length < 2) return false
6
+ const baseWidth = points[0]?.width
7
+ if (typeof baseWidth !== "number") return false
8
+ return points.some(
9
+ (point) => Math.abs(point.width - baseWidth) > Number.EPSILON,
10
+ )
11
+ }
@@ -0,0 +1,10 @@
1
+ import type { LayerRef } from "circuit-json"
2
+ import type { PcbColorMap } from "../../types"
3
+
4
+ // Resolves a copper layer to its configured trace color.
5
+ export function layerToColor(layer: LayerRef, colorMap: PcbColorMap): string {
6
+ return (
7
+ colorMap.copper[layer as keyof typeof colorMap.copper] ??
8
+ colorMap.copper.top
9
+ )
10
+ }
@@ -0,0 +1,9 @@
1
+ // Returns a unit-length trace direction; used to keep offsets/join math consistent.
2
+ export function normalizeTraceDirection(
3
+ x: number,
4
+ y: number,
5
+ ): { x: number; y: number } {
6
+ const length = Math.hypot(x, y)
7
+ if (length <= Number.EPSILON) return { x: 0, y: 0 }
8
+ return { x: x / length, y: y / length }
9
+ }
@@ -0,0 +1,60 @@
1
+ import type { PcbTrace } from "circuit-json"
2
+ import type { Matrix } from "transformation-matrix"
3
+ import { drawLine } from "../../shapes/line"
4
+ import { drawPolygon } from "../../shapes/polygon"
5
+ import type { CanvasContext, PcbColorMap } from "../../types"
6
+ import { buildTracePolygon } from "./build-trace-polygon"
7
+ import { collectTraceSegments } from "./collect-trace-segments"
8
+ import { hasVariableWidth } from "./has-variable-width"
9
+ import { layerToColor } from "./layer-to-color"
10
+
11
+ export interface DrawPcbTraceParams {
12
+ ctx: CanvasContext
13
+ trace: PcbTrace
14
+ realToCanvasMat: Matrix
15
+ colorMap: PcbColorMap
16
+ }
17
+
18
+ // Draws a PCB trace route as lines or a filled polygon when widths vary.
19
+ export function drawPcbTrace(params: DrawPcbTraceParams): void {
20
+ const { ctx, trace, realToCanvasMat, colorMap } = params
21
+
22
+ if (!trace.route || !Array.isArray(trace.route) || trace.route.length < 2) {
23
+ return
24
+ }
25
+
26
+ const segments = collectTraceSegments(trace.route)
27
+
28
+ for (const segment of segments) {
29
+ const layer = segment[0]?.layer
30
+ if (!layer) continue
31
+ const color = layerToColor(layer, colorMap)
32
+
33
+ if (hasVariableWidth(segment)) {
34
+ const polygonPoints = buildTracePolygon(segment)
35
+ drawPolygon({
36
+ ctx,
37
+ points: polygonPoints,
38
+ fill: color,
39
+ realToCanvasMat,
40
+ })
41
+ continue
42
+ }
43
+
44
+ for (let i = 0; i < segment.length - 1; i++) {
45
+ const start = segment[i]
46
+ const end = segment[i + 1]
47
+ if (!start || !end) continue
48
+
49
+ drawLine({
50
+ ctx,
51
+ start: { x: start.x, y: start.y },
52
+ end: { x: end.x, y: end.y },
53
+ strokeWidth: start.width,
54
+ stroke: color,
55
+ realToCanvasMat,
56
+ lineCap: "round",
57
+ })
58
+ }
59
+ }
60
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "circuit-to-canvas",
3
3
  "main": "dist/index.js",
4
- "version": "0.0.59",
4
+ "version": "0.0.61",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "build": "tsup-node ./lib/index.ts --format esm --dts",
@@ -1,59 +0,0 @@
1
- import type { PCBTrace } from "circuit-json"
2
- import type { Matrix } from "transformation-matrix"
3
- import type { PcbColorMap, CanvasContext } from "../types"
4
- import { drawLine } from "../shapes/line"
5
-
6
- export interface DrawPcbTraceParams {
7
- ctx: CanvasContext
8
- trace: PCBTrace
9
- realToCanvasMat: Matrix
10
- colorMap: PcbColorMap
11
- }
12
-
13
- function layerToColor(layer: string, colorMap: PcbColorMap): string {
14
- return (
15
- colorMap.copper[layer as keyof typeof colorMap.copper] ??
16
- colorMap.copper.top
17
- )
18
- }
19
-
20
- export function drawPcbTrace(params: DrawPcbTraceParams): void {
21
- const { ctx, trace, realToCanvasMat, colorMap } = params
22
-
23
- if (!trace.route || !Array.isArray(trace.route) || trace.route.length < 2) {
24
- return
25
- }
26
-
27
- // Draw each segment of the trace
28
- for (let i = 0; i < trace.route.length - 1; i++) {
29
- const start = trace.route[i]
30
- const end = trace.route[i + 1]
31
-
32
- if (!start || !end) continue
33
-
34
- // Get the layer from either point
35
- const layer =
36
- "layer" in start
37
- ? start.layer
38
- : "layer" in end
39
- ? (end as any).layer
40
- : null
41
- if (!layer) continue
42
-
43
- const color = layerToColor(layer, colorMap)
44
-
45
- // Get the trace width from either point
46
- const traceWidth =
47
- "width" in start ? start.width : "width" in end ? (end as any).width : 0.1
48
-
49
- drawLine({
50
- ctx,
51
- start: { x: start.x, y: start.y },
52
- end: { x: end.x, y: end.y },
53
- strokeWidth: traceWidth,
54
- stroke: color,
55
- realToCanvasMat,
56
- lineCap: "round",
57
- })
58
- }
59
- }