imrishabh18/rp2040-motor-controller
A library of PCB component definitions covering MOSFETs, motor drivers, USB-PD control, sensors, current monitors, transistors, diodes, resistors, speakers, connectors, and mirrored bottom-side logos, with pin mappings, footprints, and 3D models.
- Version
- 1.0.55
- License
- unset
- Stars
- 5
panel.circuit.tsx
PCB
Schematic
import type { Panel } from "@tscircuit/core";
import type { AnyCircuitElement, PcbBoard, SourceBoard } from "circuit-json";
// Refresh this input with `tsci build index.circuit.tsx` after editing the single board.
import singleBoardJson from "./dist/index/circuit.json";
const boardBuild = singleBoardJson as AnyCircuitElement[];
function getBoardDefinition() {
const board = boardBuild.find((element): element is PcbBoard => element.type === "pcb_board");
const source = boardBuild.find((element): element is SourceBoard => element.type === "source_board");
const isRouted = boardBuild.some((element) => element.type === "pcb_trace");
const hasErrors = boardBuild.some(({ type }) => type.endsWith("_error"));
if (!isRouted || hasErrors || !board?.outline || !board.subcircuit_id ||
!board.width || !board.height || !source?.source_group_id) {
throw new Error("Build index.circuit.tsx before building the panel. The panel needs its routed board and outline.");
}
return {
board,
width: board.width,
height: board.height,
sourceId: source.source_board_id,
groupId: source.source_group_id,
subcircuitId: board.subcircuit_id,
};
}
const { board, width, height, sourceId, groupId, subcircuitId } = getBoardDefinition();
const copies = ["A", "B", "C", "D"];
const boardGap = 5;
const edgePadding = 5;
const routingWidth = 2;
const tabLength = 5;
const rightTabOffset = 5;
const mouseBiteDiameter = 0.6;
const mouseBitePitch = 0.95;
const mouseBiteOffsets = [-2, -1, 0, 1, 2].map((index) => index * mouseBitePitch);
const mouseBiteEdgeOffset = 0.22;
const boardRecords = boardBuild.filter(({ type }) =>
/^(pcb|source|cad)_/.test(type) &&
!/_(warning|error)$/.test(type) &&
!["pcb_board", "source_board", "source_project_metadata"].includes(type),
).filter((record) => record.type !== "source_group" || record.source_group_id !== groupId);
type Point = { x: number; y: number };
function copyRecord(
record: AnyCircuitElement,
offset: Point,
suffix: string,
aliases: Map<string, string>,
): AnyCircuitElement {
const isPcbGeometry = record.type.startsWith("pcb_") || record.type === "cad_component";
const clone = (value: unknown, field: string): unknown => {
if (typeof value === "string" && value !== "" &&
(/_ids?$/.test(field) ||
field === "subcircuit_connectivity_map_key" ||
field === "connectsTo" || field === "connection_name")) {
return aliases.get(value) ?? `${value}_${suffix}`;
}
if (Array.isArray(value)) return value.map((item) => clone(item, field));
if (value === null || typeof value !== "object") return value;
const copied = Object.fromEntries(
Object.entries(value).map(([key, item]) => [key, clone(item, key)]),
);
// Model origin and orientation are local to the 3D model. All other x/y
// pairs in the board's PCB records are in board coordinates.
if (isPcbGeometry && field !== "rotation" && field !== "model_origin_position" &&
typeof copied.x === "number" && typeof copied.y === "number") {
copied.x += offset.x;
copied.y += offset.y;
}
return copied;
};
const copy = clone(record, "") as AnyCircuitElement;
if (copy.type === "source_component") copy.name = `${copy.name}_${suffix}`;
return copy;
}
const installed = new WeakSet<Panel>();
function populatePanel(panel: Panel | null) {
if (!panel || installed.has(panel)) return;
installed.add(panel);
// Native layout creates the board outlines and routed slots. Attach the
// already-checked copper after the mechanical panel has finished rendering.
const layout = panel.doInitialPanelLayout.bind(panel);
panel.doInitialPanelLayout = () => {
layout();
const root = panel.root;
if (root && panel.pcb_panel_id) {
const complete = () => {
root.removeListener("renderComplete", complete);
populatePlacedBoards(root.db);
};
root.on("renderComplete", complete);
}
};
}
function populatePlacedBoards(db: NonNullable<Panel["root"]>["db"]) {
const placedBoards = db.pcb_board.list();
if (placedBoards.length !== copies.length) throw new Error("Expected four panel boards.");
for (const [index, placed] of placedBoards.entries()) {
const placedGroup = db.source_group.getWhere({ subcircuit_id: placed.subcircuit_id });
const placedSource = placedGroup && db.source_board.getWhere({ source_group_id: placedGroup.source_group_id });
if (!placed.subcircuit_id || !placedGroup || !placedSource) {
throw new Error("A panel board is missing its source scope.");
}
const suffix = copies[index];
const offset = {
x: placed.center.x - board.center.x,
y: placed.center.y - board.center.y,
};
const aliases = new Map([
[board.pcb_board_id, placed.pcb_board_id],
[subcircuitId, placed.subcircuit_id],
[sourceId, placedSource.source_board_id],
[groupId, placedGroup.source_group_id],
]);
db.pcb_board.update(placed.pcb_board_id, {
...board,
center: placed.center,
outline: placed.outline,
position_mode: placed.position_mode,
display_offset_x: placed.display_offset_x,
display_offset_y: placed.display_offset_y,
pcb_board_id: placed.pcb_board_id,
source_board_id: placedSource.source_board_id,
subcircuit_id: placed.subcircuit_id,
} as PcbBoard);
// Bulk insertion assigns new IDs. Preserve the copied IDs so the ports,
// components, traces, and net references all resolve within this board.
for (const record of boardRecords) {
const copy = copyRecord(record, offset, suffix, aliases);
const table = db[copy.type] as { insert: (element: AnyCircuitElement) => unknown };
table.insert(copy);
}
}
for (const placed of placedBoards) {
arrangeBreakawayTabs(db, placed);
}
}
function arrangeBreakawayTabs(db: NonNullable<Panel["root"]>["db"], placed: PcbBoard) {
const { x, y } = placed.center;
const right = x + width / 2;
const top = y + height / 2;
const halfTab = tabLength / 2;
// The sampled outline is a few microns off-axis at some straight edges.
const tolerance = 0.001;
const slots = db.pcb_cutout.list().filter((slot) => slot.shape === "polygon");
const near = (a: number, b: number) => Math.abs(a - b) < tolerance;
const tabEnd = (matches: (point: Point) => boolean) => {
const slot = slots.find(({ points }) => points.filter(matches).length === 2);
if (!slot) throw new Error("Could not locate the routed end of a panel tab.");
return slot;
};
// Join the two top slots across the default tab to clear both USB-C shells.
const topEnd = (dx: number) => (point: Point) =>
near(point.x, x + dx) && point.y >= top - tolerance && point.y <= top + routingWidth + tolerance;
const topLeft = tabEnd(topEnd(-halfTab));
const topRight = tabEnd(topEnd(halfTab));
const rightEnds = [-halfTab, halfTab].map((dy) => {
const matches = (point: Point) => near(point.y, y + dy) &&
point.x >= right - tolerance && point.x <= right + routingWidth + tolerance;
return { slotId: tabEnd(matches).pcb_cutout_id, matches };
});
const aroundSlot = (points: Point[], matches: (point: Point) => boolean, fromInner: boolean) => {
const [a, b] = points.filter(matches).sort((a, b) => a.y - b.y);
const start = points.indexOf(fromInner ? a : b);
const end = points.indexOf(fromInner ? b : a);
const step = (start + 1) % points.length === end ? -1 : 1;
if ((start - step + points.length) % points.length !== end) {
throw new Error("The panel tab end is not a straight edge.");
}
return points.map((_, index) => points[(start + step * index + points.length) % points.length]);
};
db.pcb_cutout.update(topLeft.pcb_cutout_id, {
points: [...aroundSlot(topLeft.points, topEnd(-halfTab), true), ...aroundSlot(topRight.points, topEnd(halfTab), false)],
});
db.pcb_cutout.delete(topRight.pcb_cutout_id);
// Shift the right tab up the straight edge, clear of the side-entry JST.
for (const { slotId, matches } of rightEnds) {
// The top-right slot was merged above, so read the current polygon.
const currentId = slotId === topRight.pcb_cutout_id ? topLeft.pcb_cutout_id : slotId;
const current = db.pcb_cutout.get(currentId);
if (current?.shape !== "polygon") throw new Error("Missing right panel slot.");
db.pcb_cutout.update(currentId, {
points: current.points.map((point) => matches(point) ? { ...point, y: point.y + rightTabOffset } : point),
});
}
// Five 0.6 mm drills per tab, with 0.35 mm of material between holes.
// Bias the drills toward the waste to clear the board's edge copper.
for (const offset of mouseBiteOffsets) {
const points = [
{ x: x - width / 2 - mouseBiteEdgeOffset, y: y + offset },
{ x: right + mouseBiteEdgeOffset, y: y + rightTabOffset + offset },
{ x: x + offset, y: y - height / 2 - mouseBiteEdgeOffset },
];
for (const point of points) {
db.pcb_hole.insert({
hole_shape: "circle",
hole_diameter: mouseBiteDiameter,
...point,
});
}
}
}
export default function Rp2040MotorControllerPanel() {
return (
<panel
{...{ ref: populatePanel }}
layoutMode="grid"
row={2}
col={2}
boardGap={boardGap}
edgePadding={edgePadding}
panelizationMethod="tab-routing"
tabWidth={routingWidth}
tabLength={tabLength}
mouseBites={false}
>
{copies.map((suffix) => (
<board
key={suffix}
name={`Controller-${suffix}`}
outline={board.outline}
layers={4}
thickness={board.thickness}
routingDisabled
/>
))}
</panel>
);
}
