0hmX/dual-usb-peltier-assembly
A tscircuit package created by 0hmX.
- Version
- 0.4.12
- License
- unset
- Stars
- 0
scripts/helpers/routing.tsx
import { Circuit } from "@tscircuit/core"
import { cloneElement, isValidElement, type ReactElement, type ReactNode } from "react"
import type { AnyCircuitElement } from "circuit-json"
import type { RoutedCopperData } from "../../src/board/routed-copper"
import { assertCopperAssociations } from "../../tests/helpers/copper-associations"
import { copperPortContacts, sourceConnectivity } from "../../tests/helpers/physical-copper"
/** Keep logical traces and every via; omit only the slow native copper primitives. */
function withoutPhysicalCopper(node: ReactNode): ReactNode {
if (Array.isArray(node)) return node.map(withoutPhysicalCopper)
if (!isValidElement<Record<string, any>>(node)) return node
if (typeof node.type === "function") return withoutPhysicalCopper((node.type as Function)(node.props))
if (node.type === "pcbtrace" || node.type === "copperpour") return null
const props = { ...node.props }
for (const [key, value] of Object.entries(props)) {
if (key === "children" || isValidElement(value) || (Array.isArray(value) && value.some(isValidElement)))
props[key] = withoutPhysicalCopper(value)
}
return cloneElement(node, props)
}
export async function renderSourceIds(board: ReactElement) {
const circuit = new Circuit({ platform: { schematicDisabled: true, partsEngineDisabled: true } })
circuit.add(cloneElement(withoutPhysicalCopper(board) as ReactElement<any>, {
routingDisabled: true, routeRemaining: false, schematicDisabled: true, partsEngineDisabled: true,
}))
await circuit.renderUntilSettled()
return circuit.getCircuitJson()
}
export function assertStableSourceIds(fast: AnyCircuitElement[], full: AnyCircuitElement[]) {
const source = (items: AnyCircuitElement[]) => {
const nets = new Map(items.filter(item => item.type === "source_net").map(net => [net.source_net_id, net.name]))
return items.filter(item => item.type === "source_trace" || item.type === "source_port").map(item =>
item.type === "source_trace" ? { ...item, connected_source_net_ids: item.connected_source_net_ids.map(id => nets.get(id)) } : item)
}
// Pours affect implicit net numbering. Trace/port IDs stay exact; net names are stable.
if (JSON.stringify(source(fast)) !== JSON.stringify(source(full)))
throw new Error("Source trace IDs, pins, named nets, or constraints changed between renders")
}
export function enrichCopper(circuit: AnyCircuitElement[], data: RoutedCopperData): RoutedCopperData {
const source = sourceConnectivity(circuit)
const ports = new Map(circuit.filter(item => item.type === "source_port").map(port => [port.source_port_id, port]))
const components = new Map(circuit.filter(item => item.type === "source_component").map(component => [component.source_component_id, component]))
const nets = circuit.filter(item => item.type === "source_net")
const originalPorts = new Map([...source.ports].map(([key, id]) => [id, key]))
const traces = circuit.filter(item => item.type === "source_trace").filter(item => item.connected_source_port_ids.length &&
item.connected_source_port_ids.every(id => originalPorts.has(id)))
const contacts = copperPortContacts(circuit)
const routeEndpoints = data.routes.map(({ route }) => contacts([route[0], route[route.length - 1]].filter(point => !!point)
.map(point => ({ x: point.x, y: point.y, layers: point.route_type === "wire" ? [point.layer] : [point.from_layer, point.to_layer] }))))
const routedPorts = new Set(routeEndpoints.flatMap(endpoints => [...endpoints]))
const portName = (id: string) => {
const port = ports.get(id)!
return `${components.get(port.source_component_id!)!.name}.pin${port.pin_number ?? port.name}`
}
const constrained = (trace: typeof traces[number]) => trace.max_length !== undefined || trace.max_via_count !== undefined
const routes = data.routes.map((route, index) => {
const savedKey = route.sourceTraceKey ? JSON.parse(route.sourceTraceKey) : undefined
if (!route.sourcePort && savedKey?.[0]?.length === 1 && (savedKey[2] != null || savedKey[3] != null))
throw new Error(`Route ${index} (${route.net}) needs sourcePort to retain its saved single-pin constraint`)
const endpoints = routeEndpoints[index]!
if (route.sourcePort && ![...endpoints].some(id => portName(id) === route.sourcePort))
throw new Error(`Route ${index} (${route.net}) sourcePort ${route.sourcePort} is not a physical endpoint`)
const matches = traces.filter(trace => trace.connected_source_port_ids.every(id => source.groups.some(group =>
group.nets.includes(route.net) && group.ports.includes(originalPorts.get(id)!))) &&
(!route.sourcePort || trace.connected_source_port_ids.some(id => portName(id) === route.sourcePort)))
.map(trace => ({ trace, endpoints: trace.connected_source_port_ids.filter(id => endpoints.has(id)).length }))
// A global trunk can touch a constrained capacitor pad. Only an explicit
// owner distinguishes its local one-pin branch; length must not decide this.
const constraints = matches.filter(({ trace, endpoints }) => constrained(trace) &&
endpoints === trace.connected_source_port_ids.length && (route.sourcePort || endpoints > 1))
if (constraints.length > 1)
throw new Error(`Route ${index} (${route.net}) has ambiguous constrained source traces`)
if (route.sourcePort && matches.some(({ trace }) => constrained(trace)) && !constraints.length)
throw new Error(`Route ${index} (${route.net}) does not contact every endpoint of its sourcePort constraint`)
// A pour-only pad is physically connected but cannot represent a trace
// endpoint in native contiguity checks. Prefer a routed pin on the same net.
const represented = (trace: typeof traces[number]) => trace.connected_source_port_ids.filter(id => routedPorts.has(id)).length
const unconstrained = matches.filter(({ trace }) => !constrained(trace)).sort((a, b) => b.endpoints - a.endpoints ||
represented(b.trace) - represented(a.trace) ||
(a.trace.display_name ?? a.trace.source_trace_id).localeCompare(b.trace.display_name ?? b.trace.source_trace_id))
const trace = (constraints[0] ?? unconstrained[0])?.trace
if (!trace) throw new Error(`Route ${index} (${route.net}) has no matching original source trace; constrained branches require sourcePort`)
return { ...route, sourceTraceId: trace.source_trace_id, sourceTraceKey: JSON.stringify([
trace.connected_source_port_ids.map(portName).sort(),
trace.connected_source_net_ids.map(id => nets.find(net => net.source_net_id === id)?.name).sort(),
trace.max_length ?? null, trace.max_via_count ?? null,
]) }
})
const result = { ...data, routes }
assertCopperAssociations(circuit, result)
return result
}
export const formatRoutingData = (data: RoutedCopperData) => JSON.stringify(data, null, 2)
.replace(/\{\n(?:[ \t]+"[^\n]*\n)+[ \t]*\}/g, object => object.replace(/\n\s*/g, " ")) + "\n"