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.
This commit is contained in:
David Montero Crespo 2026-07-26 00:59:58 +02:00
parent fd6829f645
commit 0f68037c35
4 changed files with 0 additions and 376 deletions

View File

@ -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;
}
}

View File

@ -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<number>;
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<HTMLElement>;
}
export const CanvasMinimap: React.FC<Props> = ({
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<HTMLDivElement>(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<HTMLDivElement>) => {
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 (
<div
ref={minimapRef}
className="canvas-minimap"
onPointerDown={onPointerDown}
style={{ width: MINIMAP_W, height: MINIMAP_H }}
aria-label="Canvas minimap"
>
<div className="canvas-minimap-world">
{boards.map((b) => {
const size = BOARD_SIZE[b.boardKind] ?? BOARD_FALLBACK;
return (
<div
key={b.id}
className="canvas-minimap-board"
style={{
left: b.x * SCALE_X,
top: b.y * SCALE_Y,
width: Math.max(2, size.w * SCALE_X),
height: Math.max(2, size.h * SCALE_Y),
}}
/>
);
})}
{components.map((c) => (
<div
key={c.id}
className="canvas-minimap-component"
style={{
left: c.x * SCALE_X,
top: c.y * SCALE_Y,
}}
/>
))}
</div>
<div
className="canvas-minimap-viewport"
style={{
left: clampedX,
top: clampedY,
width: clampedW,
height: clampedH,
}}
/>
</div>
);
};

View File

@ -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. */}
<CanvasMinimap
pan={pan}
zoom={zoom}
setPan={(p) => {
panRef.current = p;
setPan(p);
}}
panRef={panRef}
zoomRef={zoomRef}
components={components}
boards={boards}
viewportRef={canvasRef}
/>
</div>
</div>

View File

@ -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',