circuit-to-canvas 0.0.110 → 0.0.112
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 +5 -4
- package/dist/index.js +63 -22
- package/lib/drawer/CircuitToCanvasDrawer.ts +35 -9
- package/lib/drawer/elements/helper-functions/pcb-smtpad-drill-cutouts.ts +69 -16
- package/lib/drawer/elements/pcb-plated-hole.ts +2 -2
- package/lib/drawer/elements/pcb-smtpad.ts +10 -3
- package/lib/drawer/elements/pcb-via.ts +2 -2
- package/lib/drawer/types.ts +4 -9
- package/package.json +5 -4
- package/tests/elements/__snapshots__/inner7-copper-pour-with-trace.snap.png +0 -0
- package/tests/elements/__snapshots__/inner8-copper-pour-with-trace.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-smtpad-cutout.snap.png +0 -0
- package/tests/elements/__snapshots__/pcb-via-inner-layer.snap.png +0 -0
- package/tests/elements/pcb-copper-pour-inner-layers.test.ts +12 -4
- package/tests/elements/pcb-smtpad-cutout.test.ts +39 -0
- package/tests/elements/pcb-via-inner-layer.test.ts +36 -0
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AnyCircuitElement, PcbRenderLayer, NinePointAnchor, PcbPlatedHole, PCBVia, PcbHole, PcbSmtPad, PcbVia, PcbTrace, PcbBoard, PcbPanel, PcbSilkscreenText, PcbSilkscreenRect, PcbSilkscreenCircle, PcbSilkscreenLine, PcbSilkscreenPath, PcbSilkscreenPill, PcbSilkscreenOval,
|
|
1
|
+
import { LayerRef, AnyCircuitElement, PcbRenderLayer, NinePointAnchor, PcbPlatedHole, PCBVia, PcbHole, PcbSmtPad, PcbVia, PcbCutout, PcbTrace, PcbBoard, PcbPanel, PcbSilkscreenText, PcbSilkscreenRect, PcbSilkscreenCircle, PcbSilkscreenLine, PcbSilkscreenPath, PcbSilkscreenPill, PcbSilkscreenOval, PCBKeepout, PcbCopperPour, PcbCopperText, PcbFabricationNoteText, PcbFabricationNoteRect, PcbNoteRect, PcbFabricationNotePath, PcbNotePath, PcbNoteText, PcbNoteDimension, PcbFabricationNoteDimension, PcbCourtyardCircle } from 'circuit-json';
|
|
2
2
|
import { Matrix } from 'transformation-matrix';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -44,7 +44,7 @@ interface CanvasContext {
|
|
|
44
44
|
font: string;
|
|
45
45
|
textAlign: "start" | "end" | "left" | "right" | "center";
|
|
46
46
|
}
|
|
47
|
-
type CopperLayerName =
|
|
47
|
+
type CopperLayerName = LayerRef;
|
|
48
48
|
type CopperColorMap = Record<CopperLayerName, string> & {
|
|
49
49
|
[layer: string]: string;
|
|
50
50
|
};
|
|
@@ -333,7 +333,7 @@ interface DrawPcbPlatedHoleParams {
|
|
|
333
333
|
colorMap: PcbColorMap;
|
|
334
334
|
soldermaskMargin?: number;
|
|
335
335
|
drawSoldermask?: boolean;
|
|
336
|
-
layer?:
|
|
336
|
+
layer?: LayerRef;
|
|
337
337
|
}
|
|
338
338
|
declare function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void;
|
|
339
339
|
|
|
@@ -342,7 +342,7 @@ interface DrawPcbViaParams {
|
|
|
342
342
|
via: PCBVia;
|
|
343
343
|
realToCanvasMat: Matrix;
|
|
344
344
|
colorMap: PcbColorMap;
|
|
345
|
-
layer?:
|
|
345
|
+
layer?: LayerRef;
|
|
346
346
|
}
|
|
347
347
|
declare function drawPcbVia(params: DrawPcbViaParams): void;
|
|
348
348
|
|
|
@@ -364,6 +364,7 @@ interface DrawPcbSmtPadParams {
|
|
|
364
364
|
holes?: PcbHole[];
|
|
365
365
|
platedHoles?: PcbPlatedHole[];
|
|
366
366
|
vias?: PcbVia[];
|
|
367
|
+
cutouts?: PcbCutout[];
|
|
367
368
|
}
|
|
368
369
|
declare function drawPcbSmtPad(params: DrawPcbSmtPadParams): void;
|
|
369
370
|
|
package/dist/index.js
CHANGED
|
@@ -1637,22 +1637,24 @@ function drawPadWithDrillCutouts(params) {
|
|
|
1637
1637
|
ctx.restore();
|
|
1638
1638
|
}
|
|
1639
1639
|
function getPadDrillCutouts(params) {
|
|
1640
|
-
const { pad, holes = [], platedHoles = [], vias = [] } = params;
|
|
1640
|
+
const { pad, holes = [], platedHoles = [], vias = [], cutouts = [] } = params;
|
|
1641
1641
|
const padBounds = getPadBounds(pad);
|
|
1642
1642
|
if (!padBounds) return [];
|
|
1643
|
-
const
|
|
1644
|
-
|
|
1645
|
-
const cutout = getHoleDrillCutout(hole);
|
|
1643
|
+
const drillCutouts = [];
|
|
1644
|
+
const addIfOverlapping = (cutout) => {
|
|
1646
1645
|
if (cutout && boundsOverlap(padBounds, getDrillCutoutBounds(cutout))) {
|
|
1647
|
-
|
|
1646
|
+
drillCutouts.push(cutout);
|
|
1648
1647
|
}
|
|
1648
|
+
};
|
|
1649
|
+
for (const cutout of cutouts) {
|
|
1650
|
+
addIfOverlapping(getBoardCutout(cutout));
|
|
1651
|
+
}
|
|
1652
|
+
for (const hole of holes) {
|
|
1653
|
+
addIfOverlapping(getHoleDrillCutout(hole));
|
|
1649
1654
|
}
|
|
1650
1655
|
for (const platedHole of platedHoles) {
|
|
1651
1656
|
if (!layersIncludePadLayer(platedHole.layers, pad.layer)) continue;
|
|
1652
|
-
|
|
1653
|
-
if (cutout && boundsOverlap(padBounds, getDrillCutoutBounds(cutout))) {
|
|
1654
|
-
cutouts.push(cutout);
|
|
1655
|
-
}
|
|
1657
|
+
addIfOverlapping(getPlatedHoleDrillCutout(platedHole));
|
|
1656
1658
|
}
|
|
1657
1659
|
for (const via of vias) {
|
|
1658
1660
|
if (!layersIncludePadLayer(via.layers, pad.layer)) continue;
|
|
@@ -1664,11 +1666,9 @@ function getPadDrillCutouts(params) {
|
|
|
1664
1666
|
y: via.y,
|
|
1665
1667
|
radius
|
|
1666
1668
|
};
|
|
1667
|
-
|
|
1668
|
-
cutouts.push(cutout);
|
|
1669
|
-
}
|
|
1669
|
+
addIfOverlapping(cutout);
|
|
1670
1670
|
}
|
|
1671
|
-
return
|
|
1671
|
+
return drillCutouts;
|
|
1672
1672
|
}
|
|
1673
1673
|
function appendPadPath(ctx, pad, realToCanvasMat) {
|
|
1674
1674
|
if (pad.shape === "rect" || pad.shape === "rotated_rect") {
|
|
@@ -1741,11 +1741,16 @@ function appendDrillCutoutPath(ctx, drillCutout, realToCanvasMat) {
|
|
|
1741
1741
|
});
|
|
1742
1742
|
return;
|
|
1743
1743
|
}
|
|
1744
|
+
if (drillCutout.shape === "polygon") {
|
|
1745
|
+
appendPolygonPath(ctx, drillCutout.points, realToCanvasMat);
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1744
1748
|
appendRectPath(ctx, {
|
|
1745
1749
|
x: drillCutout.x,
|
|
1746
1750
|
y: drillCutout.y,
|
|
1747
1751
|
width: drillCutout.width,
|
|
1748
1752
|
height: drillCutout.height,
|
|
1753
|
+
radius: drillCutout.radius,
|
|
1749
1754
|
rotation: drillCutout.rotation,
|
|
1750
1755
|
realToCanvasMat
|
|
1751
1756
|
});
|
|
@@ -1883,6 +1888,9 @@ function getPadBounds(pad) {
|
|
|
1883
1888
|
}
|
|
1884
1889
|
}
|
|
1885
1890
|
function getDrillCutoutBounds(cutout) {
|
|
1891
|
+
if (cutout.shape === "polygon") {
|
|
1892
|
+
return getPointBounds(cutout.points);
|
|
1893
|
+
}
|
|
1886
1894
|
if (cutout.shape === "circle") {
|
|
1887
1895
|
return boundsAroundCenter(cutout.x, cutout.y, cutout.radius, cutout.radius);
|
|
1888
1896
|
}
|
|
@@ -1906,6 +1914,32 @@ function getDrillCutoutBounds(cutout) {
|
|
|
1906
1914
|
}
|
|
1907
1915
|
return boundsAroundCenter(cutout.x, cutout.y, halfWidth, halfHeight);
|
|
1908
1916
|
}
|
|
1917
|
+
function getBoardCutout(cutout) {
|
|
1918
|
+
if (cutout.shape === "circle") {
|
|
1919
|
+
if (cutout.radius <= 0) return;
|
|
1920
|
+
return {
|
|
1921
|
+
shape: "circle",
|
|
1922
|
+
x: cutout.center.x,
|
|
1923
|
+
y: cutout.center.y,
|
|
1924
|
+
radius: cutout.radius
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
if (cutout.shape === "rect") {
|
|
1928
|
+
if (cutout.width <= 0 || cutout.height <= 0) return;
|
|
1929
|
+
return {
|
|
1930
|
+
shape: "rect",
|
|
1931
|
+
x: cutout.center.x,
|
|
1932
|
+
y: cutout.center.y,
|
|
1933
|
+
width: cutout.width,
|
|
1934
|
+
height: cutout.height,
|
|
1935
|
+
radius: cutout.corner_radius,
|
|
1936
|
+
rotation: cutout.rotation
|
|
1937
|
+
};
|
|
1938
|
+
}
|
|
1939
|
+
if (cutout.shape === "polygon" && cutout.points.length >= 3) {
|
|
1940
|
+
return { shape: "polygon", points: cutout.points };
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1909
1943
|
function getPointBounds(points) {
|
|
1910
1944
|
let minX = Infinity;
|
|
1911
1945
|
let maxX = -Infinity;
|
|
@@ -5183,6 +5217,8 @@ var DEFAULT_PCB_COLOR_MAP = {
|
|
|
5183
5217
|
inner4: "rgb(64, 224, 208)",
|
|
5184
5218
|
inner5: "rgb(138, 43, 226)",
|
|
5185
5219
|
inner6: "rgb(255, 105, 180)",
|
|
5220
|
+
inner7: "rgb(232, 178, 167)",
|
|
5221
|
+
inner8: "rgb(242, 237, 161)",
|
|
5186
5222
|
bottom: "rgb(77, 127, 196)"
|
|
5187
5223
|
},
|
|
5188
5224
|
copperPour: {
|
|
@@ -5222,9 +5258,10 @@ var DEFAULT_PCB_COLOR_MAP = {
|
|
|
5222
5258
|
// lib/drawer/CircuitToCanvasDrawer.ts
|
|
5223
5259
|
function getCopperLayer(layers) {
|
|
5224
5260
|
if (!layers || layers.length === 0) return "top";
|
|
5225
|
-
const
|
|
5226
|
-
return
|
|
5261
|
+
const copperLayer = layers.find((candidate) => candidate.endsWith("_copper"));
|
|
5262
|
+
return copperLayer ? copperLayer.slice(0, -"_copper".length) : "top";
|
|
5227
5263
|
}
|
|
5264
|
+
var isOnCopperLayer = (element, layer) => element.layers?.includes(layer) ?? true;
|
|
5228
5265
|
var CircuitToCanvasDrawer = class {
|
|
5229
5266
|
ctx;
|
|
5230
5267
|
colorMap;
|
|
@@ -5310,13 +5347,16 @@ var CircuitToCanvasDrawer = class {
|
|
|
5310
5347
|
const renderTopSoldermask = drawSoldermask && (board !== void 0 || panel !== void 0) && (options.drawSoldermaskTop ?? !hasExplicitSoldermaskLayers);
|
|
5311
5348
|
const renderBottomSoldermask = drawSoldermask && (board !== void 0 || panel !== void 0) && (options.drawSoldermaskBottom ?? false);
|
|
5312
5349
|
const drawableVias = elements.filter(
|
|
5313
|
-
(el) => shouldDrawElement(el, options) && el.type === "pcb_via"
|
|
5350
|
+
(el) => shouldDrawElement(el, options) && el.type === "pcb_via" && isOnCopperLayer(el, layer)
|
|
5314
5351
|
);
|
|
5315
5352
|
const drawableHoles = elements.filter(
|
|
5316
5353
|
(el) => shouldDrawElement(el, options) && el.type === "pcb_hole"
|
|
5317
5354
|
);
|
|
5318
5355
|
const drawablePlatedHoles = elements.filter(
|
|
5319
|
-
(el) => shouldDrawElement(el, options) && el.type === "pcb_plated_hole"
|
|
5356
|
+
(el) => shouldDrawElement(el, options) && el.type === "pcb_plated_hole" && isOnCopperLayer(el, layer)
|
|
5357
|
+
);
|
|
5358
|
+
const drawableCutouts = elements.filter(
|
|
5359
|
+
(el) => shouldDrawElement(el, options) && el.type === "pcb_cutout"
|
|
5320
5360
|
);
|
|
5321
5361
|
if (board) {
|
|
5322
5362
|
drawPcbBoard({
|
|
@@ -5349,7 +5389,8 @@ var CircuitToCanvasDrawer = class {
|
|
|
5349
5389
|
colorMap: this.colorMap,
|
|
5350
5390
|
holes: drawableHoles,
|
|
5351
5391
|
platedHoles: drawablePlatedHoles,
|
|
5352
|
-
vias: drawableVias
|
|
5392
|
+
vias: drawableVias,
|
|
5393
|
+
cutouts: drawableCutouts
|
|
5353
5394
|
});
|
|
5354
5395
|
}
|
|
5355
5396
|
if (element.type === "pcb_copper_text") {
|
|
@@ -5387,7 +5428,7 @@ var CircuitToCanvasDrawer = class {
|
|
|
5387
5428
|
drawSoldermask: renderTopSoldermask
|
|
5388
5429
|
});
|
|
5389
5430
|
}
|
|
5390
|
-
if (element.type === "pcb_plated_hole") {
|
|
5431
|
+
if (element.type === "pcb_plated_hole" && isOnCopperLayer(element, layer)) {
|
|
5391
5432
|
drawPcbPlatedHole({
|
|
5392
5433
|
ctx: this.ctx,
|
|
5393
5434
|
hole: element,
|
|
@@ -5398,7 +5439,7 @@ var CircuitToCanvasDrawer = class {
|
|
|
5398
5439
|
layer
|
|
5399
5440
|
});
|
|
5400
5441
|
}
|
|
5401
|
-
if (element.type === "pcb_via") {
|
|
5442
|
+
if (element.type === "pcb_via" && isOnCopperLayer(element, layer)) {
|
|
5402
5443
|
drawPcbVia({
|
|
5403
5444
|
ctx: this.ctx,
|
|
5404
5445
|
via: element,
|
|
@@ -5506,7 +5547,7 @@ var CircuitToCanvasDrawer = class {
|
|
|
5506
5547
|
}
|
|
5507
5548
|
for (const element of elements) {
|
|
5508
5549
|
if (!shouldDrawElement(element, options)) continue;
|
|
5509
|
-
if (element.type === "pcb_plated_hole" && !renderTopSoldermask) {
|
|
5550
|
+
if (element.type === "pcb_plated_hole" && !renderTopSoldermask && isOnCopperLayer(element, layer)) {
|
|
5510
5551
|
drawPcbPlatedHole({
|
|
5511
5552
|
ctx: this.ctx,
|
|
5512
5553
|
hole: element,
|
|
@@ -5517,7 +5558,7 @@ var CircuitToCanvasDrawer = class {
|
|
|
5517
5558
|
layer
|
|
5518
5559
|
});
|
|
5519
5560
|
}
|
|
5520
|
-
if (element.type === "pcb_via" && !renderTopSoldermask) {
|
|
5561
|
+
if (element.type === "pcb_via" && !renderTopSoldermask && isOnCopperLayer(element, layer)) {
|
|
5521
5562
|
drawPcbVia({
|
|
5522
5563
|
ctx: this.ctx,
|
|
5523
5564
|
via: element,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AnyCircuitElement,
|
|
3
|
+
LayerRef,
|
|
3
4
|
PCBKeepout,
|
|
4
5
|
PcbBoard,
|
|
5
6
|
PcbCopperPour,
|
|
@@ -94,12 +95,17 @@ interface CanvasLike {
|
|
|
94
95
|
getContext(contextId: "2d"): CanvasContext | null
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
function getCopperLayer(layers?: PcbRenderLayer[]):
|
|
98
|
+
function getCopperLayer(layers?: PcbRenderLayer[]): LayerRef {
|
|
98
99
|
if (!layers || layers.length === 0) return "top"
|
|
99
|
-
const
|
|
100
|
-
return
|
|
100
|
+
const copperLayer = layers.find((candidate) => candidate.endsWith("_copper"))
|
|
101
|
+
return copperLayer
|
|
102
|
+
? (copperLayer.slice(0, -"_copper".length) as LayerRef)
|
|
103
|
+
: "top"
|
|
101
104
|
}
|
|
102
105
|
|
|
106
|
+
const isOnCopperLayer = (element: PcbVia | PcbPlatedHole, layer: LayerRef) =>
|
|
107
|
+
element.layers?.includes(layer) ?? true
|
|
108
|
+
|
|
103
109
|
export class CircuitToCanvasDrawer {
|
|
104
110
|
private ctx: CanvasContext
|
|
105
111
|
private colorMap: PcbColorMap
|
|
@@ -233,7 +239,9 @@ export class CircuitToCanvasDrawer {
|
|
|
233
239
|
(options.drawSoldermaskBottom ?? false)
|
|
234
240
|
const drawableVias = elements.filter(
|
|
235
241
|
(el): el is PcbVia =>
|
|
236
|
-
shouldDrawElement(el, options) &&
|
|
242
|
+
shouldDrawElement(el, options) &&
|
|
243
|
+
el.type === "pcb_via" &&
|
|
244
|
+
isOnCopperLayer(el, layer),
|
|
237
245
|
)
|
|
238
246
|
const drawableHoles = elements.filter(
|
|
239
247
|
(el): el is PcbHole =>
|
|
@@ -241,7 +249,13 @@ export class CircuitToCanvasDrawer {
|
|
|
241
249
|
)
|
|
242
250
|
const drawablePlatedHoles = elements.filter(
|
|
243
251
|
(el): el is PcbPlatedHole =>
|
|
244
|
-
shouldDrawElement(el, options) &&
|
|
252
|
+
shouldDrawElement(el, options) &&
|
|
253
|
+
el.type === "pcb_plated_hole" &&
|
|
254
|
+
isOnCopperLayer(el, layer),
|
|
255
|
+
)
|
|
256
|
+
const drawableCutouts = elements.filter(
|
|
257
|
+
(el): el is PcbCutout =>
|
|
258
|
+
shouldDrawElement(el, options) && el.type === "pcb_cutout",
|
|
245
259
|
)
|
|
246
260
|
|
|
247
261
|
// Step 2: Draw board outline/material (inner board)
|
|
@@ -283,6 +297,7 @@ export class CircuitToCanvasDrawer {
|
|
|
283
297
|
holes: drawableHoles,
|
|
284
298
|
platedHoles: drawablePlatedHoles,
|
|
285
299
|
vias: drawableVias,
|
|
300
|
+
cutouts: drawableCutouts,
|
|
286
301
|
})
|
|
287
302
|
}
|
|
288
303
|
|
|
@@ -327,7 +342,10 @@ export class CircuitToCanvasDrawer {
|
|
|
327
342
|
})
|
|
328
343
|
}
|
|
329
344
|
|
|
330
|
-
if (
|
|
345
|
+
if (
|
|
346
|
+
element.type === "pcb_plated_hole" &&
|
|
347
|
+
isOnCopperLayer(element, layer)
|
|
348
|
+
) {
|
|
331
349
|
drawPcbPlatedHole({
|
|
332
350
|
ctx: this.ctx,
|
|
333
351
|
hole: element as PcbPlatedHole,
|
|
@@ -339,7 +357,7 @@ export class CircuitToCanvasDrawer {
|
|
|
339
357
|
})
|
|
340
358
|
}
|
|
341
359
|
|
|
342
|
-
if (element.type === "pcb_via") {
|
|
360
|
+
if (element.type === "pcb_via" && isOnCopperLayer(element, layer)) {
|
|
343
361
|
drawPcbVia({
|
|
344
362
|
ctx: this.ctx,
|
|
345
363
|
via: element as PcbVia,
|
|
@@ -469,7 +487,11 @@ export class CircuitToCanvasDrawer {
|
|
|
469
487
|
for (const element of elements) {
|
|
470
488
|
if (!shouldDrawElement(element, options)) continue
|
|
471
489
|
|
|
472
|
-
if (
|
|
490
|
+
if (
|
|
491
|
+
element.type === "pcb_plated_hole" &&
|
|
492
|
+
!renderTopSoldermask &&
|
|
493
|
+
isOnCopperLayer(element, layer)
|
|
494
|
+
) {
|
|
473
495
|
drawPcbPlatedHole({
|
|
474
496
|
ctx: this.ctx,
|
|
475
497
|
hole: element as PcbPlatedHole,
|
|
@@ -483,7 +505,11 @@ export class CircuitToCanvasDrawer {
|
|
|
483
505
|
})
|
|
484
506
|
}
|
|
485
507
|
|
|
486
|
-
if (
|
|
508
|
+
if (
|
|
509
|
+
element.type === "pcb_via" &&
|
|
510
|
+
!renderTopSoldermask &&
|
|
511
|
+
isOnCopperLayer(element, layer)
|
|
512
|
+
) {
|
|
487
513
|
drawPcbVia({
|
|
488
514
|
ctx: this.ctx,
|
|
489
515
|
via: element as PcbVia,
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
PcbCutout,
|
|
3
|
+
PcbHole,
|
|
4
|
+
PcbPlatedHole,
|
|
5
|
+
PcbSmtPad,
|
|
6
|
+
PcbVia,
|
|
7
|
+
} from "circuit-json"
|
|
2
8
|
import type { Matrix } from "transformation-matrix"
|
|
3
9
|
import { applyToPoint } from "transformation-matrix"
|
|
4
10
|
import type { CanvasContext } from "../../types"
|
|
@@ -34,14 +40,21 @@ interface DrillCutoutRect {
|
|
|
34
40
|
y: number
|
|
35
41
|
width: number
|
|
36
42
|
height: number
|
|
43
|
+
radius?: number
|
|
37
44
|
rotation?: number
|
|
38
45
|
}
|
|
39
46
|
|
|
47
|
+
interface DrillCutoutPolygon {
|
|
48
|
+
shape: "polygon"
|
|
49
|
+
points: Array<{ x: number; y: number }>
|
|
50
|
+
}
|
|
51
|
+
|
|
40
52
|
type DrillCutout =
|
|
41
53
|
| DrillCutoutCircle
|
|
42
54
|
| DrillCutoutOval
|
|
43
55
|
| DrillCutoutPill
|
|
44
56
|
| DrillCutoutRect
|
|
57
|
+
| DrillCutoutPolygon
|
|
45
58
|
|
|
46
59
|
interface Bounds {
|
|
47
60
|
minX: number
|
|
@@ -55,6 +68,7 @@ export interface PcbSmtPadDrillCutoutParams {
|
|
|
55
68
|
holes?: PcbHole[]
|
|
56
69
|
platedHoles?: PcbPlatedHole[]
|
|
57
70
|
vias?: PcbVia[]
|
|
71
|
+
cutouts?: PcbCutout[]
|
|
58
72
|
}
|
|
59
73
|
|
|
60
74
|
export function drawPadWithDrillCutouts(params: {
|
|
@@ -87,26 +101,28 @@ export function drawPadWithDrillCutouts(params: {
|
|
|
87
101
|
export function getPadDrillCutouts(
|
|
88
102
|
params: PcbSmtPadDrillCutoutParams,
|
|
89
103
|
): DrillCutout[] {
|
|
90
|
-
const { pad, holes = [], platedHoles = [], vias = [] } = params
|
|
104
|
+
const { pad, holes = [], platedHoles = [], vias = [], cutouts = [] } = params
|
|
91
105
|
const padBounds = getPadBounds(pad)
|
|
92
106
|
if (!padBounds) return []
|
|
93
107
|
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
for (const hole of holes) {
|
|
97
|
-
const cutout = getHoleDrillCutout(hole)
|
|
108
|
+
const drillCutouts: DrillCutout[] = []
|
|
109
|
+
const addIfOverlapping = (cutout: DrillCutout | undefined): void => {
|
|
98
110
|
if (cutout && boundsOverlap(padBounds, getDrillCutoutBounds(cutout))) {
|
|
99
|
-
|
|
111
|
+
drillCutouts.push(cutout)
|
|
100
112
|
}
|
|
101
113
|
}
|
|
102
114
|
|
|
115
|
+
for (const cutout of cutouts) {
|
|
116
|
+
addIfOverlapping(getBoardCutout(cutout))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
for (const hole of holes) {
|
|
120
|
+
addIfOverlapping(getHoleDrillCutout(hole))
|
|
121
|
+
}
|
|
122
|
+
|
|
103
123
|
for (const platedHole of platedHoles) {
|
|
104
124
|
if (!layersIncludePadLayer(platedHole.layers, pad.layer)) continue
|
|
105
|
-
|
|
106
|
-
const cutout = getPlatedHoleDrillCutout(platedHole)
|
|
107
|
-
if (cutout && boundsOverlap(padBounds, getDrillCutoutBounds(cutout))) {
|
|
108
|
-
cutouts.push(cutout)
|
|
109
|
-
}
|
|
125
|
+
addIfOverlapping(getPlatedHoleDrillCutout(platedHole))
|
|
110
126
|
}
|
|
111
127
|
|
|
112
128
|
for (const via of vias) {
|
|
@@ -120,12 +136,10 @@ export function getPadDrillCutouts(
|
|
|
120
136
|
y: via.y,
|
|
121
137
|
radius,
|
|
122
138
|
}
|
|
123
|
-
|
|
124
|
-
cutouts.push(cutout)
|
|
125
|
-
}
|
|
139
|
+
addIfOverlapping(cutout)
|
|
126
140
|
}
|
|
127
141
|
|
|
128
|
-
return
|
|
142
|
+
return drillCutouts
|
|
129
143
|
}
|
|
130
144
|
|
|
131
145
|
function appendPadPath(
|
|
@@ -215,11 +229,17 @@ function appendDrillCutoutPath(
|
|
|
215
229
|
return
|
|
216
230
|
}
|
|
217
231
|
|
|
232
|
+
if (drillCutout.shape === "polygon") {
|
|
233
|
+
appendPolygonPath(ctx, drillCutout.points, realToCanvasMat)
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
|
|
218
237
|
appendRectPath(ctx, {
|
|
219
238
|
x: drillCutout.x,
|
|
220
239
|
y: drillCutout.y,
|
|
221
240
|
width: drillCutout.width,
|
|
222
241
|
height: drillCutout.height,
|
|
242
|
+
radius: drillCutout.radius,
|
|
223
243
|
rotation: drillCutout.rotation,
|
|
224
244
|
realToCanvasMat,
|
|
225
245
|
})
|
|
@@ -429,6 +449,10 @@ function getPadBounds(pad: PcbSmtPad): Bounds | undefined {
|
|
|
429
449
|
}
|
|
430
450
|
|
|
431
451
|
function getDrillCutoutBounds(cutout: DrillCutout): Bounds {
|
|
452
|
+
if (cutout.shape === "polygon") {
|
|
453
|
+
return getPointBounds(cutout.points)
|
|
454
|
+
}
|
|
455
|
+
|
|
432
456
|
if (cutout.shape === "circle") {
|
|
433
457
|
return boundsAroundCenter(cutout.x, cutout.y, cutout.radius, cutout.radius)
|
|
434
458
|
}
|
|
@@ -455,6 +479,35 @@ function getDrillCutoutBounds(cutout: DrillCutout): Bounds {
|
|
|
455
479
|
return boundsAroundCenter(cutout.x, cutout.y, halfWidth, halfHeight)
|
|
456
480
|
}
|
|
457
481
|
|
|
482
|
+
function getBoardCutout(cutout: PcbCutout): DrillCutout | undefined {
|
|
483
|
+
if (cutout.shape === "circle") {
|
|
484
|
+
if (cutout.radius <= 0) return
|
|
485
|
+
return {
|
|
486
|
+
shape: "circle",
|
|
487
|
+
x: cutout.center.x,
|
|
488
|
+
y: cutout.center.y,
|
|
489
|
+
radius: cutout.radius,
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (cutout.shape === "rect") {
|
|
494
|
+
if (cutout.width <= 0 || cutout.height <= 0) return
|
|
495
|
+
return {
|
|
496
|
+
shape: "rect",
|
|
497
|
+
x: cutout.center.x,
|
|
498
|
+
y: cutout.center.y,
|
|
499
|
+
width: cutout.width,
|
|
500
|
+
height: cutout.height,
|
|
501
|
+
radius: cutout.corner_radius,
|
|
502
|
+
rotation: cutout.rotation,
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (cutout.shape === "polygon" && cutout.points.length >= 3) {
|
|
507
|
+
return { shape: "polygon", points: cutout.points }
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
458
511
|
function getPointBounds(points: Array<{ x: number; y: number }>): Bounds {
|
|
459
512
|
let minX = Infinity
|
|
460
513
|
let maxX = -Infinity
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PcbPlatedHole } from "circuit-json"
|
|
1
|
+
import type { LayerRef, PcbPlatedHole } from "circuit-json"
|
|
2
2
|
import type { Matrix } from "transformation-matrix"
|
|
3
3
|
import { drawCircle } from "../shapes/circle"
|
|
4
4
|
import { drawOval } from "../shapes/oval"
|
|
@@ -15,7 +15,7 @@ export interface DrawPcbPlatedHoleParams {
|
|
|
15
15
|
colorMap: PcbColorMap
|
|
16
16
|
soldermaskMargin?: number
|
|
17
17
|
drawSoldermask?: boolean
|
|
18
|
-
layer?:
|
|
18
|
+
layer?: LayerRef
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export function drawPcbPlatedHole(params: DrawPcbPlatedHoleParams): void {
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
PcbCutout,
|
|
3
|
+
PcbHole,
|
|
4
|
+
PcbPlatedHole,
|
|
5
|
+
PcbSmtPad,
|
|
6
|
+
PcbVia,
|
|
7
|
+
} from "circuit-json"
|
|
2
8
|
import type { Matrix } from "transformation-matrix"
|
|
3
|
-
import type { PcbColorMap, CanvasContext } from "../types"
|
|
4
9
|
import { drawCircle } from "../shapes/circle"
|
|
5
|
-
import { drawRect } from "../shapes/rect"
|
|
6
10
|
import { drawPill } from "../shapes/pill"
|
|
7
11
|
import { drawPolygon } from "../shapes/polygon"
|
|
12
|
+
import { drawRect } from "../shapes/rect"
|
|
13
|
+
import type { CanvasContext, PcbColorMap } from "../types"
|
|
8
14
|
import {
|
|
9
15
|
drawPadWithDrillCutouts,
|
|
10
16
|
getPadDrillCutouts,
|
|
@@ -18,6 +24,7 @@ export interface DrawPcbSmtPadParams {
|
|
|
18
24
|
holes?: PcbHole[]
|
|
19
25
|
platedHoles?: PcbPlatedHole[]
|
|
20
26
|
vias?: PcbVia[]
|
|
27
|
+
cutouts?: PcbCutout[]
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
function layerToColor(layer: string, colorMap: PcbColorMap): string {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PCBVia } from "circuit-json"
|
|
1
|
+
import type { LayerRef, PCBVia } from "circuit-json"
|
|
2
2
|
import type { Matrix } from "transformation-matrix"
|
|
3
3
|
import { drawCircle } from "../shapes/circle"
|
|
4
4
|
import type { CanvasContext, PcbColorMap } from "../types"
|
|
@@ -8,7 +8,7 @@ export interface DrawPcbViaParams {
|
|
|
8
8
|
via: PCBVia
|
|
9
9
|
realToCanvasMat: Matrix
|
|
10
10
|
colorMap: PcbColorMap
|
|
11
|
-
layer?:
|
|
11
|
+
layer?: LayerRef
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export function drawPcbVia(params: DrawPcbViaParams): void {
|
package/lib/drawer/types.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { LayerRef } from "circuit-json"
|
|
1
2
|
import type { Matrix } from "transformation-matrix"
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -56,15 +57,7 @@ export interface CanvasContext {
|
|
|
56
57
|
textAlign: "start" | "end" | "left" | "right" | "center"
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
export type CopperLayerName =
|
|
60
|
-
| "top"
|
|
61
|
-
| "bottom"
|
|
62
|
-
| "inner1"
|
|
63
|
-
| "inner2"
|
|
64
|
-
| "inner3"
|
|
65
|
-
| "inner4"
|
|
66
|
-
| "inner5"
|
|
67
|
-
| "inner6"
|
|
60
|
+
export type CopperLayerName = LayerRef
|
|
68
61
|
|
|
69
62
|
export type CopperColorMap = Record<CopperLayerName, string> & {
|
|
70
63
|
[layer: string]: string
|
|
@@ -115,6 +108,8 @@ export const DEFAULT_PCB_COLOR_MAP: PcbColorMap = {
|
|
|
115
108
|
inner4: "rgb(64, 224, 208)",
|
|
116
109
|
inner5: "rgb(138, 43, 226)",
|
|
117
110
|
inner6: "rgb(255, 105, 180)",
|
|
111
|
+
inner7: "rgb(232, 178, 167)",
|
|
112
|
+
inner8: "rgb(242, 237, 161)",
|
|
118
113
|
bottom: "rgb(77, 127, 196)",
|
|
119
114
|
},
|
|
120
115
|
copperPour: {
|
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.
|
|
4
|
+
"version": "0.0.112",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"build": "tsup-node ./lib/index.ts --format esm --dts",
|
|
@@ -13,13 +13,14 @@
|
|
|
13
13
|
"@napi-rs/canvas": "^0.1.84",
|
|
14
14
|
"@resvg/resvg-js": "^2.6.2",
|
|
15
15
|
"@tscircuit/alphabet": "^0.0.23",
|
|
16
|
-
"@tscircuit/circuit-json-util": "^0.0.
|
|
16
|
+
"@tscircuit/circuit-json-util": "^0.0.99",
|
|
17
17
|
"@tscircuit/math-utils": "^0.0.36",
|
|
18
18
|
"@types/bun": "latest",
|
|
19
19
|
"bun-match-svg": "^0.0.14",
|
|
20
|
-
"circuit-json": "^0.0.
|
|
20
|
+
"circuit-json": "^0.0.450",
|
|
21
21
|
"circuit-json-to-connectivity-map": "^0.0.23",
|
|
22
|
-
"circuit-to-svg": "^0.0.
|
|
22
|
+
"circuit-to-svg": "^0.0.390",
|
|
23
|
+
"format-si-unit": "^0.0.7",
|
|
23
24
|
"looks-same": "^10.0.1",
|
|
24
25
|
"schematic-symbols": "^0.0.202",
|
|
25
26
|
"tsup": "^8.5.1"
|
|
Binary file
|
|
Binary file
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { expect, test } from "bun:test"
|
|
2
2
|
import { createCanvas } from "@napi-rs/canvas"
|
|
3
|
-
import type { AnyCircuitElement } from "circuit-json"
|
|
3
|
+
import type { AnyCircuitElement, LayerRef, PcbRenderLayer } from "circuit-json"
|
|
4
4
|
import { CircuitToCanvasDrawer } from "../../lib/drawer"
|
|
5
5
|
|
|
6
|
-
function renderCopperPourScene(
|
|
6
|
+
function renderCopperPourScene(
|
|
7
|
+
elements: AnyCircuitElement[],
|
|
8
|
+
layer: LayerRef,
|
|
9
|
+
): Buffer {
|
|
7
10
|
const canvas = createCanvas(100, 100)
|
|
8
11
|
const ctx = canvas.getContext("2d")
|
|
9
12
|
const drawer = new CircuitToCanvasDrawer(ctx)
|
|
@@ -11,7 +14,9 @@ function renderCopperPourScene(elements: AnyCircuitElement[]): Buffer {
|
|
|
11
14
|
ctx.fillStyle = "#1a1a1a"
|
|
12
15
|
ctx.fillRect(0, 0, 100, 100)
|
|
13
16
|
|
|
14
|
-
drawer.drawElements(elements
|
|
17
|
+
drawer.drawElements(elements, {
|
|
18
|
+
layers: [`${layer}_copper` as PcbRenderLayer],
|
|
19
|
+
})
|
|
15
20
|
|
|
16
21
|
return canvas.toBuffer("image/png")
|
|
17
22
|
}
|
|
@@ -23,6 +28,8 @@ const INNER_LAYERS = [
|
|
|
23
28
|
"inner4",
|
|
24
29
|
"inner5",
|
|
25
30
|
"inner6",
|
|
31
|
+
"inner7",
|
|
32
|
+
"inner8",
|
|
26
33
|
] as const
|
|
27
34
|
|
|
28
35
|
function createCopperPourWithTraceScene(
|
|
@@ -78,9 +85,10 @@ for (const layer of INNER_LAYERS) {
|
|
|
78
85
|
test(`draw ${layer} copper pour with trace`, async () => {
|
|
79
86
|
const elements = createCopperPourWithTraceScene(layer)
|
|
80
87
|
|
|
81
|
-
const withTrace = renderCopperPourScene(elements)
|
|
88
|
+
const withTrace = renderCopperPourScene(elements, layer)
|
|
82
89
|
const withoutTrace = renderCopperPourScene(
|
|
83
90
|
elements.filter((element) => element.type !== "pcb_trace"),
|
|
91
|
+
layer,
|
|
84
92
|
)
|
|
85
93
|
|
|
86
94
|
expect(Buffer.compare(withTrace, withoutTrace)).not.toBe(0)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import { createCanvas } from "@napi-rs/canvas"
|
|
3
|
+
import type { PcbCutout, PcbSmtPad } from "circuit-json"
|
|
4
|
+
import { CircuitToCanvasDrawer } from "../../lib/drawer"
|
|
5
|
+
|
|
6
|
+
test("cutout removes overlapping SMT pad copper", async () => {
|
|
7
|
+
const scale = 4
|
|
8
|
+
const canvas = createCanvas(100 * scale, 100 * scale)
|
|
9
|
+
const ctx = canvas.getContext("2d")
|
|
10
|
+
ctx.scale(scale, scale)
|
|
11
|
+
const drawer = new CircuitToCanvasDrawer(ctx)
|
|
12
|
+
drawer.configure({
|
|
13
|
+
colorOverrides: { drill: "rgba(0,0,0,0)" },
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const pad: PcbSmtPad = {
|
|
17
|
+
type: "pcb_smtpad",
|
|
18
|
+
pcb_smtpad_id: "pad1",
|
|
19
|
+
shape: "rect",
|
|
20
|
+
layer: "top",
|
|
21
|
+
x: 50,
|
|
22
|
+
y: 50,
|
|
23
|
+
width: 40,
|
|
24
|
+
height: 20,
|
|
25
|
+
}
|
|
26
|
+
const cutout: PcbCutout = {
|
|
27
|
+
type: "pcb_cutout",
|
|
28
|
+
pcb_cutout_id: "cutout1",
|
|
29
|
+
shape: "circle",
|
|
30
|
+
center: { x: 50, y: 50 },
|
|
31
|
+
radius: 5,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
drawer.drawElements([pad, cutout])
|
|
35
|
+
|
|
36
|
+
await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
|
|
37
|
+
import.meta.path,
|
|
38
|
+
)
|
|
39
|
+
})
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import { createCanvas } from "@napi-rs/canvas"
|
|
3
|
+
import type { PcbRenderLayer, PcbVia } from "circuit-json"
|
|
4
|
+
import { CircuitToCanvasDrawer } from "../../lib/drawer"
|
|
5
|
+
|
|
6
|
+
const renderViaLayer = (via: PcbVia | undefined, layer: PcbRenderLayer) => {
|
|
7
|
+
const canvas = createCanvas(100, 100)
|
|
8
|
+
const ctx = canvas.getContext("2d")
|
|
9
|
+
const drawer = new CircuitToCanvasDrawer(ctx)
|
|
10
|
+
|
|
11
|
+
ctx.fillStyle = "#1a1a1a"
|
|
12
|
+
ctx.fillRect(0, 0, 100, 100)
|
|
13
|
+
drawer.drawElements(via ? [via] : [], { layers: [layer] })
|
|
14
|
+
|
|
15
|
+
return canvas.toBuffer("image/png")
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
test("draw buried via only on layers in its span", async () => {
|
|
19
|
+
const via: PcbVia = {
|
|
20
|
+
type: "pcb_via",
|
|
21
|
+
pcb_via_id: "via_inner8",
|
|
22
|
+
x: 50,
|
|
23
|
+
y: 50,
|
|
24
|
+
outer_diameter: 40,
|
|
25
|
+
hole_diameter: 20,
|
|
26
|
+
layers: ["inner7", "inner8"],
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const inner8 = renderViaLayer(via, "inner8_copper")
|
|
30
|
+
const inner6 = renderViaLayer(via, "inner6_copper")
|
|
31
|
+
const blank = renderViaLayer(undefined, "inner6_copper")
|
|
32
|
+
|
|
33
|
+
expect(Buffer.compare(inner8, blank)).not.toBe(0)
|
|
34
|
+
expect(Buffer.compare(inner6, blank)).toBe(0)
|
|
35
|
+
await expect(inner8).toMatchPngSnapshot(import.meta.path)
|
|
36
|
+
})
|