imrishabh18/rp2040-motor-controller
An RP2040-based NEMA17 stepper-motor controller with USB-C programming and PD motor-power negotiation, DRV8825 dual H-bridge drive, current/temperature telemetry, protected power filtering, status RGB LED, buzzer alarm, and optional magnetic encoder.
- Version
- 1.0.42
- License
- unset
- Stars
- 3
schematic/refineSchematicStyle.ts
import { analyzeSchematicPlacement } from "../vendor/schematic-style-analysis";
/** Refine the generated layout from analyzer suggestions, without component coordinate overrides. */
export function installSchematicStyleRefinement(
board: any,
notes: Array<{
text: string;
ref?: string;
sectionName?: string;
fontSize: number;
color: string;
}> = [],
) {
if (!board || board.__styleRefinementInstalled) return;
board.__styleRefinementInstalled = true;
const original = board.doInitialSchematicSheetRender.bind(board);
board.doInitialSchematicSheetRender = () => {
if (board.root.schematicDisabled) return original();
const db = board.root.db;
const components = () => db.schematic_component.list();
const byName = (name: string) => {
const source = db.source_component
.list()
.find((e: any) => e.name === name);
return components().find(
(e: any) => e.source_component_id === source?.source_component_id,
);
};
const fromBox = (box: any) =>
box && db.schematic_component.get(box.schematicComponentId);
function move(component: any, x: number, y: number, rotation = 0) {
if (!component) return;
const cx = component.center.x,
cy = component.center.y;
const angle = (rotation * Math.PI) / 180,
cos = Math.cos(angle),
sin = Math.sin(angle);
const transform = (p: any) => ({
x: x + (p.x - cx) * cos - (p.y - cy) * sin,
y: y + (p.x - cx) * sin + (p.y - cy) * cos,
});
for (const port of db.schematic_port
.list()
.filter(
(p: any) =>
p.schematic_component_id === component.schematic_component_id,
)) {
port.center = transform(port.center);
const dirs = ["right", "up", "left", "down"];
for (const field of ["facing_direction", "side_of_component"]) {
const index = dirs.indexOf(port[field]);
if (index >= 0)
port[field] = dirs[(index + Math.round(rotation / 90) + 4) % 4];
}
}
for (const text of db.schematic_text
.list()
.filter(
(t: any) =>
t.schematic_component_id === component.schematic_component_id,
))
text.position = transform(text.position);
if (rotation && component.symbol_name) {
const dirs = ["right", "up", "left", "down"];
component.symbol_name = component.symbol_name.replace(
/_(right|up|left|down)$/,
(_: any, dir: string) =>
`_${dirs[(dirs.indexOf(dir) + Math.round(rotation / 90) + 4) % 4]}`,
);
if (Math.abs(rotation) % 180 === 90)
component.size = {
width: component.size.height,
height: component.size.width,
};
}
for (const type of [
"schematic_path",
"schematic_rect",
"schematic_circle",
"schematic_line",
"schematic_arc",
]) {
for (const primitive of db[type]
.list()
.filter(
(p: any) =>
p.schematic_component_id === component.schematic_component_id,
)) {
if (primitive.center) primitive.center = transform(primitive.center);
if (primitive.points)
primitive.points = primitive.points.map(transform);
if (typeof primitive.x1 === "number") {
const a = transform({ x: primitive.x1, y: primitive.y1 }),
b = transform({ x: primitive.x2, y: primitive.y2 });
Object.assign(primitive, { x1: a.x, y1: a.y, x2: b.x, y2: b.y });
}
}
}
component.center = { x, y };
}
function reroute() {
// Trace rendering also emits schematic_text labels; discard the old
// generated text before rebuilding routes so moved labels do not accumulate.
for (const text of db.schematic_text.list())
if (text.source_trace_id)
db.schematic_text.delete(text.schematic_text_id);
for (const type of ["schematic_trace", "schematic_net_label"])
for (const e of db[type].list()) db[type].delete(e[`${type}_id`]);
for (const group of [...board.getDescendants(), board].filter(
(g: any) => g.isSubcircuit && g.doInitialSchematicTraceRender,
))
group.doInitialSchematicTraceRender();
}
// Associate custom symbol annotations with their owning component before translating it.
for (const primitive of board.getDescendants()) {
if (!primitive.schematic_text_id) continue;
const text = db.schematic_text.get(primitive.schematic_text_id);
let parent = primitive.parent;
while (parent && !parent.schematic_component_id) parent = parent.parent;
if (text && parent?.schematic_component_id)
text.schematic_component_id = parent.schematic_component_id;
}
// Restore explanatory text above its own section and include it in section packing.
const sectionNotes = new Map<string, { owner: any; notes: typeof notes }>();
for (const note of notes) {
const group = note.sectionName
? db.schematic_group
.list()
.find((g: any) => g.name === `layout_${note.sectionName}`)
: undefined;
const owner = note.ref
? byName(note.ref)
: components().find(
(c: any) => c.schematic_group_id === group?.schematic_group_id,
);
if (!owner)
throw new Error(`No section for schematic text: ${note.text}`);
const entry = sectionNotes.get(owner.schematic_group_id) ?? {
owner,
notes: [],
};
entry.notes.push(note);
sectionNotes.set(owner.schematic_group_id, entry);
}
for (const [groupId, { owner, notes: groupNotes }] of sectionNotes) {
const members = components().filter(
(c: any) => c.schematic_group_id === groupId,
);
const left = Math.min(
...members.map((c: any) => c.center.x - c.size.width / 2),
);
const top = Math.max(
...members.map((c: any) => c.center.y + c.size.height / 2),
);
const lines = groupNotes.flatMap((note) => {
const wrapped: string[] = [];
let line = "";
for (const word of note.text.split(" ")) {
if ((line + " " + word).trim().length > 54) {
wrapped.push(line);
line = word;
} else line = (line + " " + word).trim();
}
if (line) wrapped.push(line);
return wrapped.map((text) => ({ ...note, text }));
});
lines.forEach((note, i) =>
db.schematic_text.insert({
text: note.text,
position: { x: left, y: top + 1.4 + (lines.length - i) * 0.38 },
anchor: "top_left",
font_size: note.fontSize,
color: note.color,
rotation: 0,
schematic_component_id: owner.schematic_component_id,
schematic_sheet_id: owner.schematic_sheet_id,
}),
);
}
// Pack whole sections using their measured bounds; component placements remain automatic.
const sheets = db.schematic_sheet.list();
for (const sheet of sheets) {
const groups = new Map<string, any[]>();
for (const c of components().filter(
(c: any) => c.schematic_sheet_id === sheet.schematic_sheet_id,
)) {
const list = groups.get(c.schematic_group_id) ?? [];
list.push(c);
groups.set(c.schematic_group_id, list);
}
const blocks = [...groups.values()]
.map((members) => {
const texts = db.schematic_text
.list()
.filter((t: any) =>
members.some(
(c) => c.schematic_component_id === t.schematic_component_id,
),
);
const minX = Math.min(
...members.map((c) => c.center.x - c.size.width / 2),
...texts.map((t: any) => t.position.x),
);
const maxX = Math.max(
...members.map((c) => c.center.x + c.size.width / 2),
...texts.map(
(t: any) => t.position.x + t.text.length * t.font_size * 0.62,
),
);
const minY = Math.min(
...members.map((c) => c.center.y - c.size.height / 2),
);
const maxY = Math.max(
...members.map((c) => c.center.y + c.size.height / 2),
...texts.map((t: any) => t.position.y),
);
return {
members,
minX,
maxY,
width: maxX - minX + 3,
height: maxY - minY + 3,
};
})
.sort((a, b) => b.height - a.height);
let x = 0,
y = 0,
rowHeight = 0;
for (const block of blocks) {
if (x + block.width > 30 && x > 0) {
x = 0;
y += rowHeight;
rowHeight = 0;
}
const group = db.schematic_group.get(
block.members[0].schematic_group_id,
);
// Use the open space at the left of the encoder/alarm sheet. This
// translates the whole section, preserving its automatic internal layout.
const sectionOffsetX = group?.name === "layout_encoder" ? -3 : 0;
for (const c of block.members)
move(
c,
c.center.x - block.minX + x + sectionOffsetX,
c.center.y - block.maxY - y,
);
x += block.width;
rowHeight = Math.max(rowHeight, block.height);
}
}
const sections = board
.getDescendants()
.filter((c: any) => c.componentName === "SchematicSection");
// Core's section bounds omit routed labels. Include labels owned by a
// connected port in the section so a divider cannot cross their text.
for (const section of sections) {
const computeBounds = section._computeSectionBounds.bind(section);
section._computeSectionBounds = (
root: any,
name: string,
sheetId: string,
) => {
const bounds = computeBounds(root, name, sheetId);
if (!bounds) return bounds;
const memberIds = new Set(
root
.getDescendants()
.filter((c: any) => c.getSchematicSectionName() === name)
.map((c: any) => c.schematic_component_id)
.filter(Boolean),
);
for (const label of db.schematic_net_label.list()) {
if (label.schematic_sheet_id !== sheetId || !label.source_trace_id)
continue;
const trace = db.source_trace.get(label.source_trace_id);
const anchor = label.anchor_position ?? label.center;
const ports = db.schematic_port
.list()
.filter(
(p: any) =>
p.schematic_sheet_id === sheetId &&
trace?.connected_source_port_ids.includes(p.source_port_id),
);
const nearestPort = ports.sort(
(a: any, b: any) =>
Math.hypot(a.center.x - anchor.x, a.center.y - anchor.y) -
Math.hypot(b.center.x - anchor.x, b.center.y - anchor.y),
)[0];
if (!memberIds.has(nearestPort?.schematic_component_id)) continue;
const vertical =
label.anchor_side === "top" || label.anchor_side === "bottom";
const length = label.text.length * 0.13 + 0.2;
const labelBounds = vertical
? {
minX: label.center.x - 0.1,
maxX: label.center.x + 0.1,
minY: anchor.y - (label.anchor_side === "top" ? length : 0),
maxY: anchor.y + (label.anchor_side === "bottom" ? length : 0),
}
: {
minX: Math.min(anchor.x, 2 * label.center.x - anchor.x) - 0.1,
maxX: Math.max(anchor.x, 2 * label.center.x - anchor.x) + 0.1,
minY: label.center.y - 0.1,
maxY: label.center.y + 0.1,
};
bounds.minX = Math.min(bounds.minX, labelBounds.minX);
bounds.maxX = Math.max(bounds.maxX, labelBounds.maxX);
bounds.minY = Math.min(bounds.minY, labelBounds.minY);
bounds.maxY = Math.max(bounds.maxY, labelBounds.maxY);
}
return bounds;
};
}
function renderSections() {
for (const e of db.schematic_line.list())
if (e.is_dashed && !e.schematic_component_id)
db.schematic_line.delete(e.schematic_line_id);
for (const e of db.schematic_text.list())
if (
!e.schematic_component_id &&
!e.schematic_symbol_id &&
!e.source_trace_id
)
db.schematic_text.delete(e.schematic_text_id);
for (const section of sections) section.doInitialSchematicSectionRender();
}
reroute();
renderSections();
const history: any[] = [];
for (let iteration = 0; iteration < 60; iteration++) {
const issues: any[] = analyzeSchematicPlacement(db.toArray()).getIssues();
history.push(issues.map((i) => i.lineItemType));
if (!issues.length) break;
let changed = false;
const moved = new Set<string>();
const apply = (c: any, x: number, y: number, r = 0) => {
if (!c || moved.has(c.schematic_component_id)) return;
move(c, x, y, r);
moved.add(c.schematic_component_id);
changed = true;
};
for (const issue of issues) {
const type = issue.lineItemType;
if (type === "SchematicTextCollision") {
const t = db.schematic_text.get(issue.schematicTextId);
if (t && issue.suggestedMove) {
t.position = {
x: issue.suggestedMove.newSchX,
y: issue.suggestedMove.newSchY,
};
changed = true;
}
} else if (type === "ComponentOverlap") {
const suggestion = issue.correctionSuggestions[0];
const c = byName(suggestion.targetComponentName);
if (c)
apply(
c,
suggestion.newSchX + Math.sign(suggestion.deltaSchX) * 0.25,
suggestion.newSchY + Math.sign(suggestion.deltaSchY) * 0.25,
);
} else if (type === "NetLabelCollision") {
for (const m of issue.moves ?? [])
apply(byName(m.componentName), m.newSchX, m.newSchY);
} else if (type === "TraceCanBeSimplifiedByMovingComponent") {
apply(fromBox(issue.targetComponent), issue.newSchX, issue.newSchY);
} else if (
[
"CapacitorSymbolHorizontal",
"TwoPinComponentCouldBeFlipped",
"TwoPinComponentHasInvertedRails",
"TwoPinComponentShouldBeVertical",
"DiodeResistorNotAligned",
].includes(type)
) {
const c = fromBox(
issue.schematicBox ??
issue.targetComponent ??
issue.diodeSchematicBox,
);
if (c)
apply(
c,
c.center.x,
c.center.y,
issue.deltaSchRotation ??
(type === "CapacitorSymbolHorizontal" ? 90 : 180),
);
} else if (type === "DecouplingCapacitorsNotCloseTogether") {
const caps = issue.capacitorSchematicBoxes
.map(fromBox)
.filter(Boolean)
.sort((a: any, b: any) => a.center.x - b.center.x);
// Two rows keep the entire same-rail bank compact without hand-placing any capacitor.
const x = caps[0].center.x,
y = caps[0].center.y;
caps.forEach((c: any, i: number) =>
apply(c, x + (i % 3) * 1.25, y - Math.floor(i / 3) * 2.4),
);
}
}
if (!changed) break;
reroute();
// Recompute dividers when component geometry changes, but keep the
// analyzer's final title-only moves instead of resetting them each pass.
if (moved.size) renderSections();
}
const remaining = analyzeSchematicPlacement(db.toArray()).getIssues();
board.__schematicStyleHistory = history;
if (remaining.length)
throw new Error(
`Automatic schematic style refinement did not converge: ${remaining.map((i: any) => i.lineItemType).join(", ")}`,
);
// Sheet warnings must be recomputed after moving the automatic layout.
for (const e of db.schematic_element_outside_sheet_warning.list())
db.schematic_element_outside_sheet_warning.delete(
e.schematic_element_outside_sheet_warning_id,
);
for (const c of board.getDescendants())
if (c.componentName === "SchematicSheet")
c.doInitialSchematicSheetRender();
original();
};
}
