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 +2 -2
- package/dist/index.js +161 -28
- package/lib/drawer/CircuitToCanvasDrawer.ts +31 -24
- package/lib/drawer/elements/index.ts +1 -1
- package/lib/drawer/elements/pcb-hole.ts +0 -5
- package/lib/drawer/elements/pcb-trace/build-trace-polygon.ts +30 -0
- package/lib/drawer/elements/pcb-trace/collect-trace-segments.ts +55 -0
- package/lib/drawer/elements/pcb-trace/get-trace-direction-at.ts +56 -0
- package/lib/drawer/elements/pcb-trace/has-variable-width.ts +11 -0
- package/lib/drawer/elements/pcb-trace/layer-to-color.ts +10 -0
- package/lib/drawer/elements/pcb-trace/normalize-trace-direction.ts +9 -0
- package/lib/drawer/elements/pcb-trace/pcb-trace.ts +60 -0
- package/package.json +1 -1
- package/tests/elements/__snapshots__/pcb-trace-hints.snap.png +0 -0
- package/lib/drawer/elements/pcb-trace.ts +0 -59
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AnyCircuitElement, PcbRenderLayer, NinePointAnchor, PcbPlatedHole, PCBVia, PcbHole, PcbSmtPad,
|
|
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:
|
|
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
|
-
|
|
3114
|
-
|
|
3115
|
-
const
|
|
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
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
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
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
Binary file
|
|
@@ -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
|
-
}
|