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 {