diff --git a/lib/components/primitive-components/PlatedHole.ts b/lib/components/primitive-components/PlatedHole.ts index f7cba88dc..e8342a15d 100644 --- a/lib/components/primitive-components/PlatedHole.ts +++ b/lib/components/primitive-components/PlatedHole.ts @@ -7,6 +7,7 @@ import type { PcbHoleCircularWithRectPad, PcbHolePillWithRectPad, PcbHoleRotatedPillWithRectPad, + PcbHoleWithPolygonPad, } from "circuit-json" export class PlatedHole extends PrimitiveComponent { @@ -40,6 +41,25 @@ export class PlatedHole extends PrimitiveComponent { if (props.shape === "pill_hole_with_rect_pad") { return { width: props.rectPadWidth, height: props.rectPadHeight } } + if (props.shape === "hole_with_polygon_pad") { + // Calculate bounding box from pad outline + if (!props.padOutline || props.padOutline.length === 0) { + throw new Error( + "padOutline is required for hole_with_polygon_pad shape", + ) + } + const xs = props.padOutline.map((p) => + typeof p.x === "number" ? p.x : parseFloat(String(p.x)), + ) + const ys = props.padOutline.map((p) => + typeof p.y === "number" ? p.y : parseFloat(String(p.y)), + ) + const minX = Math.min(...xs) + const maxX = Math.max(...xs) + const minY = Math.min(...ys) + const maxY = Math.max(...ys) + return { width: maxX - minX, height: maxY - minY } + } throw new Error( `getPcbSize for shape "${(props as any).shape}" not implemented for ${this.componentName}`, ) @@ -252,6 +272,44 @@ export class PlatedHole extends PrimitiveComponent { pcb_group_id: this.getGroup()?.pcb_group_id ?? undefined, } as PcbHolePillWithRectPad) this.pcb_plated_hole_id = pcb_plated_hole.pcb_plated_hole_id + } else if (props.shape === "hole_with_polygon_pad") { + // Pad outline points are relative to the hole position (x, y) + const padOutline = (props.padOutline || []).map((point) => { + const x = + typeof point.x === "number" ? point.x : parseFloat(String(point.x)) + const y = + typeof point.y === "number" ? point.y : parseFloat(String(point.y)) + return { + x, + y, + } + }) + + const pcb_plated_hole = db.pcb_plated_hole.insert({ + pcb_component_id, + pcb_port_id: this.matchedPort?.pcb_port_id!, + shape: "hole_with_polygon_pad" as const, + hole_shape: props.holeShape || "circle", + hole_diameter: props.holeDiameter, + hole_width: props.holeWidth, + hole_height: props.holeHeight, + pad_outline: padOutline, + hole_offset_x: + typeof props.holeOffsetX === "number" + ? props.holeOffsetX + : parseFloat(String(props.holeOffsetX || 0)), + hole_offset_y: + typeof props.holeOffsetY === "number" + ? props.holeOffsetY + : parseFloat(String(props.holeOffsetY || 0)), + port_hints: this.getNameAndAliases(), + x: position.x, + y: position.y, + layers: ["top", "bottom"], + subcircuit_id: subcircuit?.subcircuit_id ?? undefined, + pcb_group_id: this.getGroup()?.pcb_group_id ?? undefined, + } as PcbHoleWithPolygonPad) + this.pcb_plated_hole_id = pcb_plated_hole.pcb_plated_hole_id } } diff --git a/lib/utils/createComponentsFromCircuitJson.ts b/lib/utils/createComponentsFromCircuitJson.ts index db6e209e1..7188e3acc 100644 --- a/lib/utils/createComponentsFromCircuitJson.ts +++ b/lib/utils/createComponentsFromCircuitJson.ts @@ -148,6 +148,22 @@ export const createComponentsFromCircuitJson = ( holeOffsetY: elm.hole_offset_y, }), ) + } else if (elm.shape === "hole_with_polygon_pad") { + components.push( + new PlatedHole({ + pcbX: elm.x, + pcbY: elm.y, + shape: "hole_with_polygon_pad", + holeShape: elm.hole_shape || "circle", + holeDiameter: elm.hole_diameter, + holeWidth: elm.hole_width, + holeHeight: elm.hole_height, + padOutline: elm.pad_outline || [], + holeOffsetX: elm.hole_offset_x, + holeOffsetY: elm.hole_offset_y, + portHints: elm.port_hints, + }), + ) } } else if (elm.type === "pcb_keepout" && elm.shape === "circle") { components.push( diff --git a/lib/utils/obstacles/getObstaclesFromCircuitJson.ts b/lib/utils/obstacles/getObstaclesFromCircuitJson.ts index 08fec1da0..075ee8353 100644 --- a/lib/utils/obstacles/getObstaclesFromCircuitJson.ts +++ b/lib/utils/obstacles/getObstaclesFromCircuitJson.ts @@ -237,6 +237,34 @@ export const getObstaclesFromCircuitJson = ( height: element.outer_height, connectedTo: withNetId([element.pcb_plated_hole_id]), }) + } else if (element.shape === "hole_with_polygon_pad") { + // Calculate bounding box from pad outline + if ( + "pad_outline" in element && + element.pad_outline && + element.pad_outline.length > 0 + ) { + const xs = element.pad_outline.map((p) => element.x + p.x) + const ys = element.pad_outline.map((p) => element.y + p.y) + const minX = Math.min(...xs) + const maxX = Math.max(...xs) + const minY = Math.min(...ys) + const maxY = Math.max(...ys) + const centerX = (minX + maxX) / 2 + const centerY = (minY + maxY) / 2 + obstacles.push({ + // @ts-ignore + type: "rect", + layers: EVERY_LAYER, + center: { + x: centerX, + y: centerY, + }, + width: maxX - minX, + height: maxY - minY, + connectedTo: withNetId([element.pcb_plated_hole_id]), + }) + } } } else if (element.type === "pcb_trace") { const traceObstacles = getObstaclesFromRoute( diff --git a/lib/utils/packing/getObstacleDimensionsFromElement.ts b/lib/utils/packing/getObstacleDimensionsFromElement.ts index bdbe953c9..542c105f2 100644 --- a/lib/utils/packing/getObstacleDimensionsFromElement.ts +++ b/lib/utils/packing/getObstacleDimensionsFromElement.ts @@ -81,6 +81,26 @@ export function getObstacleDimensionsFromPlatedHole( height: hole.outer_height, } + case "hole_with_polygon_pad": + // Calculate bounding box from pad outline + if ( + !("pad_outline" in hole) || + !hole.pad_outline || + hole.pad_outline.length === 0 + ) { + return null + } + const xs = hole.pad_outline.map((p) => p.x) + const ys = hole.pad_outline.map((p) => p.y) + const minX = Math.min(...xs) + const maxX = Math.max(...xs) + const minY = Math.min(...ys) + const maxY = Math.max(...ys) + return { + width: maxX - minX, + height: maxY - minY, + } + default: return null } diff --git a/package.json b/package.json index 6185aebfa..2f435c564 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "@tscircuit/math-utils": "^0.0.29", "@tscircuit/miniflex": "^0.0.4", "@tscircuit/ngspice-spice-engine": "^0.0.2", - "@tscircuit/props": "^0.0.398", + "@tscircuit/props": "^0.0.403", "@tscircuit/schematic-autolayout": "^0.0.6", "@tscircuit/schematic-match-adapt": "^0.0.16", "@tscircuit/schematic-trace-solver": "^v0.0.45", @@ -59,13 +59,13 @@ "bun-match-svg": "0.0.12", "calculate-elbow": "^0.0.12", "chokidar-cli": "^3.0.0", - "circuit-json": "^0.0.307", + "circuit-json": "^0.0.308", "circuit-json-to-bpc": "^0.0.13", "circuit-json-to-connectivity-map": "^0.0.22", "circuit-json-to-gltf": "^0.0.31", "circuit-json-to-simple-3d": "^0.0.9", "circuit-json-to-spice": "^0.0.16", - "circuit-to-svg": "^0.0.265", + "circuit-to-svg": "^0.0.269", "concurrently": "^9.1.2", "connectivity-map": "^1.0.0", "debug": "^4.3.6", diff --git a/tests/components/primitive-components/__snapshots__/pcb-hole-with-polygon-pad-pcb.snap.svg b/tests/components/primitive-components/__snapshots__/pcb-hole-with-polygon-pad-pcb.snap.svg new file mode 100644 index 000000000..ea4446598 --- /dev/null +++ b/tests/components/primitive-components/__snapshots__/pcb-hole-with-polygon-pad-pcb.snap.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/components/primitive-components/pcb-hole-with-polygon-pad.test.tsx b/tests/components/primitive-components/pcb-hole-with-polygon-pad.test.tsx new file mode 100644 index 000000000..ba2d3798f --- /dev/null +++ b/tests/components/primitive-components/pcb-hole-with-polygon-pad.test.tsx @@ -0,0 +1,33 @@ +import { test, expect } from "bun:test" +import { getTestFixture } from "tests/fixtures/get-test-fixture" + +test("pcb plated hole with polygon pad", async () => { + const { circuit } = getTestFixture() + + const footprint = ( + + + + ) + + circuit.add( + + + , + ) + + circuit.render() + expect(circuit).toMatchPcbSnapshot(import.meta.path) +})