diff --git a/lib/components/base-components/NormalComponent/NormalComponent.ts b/lib/components/base-components/NormalComponent/NormalComponent.ts index c1ab638d0..3558cc26b 100644 --- a/lib/components/base-components/NormalComponent/NormalComponent.ts +++ b/lib/components/base-components/NormalComponent/NormalComponent.ts @@ -891,10 +891,13 @@ export class NormalComponent< subcircuit_id: subcircuit.subcircuit_id ?? undefined, do_not_place: props.doNotPlace ?? false, obstructs_within_bounds: props.obstructsWithinBounds ?? true, + ...(props.shouldBeOnEdgeOfBoard + ? { should_be_on_edge_of_board: props.shouldBeOnEdgeOfBoard } + : {}), metadata: props.kicadFootprintMetadata ? { kicad_footprint: props.kicadFootprintMetadata } : undefined, - }) + } as any) const footprint = this.resolveFootprint() diff --git a/lib/components/normal-components/Chip.ts b/lib/components/normal-components/Chip.ts index fa9972406..224620f48 100644 --- a/lib/components/normal-components/Chip.ts +++ b/lib/components/normal-components/Chip.ts @@ -150,11 +150,14 @@ export class Chip extends NormalComponent< subcircuit_id: this.getSubcircuit().subcircuit_id ?? undefined, do_not_place: props.doNotPlace ?? false, obstructs_within_bounds: props.obstructsWithinBounds ?? true, + ...(props.shouldBeOnEdgeOfBoard + ? { should_be_on_edge_of_board: props.shouldBeOnEdgeOfBoard } + : {}), is_allowed_to_be_off_board: props.allowOffBoard ?? false, metadata: props.kicadFootprintMetadata ? { kicad_footprint: props.kicadFootprintMetadata } : undefined, - }) + } as any) this.pcb_component_id = pcb_component.pcb_component_id } diff --git a/lib/components/normal-components/Jumper.ts b/lib/components/normal-components/Jumper.ts index 4f1367903..ed0bea5e2 100644 --- a/lib/components/normal-components/Jumper.ts +++ b/lib/components/normal-components/Jumper.ts @@ -83,10 +83,13 @@ export class Jumper extends NormalComponent< subcircuit_id: this.getSubcircuit().subcircuit_id ?? undefined, do_not_place: props.doNotPlace ?? false, obstructs_within_bounds: props.obstructsWithinBounds ?? true, + ...(props.shouldBeOnEdgeOfBoard + ? { should_be_on_edge_of_board: props.shouldBeOnEdgeOfBoard } + : {}), metadata: props.kicadFootprintMetadata ? { kicad_footprint: props.kicadFootprintMetadata } : undefined, - }) + } as any) this.pcb_component_id = pcb_component.pcb_component_id } diff --git a/lib/components/normal-components/SolderJumper.ts b/lib/components/normal-components/SolderJumper.ts index 2819942cb..7318122ba 100644 --- a/lib/components/normal-components/SolderJumper.ts +++ b/lib/components/normal-components/SolderJumper.ts @@ -232,10 +232,13 @@ export class SolderJumper< subcircuit_id: this.getSubcircuit().subcircuit_id ?? undefined, do_not_place: props.doNotPlace ?? false, obstructs_within_bounds: props.obstructsWithinBounds ?? true, + ...(props.shouldBeOnEdgeOfBoard + ? { should_be_on_edge_of_board: props.shouldBeOnEdgeOfBoard } + : {}), metadata: props.kicadFootprintMetadata ? { kicad_footprint: props.kicadFootprintMetadata } : undefined, - }) + } as any) this.pcb_component_id = pcb_component.pcb_component_id } diff --git a/lib/components/primitive-components/Group/Group_doInitialPcbLayoutPack/Group_doInitialPcbLayoutPack.ts b/lib/components/primitive-components/Group/Group_doInitialPcbLayoutPack/Group_doInitialPcbLayoutPack.ts index 48d95a539..519c36794 100644 --- a/lib/components/primitive-components/Group/Group_doInitialPcbLayoutPack/Group_doInitialPcbLayoutPack.ts +++ b/lib/components/primitive-components/Group/Group_doInitialPcbLayoutPack/Group_doInitialPcbLayoutPack.ts @@ -113,7 +113,7 @@ export const Group_doInitialPcbLayoutPack = (group: Group) => { } // Keep all circuit elements; static components will remain fixed during packing - const filteredCircuitJson = db.toArray() + const filteredCircuitJson = db.toArray() as any // Calculate bounds if width and height are specified let bounds: diff --git a/tests/features/pcb-pack-layout/edge-boundary-placement.test.tsx b/tests/features/pcb-pack-layout/edge-boundary-placement.test.tsx new file mode 100644 index 000000000..e04c94e5d --- /dev/null +++ b/tests/features/pcb-pack-layout/edge-boundary-placement.test.tsx @@ -0,0 +1,94 @@ +import { test, expect } from "bun:test" +import { RootCircuit } from "lib/RootCircuit" + +test("pcbPack places chip on board edge when shouldBeOnEdgeOfBoard is true", async () => { + const circuit = new RootCircuit() + + circuit.add( + + + + , + ) + + await circuit.renderUntilSettled() + + const pcbComponents = circuit.db.pcb_component.list() + const sourceComponents = circuit.db.source_component.list() + const smtpads = circuit.db.pcb_smtpad.list() + + console.log("SMTPADS:") + console.log(JSON.stringify(smtpads, null, 2)) + + const u1 = pcbComponents.find((c) => { + const sourceComp = sourceComponents.find( + (sc) => sc.source_component_id === c.source_component_id, + ) + return sourceComp?.name === "U1" + }) + + expect(u1).toBeDefined() + if (!u1) return + + // Find courtyard outlines of U1 + const u1Courtyards = circuit.db.pcb_courtyard_outline + .list() + .filter((c) => c.pcb_component_id === u1.pcb_component_id) + + let minBoundaryX = Infinity, + maxBoundaryX = -Infinity + let minBoundaryY = Infinity, + maxBoundaryY = -Infinity + + if (u1Courtyards.length > 0) { + for (const cy of u1Courtyards) { + for (const pt of cy.outline) { + minBoundaryX = Math.min(minBoundaryX, pt.x) + maxBoundaryX = Math.max(maxBoundaryX, pt.x) + minBoundaryY = Math.min(minBoundaryY, pt.y) + maxBoundaryY = Math.max(maxBoundaryY, pt.y) + } + } + } else { + // Fallback to pads if no courtyard outline exists + const u1Pads = smtpads.filter( + (p) => p.pcb_component_id === u1.pcb_component_id, + ) + for (const pad of u1Pads) { + const p = pad as any + const w = p.shape === "rect" ? p.width : p.radius * 2 + const h = p.shape === "rect" ? p.height : p.radius * 2 + minBoundaryX = Math.min(minBoundaryX, p.x - w / 2) + maxBoundaryX = Math.max(maxBoundaryX, p.x + w / 2) + minBoundaryY = Math.min(minBoundaryY, p.y - h / 2) + maxBoundaryY = Math.max(maxBoundaryY, p.y + h / 2) + } + } + + console.log("Calculated boundary absolute bounds for U1:", { + minBoundaryX, + maxBoundaryX, + minBoundaryY, + maxBoundaryY, + width: maxBoundaryX - minBoundaryX, + height: maxBoundaryY - minBoundaryY, + }) + + const distToLeft = Math.abs(minBoundaryX - -20) + const distToRight = Math.abs(maxBoundaryX - 20) + const distToBottom = Math.abs(minBoundaryY - -20) + const distToTop = Math.abs(maxBoundaryY - 20) + + const minDistance = Math.min(distToLeft, distToRight, distToBottom, distToTop) + + console.log("Distance from absolute bounds to board edges:", { + distToLeft, + distToRight, + distToBottom, + distToTop, + minDistance, + }) + + // The component courtyard/pad boundary edge must be extremely close to the board boundary (within 0.1 mm) + expect(minDistance).toBeLessThan(0.1) +})