velxio/frontend/src/utils/wireUtils.ts

143 lines
3.6 KiB
TypeScript
Raw Normal View History

/**
* 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
c: '#00FFFF', // Cyan
l: '#32CD32', // Limegreen
m: '#FF00FF', // Magenta
p: '#800080', // Purple
y: '#FFFF00', // Yellow
};
/** 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();
if (lower.includes('gnd') || lower === 'ground' || lower === '-' || lower.startsWith('gnd')) {
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;
}
interface Point {
x: number;
y: number;
}
/**
* Generate an orthogonal SVG path through a sequence of points.
* Between each pair of consecutive points, an L-shape is drawn:
* - Go horizontal to the next point's X, then vertical to its Y.
* This matches Wokwi's routing style.
*/
export function generateOrthogonalPath(
start: Point,
waypoints: Point[] | undefined,
end: Point,
): string {
const points: Point[] = [start, ...(waypoints ?? []), end];
if (points.length < 2) return '';
let d = `M ${points[0].x} ${points[0].y}`;
for (let i = 1; i < points.length; i++) {
const prev = points[i - 1];
const curr = points[i];
const dx = curr.x - prev.x;
const dy = curr.y - prev.y;
if (dx === 0 || dy === 0) {
// Already axis-aligned: straight line
d += ` L ${curr.x} ${curr.y}`;
} else {
// L-shape: go horizontal first, then vertical
d += ` L ${curr.x} ${prev.y} L ${curr.x} ${curr.y}`;
}
}
return d;
}
/**
* Same as generateOrthogonalPath but for live preview:
* the last segment (to mouse cursor) adapts its elbow orientation
* based on whether the horizontal or vertical distance is larger.
*/
export function generatePreviewPath(
start: Point,
waypoints: Point[],
mouseX: number,
mouseY: number,
): string {
const fixed: Point[] = [start, ...waypoints];
const last = fixed[fixed.length - 1];
const mouse: Point = { x: mouseX, y: mouseY };
const dx = Math.abs(mouseX - last.x);
const dy = Math.abs(mouseY - last.y);
// Choose elbow orientation based on distance: longer axis goes first
let elbowX: number;
let elbowY: number;
if (dx >= dy) {
// Horizontal-first
elbowX = mouseX;
elbowY = last.y;
} else {
// Vertical-first
elbowX = last.x;
elbowY = mouseY;
}
// Build the fixed segments
let d = '';
if (fixed.length >= 2) {
d = generateOrthogonalPath(fixed[0], fixed.slice(1), fixed[fixed.length - 1]);
} else {
d = `M ${last.x} ${last.y}`;
}
// Append the live preview segment
if (dx === 0 && dy === 0) return d;
if (dx === 0 || dy === 0) {
d += ` L ${mouse.x} ${mouse.y}`;
} else {
d += ` L ${elbowX} ${elbowY} L ${mouse.x} ${mouse.y}`;
}
return d;
}