diff --git a/test-assets/sample024-mp1-gap-fill.json b/test-assets/sample024-mp1-gap-fill.json new file mode 100644 index 0000000..678de2a --- /dev/null +++ b/test-assets/sample024-mp1-gap-fill.json @@ -0,0 +1,91 @@ +{ + "source": "@tscircuit/dataset-srj24/samples/sample024.json / MP1.4", + "bounds": { + "minX": 16.57679492, + "maxX": 305.8762, + "minY": 37.96359746, + "maxY": 172.88179491999998 + }, + "boardVoid": { + "layerCount": 8, + "boardVoidRects": [ + { + "x": 16.57679492, + "y": 38.9636, + "width": 1.0000050799999975, + "height": 1.6409653693827266 + } + ] + }, + "meshNodes": [ + { + "capacityMeshNodeId": "cmn_237", + "center": { + "x": 28.774499999999996, + "y": 40.866699499999996 + }, + "width": 11.082599999999996, + "height": 3.8061989999999994, + "layer": "top", + "availableZ": [0, 1, 2, 3, 4, 5, 6, 7] + }, + { + "capacityMeshNodeId": "cmn_5235", + "center": { + "x": 20.733199579999997, + "y": 42.37039786 + }, + "width": 5.000000159999999, + "height": 5.000000159999999, + "layer": "top", + "availableZ": [0, 1, 2, 3, 4, 5, 6, 7], + "_containsObstacle": true, + "_containsTarget": true, + "_connectedTo": [ + "pcb_plated_hole_altium_6144", + "connectivity_net13098", + "pcb_trace_altium_31391", + "pcb_trace_altium_31390", + "pcb_plated_hole_altium_6244", + "pcb_plated_hole_altium_6242", + "pcb_plated_hole_altium_6239", + "pcb_plated_hole_altium_6238", + "pcb_plated_hole_altium_6222", + "pcb_plated_hole_altium_6183", + "pcb_plated_hole_altium_6172", + "pcb_plated_hole_altium_6147", + "pcb_plated_hole_altium_6146", + "pcb_plated_hole_altium_6145", + "source_trace_altium_pcb_307", + "source_port_altium_6144", + "source_port_altium_6145", + "source_port_altium_6146", + "source_port_altium_6147", + "source_port_altium_6172", + "source_port_altium_6183", + "source_port_altium_6222", + "source_port_altium_6238", + "source_port_altium_6239", + "source_port_altium_6242", + "source_port_altium_6244", + "source_net_altium_pcb_307", + "pcb_port_altium_6144", + "pcb_port_altium_6145", + "pcb_port_altium_6146", + "pcb_port_altium_6147", + "pcb_port_altium_6172", + "pcb_port_altium_6183", + "pcb_port_altium_6222", + "pcb_port_altium_6238", + "pcb_port_altium_6239", + "pcb_port_altium_6242", + "pcb_port_altium_6244" + ] + } + ], + "viaCenter": { + "x": 20.404997829999996, + "y": 39.41699889 + }, + "viaDiameter": 0.3 +} diff --git a/tests/__snapshots__/sample024-gap-via-repro.snap.svg b/tests/__snapshots__/sample024-gap-via-repro.snap.svg new file mode 100644 index 0000000..a9eff61 --- /dev/null +++ b/tests/__snapshots__/sample024-gap-via-repro.snap.svg @@ -0,0 +1,44 @@ +sample024 / MP1.4: gap-fill layer connectivity0 through-via regions / 8 gap nodesMP1.4 copperExisting free regionBlue 0.3 mm via fits all 8 layersRouting regionsavailableZ: 0availableZ: 1availableZ: 2availableZ: 3availableZ: 4availableZ: 5availableZ: 6availableZ: 7 \ No newline at end of file diff --git a/tests/sample024-gap-via-repro.test.ts b/tests/sample024-gap-via-repro.test.ts new file mode 100644 index 0000000..adb574c --- /dev/null +++ b/tests/sample024-gap-via-repro.test.ts @@ -0,0 +1,144 @@ +import { expect, test } from "bun:test" +import { + getSvgFromGraphicsObject, + type GraphicsObject, + type Rect, + type Text, +} from "graphics-debug" +import { GapFillSolverPipeline } from "../lib/solvers/GapFillSolver/GapFillSolverPipeline" +import type { CapacityMeshNode } from "../lib/types/capacity-mesh-types" +import { overlaps } from "../lib/utils/rectdiff-geometry" +import fixture from "../test-assets/sample024-mp1-gap-fill.json" + +test("show the real sample024 gap beside MP1.4 and its layer connectivity", async (): Promise => { + const inputNodes: CapacityMeshNode[] = structuredClone(fixture.meshNodes) + const solver = new GapFillSolverPipeline({ + bounds: fixture.bounds, + boardVoid: fixture.boardVoid, + meshNodes: inputNodes, + maxGapFillPasses: 1, + }) + solver.solve() + const radius = fixture.viaDiameter / 2 + const viaRect = { + x: fixture.viaCenter.x - radius, + y: fixture.viaCenter.y - radius, + width: fixture.viaDiameter, + height: fixture.viaDiameter, + } + const gapNodes = solver + .getOutput() + .outputNodes.filter( + (node): boolean => + node.center.x - node.width / 2 <= viaRect.x && + node.center.x + node.width / 2 >= viaRect.x + viaRect.width && + node.center.y - node.height / 2 <= viaRect.y && + node.center.y + node.height / 2 >= viaRect.y + viaRect.height, + ) + const fullStackNodes = gapNodes.filter( + (node): boolean => node.availableZ.length === fixture.boardVoid.layerCount, + ) + expect(solver.solved).toBe(true) + expect(gapNodes.length).toBeGreaterThan(0) + expect( + [...new Set(gapNodes.flatMap((node): number[] => node.availableZ))].sort(), + ).toEqual([0, 1, 2, 3, 4, 5, 6, 7]) + for (const node of inputNodes.filter((node): boolean => + Boolean(node._containsObstacle), + )) { + expect( + overlaps(viaRect, { + x: node.center.x - node.width / 2, + y: node.center.y - node.height / 2, + width: node.width, + height: node.height, + }), + ).toBe(false) + } + for (const outlineRect of fixture.boardVoid.boardVoidRects) { + expect(overlaps(viaRect, outlineRect)).toBe(false) + } + const gap = gapNodes[0]! + const graphics: GraphicsObject = { + coordinateSystem: "cartesian", + rects: [ + ...inputNodes.map( + (node): Rect => ({ + center: node.center, + width: node.width, + height: node.height, + fill: node._containsObstacle + ? "rgba(255,0,0,0.2)" + : "rgba(0,200,200,0.1)", + stroke: node._containsObstacle ? "red" : "#888", + }), + ), + { + center: gap.center, + width: gap.width, + height: gap.height, + fill: "rgba(0,100,255,0.15)", + stroke: "blue", + }, + ...gapNodes.map( + (node, index): Rect => ({ + center: { x: 41, y: 44 - index * 0.8 }, + width: 9, + height: 0.6, + fill: "rgba(0,200,200,0.1)", + stroke: "#666", + }), + ), + ], + circles: [ + { center: fixture.viaCenter, radius, fill: "blue", stroke: "blue" }, + ], + texts: [ + { + x: 17.6, + y: 47, + text: "sample024 / MP1.4: gap-fill layer connectivity", + fontSize: 0.65, + anchorSide: "bottom_left", + }, + { + x: 17.6, + y: 45.8, + text: `${fullStackNodes.length} through-via regions / ${gapNodes.length} gap nodes`, + fontSize: 0.55, + anchorSide: "bottom_left", + }, + { x: 20.7, y: 42.4, text: "MP1.4 copper", fontSize: 0.45 }, + { x: 28.8, y: 41, text: "Existing free region", fontSize: 0.45 }, + { + x: 17.6, + y: 37.8, + text: "Blue 0.3 mm via fits all 8 layers", + fontSize: 0.45, + anchorSide: "bottom_left", + }, + { + x: 36.5, + y: 44.8, + text: "Routing regions", + fontSize: 0.45, + anchorSide: "bottom_left", + }, + ...gapNodes.map( + (node, index): Text => ({ + x: 41, + y: 44 - index * 0.8, + text: `availableZ: ${node.availableZ.join(",")}`, + fontSize: 0.4, + }), + ), + ], + } + await expect( + getSvgFromGraphicsObject(graphics, { + backgroundColor: "white", + svgWidth: 1200, + svgHeight: 550, + }), + ).toMatchSvgSnapshot(import.meta.path) +})