diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx index 8a7ce6f0247273932ba90cde7e8940490b7d528f..20ce1321d2d9e049ff6d4253ebbc7eeb73bd6894 100644 --- a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx +++ b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx @@ -1,6 +1,6 @@ import { GraphType, GraphTypeD3, LinkType, LinkTypeD3, NodeType, NodeTypeD3 } from '../types'; import { dataColors, visualizationColors } from 'config'; -import { ReactEventHandler, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; +import { useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; import { Application, AssetsBundle, @@ -22,6 +22,7 @@ import { MultiGraph } from 'graphology'; import { Viewport } from 'pixi-viewport'; import { NodelinkVisProps } from '../nodelinkvis'; import { Tooltip, TooltipContent, TooltipTrigger } from '@graphpolaris/shared/lib/components/tooltip'; +import { MovedEvent } from 'pixi-viewport/dist/types'; type Props = { @@ -70,8 +71,6 @@ export const NLPixi = (props: Props) => { const mouseInCanvas = useRef<boolean>(false); const isSetup = useRef(false); const ml = useML(); - const dragging = useRef<{ node: NodeTypeD3; gfx: Sprite } | null>(null); - const onlyClicked = useRef(false); const searchResults = useSearchResultData(); const graph = useRef<GraphTypeD3>({ nodes: [], links: [] }); @@ -124,71 +123,58 @@ export const NLPixi = (props: Props) => { const imperative = useRef<any>(null); + const mouseClickThreshold = 200; // Time between mouse up and down events that is considered a click, and not a drag. + useImperativeHandle(imperative, () => ({ - onDragStart(node: NodeTypeD3, gfx: Sprite) { - dragging.current = { node, gfx }; - onlyClicked.current = true; - - // todo: graphology does not support fixed nodes - // todo: after vis-settings panel is there, we should to also support the original d3 force to allow interactivity if needed - if (props.layoutAlgorithm === Layouts.FORCEATLAS2WEBWORKER) return; - if (viewport.current) viewport.current.pause = true; + onMouseDown(event: FederatedPointerEvent) { + (event as any).mouseDownTimeStamp = event.timeStamp; }, + + onMouseUpNode(event: FederatedPointerEvent) { + // If its a short click (not a drag) on the stage but not on a node: clear the selection and remove all popups. + const holdDownTime = event.timeStamp - (event as any).mouseDownTimeStamp; + if(holdDownTime > mouseClickThreshold) { + return; + } - onDragMove(movementX: number, movementY: number) { - if (props.layoutAlgorithm === Layouts.FORCEATLAS2WEBWORKER) return; + const sprite = event.target as Sprite; + const node = (sprite as any).node as NodeType; - for (const idx in popups) { - const p = popups[idx]; - p.pos.x += movementX / (viewport.current?.scaled || 1); - p.pos.y += movementY / (viewport.current?.scaled || 1); - popups[idx] = p; - setPopups([...popups]); - } + - if (dragging.current) { - onlyClicked.current = false; - if (quickPopup) setQuickPopup(undefined); - const idx = popups.findIndex((p) => p.node._id === dragging.current?.node._id); - if (idx >= 0) { - - const p = popups[idx]; - p.pos.x += movementX / (viewport.current?.scaled || 1); - p.pos.y += movementY / (viewport.current?.scaled || 1); - popups[idx] = p; - setPopups([...popups]); + if (event.shiftKey) { + setPopups([...popups, { node: node, pos: toGlobal(node) }]); + } else { + setPopups([{ node: node, pos: toGlobal(node) }]); + for (const popup of popups) { + const sprite = nodeMap.current.get(popup.node._id) as Sprite; + sprite.texture = Assets.get(textureId(false)); } - - if (!dragging.current.node.fx) dragging.current.node.fx = dragging.current.node.x || 0; - if (!dragging.current.node.fy) dragging.current.node.fy = dragging.current.node.y || 0; - dragging.current.node.fx += movementX / (viewport.current?.scaled || 1); - dragging.current.node.fy += movementY / (viewport.current?.scaled || 1); - // force.simulation.alpha(0.1).restart(); } + + sprite.texture = Assets.get(textureId(true)); + + props.onClick({ node: node, pos: toGlobal(node) }); + + event.stopPropagation(); }, - onDragEnd() { - if (dragging.current) { - // dragging.current.node.fx = null; - // dragging.current.node.fy = null; - if (viewport.current) viewport.current.pause = false; - if (onlyClicked.current) { - onlyClicked.current = false; - - if (popups.filter((d) => d.node._id === dragging.current?.node._id).length > 0) { - setPopups(popups.filter((p) => p.node._id !== dragging.current?.node._id)); - props.onClick(); - } else { - setPopups([...popups, { node: props.graph.nodes[dragging.current.node._id], pos: toGlobal(dragging.current.node) }]); - props.onClick({ node: dragging.current.node, pos: toGlobal(dragging.current.node) }); - } + onMouseUpStage(event: FederatedPointerEvent) { + // If its a short click (not a drag) on the stage but not on a node: clear the selection and remove all popups. + const holdDownTime = event.timeStamp - (event as any).mouseDownTimeStamp; + if(holdDownTime < mouseClickThreshold) { + for (const popup of popups) { + const sprite = nodeMap.current.get(popup.node._id) as Sprite; + sprite.texture = Assets.get(textureId(false)); } - this.onHover(dragging.current.node); - dragging.current = null; - } else { + setPopups([]); + props.onClick(); } }, - onHover(node: NodeTypeD3) { + + onHover(event: FederatedPointerEvent) { + const sprite = event.target as Sprite; + const node = (sprite as any).node as NodeType; if ( mouseInCanvas.current && viewport?.current && @@ -202,10 +188,14 @@ export const NLPixi = (props: Props) => { onUnHover() { setQuickPopup(undefined); }, - onPan() { - setPopups([]); - props.onClick(); - }, + onMoved(event: MovedEvent) { + for (const popup of popups) { + if (popup.node.x == null || popup.node.y == null) continue; + popup.pos.x = event.viewport.transform.position.x + (popup.node.x * event.viewport.scale.x); + popup.pos.y = event.viewport.transform.position.y + (popup.node.y * event.viewport.scale.y); + } + setPopups([...popups]); + } })); function resize() { @@ -246,22 +236,7 @@ export const NLPixi = (props: Props) => { } else return { x: 0, y: 0 }; } - function onDragStart(event: FederatedPointerEvent, node: NodeTypeD3, gfx: Sprite) { - event.stopPropagation(); - if (imperative.current) imperative.current.onDragStart(node, gfx); - } - - function onDragMove(event: FederatedPointerEvent) { - event.stopPropagation(); - if (imperative.current) imperative.current.onDragMove(event.movementX, event.movementY); - } - - function onDragEnd(event: FederatedPointerEvent) { - event.stopPropagation(); - if (imperative.current) imperative.current.onDragEnd(); - } - - const updateNode = (node: NodeTypeD3) => { + const updateNode = (node: NodeType) => { const gfx = nodeMap.current.get(node._id); if (!gfx) return; @@ -279,20 +254,6 @@ export const NLPixi = (props: Props) => { gfx.position.set(node.x, node.y); - gfx.off('mouseover'); - gfx.off('mousedown'); - gfx.on('mouseover', (e) => { - e.stopPropagation(); - e.preventDefault(); - if (imperative.current) imperative.current.onHover(node); - }); - gfx.on('mouseout', (e) => { - e.stopPropagation(); - e.preventDefault(); - if (imperative.current) imperative.current.onUnHover(); - }); - gfx.on('mousedown', (e) => onDragStart(e, node, gfx)); - // if (!item.position) { // item.position = new Point(node.x, node.y); // } else { @@ -319,23 +280,28 @@ export const NLPixi = (props: Props) => { // Do not draw node if it has no position if (node.x === undefined || node.y === undefined) return; - let gfx: Sprite; + let sprite: Sprite; const texture = Assets.get(textureId()); - gfx = new Sprite(texture); + sprite = new Sprite(texture); - gfx.tint = nodeColor(nodeMeta.type); + sprite.tint = nodeColor(nodeMeta.type); const scale = (Math.max(nodeMeta.radius || 5, 5) / 70) * 2; - gfx.scale.set(scale, scale); - gfx.anchor.set(0.5, 0.5); - - nodeMap.current.set(node._id, gfx); - nodeLayer.addChild(gfx); + sprite.scale.set(scale, scale); + sprite.anchor.set(0.5, 0.5); + + sprite.eventMode = 'static'; + sprite.on('mousedown', (e) => imperative.current.onMouseDown(e)); + sprite.on('mouseup', (e) => imperative.current.onMouseUpNode(e)); + sprite.on('mouseover', (e) => imperative.current.onHover(e)); + sprite.on('mouseout', (e) => imperative.current.onUnHover(e)); + + nodeMap.current.set(node._id, sprite); + nodeLayer.addChild(sprite); updateNode(node); - gfx.name = 'node_' + node._id; - gfx.eventMode = 'dynamic'; + (sprite as any).node = node; - return gfx; + return sprite; }; // /** UpdateRadius works just like UpdateColors, but also applies radius*/ @@ -524,7 +490,6 @@ export const NLPixi = (props: Props) => { }; const update = (forceClear = false) => { - setPopups([]); if (!props.graph || !ref.current) return; if (props.graph) { @@ -613,21 +578,13 @@ export const NLPixi = (props: Props) => { viewport.current.addChild(linkGfx); viewport.current.addChild(nodeLayer); - viewport.current.on('drag-start', (event) => { - imperative.current.onPan(); - }); - viewport.current.on('moved', () => { - for (const popup in popups) { - popup.pos.x += 10; - popup.pos.y += 10; - } - setPopups([...popups]); + viewport.current.on('moved', (event) => { + imperative.current.onMoved(event) }) app.stage.eventMode = 'dynamic'; - app.stage.on('pointerup', onDragEnd); - app.stage.on('mousemove', onDragMove); - app.stage.on('mouseup', onDragEnd); + app.stage.on('mousedown', (e) => imperative.current.onMouseDown(e)); + app.stage.on('mouseup', (e) => imperative.current.onMouseUpStage(e)); nodeMap.current.clear(); linkGfx.clear(); @@ -660,12 +617,18 @@ export const NLPixi = (props: Props) => { }; return ( <> - <Tooltip open={true}> + <Tooltip open={popups.length > 0}> <TooltipTrigger x={popups[0]?.pos.x} y={popups[0]?.pos.y} /> <TooltipContent> {popups[0] != null && <NLPopup onClose={() => {}} data={popups[0]} key={popups[0].node._id} />} </TooltipContent> </Tooltip> + <Tooltip open={popups.length > 1}> + <TooltipTrigger x={popups[1]?.pos.x} y={popups[1]?.pos.y} /> + <TooltipContent> + {popups[1] != null && <NLPopup onClose={() => {}} data={popups[1]} key={popups[1].node._id} />} + </TooltipContent> + </Tooltip> <div className="h-full w-full overflow-hidden" ref={ref}