circuit-to-canvas 0.0.58 → 0.0.60
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 +149 -16
- package/lib/drawer/CircuitToCanvasDrawer.ts +1 -1
- package/lib/drawer/elements/index.ts +1 -1
- 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/tests/elements/pcb-trace-hints.test.ts +259 -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
|
@@ -3101,32 +3101,165 @@ function processElementSoldermask(params) {
|
|
|
3101
3101
|
}
|
|
3102
3102
|
}
|
|
3103
3103
|
|
|
3104
|
-
// lib/drawer/elements/pcb-trace.ts
|
|
3104
|
+
// lib/drawer/elements/pcb-trace/normalize-trace-direction.ts
|
|
3105
|
+
function normalizeTraceDirection(x, y) {
|
|
3106
|
+
const length = Math.hypot(x, y);
|
|
3107
|
+
if (length <= Number.EPSILON) return { x: 0, y: 0 };
|
|
3108
|
+
return { x: x / length, y: y / length };
|
|
3109
|
+
}
|
|
3110
|
+
|
|
3111
|
+
// lib/drawer/elements/pcb-trace/get-trace-direction-at.ts
|
|
3112
|
+
function getTraceDirectionAt(points, index) {
|
|
3113
|
+
const count = points.length;
|
|
3114
|
+
if (count < 2) return { x: 1, y: 0 };
|
|
3115
|
+
if (index <= 0) {
|
|
3116
|
+
const next2 = points[1];
|
|
3117
|
+
const current2 = points[0];
|
|
3118
|
+
if (!next2 || !current2) return { x: 1, y: 0 };
|
|
3119
|
+
const dir = normalizeTraceDirection(next2.x - current2.x, next2.y - current2.y);
|
|
3120
|
+
return dir.x === 0 && dir.y === 0 ? { x: 1, y: 0 } : dir;
|
|
3121
|
+
}
|
|
3122
|
+
if (index >= count - 1) {
|
|
3123
|
+
const prev2 = points[count - 2];
|
|
3124
|
+
const current2 = points[count - 1];
|
|
3125
|
+
if (!prev2 || !current2) return { x: 1, y: 0 };
|
|
3126
|
+
const dir = normalizeTraceDirection(current2.x - prev2.x, current2.y - prev2.y);
|
|
3127
|
+
return dir.x === 0 && dir.y === 0 ? { x: 1, y: 0 } : dir;
|
|
3128
|
+
}
|
|
3129
|
+
const prev = points[index - 1];
|
|
3130
|
+
const current = points[index];
|
|
3131
|
+
const next = points[index + 1];
|
|
3132
|
+
if (!prev || !current || !next) return { x: 1, y: 0 };
|
|
3133
|
+
const prevDir = normalizeTraceDirection(
|
|
3134
|
+
current.x - prev.x,
|
|
3135
|
+
current.y - prev.y
|
|
3136
|
+
);
|
|
3137
|
+
const nextDir = normalizeTraceDirection(
|
|
3138
|
+
next.x - current.x,
|
|
3139
|
+
next.y - current.y
|
|
3140
|
+
);
|
|
3141
|
+
const sum = normalizeTraceDirection(
|
|
3142
|
+
prevDir.x + nextDir.x,
|
|
3143
|
+
prevDir.y + nextDir.y
|
|
3144
|
+
);
|
|
3145
|
+
if (sum.x === 0 && sum.y === 0) {
|
|
3146
|
+
if (prevDir.x !== 0 || prevDir.y !== 0) return prevDir;
|
|
3147
|
+
if (nextDir.x !== 0 || nextDir.y !== 0) return nextDir;
|
|
3148
|
+
return { x: 1, y: 0 };
|
|
3149
|
+
}
|
|
3150
|
+
return sum;
|
|
3151
|
+
}
|
|
3152
|
+
|
|
3153
|
+
// lib/drawer/elements/pcb-trace/build-trace-polygon.ts
|
|
3154
|
+
function buildTracePolygon(points) {
|
|
3155
|
+
const left = [];
|
|
3156
|
+
const right = [];
|
|
3157
|
+
for (let i = 0; i < points.length; i++) {
|
|
3158
|
+
const point = points[i];
|
|
3159
|
+
if (!point) continue;
|
|
3160
|
+
const dir = getTraceDirectionAt(points, i);
|
|
3161
|
+
const normal = normalizeTraceDirection(-dir.y, dir.x);
|
|
3162
|
+
const offset = point.width / 2;
|
|
3163
|
+
left.push({
|
|
3164
|
+
x: point.x + normal.x * offset,
|
|
3165
|
+
y: point.y + normal.y * offset
|
|
3166
|
+
});
|
|
3167
|
+
right.push({
|
|
3168
|
+
x: point.x - normal.x * offset,
|
|
3169
|
+
y: point.y - normal.y * offset
|
|
3170
|
+
});
|
|
3171
|
+
}
|
|
3172
|
+
return left.concat(right.reverse());
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3175
|
+
// lib/drawer/elements/pcb-trace/collect-trace-segments.ts
|
|
3176
|
+
function collectTraceSegments(route) {
|
|
3177
|
+
const segments = [];
|
|
3178
|
+
let current = [];
|
|
3179
|
+
let currentLayer = null;
|
|
3180
|
+
for (const routePoint of route ?? []) {
|
|
3181
|
+
if (!routePoint || routePoint.route_type !== "wire") {
|
|
3182
|
+
if (current.length >= 2) segments.push(current);
|
|
3183
|
+
current = [];
|
|
3184
|
+
currentLayer = null;
|
|
3185
|
+
continue;
|
|
3186
|
+
}
|
|
3187
|
+
const layer = routePoint.layer ?? currentLayer;
|
|
3188
|
+
if (!layer) continue;
|
|
3189
|
+
const x = routePoint.x;
|
|
3190
|
+
const y = routePoint.y;
|
|
3191
|
+
if (typeof x !== "number" || typeof y !== "number") continue;
|
|
3192
|
+
if (typeof routePoint.width !== "number") continue;
|
|
3193
|
+
const width = routePoint.width;
|
|
3194
|
+
if (currentLayer && layer !== currentLayer) {
|
|
3195
|
+
if (current.length >= 2) segments.push(current);
|
|
3196
|
+
current = [];
|
|
3197
|
+
}
|
|
3198
|
+
currentLayer = layer;
|
|
3199
|
+
current.push({
|
|
3200
|
+
route_type: "wire",
|
|
3201
|
+
x,
|
|
3202
|
+
y,
|
|
3203
|
+
width,
|
|
3204
|
+
layer,
|
|
3205
|
+
start_pcb_port_id: "start_pcb_port_id" in routePoint ? routePoint.start_pcb_port_id : void 0,
|
|
3206
|
+
end_pcb_port_id: "end_pcb_port_id" in routePoint ? routePoint.end_pcb_port_id : void 0
|
|
3207
|
+
});
|
|
3208
|
+
}
|
|
3209
|
+
if (current.length >= 2) segments.push(current);
|
|
3210
|
+
return segments;
|
|
3211
|
+
}
|
|
3212
|
+
|
|
3213
|
+
// lib/drawer/elements/pcb-trace/has-variable-width.ts
|
|
3214
|
+
function hasVariableWidth(points) {
|
|
3215
|
+
if (points.length < 2) return false;
|
|
3216
|
+
const baseWidth = points[0]?.width;
|
|
3217
|
+
if (typeof baseWidth !== "number") return false;
|
|
3218
|
+
return points.some(
|
|
3219
|
+
(point) => Math.abs(point.width - baseWidth) > Number.EPSILON
|
|
3220
|
+
);
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
// lib/drawer/elements/pcb-trace/layer-to-color.ts
|
|
3105
3224
|
function layerToColor4(layer, colorMap) {
|
|
3106
3225
|
return colorMap.copper[layer] ?? colorMap.copper.top;
|
|
3107
3226
|
}
|
|
3227
|
+
|
|
3228
|
+
// lib/drawer/elements/pcb-trace/pcb-trace.ts
|
|
3108
3229
|
function drawPcbTrace(params) {
|
|
3109
3230
|
const { ctx, trace, realToCanvasMat, colorMap } = params;
|
|
3110
3231
|
if (!trace.route || !Array.isArray(trace.route) || trace.route.length < 2) {
|
|
3111
3232
|
return;
|
|
3112
3233
|
}
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
const
|
|
3116
|
-
if (!start || !end) continue;
|
|
3117
|
-
const layer = "layer" in start ? start.layer : "layer" in end ? end.layer : null;
|
|
3234
|
+
const segments = collectTraceSegments(trace.route);
|
|
3235
|
+
for (const segment of segments) {
|
|
3236
|
+
const layer = segment[0]?.layer;
|
|
3118
3237
|
if (!layer) continue;
|
|
3119
3238
|
const color = layerToColor4(layer, colorMap);
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
}
|
|
3239
|
+
if (hasVariableWidth(segment)) {
|
|
3240
|
+
const polygonPoints = buildTracePolygon(segment);
|
|
3241
|
+
drawPolygon({
|
|
3242
|
+
ctx,
|
|
3243
|
+
points: polygonPoints,
|
|
3244
|
+
fill: color,
|
|
3245
|
+
realToCanvasMat
|
|
3246
|
+
});
|
|
3247
|
+
continue;
|
|
3248
|
+
}
|
|
3249
|
+
for (let i = 0; i < segment.length - 1; i++) {
|
|
3250
|
+
const start = segment[i];
|
|
3251
|
+
const end = segment[i + 1];
|
|
3252
|
+
if (!start || !end) continue;
|
|
3253
|
+
drawLine({
|
|
3254
|
+
ctx,
|
|
3255
|
+
start: { x: start.x, y: start.y },
|
|
3256
|
+
end: { x: end.x, y: end.y },
|
|
3257
|
+
strokeWidth: start.width,
|
|
3258
|
+
stroke: color,
|
|
3259
|
+
realToCanvasMat,
|
|
3260
|
+
lineCap: "round"
|
|
3261
|
+
});
|
|
3262
|
+
}
|
|
3130
3263
|
}
|
|
3131
3264
|
}
|
|
3132
3265
|
|
|
@@ -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 {
|
|
@@ -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
|
|
|
@@ -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
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import { createCanvas } from "@napi-rs/canvas"
|
|
3
|
+
import { CircuitToCanvasDrawer } from "../../lib/drawer"
|
|
4
|
+
|
|
5
|
+
test("draw trace with port hints", async () => {
|
|
6
|
+
const canvas = createCanvas(800, 600)
|
|
7
|
+
const ctx = canvas.getContext("2d")
|
|
8
|
+
const drawer = new CircuitToCanvasDrawer(ctx)
|
|
9
|
+
|
|
10
|
+
drawer.setCameraBounds({ minX: -8, minY: -2, maxX: 0, maxY: 6 })
|
|
11
|
+
|
|
12
|
+
ctx.fillStyle = "#1a1a1a"
|
|
13
|
+
ctx.fillRect(0, 0, 800, 600)
|
|
14
|
+
|
|
15
|
+
const circuit: any = [
|
|
16
|
+
{
|
|
17
|
+
type: "source_component",
|
|
18
|
+
source_component_id: "simple_resistor_0",
|
|
19
|
+
name: "R1",
|
|
20
|
+
supplier_part_numbers: {},
|
|
21
|
+
ftype: "simple_resistor",
|
|
22
|
+
resistance: "10k",
|
|
23
|
+
pcbX: -5,
|
|
24
|
+
pcbY: 0,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
type: "schematic_component",
|
|
28
|
+
source_component_id: "simple_resistor_0",
|
|
29
|
+
schematic_component_id: "schematic_component_simple_resistor_0",
|
|
30
|
+
rotation: 0,
|
|
31
|
+
size: {
|
|
32
|
+
width: 1,
|
|
33
|
+
height: 0.3,
|
|
34
|
+
},
|
|
35
|
+
center: {
|
|
36
|
+
x: 0,
|
|
37
|
+
y: 0,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
type: "source_port",
|
|
42
|
+
name: "left",
|
|
43
|
+
source_port_id: "source_port_0",
|
|
44
|
+
source_component_id: "simple_resistor_0",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
type: "schematic_port",
|
|
48
|
+
schematic_port_id: "schematic_port_0",
|
|
49
|
+
source_port_id: "source_port_0",
|
|
50
|
+
center: {
|
|
51
|
+
x: -0.5,
|
|
52
|
+
y: 0,
|
|
53
|
+
},
|
|
54
|
+
facing_direction: "left",
|
|
55
|
+
schematic_component_id: "schematic_component_simple_resistor_0",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
type: "pcb_port",
|
|
59
|
+
pcb_port_id: "pcb_port_0",
|
|
60
|
+
source_port_id: "source_port_0",
|
|
61
|
+
pcb_component_id: "pcb_component_simple_resistor_0",
|
|
62
|
+
x: -5.95,
|
|
63
|
+
y: 0,
|
|
64
|
+
layers: ["top"],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
type: "source_port",
|
|
68
|
+
name: "right",
|
|
69
|
+
source_port_id: "source_port_1",
|
|
70
|
+
source_component_id: "simple_resistor_0",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
type: "schematic_port",
|
|
74
|
+
schematic_port_id: "schematic_port_1",
|
|
75
|
+
source_port_id: "source_port_1",
|
|
76
|
+
center: {
|
|
77
|
+
x: 0.5,
|
|
78
|
+
y: 0,
|
|
79
|
+
},
|
|
80
|
+
facing_direction: "right",
|
|
81
|
+
schematic_component_id: "schematic_component_simple_resistor_0",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
type: "pcb_port",
|
|
85
|
+
pcb_port_id: "pcb_port_1",
|
|
86
|
+
source_port_id: "source_port_1",
|
|
87
|
+
pcb_component_id: "pcb_component_simple_resistor_0",
|
|
88
|
+
x: -4.05,
|
|
89
|
+
y: 0,
|
|
90
|
+
layers: ["top"],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
type: "schematic_text",
|
|
94
|
+
text: "R1",
|
|
95
|
+
schematic_text_id: "schematic_text_0",
|
|
96
|
+
schematic_component_id: "schematic_component_simple_resistor_0",
|
|
97
|
+
anchor: "left",
|
|
98
|
+
position: {
|
|
99
|
+
x: -0.2,
|
|
100
|
+
y: -0.5,
|
|
101
|
+
},
|
|
102
|
+
rotation: 0,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
type: "schematic_text",
|
|
106
|
+
text: "10k",
|
|
107
|
+
schematic_text_id: "schematic_text_1",
|
|
108
|
+
schematic_component_id: "schematic_component_simple_resistor_0",
|
|
109
|
+
anchor: "left",
|
|
110
|
+
position: {
|
|
111
|
+
x: -0.2,
|
|
112
|
+
y: -0.3,
|
|
113
|
+
},
|
|
114
|
+
rotation: 0,
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
type: "pcb_component",
|
|
118
|
+
source_component_id: "simple_resistor_0",
|
|
119
|
+
pcb_component_id: "pcb_component_simple_resistor_0",
|
|
120
|
+
layer: "top",
|
|
121
|
+
center: {
|
|
122
|
+
x: -5,
|
|
123
|
+
y: 0,
|
|
124
|
+
},
|
|
125
|
+
rotation: 0,
|
|
126
|
+
width: 3.1,
|
|
127
|
+
height: 1.2,
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
type: "pcb_smtpad",
|
|
131
|
+
pcb_smtpad_id: "pcb_smtpad_0",
|
|
132
|
+
shape: "rect",
|
|
133
|
+
x: -5.95,
|
|
134
|
+
y: 0,
|
|
135
|
+
width: 1.2,
|
|
136
|
+
height: 1.2,
|
|
137
|
+
layer: "top",
|
|
138
|
+
pcb_component_id: "pcb_component_simple_resistor_0",
|
|
139
|
+
port_hints: ["1", "left"],
|
|
140
|
+
pcb_port_id: "pcb_port_0",
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
type: "pcb_smtpad",
|
|
144
|
+
pcb_smtpad_id: "pcb_smtpad_1",
|
|
145
|
+
shape: "rect",
|
|
146
|
+
x: -4.05,
|
|
147
|
+
y: 0,
|
|
148
|
+
width: 1.2,
|
|
149
|
+
height: 1.2,
|
|
150
|
+
layer: "top",
|
|
151
|
+
pcb_component_id: "pcb_component_simple_resistor_0",
|
|
152
|
+
port_hints: ["2", "right"],
|
|
153
|
+
pcb_port_id: "pcb_port_1",
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
type: "source_trace",
|
|
157
|
+
source_trace_id: "source_trace_0",
|
|
158
|
+
connected_source_port_ids: ["source_port_0", "source_port_1"],
|
|
159
|
+
connected_source_net_ids: [],
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
type: "schematic_trace",
|
|
163
|
+
source_trace_id: "source_trace_0",
|
|
164
|
+
schematic_trace_id: "schematic_trace_0",
|
|
165
|
+
edges: [
|
|
166
|
+
{
|
|
167
|
+
from: {
|
|
168
|
+
x: -0.6000000000000001,
|
|
169
|
+
y: 0,
|
|
170
|
+
},
|
|
171
|
+
to: {
|
|
172
|
+
x: -0.6000000000000001,
|
|
173
|
+
y: -0.19999999999999996,
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
from: {
|
|
178
|
+
x: -0.6000000000000001,
|
|
179
|
+
y: -0.19999999999999996,
|
|
180
|
+
},
|
|
181
|
+
to: {
|
|
182
|
+
x: 0.7000000000000002,
|
|
183
|
+
y: -0.19999999999999996,
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
from: {
|
|
188
|
+
x: 0.7000000000000002,
|
|
189
|
+
y: -0.19999999999999996,
|
|
190
|
+
},
|
|
191
|
+
to: {
|
|
192
|
+
x: 0.7000000000000002,
|
|
193
|
+
y: 0,
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
from: {
|
|
198
|
+
x: -0.5,
|
|
199
|
+
y: 0,
|
|
200
|
+
ti: 0,
|
|
201
|
+
},
|
|
202
|
+
to: {
|
|
203
|
+
x: -0.6000000000000001,
|
|
204
|
+
y: 0,
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
from: {
|
|
209
|
+
x: 0.5,
|
|
210
|
+
y: 0,
|
|
211
|
+
ti: 1,
|
|
212
|
+
},
|
|
213
|
+
to: {
|
|
214
|
+
x: 0.7000000000000002,
|
|
215
|
+
y: 0,
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
type: "pcb_trace",
|
|
222
|
+
pcb_trace_id: "pcb_trace_0",
|
|
223
|
+
source_trace_id: "source_trace_0",
|
|
224
|
+
route_thickness_mode: "interpolated",
|
|
225
|
+
route: [
|
|
226
|
+
{
|
|
227
|
+
route_type: "wire",
|
|
228
|
+
layer: "top",
|
|
229
|
+
width: 0.2,
|
|
230
|
+
x: -4,
|
|
231
|
+
y: 0,
|
|
232
|
+
start_pcb_port_id: "pcb_port_0",
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
route_type: "wire",
|
|
236
|
+
layer: "top",
|
|
237
|
+
width: 0.5,
|
|
238
|
+
x: -5.9,
|
|
239
|
+
y: 4,
|
|
240
|
+
start_pcb_port_id: "pcb_port_1",
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
route_type: "wire",
|
|
244
|
+
layer: "top",
|
|
245
|
+
width: 0.5,
|
|
246
|
+
x: -5.9,
|
|
247
|
+
y: 0,
|
|
248
|
+
start_pcb_port_id: "pcb_port_1",
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
},
|
|
252
|
+
]
|
|
253
|
+
|
|
254
|
+
drawer.drawElements(circuit)
|
|
255
|
+
|
|
256
|
+
await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
|
|
257
|
+
import.meta.path,
|
|
258
|
+
)
|
|
259
|
+
})
|
|
@@ -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
|
-
}
|