diff --git a/bun.lock b/bun.lock
index 9b00dc20..390e92a8 100644
--- a/bun.lock
+++ b/bun.lock
@@ -8,7 +8,7 @@
"@tscircuit/alphabet": "^0.0.8",
"@tscircuit/math-utils": "^0.0.29",
"@vitejs/plugin-react": "^5.0.2",
- "circuit-json": "^0.0.321",
+ "circuit-json": "^0.0.325",
"circuit-to-svg": "^0.0.271",
"color": "^4.2.3",
"react-supergrid": "^1.0.10",
@@ -596,7 +596,7 @@
"chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
- "circuit-json": ["circuit-json@0.0.321", "", {}, "sha512-3E6RPr/LJq7i1WV+Xsw5D9TzTs0c7DaO7k9tXRE7Y9J68AAXTZjR8bQB0o/RLm7kbuUEuVNowI4a+F5CGBjpPQ=="],
+ "circuit-json": ["circuit-json@0.0.325", "", {}, "sha512-6RBqf+G2HlyIb4Fi+w94QqTNT2L9LJA828V5T4039QEllFBhy2vORJ9GiyYB6VcfxTf/JSsLl3dfOnL3SLeYqg=="],
"circuit-json-to-bpc": ["circuit-json-to-bpc@0.0.13", "", { "peerDependencies": { "bpc-graph": "*", "circuit-json": "*", "typescript": "^5" } }, "sha512-3wSMtPa6tJkiBQN4tsm7f0Mb7Wp90X2c8dNbULoDVE4mGGoFqP1DXqBlyvvZZl+4SjqznzQQ0EioLe2SCQTOcg=="],
diff --git a/package.json b/package.json
index e8e3bf3d..c8d88e68 100644
--- a/package.json
+++ b/package.json
@@ -53,7 +53,7 @@
"@tscircuit/alphabet": "^0.0.8",
"@tscircuit/math-utils": "^0.0.29",
"@vitejs/plugin-react": "^5.0.2",
- "circuit-json": "^0.0.321",
+ "circuit-json": "^0.0.325",
"circuit-to-svg": "^0.0.271",
"color": "^4.2.3",
"react-supergrid": "^1.0.10",
diff --git a/src/examples/soldermask-margin-smtpad.fixture.tsx b/src/examples/soldermask-margin-smtpad.fixture.tsx
new file mode 100644
index 00000000..095d49c1
--- /dev/null
+++ b/src/examples/soldermask-margin-smtpad.fixture.tsx
@@ -0,0 +1,155 @@
+import { PCBViewer } from "../PCBViewer"
+
+const SoldermaskMarginSmtpadFixture = () => {
+ const circuit: any = [
+ {
+ type: "pcb_board",
+ pcb_board_id: "board0",
+ center: { x: 0, y: 0 },
+ width: 14,
+ height: 10,
+ },
+ // Rectangle with positive margin (mask extends beyond pad)
+ {
+ type: "pcb_smtpad",
+ pcb_smtpad_id: "pad_rect_positive",
+ shape: "rect",
+ layer: "top",
+ x: -4,
+ y: 2,
+ width: 1.6,
+ height: 1.1,
+ is_covered_with_solder_mask: true,
+ soldermask_margin: 0.2,
+ },
+ // Rectangle with negative margin (spacing around copper, copper visible)
+ {
+ type: "pcb_smtpad",
+ pcb_smtpad_id: "pad_rect_negative",
+ shape: "rect",
+ layer: "top",
+ x: -4,
+ y: -2,
+ width: 1.6,
+ height: 1.1,
+ is_covered_with_solder_mask: true,
+ soldermask_margin: -0.15,
+ },
+ // Circle with positive margin
+ {
+ type: "pcb_smtpad",
+ pcb_smtpad_id: "pad_circle_positive",
+ shape: "circle",
+ layer: "top",
+ x: 0,
+ y: 2,
+ radius: 0.75,
+ is_covered_with_solder_mask: true,
+ soldermask_margin: 0.15,
+ },
+ // Circle with negative margin
+ {
+ type: "pcb_smtpad",
+ pcb_smtpad_id: "pad_circle_negative",
+ shape: "circle",
+ layer: "top",
+ x: 0,
+ y: -2,
+ radius: 0.75,
+ is_covered_with_solder_mask: true,
+ soldermask_margin: -0.2,
+ },
+ // Pill with positive margin
+ {
+ type: "pcb_smtpad",
+ pcb_smtpad_id: "pad_pill_positive",
+ shape: "pill",
+ layer: "top",
+ x: 4,
+ y: 2,
+ width: 2.4,
+ height: 1,
+ radius: 0.5,
+ is_covered_with_solder_mask: true,
+ soldermask_margin: 0.1,
+ },
+ // Pill with negative margin
+ {
+ type: "pcb_smtpad",
+ pcb_smtpad_id: "pad_pill_negative",
+ shape: "pill",
+ layer: "top",
+ x: 4,
+ y: -2,
+ width: 2.4,
+ height: 1,
+ radius: 0.5,
+ is_covered_with_solder_mask: true,
+ soldermask_margin: -0.12,
+ },
+ // Silkscreen labels for positive margin pads (top row)
+ {
+ type: "pcb_silkscreen_text",
+ pcb_silkscreen_text_id: "text_rect_pos",
+ layer: "top",
+ anchor_position: { x: -4, y: 3.2 },
+ anchor_alignment: "center",
+ text: "+0.2mm",
+ font_size: 0.4,
+ },
+ {
+ type: "pcb_silkscreen_text",
+ pcb_silkscreen_text_id: "text_circle_pos",
+ layer: "top",
+ anchor_position: { x: 0, y: 3.2 },
+ anchor_alignment: "center",
+ text: "+0.15mm",
+ font_size: 0.4,
+ },
+ {
+ type: "pcb_silkscreen_text",
+ pcb_silkscreen_text_id: "text_pill_pos",
+ layer: "top",
+ anchor_position: { x: 4, y: 3.2 },
+ anchor_alignment: "center",
+ text: "+0.1mm",
+ font_size: 0.4,
+ },
+ // Silkscreen labels for negative margin pads (bottom row)
+ {
+ type: "pcb_silkscreen_text",
+ pcb_silkscreen_text_id: "text_rect_neg",
+ layer: "top",
+ anchor_position: { x: -4, y: -3.2 },
+ anchor_alignment: "center",
+ text: "-0.15mm",
+ font_size: 0.4,
+ },
+ {
+ type: "pcb_silkscreen_text",
+ pcb_silkscreen_text_id: "text_circle_neg",
+ layer: "top",
+ anchor_position: { x: 0, y: -3.2 },
+ anchor_alignment: "center",
+ text: "-0.2mm",
+ font_size: 0.4,
+ },
+ {
+ type: "pcb_silkscreen_text",
+ pcb_silkscreen_text_id: "text_pill_neg",
+ layer: "top",
+ anchor_position: { x: 4, y: -3.2 },
+ anchor_alignment: "center",
+ text: "-0.12mm",
+ font_size: 0.4,
+ },
+ ]
+
+ return (
+
+ )
+}
+
+export default SoldermaskMarginSmtpadFixture
diff --git a/src/lib/Drawer.ts b/src/lib/Drawer.ts
index b3b9498c..2fea1260 100644
--- a/src/lib/Drawer.ts
+++ b/src/lib/Drawer.ts
@@ -112,6 +112,7 @@ export class Drawer {
transform: Matrix
foregroundLayer: string = "top"
lastPoint: { x: number; y: number }
+ _tempCompositeMode?: "source-over" | "destination-out"
constructor(canvasLayerMap: Record) {
this.canvasLayerMap = canvasLayerMap
@@ -574,6 +575,12 @@ export class Drawer {
ctx.fillStyle = "rgba(0,0,0,1)"
ctx.strokeStyle = "rgba(0,0,0,1)"
}
+
+ // Override compositing mode if temporarily set
+ if (this._tempCompositeMode) {
+ ctx.globalCompositeOperation = this._tempCompositeMode
+ }
+
ctx.font = `${scaleOnly(inverse(transform), fontSize)}px sans-serif`
}
diff --git a/src/lib/convert-element-to-primitive.ts b/src/lib/convert-element-to-primitive.ts
index 97481088..39038af7 100644
--- a/src/lib/convert-element-to-primitive.ts
+++ b/src/lib/convert-element-to-primitive.ts
@@ -17,6 +17,7 @@ import { su } from "@tscircuit/circuit-json-util"
import type { Primitive } from "./types"
import { type Point, getExpandedStroke } from "./util/expand-stroke"
import { distance } from "circuit-json"
+import { color } from "bun"
type MetaData = {
_parent_pcb_component?: any
@@ -254,8 +255,7 @@ export const convertElementToPrimitives = (
case "pcb_smtpad": {
if (element.shape === "rect" || element.shape === "rotated_rect") {
- const { shape, x, y, width, height, layer, rect_border_radius } =
- element
+ const { x, y, width, height, layer, rect_border_radius } = element
const corner_radius = element.corner_radius ?? rect_border_radius ?? 0
const primitives = [
@@ -276,36 +276,170 @@ export const convertElementToPrimitives = (
},
]
- // Add solder mask if enabled
if (element.is_covered_with_solder_mask) {
+ const rawMargin = element.soldermask_margin
const maskLayer =
layer === "bottom"
? "soldermask_with_copper_bottom"
: "soldermask_with_copper_top"
- const maskPrimitive: any = {
- _pcb_drawing_object_id: `rect_${globalPcbDrawingObjectCount++}`,
- pcb_drawing_type: "rect" as const,
- x,
- y,
- w: width,
- h: height,
- layer: maskLayer,
- _element: element,
- _parent_pcb_component,
- _parent_source_component,
- _source_port,
- ccw_rotation: (element as any).ccw_rotation,
- roundness: corner_radius,
+
+ if (rawMargin === undefined || rawMargin === null) {
+ const soldermask_margin = 0
+
+ const openingWidth = Math.max(0.01, width + 2 * soldermask_margin)
+ const openingHeight = Math.max(0.01, height + 2 * soldermask_margin)
+
+ const openingPrimitive: any = {
+ _pcb_drawing_object_id: `rect_${globalPcbDrawingObjectCount++}`,
+ pcb_drawing_type: "rect" as const,
+ x,
+ y,
+ w: openingWidth,
+ h: openingHeight,
+ layer: maskLayer,
+ _element: element,
+ _parent_pcb_component,
+ _parent_source_component,
+ _source_port,
+ ccw_rotation: (element as any).ccw_rotation,
+ roundness: corner_radius,
+ ...("solder_mask_color" in element && element.solder_mask_color
+ ? { color: element.solder_mask_color }
+ : {}),
+ }
+ primitives.push(openingPrimitive)
+
+ const maskCoverageLayer =
+ layer === "bottom" ? "soldermask_bottom" : "soldermask_top"
+
+ const fullMaskCoverage: any = {
+ _pcb_drawing_object_id: `rect_${globalPcbDrawingObjectCount++}`,
+ pcb_drawing_type: "rect" as const,
+ x,
+ y,
+ w: width,
+ h: height,
+ layer: maskCoverageLayer,
+ _element: element,
+ _parent_pcb_component,
+ _parent_source_component,
+ _source_port,
+ ccw_rotation: (element as any).ccw_rotation,
+ roundness: corner_radius,
+ }
+ primitives.push(fullMaskCoverage)
+
+ const cutoutOpening: any = {
+ _pcb_drawing_object_id: `rect_${globalPcbDrawingObjectCount++}`,
+ pcb_drawing_type: "rect" as const,
+ x,
+ y,
+ w: openingWidth,
+ h: openingHeight,
+ layer: maskCoverageLayer,
+ _element: element,
+ _parent_pcb_component,
+ _parent_source_component,
+ _source_port,
+ ccw_rotation: (element as any).ccw_rotation,
+ roundness: corner_radius,
+ composite_mode: "destination-out",
+ }
+ primitives.push(cutoutOpening)
+
+ return primitives
}
- if ((element as any).solder_mask_color) {
- maskPrimitive.color = (element as any).solder_mask_color
+
+ const soldermask_margin = rawMargin
+
+ if (soldermask_margin === 0) {
+ return primitives
+ }
+
+ if (soldermask_margin > 0) {
+ const marginRing: any = {
+ _pcb_drawing_object_id: `rect_${globalPcbDrawingObjectCount++}`,
+ pcb_drawing_type: "rect" as const,
+ x,
+ y,
+ w: width + 2 * soldermask_margin,
+ h: height + 2 * soldermask_margin,
+ layer: maskLayer,
+ _element: element,
+ _parent_pcb_component,
+ _parent_source_component,
+ _source_port,
+ ccw_rotation: (element as any).ccw_rotation,
+ roundness: corner_radius,
+ color: "rgb(201, 162, 110)",
+ }
+ primitives.push(marginRing)
+
+ const cutout: any = {
+ _pcb_drawing_object_id: `rect_${globalPcbDrawingObjectCount++}`,
+ pcb_drawing_type: "rect" as const,
+ x,
+ y,
+ w: width,
+ h: height,
+ layer: maskLayer,
+ _element: element,
+ _parent_pcb_component,
+ _parent_source_component,
+ _source_port,
+ ccw_rotation: (element as any).ccw_rotation,
+ roundness: corner_radius,
+ composite_mode: "destination-out",
+ }
+ primitives.push(cutout)
+ } else {
+ const openingWidth = Math.max(0.01, width + 2 * soldermask_margin)
+ const openingHeight = Math.max(0.01, height + 2 * soldermask_margin)
+
+ const innerRingBase: any = {
+ _pcb_drawing_object_id: `rect_${globalPcbDrawingObjectCount++}`,
+ pcb_drawing_type: "rect" as const,
+ x,
+ y,
+ w: width,
+ h: height,
+ layer: maskLayer,
+ _element: element,
+ _parent_pcb_component,
+ _parent_source_component,
+ _source_port,
+ ccw_rotation: (element as any).ccw_rotation,
+ roundness: corner_radius,
+ ...("solder_mask_color" in element && element.solder_mask_color
+ ? { color: element.solder_mask_color }
+ : {}),
+ }
+ primitives.push(innerRingBase)
+
+ const innerCutout: any = {
+ _pcb_drawing_object_id: `rect_${globalPcbDrawingObjectCount++}`,
+ pcb_drawing_type: "rect" as const,
+ x,
+ y,
+ w: openingWidth,
+ h: openingHeight,
+ layer: maskLayer,
+ _element: element,
+ _parent_pcb_component,
+ _parent_source_component,
+ _source_port,
+ ccw_rotation: (element as any).ccw_rotation,
+ roundness: corner_radius,
+ composite_mode: "destination-out",
+ }
+ primitives.push(innerCutout)
}
- primitives.push(maskPrimitive)
}
return primitives
} else if (element.shape === "circle") {
const { x, y, radius, layer } = element
+
const primitives = [
{
_pcb_drawing_object_id: `circle_${globalPcbDrawingObjectCount++}`,
@@ -321,33 +455,148 @@ export const convertElementToPrimitives = (
},
]
- // Add solder mask if enabled
if (element.is_covered_with_solder_mask) {
+ const rawMargin = element.soldermask_margin
const maskLayer =
layer === "bottom"
? "soldermask_with_copper_bottom"
: "soldermask_with_copper_top"
- const maskPrimitive: any = {
- _pcb_drawing_object_id: `circle_${globalPcbDrawingObjectCount++}`,
- pcb_drawing_type: "circle" as const,
- x,
- y,
- r: radius,
- layer: maskLayer,
- _element: element,
- _parent_pcb_component,
- _parent_source_component,
- _source_port,
+
+ if (rawMargin === undefined || rawMargin === null) {
+ const soldermask_margin = 0
+
+ const openingRadius = Math.max(0.01, radius + soldermask_margin)
+
+ const openingPrimitive: any = {
+ _pcb_drawing_object_id: `circle_${globalPcbDrawingObjectCount++}`,
+ pcb_drawing_type: "circle" as const,
+ x,
+ y,
+ r: openingRadius,
+ layer: maskLayer,
+ _element: element,
+ _parent_pcb_component,
+ _parent_source_component,
+ _source_port,
+ ...("solder_mask_color" in element && element.solder_mask_color
+ ? { color: element.solder_mask_color }
+ : {}),
+ }
+ primitives.push(openingPrimitive)
+
+ const maskCoverageLayer =
+ layer === "bottom" ? "soldermask_bottom" : "soldermask_top"
+
+ const fullMaskCoverage: any = {
+ _pcb_drawing_object_id: `circle_${globalPcbDrawingObjectCount++}`,
+ pcb_drawing_type: "circle" as const,
+ x,
+ y,
+ r: radius,
+ layer: maskCoverageLayer,
+ _element: element,
+ _parent_pcb_component,
+ _parent_source_component,
+ _source_port,
+ }
+ primitives.push(fullMaskCoverage)
+
+ const cutoutOpening: any = {
+ _pcb_drawing_object_id: `circle_${globalPcbDrawingObjectCount++}`,
+ pcb_drawing_type: "circle" as const,
+ x,
+ y,
+ r: openingRadius,
+ layer: maskCoverageLayer,
+ _element: element,
+ _parent_pcb_component,
+ _parent_source_component,
+ _source_port,
+ composite_mode: "destination-out",
+ }
+ primitives.push(cutoutOpening)
+
+ return primitives
}
- if ((element as any).solder_mask_color) {
- maskPrimitive.color = (element as any).solder_mask_color
+
+ const soldermask_margin = rawMargin
+
+ if (soldermask_margin === 0) {
+ // FULL mask
+ return primitives
+ }
+
+ if (soldermask_margin > 0) {
+ const marginRing: any = {
+ _pcb_drawing_object_id: `circle_${globalPcbDrawingObjectCount++}`,
+ pcb_drawing_type: "circle" as const,
+ x,
+ y,
+ r: radius + soldermask_margin,
+ layer: maskLayer,
+ _element: element,
+ _parent_pcb_component,
+ _parent_source_component,
+ _source_port,
+ color: "rgb(201, 162, 110)",
+ }
+ primitives.push(marginRing)
+
+ const cutout: any = {
+ _pcb_drawing_object_id: `circle_${globalPcbDrawingObjectCount++}`,
+ pcb_drawing_type: "circle" as const,
+ x,
+ y,
+ r: radius,
+ layer: maskLayer,
+ _element: element,
+ _parent_pcb_component,
+ _parent_source_component,
+ _source_port,
+ composite_mode: "destination-out",
+ }
+ primitives.push(cutout)
+ } else {
+ const openingRadius = Math.max(0.01, radius + soldermask_margin)
+
+ const innerRingBase: any = {
+ _pcb_drawing_object_id: `circle_${globalPcbDrawingObjectCount++}`,
+ pcb_drawing_type: "circle" as const,
+ x,
+ y,
+ r: radius,
+ layer: maskLayer,
+ _element: element,
+ _parent_pcb_component,
+ _parent_source_component,
+ _source_port,
+ ...("solder_mask_color" in element && element.solder_mask_color
+ ? { color: element.solder_mask_color }
+ : {}),
+ }
+ primitives.push(innerRingBase)
+
+ const innerCutout: any = {
+ _pcb_drawing_object_id: `circle_${globalPcbDrawingObjectCount++}`,
+ pcb_drawing_type: "circle" as const,
+ x,
+ y,
+ r: openingRadius,
+ layer: maskLayer,
+ _element: element,
+ _parent_pcb_component,
+ _parent_source_component,
+ _source_port,
+ composite_mode: "destination-out",
+ }
+ primitives.push(innerCutout)
}
- primitives.push(maskPrimitive)
}
return primitives
} else if (element.shape === "polygon") {
const { layer, points } = element
+
const primitives = [
{
_pcb_drawing_object_id: `polygon_${globalPcbDrawingObjectCount++}`,
@@ -361,12 +610,12 @@ export const convertElementToPrimitives = (
},
]
- // Add solder mask if enabled
if (element.is_covered_with_solder_mask) {
const maskLayer =
layer === "bottom"
? "soldermask_with_copper_bottom"
: "soldermask_with_copper_top"
+
const maskPrimitive: any = {
_pcb_drawing_object_id: `polygon_${globalPcbDrawingObjectCount++}`,
pcb_drawing_type: "polygon" as const,
@@ -376,9 +625,9 @@ export const convertElementToPrimitives = (
_parent_pcb_component,
_parent_source_component,
_source_port,
- }
- if ((element as any).solder_mask_color) {
- maskPrimitive.color = (element as any).solder_mask_color
+ ...("solder_mask_color" in element && element.solder_mask_color
+ ? { color: element.solder_mask_color }
+ : {}),
}
primitives.push(maskPrimitive)
}
@@ -386,6 +635,7 @@ export const convertElementToPrimitives = (
return primitives
} else if (element.shape === "pill" || element.shape === "rotated_pill") {
const { x, y, width, height, layer } = element
+
const primitives = [
{
_pcb_drawing_object_id: `pill_${globalPcbDrawingObjectCount++}`,
@@ -403,36 +653,165 @@ export const convertElementToPrimitives = (
},
]
- // Add solder mask if enabled
if (element.is_covered_with_solder_mask) {
+ const rawMargin = (element as any).soldermask_margin
const maskLayer =
layer === "bottom"
? "soldermask_with_copper_bottom"
: "soldermask_with_copper_top"
- const maskPrimitive: any = {
- _pcb_drawing_object_id: `pill_${globalPcbDrawingObjectCount++}`,
- pcb_drawing_type: "pill" as const,
- x,
- y,
- w: width,
- h: height,
- layer: maskLayer,
- _element: element,
- _parent_pcb_component,
- _parent_source_component,
- _source_port,
- ccw_rotation: (element as PcbSmtPadRotatedPill).ccw_rotation,
+
+ if (rawMargin === undefined || rawMargin === null) {
+ const soldermask_margin = 0
+
+ const openingWidth = Math.max(0.01, width + 2 * soldermask_margin)
+ const openingHeight = Math.max(0.01, height + 2 * soldermask_margin)
+
+ const openingPrimitive: any = {
+ _pcb_drawing_object_id: `pill_${globalPcbDrawingObjectCount++}`,
+ pcb_drawing_type: "pill" as const,
+ x,
+ y,
+ w: openingWidth,
+ h: openingHeight,
+ layer: maskLayer,
+ _element: element,
+ _parent_pcb_component,
+ _parent_source_component,
+ _source_port,
+ ccw_rotation: (element as PcbSmtPadRotatedPill).ccw_rotation,
+ ...("solder_mask_color" in element && element.solder_mask_color
+ ? { color: element.solder_mask_color }
+ : {}),
+ }
+ primitives.push(openingPrimitive)
+
+ const maskCoverageLayer =
+ layer === "bottom" ? "soldermask_bottom" : "soldermask_top"
+
+ const fullMaskCoverage: any = {
+ _pcb_drawing_object_id: `pill_${globalPcbDrawingObjectCount++}`,
+ pcb_drawing_type: "pill" as const,
+ x,
+ y,
+ w: width,
+ h: height,
+ layer: maskCoverageLayer,
+ _element: element,
+ _parent_pcb_component,
+ _parent_source_component,
+ _source_port,
+ ccw_rotation: (element as PcbSmtPadRotatedPill).ccw_rotation,
+ }
+ primitives.push(fullMaskCoverage)
+
+ const cutoutOpening: any = {
+ _pcb_drawing_object_id: `pill_${globalPcbDrawingObjectCount++}`,
+ pcb_drawing_type: "pill" as const,
+ x,
+ y,
+ w: openingWidth,
+ h: openingHeight,
+ layer: maskCoverageLayer,
+ _element: element,
+ _parent_pcb_component,
+ _parent_source_component,
+ _source_port,
+ ccw_rotation: (element as PcbSmtPadRotatedPill).ccw_rotation,
+ composite_mode: "destination-out",
+ }
+ primitives.push(cutoutOpening)
+
+ return primitives
}
- if ((element as any).solder_mask_color) {
- maskPrimitive.color = (element as any).solder_mask_color
+
+ const soldermask_margin = rawMargin
+
+ if (soldermask_margin === 0) {
+ return primitives
+ }
+
+ if (soldermask_margin > 0) {
+ const marginRing: any = {
+ _pcb_drawing_object_id: `pill_${globalPcbDrawingObjectCount++}`,
+ pcb_drawing_type: "pill" as const,
+ x,
+ y,
+ w: width + 2 * soldermask_margin,
+ h: height + 2 * soldermask_margin,
+ layer: maskLayer,
+ _element: element,
+ _parent_pcb_component,
+ _parent_source_component,
+ _source_port,
+ ccw_rotation: (element as PcbSmtPadRotatedPill).ccw_rotation,
+ color: "rgb(201, 162, 110)",
+ }
+ primitives.push(marginRing)
+
+ const cutout: any = {
+ _pcb_drawing_object_id: `pill_${globalPcbDrawingObjectCount++}`,
+ pcb_drawing_type: "pill" as const,
+ x,
+ y,
+ w: width,
+ h: height,
+ layer: maskLayer,
+ _element: element,
+ _parent_pcb_component,
+ _parent_source_component,
+ _source_port,
+ ccw_rotation: (element as PcbSmtPadRotatedPill).ccw_rotation,
+ composite_mode: "destination-out",
+ }
+ primitives.push(cutout)
+ } else {
+ const openingWidth = Math.max(0.01, width + 2 * soldermask_margin)
+ const openingHeight = Math.max(0.01, height + 2 * soldermask_margin)
+
+ const innerRingBase: any = {
+ _pcb_drawing_object_id: `pill_${globalPcbDrawingObjectCount++}`,
+ pcb_drawing_type: "pill" as const,
+ x,
+ y,
+ w: width,
+ h: height,
+ layer: maskLayer,
+ _element: element,
+ _parent_pcb_component,
+ _parent_source_component,
+ _source_port,
+ ccw_rotation: (element as PcbSmtPadRotatedPill).ccw_rotation,
+ ...("solder_mask_color" in element && element.solder_mask_color
+ ? { color: element.solder_mask_color }
+ : {}),
+ }
+ primitives.push(innerRingBase)
+
+ const innerCutout: any = {
+ _pcb_drawing_object_id: `pill_${globalPcbDrawingObjectCount++}`,
+ pcb_drawing_type: "pill" as const,
+ x,
+ y,
+ w: openingWidth,
+ h: openingHeight,
+ layer: maskLayer,
+ _element: element,
+ _parent_pcb_component,
+ _parent_source_component,
+ _source_port,
+ ccw_rotation: (element as PcbSmtPadRotatedPill).ccw_rotation,
+ composite_mode: "destination-out",
+ }
+ primitives.push(innerCutout)
}
- primitives.push(maskPrimitive)
}
return primitives
}
+
return []
}
+
case "pcb_hole": {
if (element.hole_shape === "circle" || !element.hole_shape) {
const { x, y, hole_diameter } = element
diff --git a/src/lib/draw-primitives.ts b/src/lib/draw-primitives.ts
index d497cf09..bede2aa2 100644
--- a/src/lib/draw-primitives.ts
+++ b/src/lib/draw-primitives.ts
@@ -151,6 +151,12 @@ export const drawRect = (drawer: Drawer, rect: Rect) => {
layer: rect.layer,
size: rect.stroke_width,
})
+
+ // Set temporary composite mode if specified
+ if (rect.composite_mode) {
+ drawer._tempCompositeMode = rect.composite_mode
+ }
+
drawer.rect({
x: rect.x,
y: rect.y,
@@ -163,6 +169,11 @@ export const drawRect = (drawer: Drawer, rect: Rect) => {
stroke_width: rect.stroke_width,
roundness: rect.roundness,
})
+
+ // Clear temporary composite mode
+ if (rect.composite_mode) {
+ drawer._tempCompositeMode = undefined
+ }
}
export const drawRotatedRect = (drawer: Drawer, rect: Rect) => {
@@ -171,6 +182,11 @@ export const drawRotatedRect = (drawer: Drawer, rect: Rect) => {
layer: rect.layer,
})
+ // Set temporary composite mode if specified
+ if (rect.composite_mode) {
+ drawer._tempCompositeMode = rect.composite_mode
+ }
+
drawer.rotatedRect(
rect.x,
rect.y,
@@ -180,6 +196,11 @@ export const drawRotatedRect = (drawer: Drawer, rect: Rect) => {
rect.roundness,
rect.mesh_fill,
)
+
+ // Clear temporary composite mode
+ if (rect.composite_mode) {
+ drawer._tempCompositeMode = undefined
+ }
}
export const drawRotatedPill = (drawer: Drawer, pill: Pill) => {
@@ -187,7 +208,18 @@ export const drawRotatedPill = (drawer: Drawer, pill: Pill) => {
color: getColor(pill),
layer: pill.layer,
})
+
+ // Set temporary composite mode if specified
+ if (pill.composite_mode) {
+ drawer._tempCompositeMode = pill.composite_mode
+ }
+
drawer.rotatedPill(pill.x, pill.y, pill.w, pill.h, pill.ccw_rotation!)
+
+ // Clear temporary composite mode
+ if (pill.composite_mode) {
+ drawer._tempCompositeMode = undefined
+ }
}
export const drawCircle = (drawer: Drawer, circle: Circle) => {
@@ -195,7 +227,18 @@ export const drawCircle = (drawer: Drawer, circle: Circle) => {
color: getColor(circle),
layer: circle.layer,
})
+
+ // Set temporary composite mode if specified
+ if (circle.composite_mode) {
+ drawer._tempCompositeMode = circle.composite_mode
+ }
+
drawer.circle(circle.x, circle.y, circle.r, circle.mesh_fill)
+
+ // Clear temporary composite mode
+ if (circle.composite_mode) {
+ drawer._tempCompositeMode = undefined
+ }
}
export const drawOval = (drawer: Drawer, oval: Oval) => {
@@ -211,7 +254,18 @@ export const drawPill = (drawer: Drawer, pill: Pill) => {
color: getColor(pill),
layer: pill.layer,
})
+
+ // Set temporary composite mode if specified
+ if (pill.composite_mode) {
+ drawer._tempCompositeMode = pill.composite_mode
+ }
+
drawer.pill(pill.x, pill.y, pill.w, pill.h)
+
+ // Clear temporary composite mode
+ if (pill.composite_mode) {
+ drawer._tempCompositeMode = undefined
+ }
}
export const drawPolygon = (drawer: Drawer, polygon: Polygon) => {
diff --git a/src/lib/types.ts b/src/lib/types.ts
index 3ddf3a27..bcc6f8b3 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -85,6 +85,7 @@ export interface Rect extends PCBDrawingObject {
is_stroke_dashed?: boolean
ccw_rotation?: number
color?: string
+ composite_mode?: "source-over" | "destination-out"
}
export interface Circle extends PCBDrawingObject {
@@ -93,6 +94,7 @@ export interface Circle extends PCBDrawingObject {
y: number
r: number
mesh_fill?: boolean
+ composite_mode?: "source-over" | "destination-out"
}
export interface Oval extends PCBDrawingObject {
@@ -110,6 +112,7 @@ export interface Pill extends PCBDrawingObject {
w: number
h: number
ccw_rotation?: number
+ composite_mode?: "source-over" | "destination-out"
}
export interface Polygon extends PCBDrawingObject {