diff --git a/lib/components/primitive-components/CopperPour/CopperPour.ts b/lib/components/primitive-components/CopperPour/CopperPour.ts index 2c301afba..1b4e687be 100644 --- a/lib/components/primitive-components/CopperPour/CopperPour.ts +++ b/lib/components/primitive-components/CopperPour/CopperPour.ts @@ -14,6 +14,7 @@ export type { CopperPourProps } export class CopperPour extends PrimitiveComponent { isPcbPrimitive = true + pcb_copper_pour_ids: string[] = [] get config() { return { @@ -44,6 +45,9 @@ export class CopperPour extends PrimitiveComponent { } const subcircuit = this.getSubcircuit() const circuitJson = db.toArray() + const copperPourCircuitJson = props.unbroken + ? circuitJson.filter((element) => element.type !== "pcb_trace") + : circuitJson const sourceNet = circuitJson.find( (elm) => elm.type === "source_net" && elm.name === net.name, ) as SourceNet | undefined @@ -58,15 +62,18 @@ export class CopperPour extends PrimitiveComponent { "" const clearance = props.clearance ?? 0.2 - const inputProblem = convertCircuitJsonToInputProblem(circuitJson, { - layer: props.layer, - pour_connectivity_key: pourConnectivityKey, - pad_margin: props.padMargin ?? clearance, - trace_margin: props.traceMargin ?? clearance, - board_edge_margin: props.boardEdgeMargin ?? clearance, - cutout_margin: props.cutoutMargin ?? clearance, - outline: props.outline, - }) + const inputProblem = convertCircuitJsonToInputProblem( + copperPourCircuitJson, + { + layer: props.layer, + pour_connectivity_key: pourConnectivityKey, + pad_margin: props.padMargin ?? clearance, + trace_margin: props.traceMargin ?? clearance, + board_edge_margin: props.boardEdgeMargin ?? clearance, + cutout_margin: props.cutoutMargin ?? clearance, + outline: props.outline, + }, + ) const solver = new CopperPourPipelineSolver(inputProblem) @@ -79,6 +86,7 @@ export class CopperPour extends PrimitiveComponent { const { brep_shapes } = solver.getOutput() const coveredWithSolderMask = props.coveredWithSolderMask ?? false + const pcbCopperPourIds: string[] = [] for (const brep_shape of brep_shapes) { const insertedPour = db.pcb_copper_pour.insert({ @@ -89,12 +97,15 @@ export class CopperPour extends PrimitiveComponent { subcircuit_id: subcircuit?.subcircuit_id ?? undefined, covered_with_solder_mask: coveredWithSolderMask, } as PcbCopperPour) + pcbCopperPourIds.push(insertedPour.pcb_copper_pour_id) markTraceSegmentsInsideCopperPour({ db, copperPour: insertedPour, }) } + + this.pcb_copper_pour_ids = pcbCopperPourIds }) } } diff --git a/lib/components/primitive-components/Group/Group.ts b/lib/components/primitive-components/Group/Group.ts index 134e7632c..59b2fd3b5 100644 --- a/lib/components/primitive-components/Group/Group.ts +++ b/lib/components/primitive-components/Group/Group.ts @@ -438,6 +438,7 @@ export class Group = typeof groupProps> minTraceWidth: this.props.autorouter?.minTraceWidth ?? 0.15, nominalTraceWidth: this.props.nominalTraceWidth, subcircuit_id: this.subcircuit_id, + selectableRoot: this.root ?? undefined, }).simpleRouteJson, subcircuit_id: this.subcircuit_id!, }), @@ -566,6 +567,7 @@ export class Group = typeof groupProps> minTraceWidth: this.props.autorouter?.minTraceWidth ?? 0.15, nominalTraceWidth: this.props.nominalTraceWidth, subcircuit_id: this.subcircuit_id, + selectableRoot: this.root ?? undefined, }) // Enable jumpers for auto_jumper preset diff --git a/lib/components/primitive-components/Trace/Trace_doInitialPcbTraceRender.ts b/lib/components/primitive-components/Trace/Trace_doInitialPcbTraceRender.ts index 7459b14db..2b8e08a1a 100644 --- a/lib/components/primitive-components/Trace/Trace_doInitialPcbTraceRender.ts +++ b/lib/components/primitive-components/Trace/Trace_doInitialPcbTraceRender.ts @@ -12,6 +12,7 @@ import type { Port } from "../Port" import type { TraceHint } from "../TraceHint" import { getTraceLength } from "./trace-utils/compute-trace-length" import { getObstaclesFromCircuitJson } from "lib/utils/obstacles/getObstaclesFromCircuitJson" +import { getUnbrokenCopperPourIds } from "lib/utils/obstacles/getUnbrokenCopperPourIds" import { getViaDiameterDefaults } from "lib/utils/pcbStyle/getViaDiameterDefaults" import { TraceConnectionError } from "lib/errors" import { getPcbSelectorErrorForTracePort } from "./getPcbSelectorErrorForTracePort" @@ -253,7 +254,10 @@ export function Trace_doInitialPcbTraceRender(trace: Trace) { // Cache the PCB obstacles, they'll be needed for each segment between // ports/hints const [obstacles, errGettingObstacles] = tryNow( - () => getObstaclesFromCircuitJson(trace.root!.db.toArray() as any), // Remove as any when autorouting-dataset gets updated + () => + getObstaclesFromCircuitJson(trace.root!.db.toArray() as any, connMap, { + unbrokenCopperPourIds: getUnbrokenCopperPourIds(trace.root), + }), // Remove as any when autorouting-dataset gets updated ) if (errGettingObstacles) { @@ -276,7 +280,7 @@ export function Trace_doInitialPcbTraceRender(trace: Trace) { if (connectedTo.length > 0) { const netId = connMap.getNetConnectedToId(obstacle.connectedTo[0]) if (netId) { - obstacle.connectedTo.push(netId) + obstacle.connectedTo = Array.from(new Set([...connectedTo, netId])) } } } diff --git a/lib/utils/autorouting/getSimpleRouteJsonFromCircuitJson.ts b/lib/utils/autorouting/getSimpleRouteJsonFromCircuitJson.ts index 2c7b5fb05..73a3030d4 100644 --- a/lib/utils/autorouting/getSimpleRouteJsonFromCircuitJson.ts +++ b/lib/utils/autorouting/getSimpleRouteJsonFromCircuitJson.ts @@ -6,10 +6,15 @@ import { getFullConnectivityMapFromCircuitJson, } from "circuit-json-to-connectivity-map" import { getObstaclesFromCircuitJson } from "../obstacles/getObstaclesFromCircuitJson" +import { getUnbrokenCopperPourIds } from "../obstacles/getUnbrokenCopperPourIds" import type { SimpleRouteConnection } from "./SimpleRouteJson" import type { SimpleRouteJson } from "./SimpleRouteJson" import { getDescendantSubcircuitIds } from "./getAncestorSubcircuitIds" +type SelectableRoot = { + selectAll(selector: string): unknown[] +} + /** * This function can only be called in the PcbTraceRender phase or later */ @@ -19,12 +24,14 @@ export const getSimpleRouteJsonFromCircuitJson = ({ subcircuit_id, minTraceWidth = 0.1, nominalTraceWidth, + selectableRoot, }: { db?: CircuitJsonUtilObjects circuitJson?: AnyCircuitElement[] subcircuit_id?: string | null minTraceWidth?: number nominalTraceWidth?: number + selectableRoot?: SelectableRoot }): { simpleRouteJson: SimpleRouteJson; connMap: ConnectivityMap } => { if (!db && circuitJson) { db = su(circuitJson) @@ -81,6 +88,7 @@ export const getSimpleRouteJsonFromCircuitJson = ({ ...db.pcb_plated_hole.list(), ...db.pcb_hole.list(), ...db.pcb_via.list(), + ...db.pcb_copper_pour.list(), ...db.pcb_cutout.list(), // getObstaclesFromSoup is old and doesn't support diagonal traces // ...db.pcb_trace.list(), @@ -88,6 +96,9 @@ export const getSimpleRouteJsonFromCircuitJson = ({ (e) => !subcircuit_id || relevantSubcircuitIds?.has(e.subcircuit_id!), ), connMap, + { + unbrokenCopperPourIds: getUnbrokenCopperPourIds(selectableRoot), + }, ) // Add everything in the connMap to the connectedTo array of each obstacle diff --git a/lib/utils/obstacles/getObstaclesFromCircuitJson.ts b/lib/utils/obstacles/getObstaclesFromCircuitJson.ts index 075ee8353..01da06b5c 100644 --- a/lib/utils/obstacles/getObstaclesFromCircuitJson.ts +++ b/lib/utils/obstacles/getObstaclesFromCircuitJson.ts @@ -10,10 +10,77 @@ import { fillCircleWithRects } from "./fillCircleWithRects" import type { Obstacle } from "./types" const EVERY_LAYER = ["top", "inner1", "inner2", "bottom"] +const COPPER_POUR_RECT_HEIGHT = 0.6 + +const getUnbrokenCopperPourObstacles = ( + element: Extract, + withNetId: (ids: string[]) => string[], + unbrokenCopperPourIds: Set, +): Obstacle[] => { + if (!unbrokenCopperPourIds.has(element.pcb_copper_pour_id)) return [] + + const connectedTo = element.source_net_id + ? withNetId([element.source_net_id]) + : [] + + if (element.shape === "rect") { + if (element.rotation) { + const approximatingRects = generateApproximatingRects({ + center: element.center, + width: element.width, + height: element.height, + rotation: element.rotation, + }) + + return approximatingRects.map((rect) => ({ + type: "rect", + layers: [element.layer], + center: rect.center, + width: rect.width, + height: rect.height, + connectedTo: [...connectedTo], + })) + } + + return [ + { + type: "rect", + layers: [element.layer], + center: element.center, + width: element.width, + height: element.height, + connectedTo, + }, + ] + } + + if (element.shape === "brep") { + const outerRing = element.brep_shape.outer_ring.vertices.map((vertex) => ({ + x: vertex.x, + y: vertex.y, + })) + + return fillPolygonWithRects(outerRing, { + rectHeight: COPPER_POUR_RECT_HEIGHT, + }).map((rect) => ({ + type: "rect", + layers: [element.layer], + center: rect.center, + width: rect.width, + height: rect.height, + connectedTo: [...connectedTo], + })) + } + + return [] +} export const getObstaclesFromCircuitJson = ( soup: AnyCircuitElement[], connMap?: ConnectivityMap, + options?: { + unbrokenCopperPourIds?: Set + }, ) => { const withNetId = (idList: string[]) => connMap @@ -22,6 +89,7 @@ export const getObstaclesFromCircuitJson = ( ) : idList const obstacles: Obstacle[] = [] + const unbrokenCopperPourIds = options?.unbrokenCopperPourIds ?? new Set() for (const element of soup) { if (element.type === "pcb_smtpad") { if (element.shape === "circle") { @@ -266,6 +334,14 @@ export const getObstaclesFromCircuitJson = ( }) } } + } else if (element.type === "pcb_copper_pour") { + obstacles.push( + ...getUnbrokenCopperPourObstacles( + element, + withNetId, + unbrokenCopperPourIds, + ), + ) } else if (element.type === "pcb_trace") { const traceObstacles = getObstaclesFromRoute( element.route.map((rp) => ({ diff --git a/lib/utils/obstacles/getUnbrokenCopperPourIds.ts b/lib/utils/obstacles/getUnbrokenCopperPourIds.ts new file mode 100644 index 000000000..21cf4c2ec --- /dev/null +++ b/lib/utils/obstacles/getUnbrokenCopperPourIds.ts @@ -0,0 +1,34 @@ +type Selectable = { + selectAll(selector: string): unknown[] +} + +type CopperPourLike = { + lowercaseComponentName?: string + _parsedProps?: { + unbroken?: boolean + } + pcb_copper_pour_ids?: string[] +} + +const isCopperPourLike = (value: unknown): value is CopperPourLike => + typeof value === "object" && value !== null + +export const getUnbrokenCopperPourIds = ( + selectable?: Selectable | null, +): Set => { + if (!selectable) return new Set() + + const unbrokenCopperPourIds = new Set() + + for (const component of selectable.selectAll("copperpour")) { + if (!isCopperPourLike(component)) continue + if (component.lowercaseComponentName !== "copperpour") continue + if (!component._parsedProps?.unbroken) continue + + for (const pcbCopperPourId of component.pcb_copper_pour_ids ?? []) { + unbrokenCopperPourIds.add(pcbCopperPourId) + } + } + + return unbrokenCopperPourIds +} diff --git a/package.json b/package.json index b902cf793..e5cd2108c 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "@tscircuit/math-utils": "^0.0.36", "@tscircuit/miniflex": "^0.0.4", "@tscircuit/ngspice-spice-engine": "^0.0.8", - "@tscircuit/props": "^0.0.502", + "@tscircuit/props": "^0.0.503", "@tscircuit/schematic-match-adapt": "^0.0.16", "@tscircuit/schematic-trace-solver": "^v0.0.45", "@tscircuit/solver-utils": "^0.0.3", diff --git a/tests/features/__snapshots__/unbroken-copper-pour-autorouting-pcb.snap.svg b/tests/features/__snapshots__/unbroken-copper-pour-autorouting-pcb.snap.svg new file mode 100644 index 000000000..b5c086ba4 --- /dev/null +++ b/tests/features/__snapshots__/unbroken-copper-pour-autorouting-pcb.snap.svg @@ -0,0 +1 @@ +U1J1 \ No newline at end of file diff --git a/tests/features/unbroken-copper-pour-autorouting.test.tsx b/tests/features/unbroken-copper-pour-autorouting.test.tsx new file mode 100644 index 000000000..8eb5be9f3 --- /dev/null +++ b/tests/features/unbroken-copper-pour-autorouting.test.tsx @@ -0,0 +1,127 @@ +import { expect, test } from "bun:test" +import type { PcbTrace, SourcePort, SourceTrace } from "circuit-json" +import type { Board } from "lib/components/normal-components/Board" +import { getSimpleRouteJsonFromCircuitJson } from "lib/utils/autorouting/getSimpleRouteJsonFromCircuitJson" +import { getTestFixture } from "tests/fixtures/get-test-fixture" + +const traceHasBottomWireSegment = (trace: PcbTrace) => + trace.route.some( + (routePoint) => + routePoint.route_type === "wire" && routePoint.layer === "bottom", + ) + +test("unbroken copper pour acts as a same-net autorouter obstacle", async () => { + const { circuit } = getTestFixture() + + circuit.add( + + + + + + , + ) + + await circuit.renderUntilSettled() + + const [bottomCopperPour] = circuit.db.pcb_copper_pour.list() + expect(bottomCopperPour).toBeDefined() + expect(bottomCopperPour.layer).toBe("bottom") + + const board = circuit._getBoard() as Board | undefined + expect(board).toBeDefined() + board!.add() + board!.add() + board!.add() + + await circuit.renderUntilSettled() + + const gndNet = circuit.db.source_net.getWhere({ name: "GND" }) + expect(gndNet).toBeDefined() + + const pcbTraceErrors = circuit.db.pcb_trace_error.list() + expect(pcbTraceErrors).toHaveLength(0) + + const pcbTraces = circuit.db.pcb_trace.list() + const sourceTraceById = new Map( + circuit.db.source_trace + .list() + .map((sourceTrace) => [sourceTrace.source_trace_id, sourceTrace]), + ) + const sourcePortById = new Map( + circuit.db.source_port + .list() + .map((sourcePort) => [sourcePort.source_port_id, sourcePort]), + ) + const gndConnectivityKey = gndNet!.subcircuit_connectivity_map_key + + const nonGndTraces = pcbTraces.filter((trace) => { + const sourceTrace = trace.source_trace_id + ? sourceTraceById.get(trace.source_trace_id) + : null + const isGndTrace = + sourceTrace?.connected_source_net_ids.includes(gndNet!.source_net_id) || + sourceTrace?.connected_source_port_ids.some( + (sourcePortId) => + sourcePortById.get(sourcePortId)?.subcircuit_connectivity_map_key === + gndConnectivityKey, + ) + + return !isGndTrace + }) + const gndTraces = pcbTraces.filter((trace) => { + const sourceTrace = trace.source_trace_id + ? sourceTraceById.get(trace.source_trace_id) + : null + return ( + sourceTrace?.connected_source_net_ids.includes(gndNet!.source_net_id) || + sourceTrace?.connected_source_port_ids.some( + (sourcePortId) => + sourcePortById.get(sourcePortId)?.subcircuit_connectivity_map_key === + gndConnectivityKey, + ) + ) + }) + + expect(nonGndTraces).toHaveLength(2) + expect(nonGndTraces.filter(traceHasBottomWireSegment)).toHaveLength(0) + expect(gndTraces.length).toBeGreaterThan(0) + + const { simpleRouteJson } = getSimpleRouteJsonFromCircuitJson({ + db: circuit.db, + selectableRoot: circuit, + }) + + const bottomPourObstacles = simpleRouteJson.obstacles.filter( + (obstacle) => + obstacle.layers.includes("bottom") && + obstacle.connectedTo.includes(gndNet!.source_net_id), + ) + + expect(bottomPourObstacles.length).toBeGreaterThan(0) + expect( + bottomPourObstacles.some( + (obstacle) => + Math.abs(obstacle.center.x) < 1 && Math.abs(obstacle.center.y) < 1, + ), + ).toBe(true) + expect(circuit).toMatchPcbSnapshot(import.meta.path) +})