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();
	};
}