SHiNE-server/shine-UI/js/pages/network/adapter.js
AidarKC f56e531384 Связи: интерактивная карта связей (force-directed graph)
Переработка экрана «Связи» в интерактивный нод-граф с премиальными переходами.

Движок (js/pages/network/force-graph.js):
- diffing-переходы: общие узлы перелетают, новые расцветают каскадом, исчезнувшие — Ghost-слой (800мс, на месте);
- мягкая радиальная пружина + отталкивание (органичная орбита), упругий влёт фокуса;
- динамическая вязкость на старте (трение 0.92→0.82, отталкивание ослаблено) — мягкий разлёт без тряски;
- жёсткая заморозка (kill-switch) при затухании — нет «треска», экономия батареи;
- линии — SVG <path> Безье (изогнутые нити), прорастание; жесты pan с инерцией;
- хард-лимит DOM-аватарок (остальное — SVG-точки).

Интеграция и UX:
- adapter.js: getUserConnectionsGraph → модель движка (сервер не трогаем, read-only);
- фильтры (Все/Семья/Друзья/Сияющие), контекстное меню (node-menu.js), нижний сниппет, профиль;
- прицел в центре, дыхание фокуса, свечение сияющих;
- лаборатория network-view/lab на мок-данных (networkGraphUsers) для тестов без бэкенда.

Документация: shine-UI/Dev_Docs/features/interactive-network-graph.md.
Бамп client.version 1.2.135 -> 1.2.136.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 12:43:56 +03:00

73 lines
2.8 KiB
JavaScript

// Адаптер реальных данных → нейтральная модель движка force-графа.
//
// Источник — результат buildGraphModel() из network-view.js (он уже нормализует ответ
// authService.getUserConnectionsGraph в роли/направления/метки). Здесь только конвертация
// в форму, которую понимает движок ({ focusId, nodes[] }). Сервер не трогаем — это чтение.
function normLogin(value) {
return String(value || '').trim();
}
const FAMILY_ROLES = ['parent', 'child', 'spouse', 'sibling'];
function relationTypeFromRole(role) {
if (FAMILY_ROLES.includes(role)) return 'family';
if (role === 'friend') return 'friend';
return 'contact';
}
// Сила связи (0..1) → радиус орбиты в движке. Реальный API пока не отдаёт численную силу,
// поэтому выводим её эвристически из роли и направления связи:
// - родственники держим ближе всего, контакты — дальше всего;
// - взаимные дружеские связи ближе односторонних.
function deriveStrength(relation) {
if (relation.isRelative) return 0.9;
if (relation.role === 'contact') return 0.4;
if (relation.forward && relation.backward) return 0.8;
return 0.55;
}
/**
* @param {{centerLogin:string, centerMark:object|null, relations:Array}} graphModel
* @returns {{focusId:string, nodes:Array}}
*/
export function engineModelFromGraphModel(graphModel) {
const focusLogin = normLogin(graphModel?.centerLogin);
const centerMark = graphModel?.centerMark || null;
const focusNode = {
id: focusLogin,
login: focusLogin,
name: '',
avatar: centerMark?.avatar || null,
relationType: 'self',
strength: 1,
shining: Boolean(centerMark?.shine),
tier: 1,
};
const relations = Array.isArray(graphModel?.relations) ? graphModel.relations : [];
const seen = new Set();
const nodes = relations
.map((r) => {
const login = normLogin(r?.login);
const key = login.toLowerCase();
// исключаем сам фокус (не должно быть линии на собственный ник) и дубли
if (!login || key === focusLogin.toLowerCase() || seen.has(key)) return null;
seen.add(key);
return {
id: login,
login,
name: '',
avatar: r?.mark?.avatar || null,
relationType: relationTypeFromRole(r?.role),
strength: deriveStrength(r || {}),
shining: Boolean(r?.mark?.shine),
tier: 1,
};
})
.filter(Boolean);
return { focusId: focusLogin, nodes: [focusNode, ...nodes] };
}