Связи (pixel-aquarium, 10.06): партия 2 (UI-фишки) — поиск, хлебные крошки, бейджи, цветовые кластеры
Всё в лаборатории (вариант 2: реальный путь /network-view не трогаем). - Поиск + телепорт: строка .fg-search; Enter → graph.findNode(имя) → камера летит к узлу (dive в «Вселенной», иначе перецентр). - Хлебные крошки: .fg-breadcrumb «Иван › Нина › Ада» (движок шлёт onDiveChange(path), API getDivePath); клик по корню — полный сброс, по предку — навигация на его уровень. - Бейдж числа связей: .fg-node-badge (degreeById → updateBadges; у центра — число связей 1-го уровня). - Цветовые кластеры: мягкая аура узла по типу связи (CSS is-family/friend/business/contact). Автопроверки расширены до 17 ассертов (добавлены поиск/крошки/бейдж) — прогон 17/17 PASS. Фикс: TDZ breadcrumbEl (объявлен до createForceGraph, т.к. onDiveChange вызывается при монтировании). Бамп client.version → 1.2.149. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9a49cc67f0
commit
557ea96be0
@ -1,2 +1,2 @@
|
|||||||
client.version=1.2.148
|
client.version=1.2.149
|
||||||
server.version=1.2.132
|
server.version=1.2.133
|
||||||
|
|||||||
@ -137,10 +137,18 @@
|
|||||||
мигания у порога); **двойной тап по фону** и **сильный pinch-out на мин. зуме** = быстрый выход;
|
мигания у порога); **двойной тап по фону** и **сильный pinch-out на мин. зуме** = быстрый выход;
|
||||||
**префетч аватарок** детей при наведении/нырке.
|
**префетч аватарок** детей при наведении/нырке.
|
||||||
|
|
||||||
|
**Фишки (партия 2, лаборатория):**
|
||||||
|
- **Поиск + телепорт** — строка `.fg-search`; Enter → `graph.findNode(имя)` → камера летит к узлу (dive в
|
||||||
|
«Вселенной», иначе перецентр).
|
||||||
|
- **Хлебные крошки** — `.fg-breadcrumb` «Иван › Нина › Ада» (движок шлёт `onDiveChange(path)`,
|
||||||
|
API `getDivePath()`); клик по корню — полный сброс, по предку — навигация на тот уровень.
|
||||||
|
- **Бейдж числа связей** — `.fg-node-badge` (число из `degreeById`, обновляется в `updateBadges`).
|
||||||
|
- **Цветовые кластеры** — мягкая аура узла по типу связи (CSS `is-family/friend/business/contact`).
|
||||||
|
|
||||||
**Автопроверки (`?fgtest`):** `js/pages/network/selftest.js` автозапускается в лаборатории при `?fgtest`,
|
**Автопроверки (`?fgtest`):** `js/pages/network/selftest.js` автозапускается в лаборатории при `?fgtest`,
|
||||||
прогоняет 14 ассертов (центровка/collision/полукруг/spotlight/переключение/LOD/выход) через детерминированные
|
прогоняет 17 ассертов (центровка/collision/полукруг/spotlight/переключение/LOD/поиск/крошки/бейдж/выход) через
|
||||||
dev-хелперы движка `graph.debugState()` и `graph.pumpForTest()` (синхронно докручивают кадры до покоя — не
|
детерминированные dev-хелперы движка `graph.debugState()` и `graph.pumpForTest()` (синхронно докручивают кадры
|
||||||
зависят от троттлинга rAF). Результат → консоль и `window.__fgTestResults`. В обычной работе не активны.
|
до покоя — не зависят от троттлинга rAF). Результат → консоль и `window.__fgTestResults`. В обычной работе не активны.
|
||||||
|
|
||||||
> ⚠️ Эксперименты на ветках `pixel-web` (паутина) и `pixel-aquarium` (Smart Zoom) — для отката.
|
> ⚠️ Эксперименты на ветках `pixel-web` (паутина) и `pixel-aquarium` (Smart Zoom) — для отката.
|
||||||
> Реальный путь `/network-view` не затронут: deep-код под `tier ≥ 2` / `hasDeep`, dive — только tier≥2
|
> Реальный путь `/network-view` не затронут: deep-код под `tier ≥ 2` / `hasDeep`, dive — только tier≥2
|
||||||
|
|||||||
@ -143,7 +143,7 @@ function hash01(str) {
|
|||||||
* @param {Function} [opts.onNodeLongPress] - долгое нажатие (node, screenPoint) => void
|
* @param {Function} [opts.onNodeLongPress] - долгое нажатие (node, screenPoint) => void
|
||||||
* @returns {{ destroy: Function, recenter: Function, setModel: Function, getFocusNode: Function }}
|
* @returns {{ destroy: Function, recenter: Function, setModel: Function, getFocusNode: Function }}
|
||||||
*/
|
*/
|
||||||
export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeLongPress, onNodeHover } = {}) {
|
export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeLongPress, onNodeHover, onDiveChange } = {}) {
|
||||||
// Слои DOM
|
// Слои DOM
|
||||||
const edgesSvg = document.createElementNS(SVGNS, 'svg');
|
const edgesSvg = document.createElementNS(SVGNS, 'svg');
|
||||||
edgesSvg.setAttribute('class', 'fg-edges');
|
edgesSvg.setAttribute('class', 'fg-edges');
|
||||||
@ -173,18 +173,25 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
|||||||
let diveZoom = 1; // целевой зум активного погружения
|
let diveZoom = 1; // целевой зум активного погружения
|
||||||
let surfacing = false; // идёт «всплытие» назад (камера/зум возвращаются к корню)
|
let surfacing = false; // идёт «всплытие» назад (камера/зум возвращаются к корню)
|
||||||
let childCountByParent = new Map(); // parentId → число детей (для адаптивного радиуса орбиты, без слипания)
|
let childCountByParent = new Map(); // parentId → число детей (для адаптивного радиуса орбиты, без слипания)
|
||||||
|
let degreeById = new Map(); // id → число связей узла (для бейджа-счётчика на аватарке)
|
||||||
const rebuildIndex = () => {
|
const rebuildIndex = () => {
|
||||||
nodeById = new Map(nodes.map((n) => [String(n.id), n]));
|
nodeById = new Map(nodes.map((n) => [String(n.id), n]));
|
||||||
hasDeep = nodes.some((n) => n.tier >= 2);
|
hasDeep = nodes.some((n) => n.tier >= 2);
|
||||||
// число детей у родителя + порядковый индекс ребёнка среди братьев (для веера «полукругом наружу»)
|
// число детей у родителя + порядковый индекс ребёнка среди братьев (для веера «полукругом наружу»)
|
||||||
childCountByParent = new Map();
|
childCountByParent = new Map();
|
||||||
|
degreeById = new Map();
|
||||||
|
let tier1count = 0;
|
||||||
for (const n of nodes) {
|
for (const n of nodes) {
|
||||||
if (n.tier >= 2 && n.parentId) {
|
if (n.tier >= 2 && n.parentId) {
|
||||||
const i = childCountByParent.get(n.parentId) || 0;
|
const i = childCountByParent.get(n.parentId) || 0;
|
||||||
n.sibIndex = i;
|
n.sibIndex = i;
|
||||||
childCountByParent.set(n.parentId, i + 1);
|
childCountByParent.set(n.parentId, i + 1);
|
||||||
|
degreeById.set(n.parentId, (degreeById.get(n.parentId) || 0) + 1); // у родителя +1 связь
|
||||||
|
} else if (n.tier === 1 && String(n.id) !== focusId) {
|
||||||
|
tier1count += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
degreeById.set(focusId, tier1count); // у центра — число связей 1-го уровня
|
||||||
};
|
};
|
||||||
|
|
||||||
// Spotlight: при закреплённой кликом ветке остальной граф тускнеет до SPOTLIGHT_DIM (0.25), чтобы
|
// Spotlight: при закреплённой кликом ветке остальной граф тускнеет до SPOTLIGHT_DIM (0.25), чтобы
|
||||||
@ -212,12 +219,34 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
|||||||
return set;
|
return set;
|
||||||
}
|
}
|
||||||
let _pathSet = new Set();
|
let _pathSet = new Set();
|
||||||
let _pathSetKey = ' | |||||||