ShiboSoftwareDev/linux-gameboy-advance
Multilayer PCB carrier board integrating USB-C, battery charging/power management, buck-boost regulation, SoC/display/SD interfaces, audio controls, status LEDs, backlight, and UART/I²C/SPI service connectors.
- Version
- 1.0.131
- 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 { makeLayout } from "./layout";
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,
};
/** Keep the checked-in routed module compatible with source-level footprint
* improvements. Moving only a component endpoint preserves every reviewed
* stored segment while giving the new courtyard its required assembly gap.
* Once all generated profiles are refreshed, this becomes a no-op. */
function applyStoredCourtyardRepairs(json: CircuitJson, profile: LayoutProfile) {
const records = json as any[];
const layout = makeLayout(profile);
const desired = new Map([
...layout.passives
.filter((part) => ["C_TV_REF", "C_OSCI", "C_OSCO"].includes(part.name))
.map((part) => [part.name, { x: part.x, y: part.y }] as const),
["Y1", { x: layout.crystal.x, y: layout.crystal.y }] as const,
]);
const componentFor = (name: string) => {
const source = records.find(
(record) => record.type === "source_component" && record.name === name,
);
if (!source) throw new Error(`Missing stored component ${name}`);
const pcb = records.find(
(record) =>
record.type === "pcb_component" &&
record.source_component_id === source.source_component_id,
);
if (!pcb) throw new Error(`Missing stored PCB component ${name}`);
return { source, pcb };
};
const shiftPoint = (point: any, dx: number, dy: number) => {
if (!point) return;
if (typeof point.x === "number") point.x += dx;
if (typeof point.y === "number") point.y += dy;
};
for (const [name, target] of desired) {
const { source, pcb } = componentFor(name);
const origin = {
x: pcb.display_offset_x ?? pcb.center.x,
y: pcb.display_offset_y ?? pcb.center.y,
};
const dx = target.x - origin.x;
const dy = target.y - origin.y;
if (Math.hypot(dx, dy) < 1e-9) continue;
const portIds = new Set(
records
.filter(
(record) =>
record.type === "pcb_port" &&
record.pcb_component_id === pcb.pcb_component_id,
)
.map((record) => record.pcb_port_id),
);
for (const record of records.filter(
(record) => record.pcb_component_id === pcb.pcb_component_id,
)) {
shiftPoint(record, dx, dy);
shiftPoint(record.center, dx, dy);
shiftPoint(record.position, dx, dy);
shiftPoint(record.anchor_position, dx, dy);
if (typeof record.display_offset_x === "number") record.display_offset_x += dx;
if (typeof record.display_offset_y === "number") record.display_offset_y += dy;
for (const point of record.route ?? []) shiftPoint(point, dx, dy);
for (const point of record.outline ?? []) shiftPoint(point, dx, dy);
}
for (const trace of records.filter((record) => record.type === "pcb_trace"))
for (const point of trace.route ?? [])
if (portIds.has(point.pcb_port_id)) shiftPoint(point, dx, dy);
}
const courtyards = [
["C_TV_REF", 2.7, 1.4],
["Y1", 3.6, 2.8],
] as const;
for (const [name, width, height] of courtyards) {
const { pcb } = componentFor(name);
if (records.some(
(record) =>
(record.type === "pcb_courtyard_rect" || record.type === "pcb_courtyard_outline") &&
record.pcb_component_id === pcb.pcb_component_id,
)) continue;
records.push({
type: "pcb_courtyard_rect",
pcb_courtyard_rect_id: `pcb_courtyard_rect_repaired_${name}`,
pcb_component_id: pcb.pcb_component_id,
layer: pcb.layer,
center: { ...pcb.center },
width,
height,
ccw_rotation: pcb.rotation ?? 0,
subcircuit_id: pcb.subcircuit_id,
pcb_group_id: pcb.pcb_group_id,
});
}
return json;
}
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";
/** Finished drill used when replaying stored vias. Defaults to the module fixture's 0.20 mm. */
minViaHoleDiameter?: number;
/** 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 applyStoredCourtyardRepairs(
structuredClone(profiles[layoutProfile]) as CircuitJson,
layoutProfile,
);
}
/** 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",
minViaHoleDiameter,
...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 storedCircuitJson = getF1C100SCircuitJson(layoutProfile);
const effectiveViaHoleDiameter = minViaHoleDiameter ??
(storedCircuitJson as any[]).find((record) => record.type === "pcb_via")?.hole_diameter ??
0.2;
const { circuitJson, paths } = selectBreakouts(
storedCircuitJson,
selected,
effectiveViaHoleDiameter,
);
return (
<>
<subcircuit
name={name}
{...{
ref: (instance: Parameters<typeof attachSchematicLayout>[0]) =>
attachSchematicLayout(
instance,
schematicLayout,
layoutProfile,
selected,
paths,
),
}}
minTraceWidth={0.12}
minViaPadDiameter={0.45}
minViaHoleDiameter={effectiveViaHoleDiameter}
preflightRoutingCheckPolicy="none"
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;