Skip to content

Commit 753ea2e

Browse files
CodeAbraclaudexnllllh
committed
fix(brainview): keep the dashboard graph centered on zoom/pan with a recenter control [user-impact-checked]
Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: XNLLLLH <XNLLLLH@users.noreply.github.com>
1 parent e17ade9 commit 753ea2e

2 files changed

Lines changed: 70 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [2.0.0]Unreleased
8+
## [2.0.0]2026-07-10
99

1010
### Changed
1111

@@ -25,6 +25,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2525
- The last GPL-adjacent dependencies; the project now ships its own storage and
2626
graph engines under MIT.
2727

28+
### Fixed
29+
30+
- **Dashboard graph view** stays centered on zoom and pan, with a recenter
31+
control that appears when the view leaves its home position.
32+
2833
## [1.2.1] — 2026-06-30
2934

3035
### Fixed

src/iai_mcp/_deploy/brainview/index.html

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,16 @@
201201
.lg-tier.focused { color: var(--text); }
202202
.lg-tier.focused b { color: var(--epi); }
203203

204+
#btn-home { position: fixed; right: 34px; bottom: 108px; z-index: 20;
205+
background: rgba(9,7,18,.78); color: var(--dim); cursor: pointer;
206+
border: 1px solid rgba(186,220,240,.28); border-radius: 999px;
207+
padding: 8px 18px; font: 300 10.5px "Avenir Next", Futura, system-ui, sans-serif;
208+
letter-spacing: .22em; text-transform: uppercase;
209+
opacity: 0; pointer-events: none; transition: opacity .25s; }
210+
#btn-home.show { opacity: 1; pointer-events: auto; }
211+
#btn-home:hover { color: var(--text); border-color: var(--epi);
212+
box-shadow: 0 0 28px rgba(143,231,255,.14); }
213+
204214
#toast { position: fixed; top: 80px; left: 50%; transform: translateX(-50%); z-index: 30;
205215
color: var(--epi); font-size: 13px; letter-spacing: .06em; display: none;
206216
text-shadow: 0 0 20px rgba(143,231,255,.6); }
@@ -349,6 +359,7 @@ <h6>Live activity</h6>
349359
</div>
350360
<div id="toast"></div>
351361
<div id="tip"></div>
362+
<button id="btn-home">⌖ center the brain</button>
352363
<div id="hint">grab &amp; spin freely · scroll to zoom · double-click to reset · click a memory or a folder · drop a file to teach</div>
353364

354365
<script>
@@ -524,16 +535,15 @@ <h6>Live activity</h6>
524535
// how face-on the view is: 1 when the depth axis is unrotated, →0 edge-on
525536
return Math.abs(ROT.m[8]);
526537
}
538+
// The organ is pinned to screen center: zoom scales AROUND the brain (never
539+
// toward the cursor — cursor-anchored zoom is how residual pan used to park
540+
// it off-center), and any drag-pan springs back home on release.
541+
let HOMING = false;
527542
canvas.addEventListener("wheel", e => {
528543
e.preventDefault();
544+
HOMING = false;
529545
const factor = Math.exp(-e.deltaY * 0.0016);
530-
const k2 = Math.min(7, Math.max(0.5, VIEW.k * factor));
531-
const f = k2 / VIEW.k;
532-
// keep the point under the cursor fixed while zooming
533-
const cxs = e.clientX - W / 2, cys = e.clientY - H / 2;
534-
VIEW.ox = cxs - (cxs - VIEW.ox) * f;
535-
VIEW.oy = cys - (cys - VIEW.oy) * f;
536-
VIEW.k = k2;
546+
VIEW.k = Math.min(7, Math.max(0.5, VIEW.k * factor));
537547
dismissHint(false);
538548
}, { passive: false });
539549
let panDrag = null;
@@ -543,15 +553,31 @@ <h6>Live activity</h6>
543553
ox: VIEW.ox, oy: VIEW.oy, moved: false,
544554
pan: e.shiftKey || e.button === 1 };
545555
ROT.vx = 0; ROT.vy = 0; // grab kills any leftover spin
556+
HOMING = false; // ...and any homing in flight
546557
canvas.style.cursor = "grabbing";
547558
});
548559
window.addEventListener("mouseup", () => { panDrag = null; canvas.style.cursor = "grab"; });
560+
// The ambient idle turntable also rotates the organ, so raw orientation says
561+
// nothing about intent: only a USER-caused turn makes the view "not home".
562+
let USER_TURNED = false;
563+
function goHome() {
564+
HOMING = true;
565+
ROT.vx = 0; ROT.vy = 0;
566+
}
549567
canvas.addEventListener("dblclick", e => {
550-
if (!pick(e.clientX, e.clientY)) {
551-
VIEW.k = 1; VIEW.ox = 0; VIEW.oy = 0;
552-
ROT.m = [1,0,0, 0,1,0, 0,0,1]; ROT.vx = 0; ROT.vy = 0;
553-
}
568+
if (!pick(e.clientX, e.clientY)) goHome();
554569
});
570+
const HOME_BTN = document.getElementById("btn-home");
571+
HOME_BTN.addEventListener("click", goHome);
572+
const IDENT = [1,0,0, 0,1,0, 0,0,1];
573+
function viewIsHome() {
574+
if (Math.abs(VIEW.k - 1) > 0.01) return false;
575+
if (Math.abs(VIEW.ox) > 0.5 || Math.abs(VIEW.oy) > 0.5) return false;
576+
return !USER_TURNED;
577+
}
578+
function rotNearIdentity() {
579+
return ROT.m[0] > 0.9995 && ROT.m[4] > 0.9995 && ROT.m[8] > 0.9995;
580+
}
555581

