mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-13 21:27:41 +02:00
[eric] dashboard: spawn camera is reveal-only (no zoom-in ever, no move when visible, minimal pan otherwise)
This commit is contained in:
@@ -687,6 +687,46 @@ export function useCanvasControls(zoomSensitivity: number = 50, contentBounds?:
|
||||
[cancelAnimation, animateTo, computeFitTarget],
|
||||
);
|
||||
|
||||
// Figma-style spawn camera: never zoom IN, never move if the cards are already on screen; otherwise the minimal pan that reveals them, zooming out only when they cannot fit at the current zoom.
|
||||
const revealCards = useCallback(
|
||||
(cardRects: Array<{ x: number; y: number; width: number; height: number }>) => {
|
||||
const viewport = viewportRef.current;
|
||||
if (!viewport || cardRects.length === 0) return;
|
||||
const v = viewport.getBoundingClientRect();
|
||||
if (v.width <= 0 || v.height <= 0) return;
|
||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||
for (const r of cardRects) {
|
||||
minX = Math.min(minX, r.x);
|
||||
minY = Math.min(minY, r.y);
|
||||
maxX = Math.max(maxX, r.x + r.width);
|
||||
maxY = Math.max(maxY, r.y + r.height);
|
||||
}
|
||||
if (!isFinite(minX)) return;
|
||||
const REVEAL_MARGIN = 48;
|
||||
const cur = stateRef.current;
|
||||
const fitZoom = Math.min(
|
||||
(v.width - REVEAL_MARGIN * 2) / (maxX - minX),
|
||||
(v.height - REVEAL_MARGIN * 2) / (maxY - minY),
|
||||
);
|
||||
const zoom = clamp(Math.min(cur.zoom, fitZoom), MIN_ZOOM, MAX_ZOOM);
|
||||
// If zooming out, keep the viewport-center world point fixed first, then clamp.
|
||||
const ratio = zoom / cur.zoom;
|
||||
let panX = v.width / 2 - (v.width / 2 - cur.panX) * ratio;
|
||||
let panY = v.height / 2 - (v.height / 2 - cur.panY) * ratio;
|
||||
const left = minX * zoom + panX, right = maxX * zoom + panX;
|
||||
if (left < REVEAL_MARGIN) panX += REVEAL_MARGIN - left;
|
||||
else if (right > v.width - REVEAL_MARGIN) panX -= right - (v.width - REVEAL_MARGIN);
|
||||
const top = minY * zoom + panY, bottom = maxY * zoom + panY;
|
||||
if (top < REVEAL_MARGIN) panY += REVEAL_MARGIN - top;
|
||||
else if (bottom > v.height - REVEAL_MARGIN) panY -= bottom - (v.height - REVEAL_MARGIN);
|
||||
const cur2 = stateRef.current;
|
||||
if (Math.abs(panX - cur2.panX) < 2 && Math.abs(panY - cur2.panY) < 2 && Math.abs(zoom - cur2.zoom) < 0.005) return;
|
||||
cancelAnimation();
|
||||
animateTo({ panX, panY, zoom }, FIT_DURATION);
|
||||
},
|
||||
[cancelAnimation, animateTo],
|
||||
);
|
||||
|
||||
const handlers = useMemo(() => ({
|
||||
onMouseDown: handleMouseDown,
|
||||
onMouseMove: handleMouseMove,
|
||||
@@ -694,8 +734,8 @@ export function useCanvasControls(zoomSensitivity: number = 50, contentBounds?:
|
||||
}), [handleMouseDown, handleMouseMove, handleMouseUp]);
|
||||
|
||||
const actions = useMemo(() => ({
|
||||
zoomIn, zoomOut, resetZoom, fitToView, fitToCards, animateTo, cancelAnimation, setState,
|
||||
}), [zoomIn, zoomOut, resetZoom, fitToView, fitToCards, animateTo, cancelAnimation]);
|
||||
zoomIn, zoomOut, resetZoom, fitToView, fitToCards, revealCards, animateTo, cancelAnimation, setState,
|
||||
}), [zoomIn, zoomOut, resetZoom, fitToView, fitToCards, revealCards, animateTo, cancelAnimation]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
EXPANDED_CARD_MIN_H,
|
||||
GRID_GAP,
|
||||
type CardPosition,
|
||||
SPAWN_FOCUS_MAX_ZOOM,
|
||||
} from '@/shared/state/dashboardLayoutSlice';
|
||||
import { generateDashboardName } from '@/shared/state/dashboardsSlice';
|
||||
import type { ContextPath } from '@/app/components/editor/DirectoryBrowser';
|
||||
@@ -199,7 +198,7 @@ export function useAgentSpawn({
|
||||
if (bc) rects.push({ x: bc.x, y: bc.y, width: bc.width, height: bc.height });
|
||||
}
|
||||
}
|
||||
canvasActions.fitToCards(rects, SPAWN_FOCUS_MAX_ZOOM, true, undefined, true);
|
||||
canvasActions.revealCards(rects);
|
||||
handleHighlightCard(draftId);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
DEFAULT_NOTE_W,
|
||||
DEFAULT_NOTE_H,
|
||||
EXPANDED_CARD_MIN_H,
|
||||
SPAWN_FOCUS_MAX_ZOOM,
|
||||
} from '@/shared/state/dashboardLayoutSlice';
|
||||
import type { CardType, useDashboardSelection } from '../state/useDashboardSelection';
|
||||
import type { CanvasActions } from '../interaction/useCanvasControls';
|
||||
@@ -66,7 +65,7 @@ export function useDashboardCardActions({
|
||||
}
|
||||
const card = viewCards[focusKey];
|
||||
if (card) {
|
||||
canvasActions.fitToCards([{ x: card.x, y: card.y, width: card.width, height: card.height }], SPAWN_FOCUS_MAX_ZOOM, true, undefined, true);
|
||||
canvasActions.revealCards([{ x: card.x, y: card.y, width: card.width, height: card.height }]);
|
||||
handleHighlightCard(focusKey);
|
||||
}
|
||||
}, 200);
|
||||
@@ -89,7 +88,7 @@ export function useDashboardCardActions({
|
||||
const newId = Object.keys(allNotes).find((id) => !prevIds.has(id));
|
||||
if (newId) {
|
||||
const note = allNotes[newId];
|
||||
canvasActions.fitToCards([{ x: note.x, y: note.y, width: note.width, height: note.height }], SPAWN_FOCUS_MAX_ZOOM, true, undefined, true);
|
||||
canvasActions.revealCards([{ x: note.x, y: note.y, width: note.width, height: note.height }]);
|
||||
handleHighlightCard(newId);
|
||||
}
|
||||
}, 200);
|
||||
@@ -110,7 +109,7 @@ export function useDashboardCardActions({
|
||||
setTimeout(() => {
|
||||
const card = store.getState().dashboardLayout.cards[sessionId];
|
||||
if (card) {
|
||||
canvasActions.fitToCards([{ x: card.x, y: card.y, width: card.width, height: card.height }], SPAWN_FOCUS_MAX_ZOOM, true);
|
||||
canvasActions.revealCards([{ x: card.x, y: card.y, width: card.width, height: card.height }]);
|
||||
handleHighlightCard(sessionId);
|
||||
}
|
||||
}, 200);
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
clearPendingFocusWorkflowId,
|
||||
clearPendingFocusWorkflowsHub,
|
||||
type ViewCardPosition,
|
||||
SPAWN_FOCUS_MAX_ZOOM,
|
||||
} from '@/shared/state/dashboardLayoutSlice';
|
||||
import { fetchOutputs, type Output } from '@/shared/state/outputsSlice';
|
||||
import { generateDashboardName } from '@/shared/state/dashboardsSlice';
|
||||
@@ -216,7 +215,7 @@ export function useDashboardLifecycle({
|
||||
setTimeout(() => {
|
||||
const card = store.getState().dashboardLayout.cards[agentId];
|
||||
if (card) {
|
||||
canvasActions.fitToCards([{ x: card.x, y: card.y, width: card.width, height: card.height }], SPAWN_FOCUS_MAX_ZOOM, true);
|
||||
canvasActions.revealCards([{ x: card.x, y: card.y, width: card.width, height: card.height }]);
|
||||
handleHighlightCard(agentId);
|
||||
}
|
||||
}, 350);
|
||||
@@ -232,13 +231,7 @@ export function useDashboardLifecycle({
|
||||
setTimeout(() => {
|
||||
const card = store.getState().dashboardLayout.browserCards[browserId];
|
||||
if (card) {
|
||||
canvasActions.fitToCards(
|
||||
[{ x: card.x, y: card.y, width: card.width, height: card.height }],
|
||||
1.15,
|
||||
true,
|
||||
0.8,
|
||||
true,
|
||||
);
|
||||
canvasActions.revealCards([{ x: card.x, y: card.y, width: card.width, height: card.height }]);
|
||||
handleHighlightCard(browserId);
|
||||
}
|
||||
}, 200);
|
||||
@@ -254,7 +247,7 @@ export function useDashboardLifecycle({
|
||||
setTimeout(() => {
|
||||
const card = store.getState().dashboardLayout.viewCards[cardKey];
|
||||
if (card) {
|
||||
canvasActions.fitToCards([{ x: card.x, y: card.y, width: card.width, height: card.height }], SPAWN_FOCUS_MAX_ZOOM, true);
|
||||
canvasActions.revealCards([{ x: card.x, y: card.y, width: card.width, height: card.height }]);
|
||||
handleHighlightCard(cardKey);
|
||||
}
|
||||
}, 200);
|
||||
@@ -269,11 +262,7 @@ export function useDashboardLifecycle({
|
||||
setTimeout(() => {
|
||||
const card = store.getState().dashboardLayout.workflowCards[workflowId];
|
||||
if (card) {
|
||||
canvasActions.fitToCards(
|
||||
[{ x: card.x, y: card.y, width: card.width, height: card.height }],
|
||||
SPAWN_FOCUS_MAX_ZOOM,
|
||||
true,
|
||||
);
|
||||
canvasActions.revealCards([{ x: card.x, y: card.y, width: card.width, height: card.height }]);
|
||||
handleHighlightCard(workflowId);
|
||||
}
|
||||
}, 200);
|
||||
@@ -353,7 +342,7 @@ export function useDashboardLifecycle({
|
||||
const rects = [{ x: vc.x, y: vc.y, width: vc.width, height: vc.height }];
|
||||
const ac = store.getState().dashboardLayout.cards[sid];
|
||||
if (ac) rects.push({ x: ac.x, y: ac.y, width: ac.width, height: ac.height });
|
||||
canvasActions.fitToCards(rects, SPAWN_FOCUS_MAX_ZOOM, true);
|
||||
canvasActions.revealCards(rects);
|
||||
handleHighlightCard(outputId);
|
||||
}, 200);
|
||||
}
|
||||
|
||||
@@ -26,8 +26,6 @@ export const DEFAULT_WORKFLOWS_HUB_W = DEFAULT_BROWSER_CARD_W;
|
||||
export const DEFAULT_WORKFLOWS_HUB_H = DEFAULT_BROWSER_CARD_H;
|
||||
export const EXPANDED_CARD_MIN_H = 620;
|
||||
export const GRID_GAP = 24;
|
||||
// Zoom ceiling when the camera frames a freshly spawned card. 1.15 slammed the user to 115% on every spawn; 0.75 keeps the new card readable without the lurch-into-your-face zoom.
|
||||
export const SPAWN_FOCUS_MAX_ZOOM = 0.75;
|
||||
// Gap between the Workflows window and the cards it spawns (run monitor, that monitor's browser). Keeps the hub -> monitor -> browser row evenly spaced.
|
||||
export const WORKFLOW_CARD_GAP = 140;
|
||||
const GRID_ORIGIN = { x: 40, y: 100 };
|
||||
|
||||
Reference in New Issue
Block a user