ShiboSoftwareDev/linux-gameboy-advance
Restores PCB assembly metadata by assigning part numbers and 3D CAD models, removing test-point bodies, and validating top-side SMT solder-paste features without altering copper or pads.
- Version
- 1.0.17
- License
- unset
- Stars
- 0
modules/f1c100s/src/index.tsx
import type { CircuitJson } from "circuit-json";
import { selectBreakouts } from "./selective-breakouts";
import { joinSavedPathExits } from "./saved-paths";
import { attachSchematicLayout } from "./schematic";
import {
LAYOUT_PROFILES,
assertLayoutProfile,
type LayoutProfile,
} from "./profiles";
import { EXTERNAL_NETS } from "./pin-map";
import partMap from "../../../assembly-parts.json";
import native from "./generated/native.circuit.json";
import lcdTopStorageRight from "./generated/lcd_top_storage_right.circuit.json";
import lcdRightStorageBottom from "./generated/lcd_right_storage_bottom.circuit.json";
import lcdTopStorageBottom from "./generated/lcd_top_storage_bottom.circuit.json";
import lcdRightStorageLeft from "./generated/lcd_right_storage_left.circuit.json";
export { LAYOUT_PROFILES, type LayoutProfile };
export {
F1C100SLcdSchematicBox,
F1C100SStorageSchematicBox,
F1C100SGpioSchematicBox,
F1C100SAudioSchematicBox,
F1C100SVideoTouchSchematicBox,
F1C100SSystemSchematicBox,
F1C100SPowerSchematicBox,
type F1C100SSchematicBoxProps,
} from "./schematic-boxes";
const profiles: Record<LayoutProfile, unknown> = {
native,
lcd_top_storage_right: lcdTopStorageRight,
lcd_right_storage_bottom: lcdRightStorageBottom,
lcd_top_storage_bottom: lcdTopStorageBottom,
lcd_right_storage_left: lcdRightStorageLeft,
};
export interface F1C100SModuleProps {
name: string;
layoutProfile?: LayoutProfile;
pcbX?: number;
pcbY?: number;
/** Rotation of the complete stored module, including its exit pads. */
pcbRotation?: number;
schX?: number;
schY?: number;
schSheetName?: string;
/** Custom mode lets callers place the exported schematic-box components. */
schematicLayout?: "default" | "custom";
/** Only truthy entries create breakout pads and external copper. Internal support is always retained. */
connections?: Partial<Record<string, string>>;
}
/** A fresh copy prevents one placed instance from mutating another's routes. */
export function getF1C100SCircuitJson(
layoutProfile: LayoutProfile = "native",
): CircuitJson {
assertLayoutProfile(layoutProfile);
return structuredClone(profiles[layoutProfile]) as CircuitJson;
}
/** Inflate the validated components, connectivity and stored copper together. */
function prepareForInflation(json: CircuitJson): CircuitJson {
// Dedicated decouplers have explicit two-port source traces. Their supply
// connection goes through the assigned U1 pin; only their return joins GND.
const records = json as any[];
const parts = partMap as Record<string, any>;
for (const component of records.filter((e) => e.type === "source_component")) {
const metadata = parts[component.name];
if (!metadata) continue;
component.manufacturer_part_number = metadata.manufacturerPartNumber;
component.supplier_part_numbers = metadata.supplierPartNumbers;
}
// These six two-terminal, non-polar support parts are electrically and
// mechanically symmetric. Store the assembly rotation that matches the
// already-routed pad assignment so the placement checker and PnP output agree.
const assemblyFlipNames = new Set([
"C_B_VDD_CORE",
"C_B_VCC_DRAM",
"R_SV_H",
"C_SV_L",
"R_PU_SDMMC0_D1",
"R_PU_TWI0_SCL",
]);
for (const component of records.filter(
(e) => e.type === "source_component" && assemblyFlipNames.has(e.name),
)) {
const pcbComponent = records.find(
(e) =>
e.type === "pcb_component" &&
e.source_component_id === component.source_component_id,
);
if (pcbComponent)
pcbComponent.rotation = ((pcbComponent.rotation ?? 0) + 180) % 360;
}
// Normalize the generated exposed pad so circuit-json inflation binds it to
// pin 89. The newer EasyEDA parser emits a rotated_rect with only the
// "thermalpad" hint, which older core versions render as unconnected copper.
const processor = records.find(
(e) => e.type === "source_component" && e.name === "U1",
);
const thermalSourcePort = records.find(
(e) =>
e.type === "source_port" &&
e.source_component_id === processor.source_component_id &&
e.pin_number === 89,
);
const thermalPcbPort = records.find(
(e) => e.type === "pcb_port" && e.source_port_id === thermalSourcePort.source_port_id,
);
const thermalPad = records.find(
(e) =>
e.type === "pcb_smtpad" &&
e.pcb_component_id === thermalPcbPort.pcb_component_id &&
(e.port_hints?.includes("thermalpad") || e.pcb_port_id === thermalPcbPort.pcb_port_id),
);
if (!thermalPad) throw new Error("Missing F1C100S thermal pad");
Object.assign(thermalPad, {
shape: "rect",
pcb_port_id: thermalPcbPort.pcb_port_id,
port_hints: ["pin89"],
});
delete thermalPad.ccw_rotation;
delete thermalPad.corner_radius;
// Dedicated decouplers have explicit two-port source traces. Their supply
// connection goes through the assigned U1 pin; only their return joins GND.
for (const cap of records.filter(
(e) => e.type === "source_component" && /^C_D\d+$/.test(e.name),
)) {
const capPort = records.find(
(e) =>
e.type === "source_port" &&
e.source_component_id === cap.source_component_id &&
e.pin_number === 1,
);
const pinPort = records.find(
(e) =>
e.type === "source_port" &&
e.source_component_id === processor.source_component_id &&
e.pin_number === Number(cap.name.slice(3)),
);
const rail = records.find(
(e) =>
e.type === "source_trace" &&
e.connected_source_port_ids.includes(capPort.source_port_id),
);
if (!rail || !rail.connected_source_port_ids.includes(pinPort.source_port_id))
throw Error(`Missing decoupling target for ${cap.name}`);
rail.connected_source_port_ids = rail.connected_source_port_ids.filter(
(id: string) => id !== capPort.source_port_id,
);
records.push({
type: "source_trace",
source_trace_id: `source_trace_${cap.name}_direct`,
name: `D_${cap.name}_U1_pin${pinPort.pin_number}`,
connected_source_port_ids: [capPort.source_port_id, pinPort.source_port_id],
connected_source_net_ids: [],
subcircuit_id: rail.subcircuit_id,
min_trace_thickness: 0.12,
max_length: 1000,
});
}
// Explicit local nets give the schematic conventional named connections.
// The existing source traces and stored copper retain the same endpoints.
for (const e of [...json] as any[]) {
if (e.type !== "source_trace" || !e.name?.startsWith("N_")) continue;
const netName = e.name.slice(2);
const id = `source_net_${netName}`;
(json as any[]).push({
type: "source_net",
source_net_id: id,
name: netName,
member_source_group_ids: [],
});
e.connected_source_net_ids = [id];
}
return json
.filter((e) => e.type !== "pcb_trace" && e.type !== "pcb_via")
.map((e) =>
// The pinned inflator lacks crystal and testpoint cases. Temporary
// chips retain selectors until native components replace them.
e.type === "source_component" &&
["simple_crystal", "simple_test_point"].includes(e.ftype)
? { ...e, ftype: "simple_chip" as const }
: e,
) as CircuitJson;
}
/** Four-layer, top-mounted, pre-routed F1C100S + decoupling module.
* The package uses stored copper. No fanout solver runs during instantiation.
*/
export function F1C100SModule(props: F1C100SModuleProps) {
if ("busProfile" in props || "variant" in props || "busExits" in props)
throw new Error("Use layoutProfile to select a stored F1C100S layout");
const {
name,
layoutProfile = "native",
connections = {},
schematicLayout = "default",
...placement
} = props;
assertLayoutProfile(layoutProfile);
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
throw new Error("Module name must be a selector-safe identifier");
for (const port of Object.keys(connections))
if (!EXTERNAL_NETS.includes(port))
throw new Error(`Unknown F1C100S terminal '${port}'`);
const selected = new Set(
Object.entries(connections)
.filter(([, target]) => Boolean(target))
.map(([port]) => port),
);
const { circuitJson, paths } = selectBreakouts(
getF1C100SCircuitJson(layoutProfile),
selected,
);
return (
<>
<subcircuit
name={name}
{...{
ref: (instance: Parameters<typeof attachSchematicLayout>[0]) =>
attachSchematicLayout(
instance,
schematicLayout,
layoutProfile,
selected,
paths,
),
}}
minTraceWidth={0.12}
minViaPadDiameter={0.45}
minViaHoleDiameter={0.3}
autorouter={{ local: true, algorithmFn: joinSavedPathExits }}
schTraceAutoLabelEnabled
schMaxTraceDistance={3}
circuitJson={prepareForInflation(circuitJson)}
{...placement}
/>
{Object.entries(connections).map(([port, target]) =>
target ? (
<trace
key={port}
name={`${name}_${port}_external`}
from={`.${name} .${port} > .pin1`}
to={target}
/>
) : null,
)}
</>
);
}
export default F1C100SModule;