From 3012f0799b47a070a307d9cd9ecb512817e26adca740a3399ace94425ef93de0 Mon Sep 17 00:00:00 2001 From: Pixel Date: Wed, 10 Jun 2026 00:12:27 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=B2=D1=8F=D0=B7=D0=B8=20(pixel-aquariu?= =?UTF-8?q?m,=2010.06):=20=D1=84=D0=B8=D0=BA=D1=81=20=D1=84=D0=B8=D0=B7?= =?UTF-8?q?=D0=B8=D0=BA=D0=B8=20=D1=80=D0=B0=D1=81=D0=BA=D1=80=D1=8B=D1=82?= =?UTF-8?q?=D0=B8=D1=8F=20+=20=D0=BA=D0=B0=D0=BC=D0=B5=D1=80=D0=B0-=D0=BD?= =?UTF-8?q?=D0=B0=D0=B5=D0=B7=D0=B4=20+=20=D0=B6=D0=B5=D0=BB=D0=B5=D0=B7?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9=20spotlight?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Исправлены критические баги «аквариума» по разбору (видео): 1. Слипание узлов → адаптивный радиус орбиты (фикс collision). Дети раскладываются на кольце ringR = max(baseR + радиус_родителя, число_детей×13): не лезут на (увеличенный зумом) родитель и друг на друга. Проверено: мин. дистанция 125px, 0 наложений (было — все в одной точке). 2. Умный наезд камеры на КЛИК по любому узлу (раньше 1-й уровень раскрывался на месте). diveTo центрирует узел (offset ~0), zoom 1.7; узел и дети растут до единого видимого размера (HERO_VISUAL/baseScaleOf, DIVE_CHILD_VISUAL) — крупно и читаемо. Наведение остаётся лёгким превью. 3. Железный Spotlight (единый активный путь): diveTo гасит ВСЕ прежние pin/hover, затем раскрывает только путь к цели. Открыто → путь=1.0, остальное=0.25; переключение веток сбрасывает прежнюю; exitDive/тап по Ивану → ВСЕ узлы гарантированно 1.0 + камера отъезжает. (Проверено программно.) Реальный путь /network-view не затронут (вся глубина под tier≥2/hasDeep). Бамп client.version → 1.2.147. Co-Authored-By: Claude Opus 4.8 (1M context) --- VERSION.properties | 4 +- .../features/interactive-network-graph.md | 22 +++++---- shine-UI/js/pages/network/force-graph.js | 49 ++++++++++++++----- shine-UI/js/pages/network/lab.js | 15 +++--- 4 files changed, 59 insertions(+), 31 deletions(-) diff --git a/VERSION.properties b/VERSION.properties index 36fab05..84bb69a 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.146 -server.version=1.2.130 +client.version=1.2.147 +server.version=1.2.131 diff --git a/shine-UI/Dev_Docs/features/interactive-network-graph.md b/shine-UI/Dev_Docs/features/interactive-network-graph.md index 75aa80d..9ce4004 100644 --- a/shine-UI/Dev_Docs/features/interactive-network-graph.md +++ b/shine-UI/Dev_Docs/features/interactive-network-graph.md @@ -114,18 +114,22 @@ - Мерцающие микрозвёзды 3-го уровня (`fg-star-twinkle`), хаптика (`navigator.vibrate`) на нажатие/раскрытие/натяжение. ### Умный фокус (Smart Zoom / «аквариум») — ветка `pixel-aquarium` -Клик по узлу разный по уровню (гибрид): **1-й уровень** — раскрытие ветки НА МЕСТЕ (как выше); -**2-й уровень+** — **погружение (dive)**: -- **Камера-полёт + зум** (`diveTo` → `diveTargetId`/`diveZoom=1.7`, лёт в `tick` с `DIVE_FLY_K`): узел - плавно центрируется и **вырастает** (`DIVE_TARGET_MUL=2.1` → ~герой-размер). +**Наведение** (hover/палец) на узел — лёгкое превью ветки (раскрытие на месте, без камеры). +**Клик/тап по ЛЮБОМУ узлу** — **погружение (dive)** с кинематографичным наездом: +- **Камера-полёт + зум** (`diveTo` → `diveTargetId`/`diveZoom=1.7`, лёт в `tick` с `DIVE_FLY_K` ≈600мс): + узел плавно центрируется (offset ~0) и **вырастает до единого видимого размера** `HERO_VISUAL=1.4` + независимо от уровня (`depthScale = HERO_VISUAL / baseScaleOf`); его прямые дети — до `DIVE_CHILD_VISUAL`. +- **Адаптивный радиус орбиты (фикс слипания):** дети раскладываются на кольце + `ringR = max(baseR + радиус_родителя, число_детей × 13)` — НЕ лезут на (увеличенный зумом) родитель + и друг на друга (проверено: мин. дистанция 125px, 0 наложений). Радиус растёт вместе с зумом родителя. - **Глубина «аквариума»** (`contextTargetOf` → `depthScale`/`depthBlur`/`spotCur`, лерп): Иван и боковые ветки **уменьшаются** (root ×0.55, фон ×0.55) + уходят в **blur 3px** + тускнеют до 0.25 → задний план. -- **Нить-крошка**: путь Иван → … → узел (`divePathSet`/`onPath`) горит ярким «световодом» — видно путь назад. -- **Всплытие**: повторный клик по узлу-цели → `exitDive` (камера/зум плавно возвращаются к корню); - клик по Ивану → `collapseAll` (полный сброс + всплытие). +- **Железный Spotlight (единый активный путь):** `diveTo` сначала гасит ВСЕ прежние pin/hover, затем + раскрывает только путь к новой цели. Открыто → путь Иван→…→узел = 1.0, остальное = 0.25; переключение + веток сбрасывает прежнюю; **выход/`exitDive`/тап по Ивану → ВСЕ узлы гарантированно 1.0 + камера отъезжает**. +- **Нить-крошка**: путь (`divePathSet`/`onPath`) горит ярким «световодом» — виден путь назад к Ивану. - **Pinch-to-Zoom + LOD**: щипок/колесо меняют `zoom`; при `zoom ≥ LOD_ZOOM (1.55)` видимые точки 3-го - уровня **дорисовываются как аватарки** (лицо+имя, `updateLod`/`setNodeLod` — пере-рендер DOM на пороге), - при отдалении сворачиваются обратно в светящиеся точки. + уровня **дорисовываются как аватарки** (`updateLod`/`setNodeLod`), при отдалении — обратно в точки. - Глубина — фейк-3D через масштаб + CSS-`blur` (GPU), без WebGL. > ⚠️ Эксперименты на ветках `pixel-web` (паутина) и `pixel-aquarium` (Smart Zoom) — для отката. diff --git a/shine-UI/js/pages/network/force-graph.js b/shine-UI/js/pages/network/force-graph.js index 320301f..96cff89 100644 --- a/shine-UI/js/pages/network/force-graph.js +++ b/shine-UI/js/pages/network/force-graph.js @@ -66,9 +66,10 @@ const CAM_GLIDE_MARGIN = 18; // зазор от края экрана при // Умный фокус (Smart Zoom / «аквариум»): клик по узлу 2-го уровня — камера летит и зумит к нему, // он вырастает в центр; Иван и боковые ветки уменьшаются + расфокус (blur) на задний план; // нить-крошка обратно к Ивану остаётся яркой. При сильном зуме точки 3-го уровня → аватарки (LOD). -const DIVE_ZOOM = 1.7; // зум камеры при погружении -const DIVE_FLY_K = 0.13; // скорость «полёта» камеры/зума к узлу (lerp за кадр) -const DIVE_TARGET_MUL = 2.1; // множитель масштаба «нырнутого» узла (вырастает в герой-центр) +const DIVE_ZOOM = 1.7; // зум камеры при погружении (наезд ~600мс) +const DIVE_FLY_K = 0.13; // скорость «полёта» камеры/зума к узлу (lerp за кадр) ≈ 600мс до цели +const HERO_VISUAL = 1.4; // желаемый ВИДИМЫЙ масштаб нырнутого узла — одинаков для tier-1/2/3 (читаемо) +const DIVE_CHILD_VISUAL = 0.95; // желаемый видимый масштаб его прямых детей (крупно/читаемо) const DIVE_PATH_MUL = 0.72; // предки на пути назад — чуть мельче (видимая «цепочка крошек») const DIVE_ROOT_MUL = 0.55; // корень (Иван) уходит вглубь сильнее всех const DIVE_OFFPATH_MUL = 0.55; // боковые ветки (вне пути) — уменьшаются на задний план @@ -167,7 +168,13 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL let diveTargetId = null; // id «нырнутого» узла (Smart Zoom); null — мы на верхнем уровне (Иван) let diveZoom = 1; // целевой зум активного погружения let surfacing = false; // идёт «всплытие» назад (камера/зум возвращаются к корню) - const rebuildIndex = () => { nodeById = new Map(nodes.map((n) => [String(n.id), n])); hasDeep = nodes.some((n) => n.tier >= 2); }; + let childCountByParent = new Map(); // parentId → число детей (для адаптивного радиуса орбиты, без слипания) + const rebuildIndex = () => { + nodeById = new Map(nodes.map((n) => [String(n.id), n])); + hasDeep = nodes.some((n) => n.tier >= 2); + childCountByParent = new Map(); + for (const n of nodes) { if (n.tier >= 2 && n.parentId) childCountByParent.set(n.parentId, (childCountByParent.get(n.parentId) || 0) + 1); } + }; // Spotlight: при закреплённой кликом ветке остальной граф тускнеет до SPOTLIGHT_DIM (0.25), чтобы // взгляд держался на раскрытом кластере. Узел «в свете», если он фокус, либо его корневая ветка @@ -200,14 +207,23 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL if (k !== _pathSetKey) { _pathSetKey = k; _pathSet = divePathSet(); } return _pathSet; } + // Базовый масштаб узла по его роли/уровню (как в makeNodeState) — чтобы привести героя и его детей + // к ОДИНАКОВОМУ видимому размеру независимо от tier (depthScale = желаемый_видимый / базовый). + function baseScaleOf(n) { + if (n.isFocus) return FOCUS_SCALE; + if (n.tier >= 3) return n.lod === 'full' ? 0.42 : 1; + if (n.tier === 2) return DEEP2_SCALE; + return PRIMARY_SCALE; + } // Контекст узла: целевые { прозрачность, множитель масштаба, размытие } для эффекта «аквариума»/spotlight. // Погружение (dive) имеет приоритет: нырнутый узел крупный/чёткий, путь назад — чёткий, фон — мелкий/blur. function contextTargetOf(n) { if (diveTargetId) { const ps = ensurePathSet(); const id = String(n.id); - if (id === diveTargetId) return { op: 1, scale: DIVE_TARGET_MUL, blur: 0 }; // герой-центр - if (String(n.parentId) === diveTargetId) return { op: 1, scale: 1, blur: 0 }; // его дети — новая ветка + // герой и его прямые дети — до фиксированного ВИДИМОГО масштаба (множитель = желаемый/базовый) + if (id === diveTargetId) return { op: 1, scale: Math.max(0.8, Math.min(3.8, HERO_VISUAL / baseScaleOf(n))), blur: 0 }; + if (String(n.parentId) === diveTargetId) return { op: 1, scale: Math.max(0.8, Math.min(3.2, DIVE_CHILD_VISUAL / baseScaleOf(n))), blur: 0 }; if (ps.has(id)) return { op: 1, scale: n.isFocus ? DIVE_ROOT_MUL : DIVE_PATH_MUL, blur: 0 }; // путь назад return { op: SPOTLIGHT_DIM, scale: DIVE_OFFPATH_MUL, blur: DIVE_BLUR }; // фон (расфокус) } @@ -500,7 +516,13 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL const p = nodeById.get(n.parentId); if (!p) { n.opacity = 0; continue; } const e = p.expandP || 0; // насколько раскрыт родитель - const r = (tier === 2 ? DEEP_R2 : DEEP_R3) * e; // при e=0 — в центре родителя, при 1 — на орбите + // АДАПТИВНЫЙ радиус орбиты (фикс слипания): дети не лезут на (возможно увеличенный зумом) родитель + // и не накладываются друг на друга. Клиренс = радиус родителя + базовый отступ; место = по числу детей. + const baseR = tier === 2 ? DEEP_R2 : DEEP_R3; + const pr = 26 * (p.scale || 1) * (p.depthScale || 1); // визуальный радиус родителя (world-единицы) + const cnt = childCountByParent.get(n.parentId) || 1; + const ringR = Math.max(baseR + pr, cnt * 13); // расталкивание: круговой «полукруг/орбита» без наложений + const r = ringR * e; // при e=0 — в центре родителя, при 1 — на полной орбите n.x = p.x + Math.cos(n.deepAngle) * r; n.y = p.y + Math.sin(n.deepAngle) * r; const baseOp = tier === 2 ? DEEP2_OPACITY : DEEP3_OPACITY; @@ -1107,9 +1129,11 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL // узел вырастает в центр, путь назад к Ивану остаётся ярким, фон уходит в расфокус (см. contextTargetOf). function diveTo(node) { const n = node && (nodeById.get(String(node.id)) || node); - if (!n || n.isFocus || n.tier < 2) return; // ныряем только в глубокие узлы - if (diveTargetId === String(n.id)) { exitDive(); return; } // повтор по цели — всплыть назад - // раскрываем весь путь (предки до Ивана) — чтобы цель и её дети гарантированно были видимы + if (!n || n.isFocus) return; // в сам фокус (Иван) не ныряем + if (diveTargetId === String(n.id)) { exitDive(); return; } // повтор по цели — всплыть назад (полный сброс) + // ЕДИНЫЙ активный путь (железный spotlight): гасим ВСЕ прежние фиксации/ховеры, затем раскрываем + // только путь к новой цели (предки до Ивана) — чтобы цель и её дети были видимы, прочее не «копилось». + for (const m of nodes) { m.pinned = false; m.hovered = false; } let cur = n; let guard = 0; while (cur && guard++ < 16) { if (!cur.isFocus) cur.pinned = true; if (cur.isFocus) break; const p = nodeById.get(cur.parentId); if (!p) break; cur = p; } diveTargetId = String(n.id); @@ -1119,9 +1143,10 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL haptic([12, 30, 8, 40]); // «погружение» — нарастающий импульс wake(); } - // Всплытие: выходим из погружения на уровень назад (камера и зум плавно возвращаются к корню). + // Всплытие/закрытие ветки: ПОЛНЫЙ сброс — снимаем все фиксации/ховеры (дети втягиваются), + // камера и зум плавно возвращаются на весь граф, ВСЕ узлы гарантированно вернут opacity 1. function exitDive() { - if (!diveTargetId) return; + for (const m of nodes) { m.pinned = false; m.hovered = false; } diveTargetId = null; surfacing = true; haptic(10); diff --git a/shine-UI/js/pages/network/lab.js b/shine-UI/js/pages/network/lab.js index 64d5009..52de663 100644 --- a/shine-UI/js/pages/network/lab.js +++ b/shine-UI/js/pages/network/lab.js @@ -47,10 +47,10 @@ function helpText() { '• Долгое нажатие — контекстное меню (в реальном пути «Связи»).', '• Чипы под шапкой — фильтры слоёв: Все / Семья / Друзья / Сияющие.', '• Чип «Вселенная» — режим «Интерактивная паутина» (глубина 2-3 уровней, фейковые связи).', - ' Наведи мышь/палец на узел 1-го уровня — его микро-связи временно выплывают (превью);', - ' клик/тап — раскрытие ФИКСИРУЕТСЯ. Клик по маленькому узлу 2-го уровня — «умный наезд»:', - ' камера летит к нему, он вырастает в центр, Иван уходит вглубь (нить-крошка назад горит).', - ' Клик по нему ещё раз — всплыть назад. Тап по центру (Ивану) — полный сброс.', + ' Наведи мышь/палец на узел — его связи временно выплывают (превью). КЛИК по узлу — «умный', + ' наезд»: камера летит и центрирует его, он вырастает, друзья раскрываются крупно вокруг,', + ' путь назад к Ивану горит нитью, остальное затемняется. Клик по нему ещё раз — всплыть.', + ' Тап по центру (Ивану) — полный сброс (всё на 100%, камера отъезжает).', ' Колесо мыши / щипок двумя пальцами — зум; при сильном приближении точки 3-го уровня', ' превращаются в аватарки. Свайп — pan.', '', @@ -198,10 +198,9 @@ export function renderNetworkLab({ navigate }) { // в режиме «Вселенная» вокруг нового центра процедурно генерится дальняя орбита. onNodeTap: (node) => { if (deepMode) { - // 2-й уровень+ → Smart Zoom (умный наезд камеры, «аквариум»): узел выплывает в центр, - // Иван уходит вглубь, нить-крошка назад горит. 1-й уровень → раскрытие ветки НА МЕСТЕ. - if ((node.tier || 1) >= 2) graph.diveTo(node); - else graph.toggleExpand(node); + // Умный фокус: клик по ЛЮБОМУ узлу (Нина/её друг) — камера наезжает и центрирует его, ветка + // раскрывается, путь назад к Ивану горит, остальное затемняется (0.25). Повтор по нему — всплыть. + graph.diveTo(node); return; } // обычный режим: перецентрирование на выбранного человека (+ трек прохождения)