SHA256
Доработать граф связей
This commit is contained in:
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
client.version=1.12.13
|
client.version=1.12.14
|
||||||
server.version=1.10.4
|
server.version=1.10.4
|
||||||
|
|||||||
+575
-310
File diff suppressed because it is too large
Load Diff
@@ -222,9 +222,10 @@ function hash01(str) {
|
|||||||
* @param {Function} [opts.onCenterTap] - тап по центральному узлу (node) => void
|
* @param {Function} [opts.onCenterTap] - тап по центральному узлу (node) => void
|
||||||
* @param {Function} [opts.onNodeTap] - тап по периферийному узлу (node) => void (вызывается ДО центрирования)
|
* @param {Function} [opts.onNodeTap] - тап по периферийному узлу (node) => void (вызывается ДО центрирования)
|
||||||
* @param {Function} [opts.onNodeLongPress] - долгое нажатие (node, screenPoint) => void
|
* @param {Function} [opts.onNodeLongPress] - долгое нажатие (node, screenPoint) => void
|
||||||
|
* @param {Function} [opts.onNodeMoveEnd] - ручное перемещение периферийного узла (node, {x,y}) => 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, onDiveChange } = {}) {
|
export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeLongPress, onNodeMoveEnd, 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');
|
||||||
@@ -1467,6 +1468,10 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
|||||||
let camStartY = 0;
|
let camStartY = 0;
|
||||||
let moved = false;
|
let moved = false;
|
||||||
let downNodeEl = null;
|
let downNodeEl = null;
|
||||||
|
let downNode = null;
|
||||||
|
let nodeDragActive = false;
|
||||||
|
let nodeDragStartX = 0;
|
||||||
|
let nodeDragStartY = 0;
|
||||||
let longTimer = 0;
|
let longTimer = 0;
|
||||||
let longFired = false;
|
let longFired = false;
|
||||||
const activePointers = new Map(); // id → {x, y}: для щипкового зума двумя пальцами
|
const activePointers = new Map(); // id → {x, y}: для щипкового зума двумя пальцами
|
||||||
@@ -1595,7 +1600,10 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
|||||||
longFired = false;
|
longFired = false;
|
||||||
downNodeEl = ev.target instanceof Element ? ev.target.closest('.fg-node') : null;
|
downNodeEl = ev.target instanceof Element ? ev.target.closest('.fg-node') : null;
|
||||||
if (downNodeEl) { downNodeEl.classList.add('is-pressed'); haptic(6); } // тактильный «клик» вдавливания
|
if (downNodeEl) { downNodeEl.classList.add('is-pressed'); haptic(6); } // тактильный «клик» вдавливания
|
||||||
const downNode = nodeFromEvent(ev);
|
downNode = nodeFromEvent(ev);
|
||||||
|
nodeDragActive = false;
|
||||||
|
nodeDragStartX = Number(downNode?.x) || 0;
|
||||||
|
nodeDragStartY = Number(downNode?.y) || 0;
|
||||||
// касание пальцем по узлу = «наведение» (превью ветки), как ховер мышью; мышь обслуживают over/out
|
// касание пальцем по узлу = «наведение» (превью ветки), как ховер мышью; мышь обслуживают over/out
|
||||||
if (downNode && ev.pointerType !== 'mouse' && typeof onNodeHover === 'function') onNodeHover(downNode, true);
|
if (downNode && ev.pointerType !== 'mouse' && typeof onNodeHover === 'function') onNodeHover(downNode, true);
|
||||||
if (downNode && typeof onNodeLongPress === 'function') {
|
if (downNode && typeof onNodeLongPress === 'function') {
|
||||||
@@ -1646,13 +1654,29 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
|||||||
if (!moved && Math.hypot(dx, dy) > PAN_THRESHOLD) {
|
if (!moved && Math.hypot(dx, dy) > PAN_THRESHOLD) {
|
||||||
moved = true;
|
moved = true;
|
||||||
if (longTimer) { window.clearTimeout(longTimer); longTimer = 0; }
|
if (longTimer) { window.clearTimeout(longTimer); longTimer = 0; }
|
||||||
if (downNodeEl) downNodeEl.classList.remove('is-pressed'); // это свайп, а не нажатие
|
if (downNodeEl) downNodeEl.classList.remove('is-pressed'); // это drag/pan, а не нажатие
|
||||||
|
// Не центральный аватар перетаскивается сам. Пустой фон или центральный узел продолжают панорамировать карту.
|
||||||
|
nodeDragActive = Boolean(downNode && !downNode.isFocus);
|
||||||
|
if (nodeDragActive && cssBloom) endCssBloom();
|
||||||
// палец «съехал» с узла — снимаем временный ховер-превью (касанием), если он был
|
// палец «съехал» с узла — снимаем временный ховер-превью (касанием), если он был
|
||||||
if (ev.pointerType !== 'mouse' && typeof onNodeHover === 'function') onNodeHover(null, false);
|
if (ev.pointerType !== 'mouse' && typeof onNodeHover === 'function') onNodeHover(null, false);
|
||||||
camTargetX = null; camTargetY = null; // свайп отменяет доводчик камеры (приоритет жеста)
|
camTargetX = null; camTargetY = null; // свайп отменяет доводчик камеры (приоритет жеста)
|
||||||
cancelTween(); // жест прерывает анимацию центрирования
|
cancelTween(); // жест прерывает анимацию центрирования
|
||||||
dragging = true;
|
dragging = true;
|
||||||
}
|
}
|
||||||
|
if (moved && nodeDragActive && downNode) {
|
||||||
|
// dx/dy приходят в экранных пикселях, координаты узла живут в world-space — делим на текущий zoom.
|
||||||
|
const nx = nodeDragStartX + dx / Math.max(0.001, zoom);
|
||||||
|
const ny = nodeDragStartY + dy / Math.max(0.001, zoom);
|
||||||
|
downNode.x = nx; downNode.y = ny;
|
||||||
|
downNode.tx = nx; downNode.ty = ny;
|
||||||
|
downNode.bfx = nx; downNode.bfy = ny;
|
||||||
|
downNode.vx = 0; downNode.vy = 0;
|
||||||
|
downNode.fixedLayout = true;
|
||||||
|
renderNodes();
|
||||||
|
renderEdges();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (moved) {
|
if (moved) {
|
||||||
const newCamX = camStartX + dx;
|
const newCamX = camStartX + dx;
|
||||||
const newCamY = camStartY + dy;
|
const newCamY = camStartY + dy;
|
||||||
@@ -1683,14 +1707,23 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
|||||||
try { stage.releasePointerCapture(ev.pointerId); } catch { /* не было захвата — ок */ }
|
try { stage.releasePointerCapture(ev.pointerId); } catch { /* не было захвата — ок */ }
|
||||||
const wasMoved = moved;
|
const wasMoved = moved;
|
||||||
const wasLong = longFired;
|
const wasLong = longFired;
|
||||||
|
const movedNode = nodeDragActive ? downNode : null;
|
||||||
pointerId = null;
|
pointerId = null;
|
||||||
dragging = false;
|
dragging = false;
|
||||||
|
nodeDragActive = false;
|
||||||
// касание: убрали палец — снимаем временный ховер-превью (фиксацию ниже делает тап через onNodeTap)
|
// касание: убрали палец — снимаем временный ховер-превью (фиксацию ниже делает тап через onNodeTap)
|
||||||
if (ev.pointerType !== 'mouse' && typeof onNodeHover === 'function') onNodeHover(null, false);
|
if (ev.pointerType !== 'mouse' && typeof onNodeHover === 'function') onNodeHover(null, false);
|
||||||
|
|
||||||
if (wasMoved || wasLong) {
|
if (wasMoved || wasLong) {
|
||||||
// после pan даём физике чуть устаканиться и уснуть
|
if (wasMoved && movedNode) {
|
||||||
if (wasMoved) wake();
|
// Передаём окончательную world-позицию наружу, чтобы ручная правка пережила следующий setModel/history render.
|
||||||
|
if (typeof onNodeMoveEnd === 'function') onNodeMoveEnd(movedNode, { x: movedNode.x, y: movedNode.y });
|
||||||
|
renderEdges();
|
||||||
|
} else if (wasMoved) {
|
||||||
|
// после pan даём физике чуть устаканиться и уснуть
|
||||||
|
wake();
|
||||||
|
}
|
||||||
|
downNode = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// это был тап
|
// это был тап
|
||||||
@@ -1706,6 +1739,7 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
|||||||
}
|
}
|
||||||
if (tapNode.isFocus) {
|
if (tapNode.isFocus) {
|
||||||
if (typeof onCenterTap === 'function') onCenterTap(tapNode);
|
if (typeof onCenterTap === 'function') onCenterTap(tapNode);
|
||||||
|
downNode = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (typeof onNodeTap === 'function') {
|
if (typeof onNodeTap === 'function') {
|
||||||
@@ -1716,6 +1750,7 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
|||||||
// нет внешнего обработчика — внутреннее перецентрирование (фолбэк)
|
// нет внешнего обработчика — внутреннее перецентрирование (фолбэк)
|
||||||
startRecenterTween(tapNode.id);
|
startRecenterTween(tapNode.id);
|
||||||
}
|
}
|
||||||
|
downNode = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onResize() {
|
function onResize() {
|
||||||
|
|||||||
Reference in New Issue
Block a user