From 5b543ca255af3c91e4ff716bd32468a90ba69a95 Mon Sep 17 00:00:00 2001 From: Milho001 <l.milhomemfrancochristino@uu.nl> Date: Tue, 19 Sep 2023 13:21:10 +0000 Subject: [PATCH] feat(nodelink): popup persists on click --- .../lib/vis/nodelink/components/NLPixi.tsx | 157 ++++++++++++------ libs/shared/lib/vis/nodelink/nodelinkvis.tsx | 9 +- 2 files changed, 110 insertions(+), 56 deletions(-) diff --git a/libs/shared/lib/vis/nodelink/components/NLPixi.tsx b/libs/shared/lib/vis/nodelink/components/NLPixi.tsx index 79202fad0..2370370e5 100644 --- a/libs/shared/lib/vis/nodelink/components/NLPixi.tsx +++ b/libs/shared/lib/vis/nodelink/components/NLPixi.tsx @@ -1,6 +1,6 @@ import { GraphType, LinkType, NodeType } from '../Types'; import { tailwindColors } from 'config'; -import { ReactEventHandler, useEffect, useMemo, useRef, useState } from 'react'; +import { ReactEventHandler, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; import { Application, Circle, Container, FederatedPointerEvent, Graphics, IPointData } from 'pixi.js'; import { binaryColor, nodeColor as nodeColor } from './utils'; import { select, zoom as d3zoom, drag as d3drag } from 'd3'; @@ -10,10 +10,12 @@ import { GraphQueryResult, GraphQueryResultFromBackendPayload } from '@graphpola import { useAppDispatch, useML } from '@graphpolaris/shared/lib/data-access'; import { ML, setShortestPathSource, setShortestPathTarget } from '@graphpolaris/shared/lib/data-access/store/mlSlice'; import { parseQueryResult } from './query2NL'; +import { NLPopup } from './NLPopup'; type Props = { - onClick: (node: NodeType) => void; - onHover: (data: { node: NodeType; pos: IPointData } | undefined) => void; + onClick: (node: NodeType, pos: IPointData) => void; + // onHover: (data: { node: NodeType; pos: IPointData }) => void; + // onUnHover: (data: { node: NodeType; pos: IPointData }) => void; highlightNodes: NodeType[]; currentShortestPathEdges?: LinkType[]; highlightedLinks?: LinkType[]; @@ -29,6 +31,9 @@ const links = new Container(); ////////////////// export const NLPixi = (props: Props) => { + const [quickPopup, setQuickPopup] = useState<{ node: NodeType; pos: IPointData } | undefined>(); + const [popups, setPopups] = useState<{ node: NodeType; pos: IPointData }[]>([]); + const nodeMap = useRef(new Map<string, Graphics>()); const linkMap = useRef(new Map<string, Graphics>()); const viewport = useRef<Viewport>(); @@ -39,6 +44,71 @@ export const NLPixi = (props: Props) => { const onlyClicked = useRef(false); const dispatch = useAppDispatch(); + const imperative = useRef<any>(null); + + useImperativeHandle(imperative, () => ({ + onDragStart(node: NodeType, gfx: Graphics) { + if (viewport.current) viewport.current.pause = true; + dragging.current = { node, gfx }; + onlyClicked.current = true; + }, + + onDragMove(movementX: number, movementY: number) { + if (dragging.current) { + onlyClicked.current = false; + // if (popups.length > 0) setPopups([]); + 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 (!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(); + } + }, + + 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)); + } else { + console.log('clicked', popups); + setPopups([...popups, { node: dragging.current.node, pos: toGlobal(dragging.current.node) }]); + } + + props.onClick(dragging.current.node, toGlobal(dragging.current.node)); + } + this.onHover(dragging.current.node); + dragging.current = null; + } + }, + onHover(node: NodeType) { + if (viewport?.current && !viewport?.current?.pause && node && popups.filter((p) => p.node.id === node.id).length === 0) { + setQuickPopup({ node, pos: toGlobal(node) }); + } + }, + onUnHover() { + setQuickPopup(undefined); + }, + onPan() { + setPopups([]); + }, + })); + // useEffect(() => { // app.renderer.resize(props.windowSize.width, props.windowSize.height); // app.render(); @@ -73,57 +143,28 @@ export const NLPixi = (props: Props) => { } }, [ref]); - function onDragStart(event: FederatedPointerEvent, node: NodeType, gfx: Graphics) { - // store a reference to the data - // the reason for this is because of multitouch - // we want to track the movement of this particular touch - event.stopPropagation(); - console.log('dragstart', node); - onHover(event, undefined); - - if (viewport.current) viewport.current.pause = true; - dragging.current = { node, gfx }; - onlyClicked.current = true; - } - - function onHover(event: FederatedPointerEvent, node: NodeType | undefined) { - event.stopPropagation(); - if (viewport?.current && !viewport?.current?.pause && node) { + function toGlobal(node: NodeType): IPointData { + if (viewport?.current) { const rect = ref.current?.getBoundingClientRect(); const x = (rect?.x || 0) + (node.x || 0); const y = (rect?.y || 0) + (node.y || 0); - props.onHover({ node, pos: viewport.current.toScreen(x, y) }); - } else { - props.onHover(undefined); - } + return viewport.current.toScreen(x, y); + } else return { x: 0, y: 0 }; } - function onDragEnd(event: FederatedPointerEvent) { - if (dragging.current) { - event.stopPropagation(); - dragging.current.node.fx = null; - dragging.current.node.fy = null; - if (viewport.current) viewport.current.pause = false; - if (onlyClicked.current) { - onlyClicked.current = false; - props.onClick(dragging.current.node); - } - onHover(event, dragging.current.node); - dragging.current = null; - } + function onDragStart(event: FederatedPointerEvent, node: NodeType, gfx: Graphics) { + event.stopPropagation(); + imperative.current.onDragStart(node, gfx); } function onDragMove(event: FederatedPointerEvent) { - if (dragging.current) { - onlyClicked.current = false; - event.stopPropagation(); - - 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 += event.movementX / (viewport.current?.scaled || 1); - dragging.current.node.fy += event.movementY / (viewport.current?.scaled || 1); - force.simulation.alpha(0.1).restart(); - } + event.stopPropagation(); + imperative.current.onDragMove(event.movementX, event.movementY); + } + + function onDragEnd(event: FederatedPointerEvent) { + event.stopPropagation(); + imperative.current.onDragEnd(); } const updateNode = (node: NodeType) => { @@ -151,8 +192,14 @@ export const NLPixi = (props: Props) => { gfx.off('mouseover'); gfx.off('mousedown'); - gfx.on('mouseover', (e) => onHover(e, node)); - gfx.on('mouseout', (e) => onHover(e, undefined)); + gfx.on('mouseover', (e) => { + e.stopPropagation(); + imperative.current.onHover(node); + }); + gfx.on('mouseout', (e) => { + e.stopPropagation(); + imperative.current.onUnHover(); + }); gfx.on('mousedown', (e) => onDragStart(e, node, gfx)); // if (!item.position) { @@ -394,6 +441,10 @@ export const NLPixi = (props: Props) => { viewport.current.addChild(links); viewport.current.addChild(nodes); + viewport.current.on('drag-start', (event) => { + imperative.current.onPan(); + }); + // app.stage.addChild(links); // app.stage.addChild(nodes); app.stage.eventMode = 'dynamic'; @@ -410,5 +461,13 @@ export const NLPixi = (props: Props) => { isSetup.current = true; }; - return <div className="h-full w-full overflow-hidden" ref={ref}></div>; + return ( + <> + {popups.map((popup) => ( + <NLPopup onClose={() => {}} data={popup} key={popup.node.id} /> + ))} + {quickPopup && <NLPopup onClose={() => {}} data={quickPopup} />} + <div className="h-full w-full overflow-hidden" ref={ref}></div> + </> + ); }; diff --git a/libs/shared/lib/vis/nodelink/nodelinkvis.tsx b/libs/shared/lib/vis/nodelink/nodelinkvis.tsx index 74baf7208..5b2c7737d 100644 --- a/libs/shared/lib/vis/nodelink/nodelinkvis.tsx +++ b/libs/shared/lib/vis/nodelink/nodelinkvis.tsx @@ -46,7 +46,6 @@ export const NodeLinkVis = React.memo((props: Props) => { const [graph, setGraph] = useImmer<GraphType | undefined>(undefined); const [highlightNodes, setHighlightNodes] = useState<NodeType[]>([]); const [highlightedLinks, setHighlightedLinks] = useState<LinkType[]>([]); - const [popup, setPopup] = useState<undefined | { node: NodeType; pos: PIXI.IPointData }>(undefined); const graphQueryResult = useGraphQueryResult(); const ml = useML(); @@ -67,6 +66,7 @@ export const NodeLinkVis = React.memo((props: Props) => { const onClickedNode = (node: NodeType, ml: ML) => { console.log('shortestPath', graph, ml.shortestPath.enabled); + if (graph) { if (ml.shortestPath.enabled) { console.log('shortestPath'); @@ -103,18 +103,13 @@ export const NodeLinkVis = React.memo((props: Props) => { return ( <> <div className="h-full w-full overflow-hidden" ref={ref}> - {!!popup && <NLPopup onClose={() => setPopup(undefined)} data={popup} />} <NLPixi graph={graph} highlightNodes={highlightNodes} highlightedLinks={highlightedLinks} - onClick={(node) => { - console.log(ml.shortestPath); + onClick={(node, pos) => { onClickedNode(node, ml); }} - onHover={(data) => { - setPopup(data); - }} /> {/* <VisConfigPanelComponent> */} -- GitLab