Переработка экрана «Связи» в интерактивный нод-граф с премиальными переходами. Движок (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>
73 lines
2.8 KiB
JavaScript
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] };
|
|
}
|