From 0f68037c354264646477a7b5d2dcbd65b9dc3ff5 Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Sun, 26 Jul 2026 00:59:58 +0200 Subject: [PATCH] feat(canvas): fuera el minimapa Se anclaba a la esquina inferior derecha del lienzo y tapaba una parte del area util del circuito, que es justo donde se trabaja. Quien pana y hace zoom no lo necesitaba para orientarse, asi que se retira entero en vez de esconderlo tras un interruptor mas. Se van el componente y su CSS (no los usaba nadie mas) y la linea del listado de novedades que lo anunciaba, que ya no seria cierta. --- .../components/simulator/CanvasMinimap.css | 78 ----- .../components/simulator/CanvasMinimap.tsx | 278 ------------------ .../components/simulator/SimulatorCanvas.tsx | 19 -- frontend/src/pages/Velxio3Page.tsx | 1 - 4 files changed, 376 deletions(-) delete mode 100644 frontend/src/components/simulator/CanvasMinimap.css delete mode 100644 frontend/src/components/simulator/CanvasMinimap.tsx diff --git a/frontend/src/components/simulator/CanvasMinimap.css b/frontend/src/components/simulator/CanvasMinimap.css deleted file mode 100644 index 79f5590a..00000000 --- a/frontend/src/components/simulator/CanvasMinimap.css +++ /dev/null @@ -1,78 +0,0 @@ -/* Minimap overlay — bottom-right corner of .canvas-content. */ -.canvas-minimap { - position: absolute; - bottom: 16px; - right: 16px; - z-index: 20; - - /* Frame: subtle dark surface that sits one step above the canvas bg. */ - background: rgba(20, 20, 22, 0.9); - border: 1px solid rgba(255, 255, 255, 0.12); - border-radius: 8px; - box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35); - backdrop-filter: blur(6px); - - /* Disable text selection during drag — touch-action:none on the - * canvas-content already prevents browser-level pan/zoom, but we set - * it here too so the minimap works inside other containers. */ - user-select: none; - touch-action: none; - overflow: hidden; - cursor: crosshair; -} - -.canvas-minimap-world { - position: absolute; - inset: 0; -} - -/* Boards are the visual anchor — render them in the brand blue with - * enough opacity to dominate over the component dots. */ -.canvas-minimap-board { - position: absolute; - background: rgba(0, 113, 227, 0.55); - border: 0.5px solid rgba(0, 113, 227, 0.85); - border-radius: 1px; - pointer-events: none; -} - -/* Components: small fixed-size dots, slightly transparent so a cluster - * still reads as 'busy area' without becoming a solid bar. */ -.canvas-minimap-component { - position: absolute; - width: 3px; - height: 3px; - background: rgba(255, 255, 255, 0.55); - border-radius: 1px; - pointer-events: none; -} - -/* Viewport rectangle — white outline + slight inner fill. The - * pointer-events: none lets the minimap's pointerdown handler decide - * whether the click landed inside (drag) or outside (teleport) - * based on the calculated rect bounds. */ -.canvas-minimap-viewport { - position: absolute; - border: 1.5px solid rgba(239, 68, 68, 0.95); - background: rgba(239, 68, 68, 0.10); - border-radius: 2px; - pointer-events: none; - /* No transition on left/top — those values change every frame during - * a pan and a transition would lag the user's drag. */ -} - -/* Hover state on the minimap itself: subtle brightness bump so the user - * knows it's interactive. */ -.canvas-minimap:hover { - border-color: rgba(255, 255, 255, 0.22); -} - -@media (max-width: 720px) { - /* On phones the minimap eats too much of the canvas; shrink it. */ - .canvas-minimap { - width: 90px !important; - height: 68px !important; - bottom: 12px; - right: 12px; - } -} diff --git a/frontend/src/components/simulator/CanvasMinimap.tsx b/frontend/src/components/simulator/CanvasMinimap.tsx deleted file mode 100644 index 164157ea..00000000 --- a/frontend/src/components/simulator/CanvasMinimap.tsx +++ /dev/null @@ -1,278 +0,0 @@ -/** - * CanvasMinimap — bottom-right corner overview of the whole canvas with a - * draggable viewport rectangle. - * - * Geometry mirrors the canvas: - * .canvas-world is 4000×3000 px in world space. - * .canvas-content is the viewport, sized to the available area at runtime. - * .canvas-world's transform is `translate(pan.x, pan.y) scale(zoom)`. - * - * The minimap is MINIMAP_W × MINIMAP_H (same 4:3 aspect as the world, so - * SCALE_X === SCALE_Y). A world point (wx, wy) renders at (wx*SCALE, wy*SCALE). - * - * The viewport rectangle shows what fraction of the world is visible: - * rect.x = -pan.x / zoom * SCALE - * rect.y = -pan.y / zoom * SCALE - * rect.w = viewport.width / zoom * SCALE - * rect.h = viewport.height / zoom * SCALE - * - * LIVE TRACKING: while the user pans/zooms, SimulatorCanvas deliberately - * bypasses React state — it writes panRef/zoomRef and mutates the - * .canvas-world transform directly for zero-lag dragging, committing to - * state only on pointer-up. The minimap therefore CANNOT rely on the pan - * prop alone (the red rectangle would freeze mid-drag — reported bug). It - * reads panRef/zoomRef inside a requestAnimationFrame loop and updates its - * own tiny local state only when the values actually change: the canvas - * stays render-free during the gesture, and only this small component - * re-renders, at most once per frame. - * - * Interaction: - * - Pointer down OUTSIDE the rectangle → teleport: center the viewport - * on the clicked world point. - * - Pointer down INSIDE the rectangle → drag mode: live-pan the canvas. - */ -import { useCallback, useEffect, useRef, useState } from 'react'; -import type { Component } from '../../types/components'; -import type { BoardInstance } from '../../types/board'; -import { BOARD_SIZE } from './BoardOnCanvas'; -import './CanvasMinimap.css'; - -const MINIMAP_W = 160; -const MINIMAP_H = 120; -const WORLD_W = 4000; -const WORLD_H = 3000; -const SCALE_X = MINIMAP_W / WORLD_W; -const SCALE_Y = MINIMAP_H / WORLD_H; -// Fallback footprint for boards missing from the BOARD_SIZE table. -const BOARD_FALLBACK = { w: 300, h: 200 }; - -interface Props { - pan: { x: number; y: number }; - zoom: number; - setPan: (p: { x: number; y: number }) => void; - /** Live pan/zoom refs from SimulatorCanvas. During a canvas drag these are - * the ONLY up-to-date source — React state lags until pointer-up. */ - panRef: React.MutableRefObject<{ x: number; y: number }>; - zoomRef: React.MutableRefObject; - components: Component[]; - boards: BoardInstance[]; - /** Ref to the .canvas-content element — we read its size to compute the - * viewport rectangle, since the canvas viewport changes when the user - * resizes their window or toggles a sidebar. */ - viewportRef: React.RefObject; -} - -export const CanvasMinimap: React.FC = ({ - pan, - zoom, - setPan, - panRef, - zoomRef, - components, - boards, - viewportRef, -}) => { - // Live pan/zoom mirrored from the refs via rAF (see header comment). - const [live, setLive] = useState({ x: pan.x, y: pan.y, zoom }); - useEffect(() => { - let raf = 0; - const tick = () => { - const p = panRef.current; - const z = zoomRef.current; - setLive((cur) => - cur.x === p.x && cur.y === p.y && cur.zoom === z ? cur : { x: p.x, y: p.y, zoom: z }, - ); - raf = requestAnimationFrame(tick); - }; - raf = requestAnimationFrame(tick); - return () => cancelAnimationFrame(raf); - }, [panRef, zoomRef]); - - // Visible viewport size in CSS pixels — listens for resize so the - // rectangle stays accurate when the user opens / closes side panels. - const [vp, setVp] = useState<{ w: number; h: number }>({ w: 0, h: 0 }); - useEffect(() => { - const el = viewportRef.current; - if (!el) return; - const update = () => { - const r = el.getBoundingClientRect(); - setVp({ w: r.width, h: r.height }); - }; - update(); - const ro = new ResizeObserver(update); - ro.observe(el); - return () => ro.disconnect(); - }, [viewportRef]); - - // The viewport rectangle in minimap coordinates (from LIVE values). - const rectX = (-live.x / live.zoom) * SCALE_X; - const rectY = (-live.y / live.zoom) * SCALE_Y; - const rectW = (vp.w / live.zoom) * SCALE_X; - const rectH = (vp.h / live.zoom) * SCALE_Y; - - // Clamp the rectangle to the minimap bounds so it never paints outside - // (transiently during pinch-zoom-out, and persistently when the user - // pans past a world edge). We hit-test against these clamped values - // too — otherwise clicking the visible red rect can fall outside the - // unclamped logical rect and erroneously trigger a teleport instead - // of a drag. - const clampedW = Math.min(rectW, MINIMAP_W); - const clampedH = Math.min(rectH, MINIMAP_H); - const clampedX = Math.max(0, Math.min(MINIMAP_W - clampedW, rectX)); - const clampedY = Math.max(0, Math.min(MINIMAP_H - clampedH, rectY)); - - // Drag state. We use a ref + window listeners (rather than React's - // onMouseMove on the minimap div) so the gesture keeps working even if - // the cursor leaves the minimap during a fast pan. - const dragRef = useRef< - | { - // Mouse position at mousedown. - mouseX: number; - mouseY: number; - // Pan at mousedown — we add (delta-converted-to-world) to this. - panX: number; - panY: number; - } - | null - >(null); - - const minimapRef = useRef(null); - - // Clamp pan so the viewport rectangle never escapes the minimap. World - // is 4000×3000; minimum visible is whatever fits at the current zoom. - const clampPan = useCallback( - (next: { x: number; y: number }): { x: number; y: number } => { - const z = zoomRef.current; - // Visible-area limits expressed in pan-space: - // pan.x = 0 → world x=0 at left edge of viewport. - // pan.x = -(WORLD_W*zoom - vp.w) → world right edge at right of viewport. - const minX = -(WORLD_W * z - vp.w); - const minY = -(WORLD_H * z - vp.h); - return { - x: Math.min(0, Math.max(minX, next.x)), - y: Math.min(0, Math.max(minY, next.y)), - }; - }, - [zoomRef, vp.w, vp.h], - ); - - const teleportTo = useCallback( - (worldX: number, worldY: number) => { - const z = zoomRef.current; - // Center the viewport on (worldX, worldY). - setPan( - clampPan({ - x: -worldX * z + vp.w / 2, - y: -worldY * z + vp.h / 2, - }), - ); - }, - [zoomRef, vp.w, vp.h, setPan, clampPan], - ); - - const onPointerDown = useCallback( - (e: React.PointerEvent) => { - e.stopPropagation(); - const minimap = minimapRef.current; - if (!minimap) return; - const rect = minimap.getBoundingClientRect(); - const localX = e.clientX - rect.left; - const localY = e.clientY - rect.top; - const insideRect = - localX >= clampedX && - localX <= clampedX + clampedW && - localY >= clampedY && - localY <= clampedY + clampedH; - if (insideRect) { - // Drag mode — record start state, window listeners do the rest. - dragRef.current = { - mouseX: e.clientX, - mouseY: e.clientY, - panX: panRef.current.x, - panY: panRef.current.y, - }; - e.currentTarget.setPointerCapture(e.pointerId); - } else { - // Teleport — convert minimap-local click to world coords. - teleportTo(localX / SCALE_X, localY / SCALE_Y); - } - }, - [clampedX, clampedY, clampedW, clampedH, panRef, teleportTo], - ); - - useEffect(() => { - const onMove = (e: PointerEvent) => { - const drag = dragRef.current; - if (!drag) return; - const dx = e.clientX - drag.mouseX; - const dy = e.clientY - drag.mouseY; - // (delta in minimap px) / SCALE = (delta in world units, unscaled). - // (delta in world units) * zoom = (delta to add to pan, but inverted). - const z = zoomRef.current; - setPan( - clampPan({ - x: drag.panX - (dx / SCALE_X) * z, - y: drag.panY - (dy / SCALE_Y) * z, - }), - ); - }; - const onUp = () => { - dragRef.current = null; - }; - window.addEventListener('pointermove', onMove); - window.addEventListener('pointerup', onUp); - window.addEventListener('pointercancel', onUp); - return () => { - window.removeEventListener('pointermove', onMove); - window.removeEventListener('pointerup', onUp); - window.removeEventListener('pointercancel', onUp); - }; - }, [zoomRef, setPan, clampPan]); - - return ( -
-
- {boards.map((b) => { - const size = BOARD_SIZE[b.boardKind] ?? BOARD_FALLBACK; - return ( -
- ); - })} - {components.map((c) => ( -
- ))} -
-
-
- ); -}; diff --git a/frontend/src/components/simulator/SimulatorCanvas.tsx b/frontend/src/components/simulator/SimulatorCanvas.tsx index 614b9234..13f02796 100644 --- a/frontend/src/components/simulator/SimulatorCanvas.tsx +++ b/frontend/src/components/simulator/SimulatorCanvas.tsx @@ -21,7 +21,6 @@ import { WireLayer } from './WireLayer'; import type { SegmentHandle, WaypointHandle, AlignmentGuide } from './WireLayer'; import { ElectricalOverlay } from '../analog-ui/ElectricalOverlay'; import { BoardOnCanvas } from './BoardOnCanvas'; -import { CanvasMinimap } from './CanvasMinimap'; import { PartSimulationRegistry } from '../../simulation/parts'; import { PROPERTY_CHANGE_EVENT, type PropertyChangeDetail } from '../../simulation/parts/partUtils'; import { mountDigitalGateEngine } from '../../simulation/digital/digitalGateController'; @@ -2993,24 +2992,6 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => { } return null; })()} - - {/* Minimap — small overview of the world with a draggable viewport - rectangle, anchored to the canvas-content bottom-right corner. - Sits inside .canvas-content (not .canvas-world) so it stays - fixed while the world pans / zooms. */} - { - panRef.current = p; - setPan(p); - }} - panRef={panRef} - zoomRef={zoomRef} - components={components} - boards={boards} - viewportRef={canvasRef} - />
diff --git a/frontend/src/pages/Velxio3Page.tsx b/frontend/src/pages/Velxio3Page.tsx index 01854d43..b9c524ef 100644 --- a/frontend/src/pages/Velxio3Page.tsx +++ b/frontend/src/pages/Velxio3Page.tsx @@ -238,7 +238,6 @@ const CHANGE_SECTIONS = [ color: '#c8701a', items: [ 'Full undo/redo: components, wires, moves, rotations, properties', - 'Draggable minimap with a moveable viewport', 'Drag-to-move parts while the simulation runs', 'Wires follow component rotation automatically', 'Component deletion cascades to connected wires',