mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-10-01 05:54:56 +02:00
[eric] canvas: stop the spawn camera ratcheting out forever, it reached 18% and nothing was readable
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
// Run: cd frontend && npx tsx --test src/app/pages/Dashboard/canvas/revealZoom.test.ts
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { revealZoom, REVEAL_MIN_ZOOM } from './revealZoom.js';
|
||||
|
||||
const MIN = 0.15, MAX = 3.0;
|
||||
|
||||
test('a spawn never zooms IN, which is the behaviour worth keeping', () => {
|
||||
assert.equal(revealZoom(0.8, 2.0, MIN, MAX), 0.8);
|
||||
});
|
||||
|
||||
test('it still zooms out when the card genuinely does not fit', () => {
|
||||
assert.equal(revealZoom(1.0, 0.7, MIN, MAX), 0.7);
|
||||
});
|
||||
|
||||
test('but never below readable, which is the bug', () => {
|
||||
assert.equal(revealZoom(1.0, 0.05, MIN, MAX), REVEAL_MIN_ZOOM);
|
||||
assert.equal(revealZoom(0.6, 0.18, MIN, MAX), REVEAL_MIN_ZOOM);
|
||||
});
|
||||
|
||||
test('the measured death spiral bottoms out instead of reaching 18%', () => {
|
||||
// Replays the real session: each step is a reveal whose fit was tighter than the last.
|
||||
let z = 1.0;
|
||||
for (const fit of [0.88, 0.79, 0.76, 0.61, 0.36, 0.34, 0.18]) z = revealZoom(z, fit, MIN, MAX);
|
||||
assert.equal(z, REVEAL_MIN_ZOOM);
|
||||
assert.ok(z >= REVEAL_MIN_ZOOM, 'must not ratchet past the floor no matter how many spawns');
|
||||
});
|
||||
|
||||
test('a camera the user already zoomed out by hand is left alone', () => {
|
||||
// Below the floor already: the reveal must not yank them back IN, that would fight the user.
|
||||
assert.equal(revealZoom(0.2, 0.9, MIN, MAX), 0.2);
|
||||
});
|
||||
|
||||
test('it respects the hard bounds', () => {
|
||||
assert.equal(revealZoom(5.0, 9.0, MIN, MAX), MAX);
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
// How far the camera is allowed to pull back when a new card appears.
|
||||
//
|
||||
// The reveal takes min(current, fit) so a spawn never zooms IN, which is right. But that made the
|
||||
// camera monotonically decreasing: every spawn that did not fit ratcheted it out and nothing ever
|
||||
// brought it back. Measured over one ordinary session: 100% -> 88% -> 79% -> 61% -> 36% -> 18%, at
|
||||
// which point the workspace is a fifth of the viewport and not one word is readable.
|
||||
//
|
||||
// So the reveal gets a floor. Fitting every card on screen is not the goal; seeing the new one is.
|
||||
// A hand-driven zoom is untouched and can still go to the hard minimum.
|
||||
|
||||
export const REVEAL_MIN_ZOOM = 0.5;
|
||||
|
||||
export function revealZoom(currentZoom: number, fitZoom: number, minZoom: number, maxZoom: number): number {
|
||||
// Floor the FIT, not the result. Flooring the result would drag a user who had deliberately zoomed
|
||||
// out back IN, which breaks the one rule this function already had: a reveal never zooms in.
|
||||
const floor = Math.max(minZoom, REVEAL_MIN_ZOOM);
|
||||
return Math.min(Math.min(currentZoom, Math.max(fitZoom, floor)), maxZoom);
|
||||
}
|
||||
@@ -7,8 +7,14 @@ import { getScrollFocusedCard } from '@/shared/cardScrollFocus';
|
||||
import { getWebview } from '@/shared/browserRegistry';
|
||||
import { applyBrowserZoom } from '@/shared/browserZoom';
|
||||
import { syncTiledGeometry } from '../../canvas/tiledGeometry';
|
||||
import { revealZoom } from '../../canvas/revealZoom';
|
||||
|
||||
const MIN_ZOOM = 0.15;
|
||||
// The floor for AUTOMATIC reveals only. revealCards takes min(current, fit), which can only ever go
|
||||
// down, so every spawn that did not fit ratcheted the camera out and nothing ever brought it back:
|
||||
// measured 100% -> 88% -> 79% -> 61% -> 36% -> 18% over one ordinary session, at which point no word
|
||||
// on the canvas is readable. A hand-driven zoom can still go all the way to MIN_ZOOM.
|
||||
|
||||
const MAX_ZOOM = 3.0;
|
||||
const ZOOM_IN_FACTOR = 1.1;
|
||||
const ZOOM_OUT_FACTOR = 1 / ZOOM_IN_FACTOR;
|
||||
@@ -792,7 +798,8 @@ export function useCanvasControls(zoomSensitivity: number = 50, contentBounds?:
|
||||
(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);
|
||||
// Never auto-zoom below readable: showing every card at 18% is worse than showing the new one at 50%, and the pan below still brings it into view.
|
||||
const zoom = clamp(revealZoom(cur.zoom, fitZoom, MIN_ZOOM, MAX_ZOOM), 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;
|
||||
|
||||
Reference in New Issue
Block a user