astra/f1c100s

This code defines and renders a four-layer, top-mounted F1C100S module with detailed pin assignments, schematic annotations, and layout profiles, facilitating design, validation, and routing of the hardware including support for decoupling capacitors, crystal oscillators, and external connectors.

Version
0.11.0
License
unset
Stars
0

src/saved-paths.tsx

import type { FanoutTracePath } from "@tscircuit/props";
import type {
	GenericLocalAutorouter,
	SimpleRouteJson,
	Subcircuit,
} from "tscircuit";
import type { LayoutProfile } from "./profiles";
import native from "./generated/native.trace-paths.json";
import lcdTopStorageRight from "./generated/lcd_top_storage_right.trace-paths.json";
import lcdRightStorageBottom from "./generated/lcd_right_storage_bottom.trace-paths.json";
import lcdTopStorageBottom from "./generated/lcd_top_storage_bottom.trace-paths.json";
import lcdRightStorageLeft from "./generated/lcd_right_storage_left.trace-paths.json";
const profiles = {
	native,
	lcd_top_storage_right: lcdTopStorageRight,
	lcd_right_storage_bottom: lcdRightStorageBottom,
	lcd_top_storage_bottom: lcdTopStorageBottom,
	lcd_right_storage_left: lcdRightStorageLeft,
};

const rootsWithPlatedLayerAccess = new WeakSet<object>();

/** The pinned core drops pcb_port.layers when making router input. Restore
 * layer access from actual plated-hole obstacles immediately before routing,
 * for both saved fanouts and the carrier's configured local autorouter. */
function preservePlatedLayerAccess(instance: Subcircuit) {
	const root = instance.root!;
	if (rootsWithPlatedLayerAccess.has(root)) return;
	rootsWithPlatedLayerAccess.add(root);
	root.on(
		"autorouting:start",
		({ simpleRouteJson: input }: { simpleRouteJson: SimpleRouteJson }) => {
			if (!input) return;
			for (const connection of input.connections)
				for (const point of connection.pointsToConnect) {
					const hole = input.obstacles.find(
						(o) =>
							o.circuitJsonMetadata?.pcb_plated_hole_id &&
							Math.hypot(o.center.x - point.x, o.center.y - point.y) < 1e-5 &&
							(o.circuitJsonMetadata.pcb_port_id === point.pcb_port_id ||
								o.connectedTo.includes(
									connection.source_trace_id ?? connection.name,
								)),
					);
					if (hole) point.layers = [...hole.layers];
				}
		},
	);
}

/** Every saved path on a net ends at the same junction. This final phase checks
 * that invariant; it never solves or invents copper. Parent routing is separate. */
export async function joinSavedPathExits(
	input: SimpleRouteJson,
): Promise<GenericLocalAutorouter> {
	for (const connection of input.connections) {
		const points = connection.pointsToConnect,
			first = points[0];
		// Core's SRJ currently defaults a physical port to its first layer.
		// A real plated barrel joins coincident exits on different layers.
		const barrel =
			first &&
			input.obstacles.find(
				(o) =>
					o.circuitJsonMetadata?.pcb_plated_hole_id &&
					Math.hypot(o.center.x - first.x, o.center.y - first.y) < 1e-4 &&
					o.connectedTo.some(
						(id) =>
							id === connection.name ||
							id === connection.source_trace_id ||
							points.some((p) => p.pcb_port_id === id),
					) &&
					points.every((p) =>
						(p.layers ?? [p.layer]).some((l) => o.layers.includes(l)),
					),
			);
		if (
			first &&
			points.some(
				(p) =>
					Math.hypot(p.x - first.x, p.y - first.y) > 1e-4 ||
					(!barrel &&
						!(p.layers ?? [p.layer]).some((l) =>
							(first.layers ?? [first.layer]).includes(l),
						)),
			)
		)
			throw new Error(`Saved F1C100S paths do not meet for ${connection.name}`);
	}
	const listeners: Record<string, ((event: any) => void)[]> = {
		complete: [],
		error: [],
		progress: [],
	};
	return {
		input,
		isRouting: false,
		on(event, listener) {
			listeners[event]!.push(listener);
		},
		start() {
			queueMicrotask(() =>
				listeners.complete!.forEach((f) => f({ type: "complete", traces: [] })),
			);
		},
		stop() {},
		solveSync() {
			return [];
		},
	};
}

/** Use the native saved-fanout API for copper, retaining the imported footprint
 * and schematic symbols. Electrical traces remain outside the fanout. */
export function attachSavedPaths(
	instance: Subcircuit,
	profile: LayoutProfile,
	paths?: FanoutTracePath[],
) {
	preservePlatedLayerAccess(instance);
	(instance as any)._isInflatedFromCircuitJson = false;
	const check = instance.doInitialPcbDesignRuleChecks.bind(instance);
	instance.doInitialPcbDesignRuleChecks = () => {
		deduplicateSharedVias(instance);
		check();
	};
	const module = instance.selectOne(".MODULE") as any;
	const components = [...module.children];
	module.add(
		<fanout
			name="COPPER"
			pcbX={0}
			pcbY={0}
			schLayout={{ layoutMode: "relative" }}
			pcbTracePaths={
				paths ?? (structuredClone(profiles[profile]) as FanoutTracePath[])
			}
		/>,
	);
	const fanout = module.selectOne(".COPPER");
	fanout._doInitialSchematicLayoutSections = () => {};
	for (const component of components) {
		module.children = module.children.filter((c: any) => c !== component);
		fanout.add(component);
	}
	for (const trace of instance.selectAll("trace") as any[]) {
		for (const props of [trace.props, trace._parsedProps]) {
			delete props.pcbPath;
			delete props.pcbStraightLine;
			props.maxLength = /^N_HOSC[IO]$/.test(props.name ?? "") ? 10 : 1000;
			if (/^N_HOSC[IO]$/.test(props.name ?? "")) props.maxViaCount = 0;
			props.path = props.path?.map((p: string) =>
				p.replace(".MODULE > ", ".MODULE .COPPER > "),
			);
		}
	}
}

/** Core currently emits a via for every path that shares it. Keep one physical
 * drill per same-net location before either native or independent DRC runs. */
function deduplicateSharedVias(instance: Subcircuit) {
	const db = instance.root!.db;
	const seen = new Map<string, string>();
	for (const via of db.pcb_via.list()) {
		if (via.subcircuit_id !== instance.subcircuit_id) continue;
		const key = [
			via.x.toFixed(6),
			via.y.toFixed(6),
			via.hole_diameter,
			via.outer_diameter,
			[...via.layers].sort().join(","),
		].join(":");
		const traceId = db.pcb_trace.get(via.pcb_trace_id!)?.source_trace_id;
		// Separate pin-to-cap traces can belong to one electrical supply net.
		const net =
			(traceId &&
				db.source_trace.get(traceId)?.subcircuit_connectivity_map_key) ??
			traceId ??
			"";
		if (seen.has(key)) {
			if (seen.get(key) !== net)
				throw new Error("Different nets share an F1C100S via");
			db.pcb_via.delete(via.pcb_via_id);
		} else seen.set(key, net);
	}
}