2026-03-15 08:55:13 +07:00
|
|
|
/**
|
|
|
|
|
* Wire utilities: path generation, auto-coloring, keyboard color shortcuts.
|
|
|
|
|
* Implements Wokwi-style orthogonal wire routing.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/** Keyboard shortcut → hex color (matches Wokwi's color palette) */
|
|
|
|
|
export const WIRE_KEY_COLORS: Record<string, string> = {
|
|
|
|
|
'0': '#000000', // Black
|
|
|
|
|
'1': '#8B4513', // Brown
|
|
|
|
|
'2': '#cc0000', // Red
|
|
|
|
|
'3': '#FF8C00', // Orange
|
|
|
|
|
'4': '#FFD700', // Gold
|
|
|
|
|
'5': '#22c55e', // Green
|
|
|
|
|
'6': '#0000cc', // Blue
|
|
|
|
|
'7': '#8B00FF', // Violet
|
|
|
|
|
'8': '#808080', // Gray
|
|
|
|
|
'9': '#FFFFFF', // White
|
2026-04-22 02:45:45 +07:00
|
|
|
c: '#00FFFF', // Cyan
|
|
|
|
|
l: '#32CD32', // Limegreen
|
|
|
|
|
m: '#FF00FF', // Magenta
|
|
|
|
|
p: '#800080', // Purple
|
|
|
|
|
y: '#FFFF00', // Yellow
|
2026-03-15 08:55:13 +07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** Default wire color when no specific signal is detected */
|
|
|
|
|
export const DEFAULT_WIRE_COLOR = '#22c55e';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Automatically determine wire color from the starting pin name.
|
|
|
|
|
* GND → black, VCC/5V/3.3V/VBUS/VIN → red, everything else → green.
|
|
|
|
|
*/
|
|
|
|
|
export function autoWireColor(pinName: string): string {
|
|
|
|
|
const lower = pinName.toLowerCase();
|
2026-04-22 02:45:45 +07:00
|
|
|
if (lower.includes('gnd') || lower === 'ground' || lower === '-' || lower.startsWith('gnd')) {
|
2026-03-15 08:55:13 +07:00
|
|
|
return '#000000';
|
|
|
|
|
}
|
|
|
|
|
if (
|
|
|
|
|
lower.includes('vcc') ||
|
|
|
|
|
lower.includes('5v') ||
|
|
|
|
|
lower.includes('3.3v') ||
|
|
|
|
|
lower.includes('3v3') ||
|
|
|
|
|
lower.includes('vbus') ||
|
|
|
|
|
lower.includes('vin') ||
|
|
|
|
|
lower === 'power' ||
|
|
|
|
|
lower === '+' ||
|
|
|
|
|
lower.startsWith('vcc') ||
|
|
|
|
|
lower.startsWith('v+')
|
|
|
|
|
) {
|
|
|
|
|
return '#cc0000';
|
|
|
|
|
}
|
|
|
|
|
return DEFAULT_WIRE_COLOR;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
interface Point {
|
|
|
|
|
x: number;
|
|
|
|
|
y: number;
|
|
|
|
|
}
|
2026-03-15 08:55:13 +07:00
|
|
|
|
|
|
|
|
/**
|
2026-07-18 10:02:39 +07:00
|
|
|
* Corner radius (world px) for rounded wire bends, Wokwi-style. Each bend
|
|
|
|
|
* clamps to half the length of its shorter adjacent segment so short
|
|
|
|
|
* segments never overshoot.
|
2026-03-15 08:55:13 +07:00
|
|
|
*/
|
2026-07-18 10:02:39 +07:00
|
|
|
export const WIRE_BEND_RADIUS = 7;
|
2026-03-15 08:55:13 +07:00
|
|
|
|
2026-07-18 10:02:39 +07:00
|
|
|
/**
|
|
|
|
|
* Expand a stored point chain into the rendered orthogonal polyline.
|
|
|
|
|
* Between consecutive points that are not axis-aligned an L-shape corner
|
|
|
|
|
* is inserted: horizontal to the next point's X, then vertical to its Y.
|
|
|
|
|
*/
|
|
|
|
|
export function expandOrthogonalPoints(points: Point[]): Point[] {
|
|
|
|
|
if (points.length < 2) return points.map((p) => ({ ...p }));
|
|
|
|
|
const out: Point[] = [{ ...points[0] }];
|
2026-03-15 08:55:13 +07:00
|
|
|
for (let i = 1; i < points.length; i++) {
|
|
|
|
|
const prev = points[i - 1];
|
|
|
|
|
const curr = points[i];
|
2026-07-18 10:02:39 +07:00
|
|
|
if (prev.x !== curr.x && prev.y !== curr.y) {
|
|
|
|
|
out.push({ x: curr.x, y: prev.y });
|
2026-03-15 08:55:13 +07:00
|
|
|
}
|
2026-07-18 10:02:39 +07:00
|
|
|
out.push({ ...curr });
|
2026-03-15 08:55:13 +07:00
|
|
|
}
|
2026-07-18 10:02:39 +07:00
|
|
|
return out;
|
|
|
|
|
}
|
2026-03-15 08:55:13 +07:00
|
|
|
|
2026-07-18 10:02:39 +07:00
|
|
|
/**
|
|
|
|
|
* Simplify an orthogonal polyline by removing duplicate points and
|
|
|
|
|
* collapsing collinear/U-turn triples.
|
|
|
|
|
*
|
|
|
|
|
* Three consecutive points sharing the same x (or same y) make the middle
|
|
|
|
|
* one redundant — whether the path goes straight through (collinear) or
|
|
|
|
|
* doubles back over itself (U-turn). Dropping the middle point handles
|
|
|
|
|
* both, which is what eliminates wires rendered on top of themselves.
|
|
|
|
|
*/
|
|
|
|
|
export function simplifyOrthogonalPath(pts: Point[]): Point[] {
|
|
|
|
|
if (pts.length <= 2) return pts.map((p) => ({ ...p }));
|
|
|
|
|
|
|
|
|
|
// Drop consecutive duplicates first
|
|
|
|
|
const dedup: Point[] = [];
|
|
|
|
|
for (const p of pts) {
|
|
|
|
|
const last = dedup[dedup.length - 1];
|
|
|
|
|
if (!last || last.x !== p.x || last.y !== p.y) dedup.push({ ...p });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Iteratively collapse three-in-a-row on the same axis until stable
|
|
|
|
|
let result = dedup;
|
|
|
|
|
let changed = true;
|
|
|
|
|
while (changed && result.length > 2) {
|
|
|
|
|
changed = false;
|
|
|
|
|
for (let i = 1; i < result.length - 1; i++) {
|
|
|
|
|
const prev = result[i - 1];
|
|
|
|
|
const curr = result[i];
|
|
|
|
|
const next = result[i + 1];
|
|
|
|
|
if ((prev.x === curr.x && curr.x === next.x) || (prev.y === curr.y && curr.y === next.y)) {
|
|
|
|
|
result = [...result.slice(0, i), ...result.slice(i + 1)];
|
|
|
|
|
changed = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Build an SVG path through an orthogonal polyline with rounded bends.
|
|
|
|
|
* Every interior corner is shortened by the bend radius on both sides and
|
|
|
|
|
* bridged with a quadratic curve whose control point is the corner itself.
|
|
|
|
|
* Corners whose adjacent segments are too short to fit a visible arc fall
|
|
|
|
|
* back to a hard corner.
|
|
|
|
|
*/
|
|
|
|
|
export function roundedPathFromPoints(pts: Point[], radius: number = WIRE_BEND_RADIUS): string {
|
|
|
|
|
if (pts.length === 0) return '';
|
|
|
|
|
if (pts.length === 1) return `M ${pts[0].x} ${pts[0].y}`;
|
|
|
|
|
|
|
|
|
|
let d = `M ${pts[0].x} ${pts[0].y}`;
|
|
|
|
|
for (let i = 1; i < pts.length - 1; i++) {
|
|
|
|
|
const prev = pts[i - 1];
|
|
|
|
|
const corner = pts[i];
|
|
|
|
|
const next = pts[i + 1];
|
|
|
|
|
const inLen = Math.hypot(corner.x - prev.x, corner.y - prev.y);
|
|
|
|
|
const outLen = Math.hypot(next.x - corner.x, next.y - corner.y);
|
|
|
|
|
const r = Math.min(radius, inLen / 2, outLen / 2);
|
|
|
|
|
if (r < 0.75 || inLen === 0 || outLen === 0) {
|
|
|
|
|
d += ` L ${corner.x} ${corner.y}`;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const inX = corner.x - ((corner.x - prev.x) / inLen) * r;
|
|
|
|
|
const inY = corner.y - ((corner.y - prev.y) / inLen) * r;
|
|
|
|
|
const outX = corner.x + ((next.x - corner.x) / outLen) * r;
|
|
|
|
|
const outY = corner.y + ((next.y - corner.y) / outLen) * r;
|
|
|
|
|
d += ` L ${inX} ${inY} Q ${corner.x} ${corner.y} ${outX} ${outY}`;
|
|
|
|
|
}
|
|
|
|
|
const last = pts[pts.length - 1];
|
|
|
|
|
d += ` L ${last.x} ${last.y}`;
|
2026-03-15 08:55:13 +07:00
|
|
|
return d;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-07-18 10:02:39 +07:00
|
|
|
* Generate the SVG path for a wire: expand the stored points into the
|
|
|
|
|
* orthogonal polyline, drop degenerate geometry (duplicates, collinear
|
|
|
|
|
* runs, U-turns that render the wire on top of itself), then emit rounded
|
|
|
|
|
* bends. The cleanup runs at render time so wires saved with degenerate
|
|
|
|
|
* waypoints display correctly without touching the stored data.
|
|
|
|
|
*/
|
|
|
|
|
export function generateOrthogonalPath(
|
|
|
|
|
start: Point,
|
|
|
|
|
waypoints: Point[] | undefined,
|
|
|
|
|
end: Point,
|
|
|
|
|
): string {
|
|
|
|
|
const points: Point[] = [start, ...(waypoints ?? []), end];
|
|
|
|
|
if (points.length < 2) return '';
|
|
|
|
|
return roundedPathFromPoints(simplifyOrthogonalPath(expandOrthogonalPoints(points)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Elbow point for a leg between `from` and a free point (mouse cursor or
|
|
|
|
|
* the just-clicked destination pin): the longer axis goes first. Returns
|
|
|
|
|
* null when the leg is already axis-aligned (no elbow needed).
|
|
|
|
|
*/
|
|
|
|
|
export function previewElbow(from: Point, x: number, y: number): Point | null {
|
|
|
|
|
const dx = Math.abs(x - from.x);
|
|
|
|
|
const dy = Math.abs(y - from.y);
|
|
|
|
|
if (dx === 0 || dy === 0) return null;
|
|
|
|
|
return dx >= dy ? { x, y: from.y } : { x: from.x, y };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Live preview while drawing: fixed waypoints render like a committed wire;
|
|
|
|
|
* the last leg (to the mouse cursor) adapts its elbow orientation based on
|
|
|
|
|
* whether the horizontal or vertical distance is larger.
|
2026-03-15 08:55:13 +07:00
|
|
|
*/
|
|
|
|
|
export function generatePreviewPath(
|
|
|
|
|
start: Point,
|
|
|
|
|
waypoints: Point[],
|
|
|
|
|
mouseX: number,
|
|
|
|
|
mouseY: number,
|
|
|
|
|
): string {
|
2026-07-18 10:02:39 +07:00
|
|
|
const fixed = expandOrthogonalPoints([start, ...waypoints]);
|
2026-03-15 08:55:13 +07:00
|
|
|
const last = fixed[fixed.length - 1];
|
2026-07-18 10:02:39 +07:00
|
|
|
const elbow = previewElbow(last, mouseX, mouseY);
|
|
|
|
|
const pts = simplifyOrthogonalPath([
|
|
|
|
|
...fixed,
|
|
|
|
|
...(elbow ? [elbow] : []),
|
|
|
|
|
{ x: mouseX, y: mouseY },
|
|
|
|
|
]);
|
|
|
|
|
return roundedPathFromPoints(pts);
|
|
|
|
|
}
|
2026-03-15 08:55:13 +07:00
|
|
|
|
2026-07-18 10:02:39 +07:00
|
|
|
/**
|
|
|
|
|
* Canonical stored waypoints for a wire: every interior corner of the
|
|
|
|
|
* simplified orthogonal polyline, so the stored data matches exactly what
|
|
|
|
|
* is rendered. Call with the final endpoint positions (creation and drag
|
|
|
|
|
* commits) — not with stale/unresolved pins.
|
|
|
|
|
*/
|
|
|
|
|
export function normalizeWireWaypoints(start: Point, waypoints: Point[], end: Point): Point[] {
|
|
|
|
|
const simplified = simplifyOrthogonalPath(expandOrthogonalPoints([start, ...waypoints, end]));
|
|
|
|
|
return simplified.slice(1, -1).map((p) => ({ x: p.x, y: p.y }));
|
2026-03-15 08:55:13 +07:00
|
|
|
}
|