556582
/* ---------- luminous sprites (cheap, soft glow) ---------- */
557583
function makeSprite(color, size) {
@@ -987,6 +1013,10 @@ <h6>Live activity</h6>
9871013

9881014
function loop(now) {
9891015
if (document.hidden) { setTimeout(() => requestAnimationFrame(loop), 900); return; }
1016+
// A webview can finish its first layout AFTER the boot-time resize() ran,
1017+
// or swallow the resize event entirely — a stale W/H bakes the organ into a
1018+
// wrong-sized viewport (off-center, wrong scale) until a manual resize.
1019+
if (W !== window.innerWidth || H !== window.innerHeight) resize();
9901020
// Watching without touching the mouse is a real mode: ambient stays a
9911021
// smooth ~30fps (half the cost of active), near-zero only when hidden.
9921022
const idle = now - lastInteract > 6000;
@@ -996,11 +1026,32 @@ <h6>Live activity</h6>
9961026
if (Math.abs(ROT.vx) > 0.01 || Math.abs(ROT.vy) > 0.01) {
9971027
tumble(ROT.vy * ROT_SPEED, ROT.vx * ROT_SPEED); // flick keeps spinning
9981028
ROT.vx *= 0.90; ROT.vy *= 0.90;
999-
} else if (idle) {
1029+
} else if (idle && !HOMING) {
10001030
tumble(0, 0.0022); // slow turntable while idle
10011031
}
10021032
}
10031033
}
1034+
// The organ always finds its way home: a released drag-pan springs back to
1035+
// dead center at any zoom, and homing (button / double-click) additionally
1036+
// eases zoom and orientation back to the default view.
1037+
if (!panDrag) {
1038+
if (VIEW.ox !== 0 || VIEW.oy !== 0) {
1039+
VIEW.ox *= 0.88; VIEW.oy *= 0.88;
1040+
if (Math.abs(VIEW.ox) < 0.4) VIEW.ox = 0;
1041+
if (Math.abs(VIEW.oy) < 0.4) VIEW.oy = 0;
1042+
}
1043+
if (HOMING) {
1044+
VIEW.k += (1 - VIEW.k) * 0.16;
1045+
const m = ROT.m, t = 0.18;
1046+
ROT.m = rotOrtho(m.map((v, i) => v + (IDENT[i] - v) * t));
1047+
if (Math.abs(VIEW.k - 1) < 0.01 && rotNearIdentity()) {
1048+
VIEW.k = 1; VIEW.ox = 0; VIEW.oy = 0;
1049+
ROT.m = IDENT.slice();
1050+
HOMING = false; USER_TURNED = false;
1051+
}
1052+
}
1053+
}
1054+
HOME_BTN.classList.toggle("show", !viewIsHome());
10041055
const rate = idle ? 0.04 : Math.min(0.5, 0.06 + edges.length / 350);
10051056
if (!REDUCED && Math.random() < rate) spawnPulse(false);
10061057
draw(now);
@@ -1031,6 +1082,7 @@ <h6>Live activity</h6>
10311082
if (Math.abs(dx) + Math.abs(dy) > 4) panDrag.moved = true;
10321083
if (panDrag.moved && !panDrag.pan) {
10331084
const ix = ev.clientX - panDrag.lx, iy = ev.clientY - panDrag.ly; // step since last frame
1085+
USER_TURNED = true;
10341086
tumble(iy * ROT_SPEED, ix * ROT_SPEED);
10351087
ROT.vx = ix; ROT.vy = iy; // carry velocity for the flick
10361088
panDrag.lx = ev.clientX; panDrag.ly = ev.clientY;

0 commit comments

Comments
 (0)