feat: implement auto-pan feature for board visibility after project load and normalize pin names for component connections
parent
66b4fa6030
commit
1206a34e64
|
|
@ -439,6 +439,37 @@ export const SimulatorCanvas = () => {
|
||||||
return () => timers.forEach(t => clearTimeout(t));
|
return () => timers.forEach(t => clearTimeout(t));
|
||||||
}, [components, recalculateAllWirePositions]);
|
}, [components, recalculateAllWirePositions]);
|
||||||
|
|
||||||
|
// Auto-pan to keep the board visible after a project import/load.
|
||||||
|
// We track the previous component count and only re-center when the count
|
||||||
|
// jumps (indicating the user loaded a new circuit, not just added one part).
|
||||||
|
const prevComponentCountRef = useRef(-1);
|
||||||
|
useEffect(() => {
|
||||||
|
const prev = prevComponentCountRef.current;
|
||||||
|
const curr = components.length;
|
||||||
|
prevComponentCountRef.current = curr;
|
||||||
|
|
||||||
|
// Only re-center when the component list transitions from empty/different
|
||||||
|
// project to a populated one (i.e., a load/import event).
|
||||||
|
const isLoad = curr > 0 && (prev <= 0 || Math.abs(curr - prev) > 2);
|
||||||
|
if (!isLoad) return;
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!canvas) return;
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const currentZoom = zoomRef.current;
|
||||||
|
const newPan = {
|
||||||
|
x: rect.width / 4 - boardPosition.x * currentZoom,
|
||||||
|
y: rect.height / 4 - boardPosition.y * currentZoom,
|
||||||
|
};
|
||||||
|
panRef.current = newPan;
|
||||||
|
setPan(newPan);
|
||||||
|
}, 150);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [components.length]);
|
||||||
|
|
||||||
// Render component using dynamic renderer
|
// Render component using dynamic renderer
|
||||||
const renderComponent = (component: any) => {
|
const renderComponent = (component: any) => {
|
||||||
const metadata = registry.getById(component.metadataId);
|
const metadata = registry.getById(component.metadataId);
|
||||||
|
|
|
||||||
|
|
@ -497,6 +497,11 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
||||||
);
|
);
|
||||||
if (startPos) {
|
if (startPos) {
|
||||||
updated.start = { ...wire.start, x: startPos.x, y: startPos.y };
|
updated.start = { ...wire.start, x: startPos.x, y: startPos.y };
|
||||||
|
} else {
|
||||||
|
// Pin name not found in element's pinInfo (e.g. board type mismatch).
|
||||||
|
// Fall back to the component/board position so the wire renders near
|
||||||
|
// its endpoint rather than at the canvas origin (0,0).
|
||||||
|
updated.start = { ...wire.start, x: startX, y: startY };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve end component position
|
// Resolve end component position
|
||||||
|
|
@ -512,6 +517,8 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
||||||
);
|
);
|
||||||
if (endPos) {
|
if (endPos) {
|
||||||
updated.end = { ...wire.end, x: endPos.x, y: endPos.y };
|
updated.end = { ...wire.end, x: endPos.x, y: endPos.y };
|
||||||
|
} else {
|
||||||
|
updated.end = { ...wire.end, x: endX, y: endY };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-generate control points for wires that have none
|
// Auto-generate control points for wires that have none
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,22 @@ const BOARD_TO_WOKWI_ID: Record<string, string> = {
|
||||||
'raspberry-pi-pico': 'pico',
|
'raspberry-pi-pico': 'pico',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Pin name aliases ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Maps Wokwi connection "signal" pin names to wokwi-element physical pin names.
|
||||||
|
// Wokwi boards (e.g. board-ssd1306) use different naming than the bare elements.
|
||||||
|
const COMPONENT_PIN_ALIASES: Record<string, Record<string, string>> = {
|
||||||
|
'ssd1306': {
|
||||||
|
'SDA': 'DATA',
|
||||||
|
'SCL': 'CLK',
|
||||||
|
'VCC': 'VIN',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizePinName(metadataId: string, pinName: string): string {
|
||||||
|
return COMPONENT_PIN_ALIASES[metadataId]?.[pinName] ?? pinName;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Color helpers ─────────────────────────────────────────────────────────────
|
// ── Color helpers ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const COLOR_NAME_TO_HEX: Record<string, string> = {
|
const COLOR_NAME_TO_HEX: Record<string, string> = {
|
||||||
|
|
@ -205,20 +221,27 @@ export async function importFromWokwiZip(file: File): Promise<ImportResult> {
|
||||||
};
|
};
|
||||||
const velxioBoardId = VELXIO_BOARD_ID[boardType] ?? 'arduino-uno';
|
const velxioBoardId = VELXIO_BOARD_ID[boardType] ?? 'arduino-uno';
|
||||||
|
|
||||||
// Board position from diagram (use directly as Velxio board position)
|
// Board position from diagram. Apply a minimum offset so the board is never
|
||||||
|
// crammed against the canvas top-left corner (Wokwi diagrams often use 0,0).
|
||||||
|
const MIN_OFFSET = 50;
|
||||||
|
const rawBoardX = boardPart?.left ?? MIN_OFFSET;
|
||||||
|
const rawBoardY = boardPart?.top ?? MIN_OFFSET;
|
||||||
|
const offsetX = rawBoardX < MIN_OFFSET ? MIN_OFFSET - rawBoardX : 0;
|
||||||
|
const offsetY = rawBoardY < MIN_OFFSET ? MIN_OFFSET - rawBoardY : 0;
|
||||||
const boardPosition = {
|
const boardPosition = {
|
||||||
x: boardPart?.left ?? 50,
|
x: rawBoardX + offsetX,
|
||||||
y: boardPart?.top ?? 50,
|
y: rawBoardY + offsetY,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Convert non-board parts to Velxio components (use Wokwi coords directly)
|
// Convert non-board parts to Velxio components.
|
||||||
|
// Apply the same offset so components keep their relative position to the board.
|
||||||
const components: VelxioComponent[] = diagram.parts
|
const components: VelxioComponent[] = diagram.parts
|
||||||
.filter((p) => !WOKWI_TYPE_TO_BOARD[p.type])
|
.filter((p) => !WOKWI_TYPE_TO_BOARD[p.type])
|
||||||
.map((p) => ({
|
.map((p) => ({
|
||||||
id: p.id,
|
id: p.id,
|
||||||
metadataId: wokwiTypeToMetadataId(p.type),
|
metadataId: wokwiTypeToMetadataId(p.type),
|
||||||
x: p.left,
|
x: p.left + offsetX,
|
||||||
y: p.top,
|
y: p.top + offsetY,
|
||||||
properties: { ...p.attrs },
|
properties: { ...p.attrs },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
@ -236,10 +259,17 @@ export async function importFromWokwiZip(file: File): Promise<ImportResult> {
|
||||||
const startId = startCompRaw === boardId ? velxioBoardId : startCompRaw;
|
const startId = startCompRaw === boardId ? velxioBoardId : startCompRaw;
|
||||||
const endId = endCompRaw === boardId ? velxioBoardId : endCompRaw;
|
const endId = endCompRaw === boardId ? velxioBoardId : endCompRaw;
|
||||||
|
|
||||||
|
// Normalize pin names: Wokwi uses signal names (SDA, SCL, VCC) while
|
||||||
|
// wokwi-elements use physical/board pin names (DATA, CLK, VIN).
|
||||||
|
const startMetadataId = components.find((c) => c.id === startId)?.metadataId ?? '';
|
||||||
|
const endMetadataId = components.find((c) => c.id === endId)?.metadataId ?? '';
|
||||||
|
const normalizedStartPin = normalizePinName(startMetadataId, startPin);
|
||||||
|
const normalizedEndPin = normalizePinName(endMetadataId, endPin);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: `wire-${i}-${Date.now()}`,
|
id: `wire-${i}-${Date.now()}`,
|
||||||
start: { componentId: startId, pinName: startPin, x: 0, y: 0 },
|
start: { componentId: startId, pinName: normalizedStartPin, x: 0, y: 0 },
|
||||||
end: { componentId: endId, pinName: endPin, x: 0, y: 0 },
|
end: { componentId: endId, pinName: normalizedEndPin, x: 0, y: 0 },
|
||||||
controlPoints: [],
|
controlPoints: [],
|
||||||
color: colorToHex(color),
|
color: colorToHex(color),
|
||||||
signalType: 'digital' as const,
|
signalType: 'digital' as const,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue