diff options
Diffstat (limited to 'pages/index.tsx')
-rw-r--r-- | pages/index.tsx | 205 |
1 files changed, 142 insertions, 63 deletions
diff --git a/pages/index.tsx b/pages/index.tsx index b22e678..42170a1 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -73,6 +73,10 @@ export default function Home() { } export function GraphPage() { + const [threeDim, setThreeDim] = usePersistantState('3d', false) + const [tagColors, setTagColors] = usePersistantState<TagColors>('tagCols', {}) + const [scope, setScope] = useState<Scope>({ nodeIds: [] }) + const [physics, setPhysics] = usePersistantState('physics', initialPhysics) const [filter, setFilter] = usePersistantState('filter', initialFilter) const [visuals, setVisuals] = usePersistantState('visuals', initialVisuals) @@ -220,9 +224,6 @@ export function GraphPage() { const { setEmacsTheme } = useContext(ThemeContext) - const [threeDim, setThreeDim] = usePersistantState('3d', false) - const [tagColors, setTagColors] = usePersistantState<TagColors>('tagCols', {}) - const [scope, setScope] = useState<Scope>({ nodeIds: [] }) const scopeRef = useRef<Scope>({ nodeIds: [] }) const behaviorRef = useRef(initialBehavior) behaviorRef.current = behavior @@ -248,26 +249,28 @@ export function GraphPage() { ), ) if (command === 'zoom') { - console.log(sr) if (sr.nodeIds.length) { - console.log('emptying') - console.log('scope ' + sr.nodeIds) setScope({ nodeIds: [] }) } - setTimeout(() => fg.zoomToFit(speed, padding, (node: OrgRoamNode) => nodes[node.id!]), 50) + setTimeout( + () => fg.zoomToFit(speed, padding, (node: NodeObject) => nodes[node.id as string]), + 50, + ) return } if (!sr.nodeIds.length) { setScope({ nodeIds: [emacsNode] }) setTimeout(() => { - fg.centerAt(0, 0, speed) + fg.centerAt(0, 0, 10) + fg.zoomToFit(1, padding) }, 50) return } if (bh.localSame !== 'add') { setScope({ nodeIds: [emacsNode] }) setTimeout(() => { - fg.centerAt(0, 0, speed) + fg.centerAt(0, 0, 10) + fg.zoomToFit(1, padding) }, 50) return } @@ -281,7 +284,8 @@ export function GraphPage() { ) { setScope({ nodeIds: [emacsNode] }) setTimeout(() => { - fg.centerAt(0, 0, speed) + fg.centerAt(0, 0, 10) + fg.zoomToFit(1, padding) }, 50) return } @@ -289,10 +293,14 @@ export function GraphPage() { ...currentScope, nodeIds: [...currentScope.nodeIds, emacsNode as string], })) - setTimeout(() => fg.zoomToFit(speed, padding, (node: OrgRoamNode) => nodes[node.id!]), 50) + setTimeout(() => { + fg.centerAt(0, 0, 10) + fg.zoomToFit(1, padding) + }, 50) } useEffect(() => { + // initialize websocket WebSocketRef.current = new ReconnectingWebSocket('ws://localhost:35903') WebSocketRef.current.addEventListener('open', () => { console.log('Connection with Emacs established') @@ -332,6 +340,20 @@ export function GraphPage() { }) }, []) + useEffect(() => { + const fg = graphRef.current + if (!fg || scope.nodeIds.length > 1) { + return + } + if (!scope.nodeIds.length && physics.gravityOn) { + fg.zoomToFit() + return + } + setTimeout(() => { + fg.zoomToFit(5, 200) + }, 50) + }, [scope.nodeIds]) + if (!graphData) { return null } @@ -424,18 +446,21 @@ export const Graph = forwardRef(function (props: GraphProps, graphRef: any) { const [hoverNode, setHoverNode] = useState<NodeObject | null>(null) + const [rightClickedNode, setRightClickedNode] = useState<OrgRoamNode | null>(null) + const [contextPos, setContextPos] = useState([0, 0]) + const theme = useTheme() const { emacsTheme } = useContext<ThemeContextProps>(ThemeContext) const handleLocal = (node: OrgRoamNode, add: string) => { - if (scope.nodeIds.includes(node.id as string)) { - return - } if (add === 'replace') { setScope({ nodeIds: [node.id] }) return } + if (scope.nodeIds.includes(node.id as string)) { + return + } setScope((currentScope: Scope) => ({ ...currentScope, nodeIds: [...currentScope.nodeIds, node.id as string], @@ -461,7 +486,15 @@ export const Graph = forwardRef(function (props: GraphProps, graphRef: any) { sendMessageToEmacs('create', { id: node.id, title: node.title, ref: node.properties.ROAM_REFS }) } - const handleClick = (click: string, node: OrgRoamNode) => { + const contextMenu = useDisclosure() + + const openContextMenu = (node: OrgRoamNode, event: any) => { + setContextPos([event.pageX, event.pageY]) + setRightClickedNode(node) + contextMenu.onOpen() + } + + const handleClick = (click: string, node: OrgRoamNode, event: any) => { switch (click) { case mouse.local: { handleLocal(node, behavior.localSame) @@ -471,11 +504,41 @@ export const Graph = forwardRef(function (props: GraphProps, graphRef: any) { openNodeInEmacs(node) break } + case mouse.context: { + openContextMenu(node, event) + } default: break } } + const findNthNeighbors = (ids: string[], n: number) => { + let queue = [ids[0]] + let todo: string[] = [] + const completed = [ids[0]] + Array.from({ length: n }, () => { + queue.forEach((node) => { + const links = linksByNodeId[node as string] ?? [] + links.forEach((link) => { + const [sourceId, targetId] = normalizeLinkEnds(link) + if (!completed.includes(sourceId)) { + todo.push(sourceId) + return + } + if (!completed.includes(targetId)) { + todo.push(targetId) + return + } + return + }) + }) + queue = todo + todo.forEach((neighbor) => neighbor && completed.push(neighbor)) + todo = [] + }) + return completed + } + const centralHighlightedNode = useRef<NodeObject | null>(null) useEffect(() => { @@ -560,55 +623,79 @@ export const Graph = forwardRef(function (props: GraphProps, graphRef: any) { const filteredLinks = graphData.links.filter((link) => { const [sourceId, targetId] = normalizeLinkEnds(link) if ( - filter.bad || - filter.tagsBlacklist.length || - filter.tagsWhitelist.length || - filter.filelessCites + !filteredNodeIds.includes(sourceId as string) || + !filteredNodeIds.includes(targetId as string) ) { - return ( - filteredNodeIds.includes(sourceId as string) && - filteredNodeIds.includes(targetId as string) - ) + return false } const linkRoam = link as OrgRoamLink return filter.parents || linkRoam.type !== 'parent' }) - return { filteredNodes, filteredLinks } + return { nodes: filteredNodes, links: filteredLinks } }, [filter, graphData]) - const scopedGraphData = useMemo(() => { - const scopedNodes = filteredGraphData.filteredNodes.filter((node) => { - const links = linksByNodeId[node.id as string] ?? [] - return ( - scope.nodeIds.includes(node.id as string) || - links.some((link) => { - return scope.nodeIds.includes(link.source) || scope.nodeIds.includes(link.target) - }) - ) - }) + const [scopedGraphData, setScopedGraphData] = useState<GraphData>({ nodes: [], links: [] }) + useEffect(() => { + if (!scope.nodeIds.length) { + return + } + const oldScopedNodes = scope.nodeIds.length > 1 ? scopedGraphData.nodes : [] + const oldScopedNodeIds = oldScopedNodes.map((node) => node.id as string) + const neighbs = findNthNeighbors(scope.nodeIds, 1) + const newScopedNodes = filteredGraphData.nodes + .filter((node) => { + if (oldScopedNodes.length) { + if (oldScopedNodeIds.includes(node.id as string)) { + return false + } + const links = linksByNodeId[node.id as string] ?? [] + return links.some((link) => { + return scope.nodeIds.includes(link.source) || scope.nodeIds.includes(link.target) + }) + } + return neighbs.includes(node.id as string) + // this creates new nodes, to separate them from the nodes in the global graph + // and positions them in the center, so that the camera is not so jumpy + }) + .map((node) => { + return { ...node, x: 0, y: 0, vy: 0, vx: 0 } + }) + const scopedNodes = [...oldScopedNodes, ...newScopedNodes] const scopedNodeIds = scopedNodes.map((node) => node.id as string) - const scopedLinks = filteredGraphData.filteredLinks.filter((link) => { - const [sourceId, targetId] = normalizeLinkEnds(link) - return ( - scopedNodeIds.includes(sourceId as string) && scopedNodeIds.includes(targetId as string) - ) - }) - - return scope.nodeIds.length === 0 - ? { nodes: filteredGraphData.filteredNodes, links: filteredGraphData.filteredLinks } - : { - nodes: scopedNodes, - links: scopedLinks, + const oldScopedLinks = scope.nodeIds.length > 1 ? scopedGraphData.links : [] + const newScopedLinks = filteredGraphData.links + .filter((link) => { + // we need to cover both because force-graph modifies the original data + // but if we supply the original data on each render, the graph will re-render sporadically + const [sourceId, targetId] = normalizeLinkEnds(link) + if ( + oldScopedLinks.length && + oldScopedNodeIds.includes(targetId) && + oldScopedNodeIds.includes(sourceId) + ) { + return false } + return ( + scopedNodeIds.includes(sourceId as string) && scopedNodeIds.includes(targetId as string) + ) + }) + .map((link) => { + const [sourceId, targetId] = normalizeLinkEnds(link) + return { source: sourceId, target: targetId } + }) + + const scopedLinks = [...oldScopedLinks, ...newScopedLinks] + + setScopedGraphData({ nodes: scopedNodes, links: scopedLinks }) }, [filter, scope, graphData]) useEffect(() => { ;(async () => { const fg = graphRef.current const d3 = await d3promise - if (physics.gravityOn) { + if (physics.gravityOn && !(scope.nodeIds.length && !physics.gravityLocal)) { fg.d3Force('x', d3.forceX().strength(physics.gravity)) fg.d3Force('y', d3.forceY().strength(physics.gravity)) threeDim && fg.d3Force('z', d3.forceZ().strength(physics.gravity)) @@ -628,18 +715,18 @@ export const Graph = forwardRef(function (props: GraphProps, graphRef: any) { physics.collision ? d3.forceCollide().radius(physics.collisionStrength) : null, ) })() - }, [physics]) + }, [physics, threeDim, scope]) // Normally the graph doesn't update when you just change the physics parameters // This forces the graph to make a small update when you do useEffect(() => { graphRef.current?.d3ReheatSimulation() - }, [physics]) + }, [physics, scope.nodeIds.length]) // shitty handler to check for doubleClicks const lastNodeClickRef = useRef(0) - const [opacity, setOpacity] = useState<number>(1) + const [opacity, setOpacity] = useState(1) const [fadeIn, cancel] = useAnimation((x) => setOpacity(x), { duration: visuals.animationSpeed, algorithm: algos[visuals.algorithmName], @@ -811,20 +898,13 @@ export const Graph = forwardRef(function (props: GraphProps, graphRef: any) { } const [dragging, setDragging] = useState(false) - const contextMenu = useDisclosure() - const [rightClickedNode, setRightClickedNode] = useState<OrgRoamNode | null>(null) - const [contextPos, setContextPos] = useState([0, 0]) - const openContextMenu = (node: OrgRoamNode, event: any) => { - setContextPos([event.pageX, event.pageY]) - setRightClickedNode(node) - contextMenu.onOpen() - } const graphCommonProps: ComponentPropsWithoutRef<typeof TForceGraph2D> = { - graphData: scopedGraphData, + graphData: scope.nodeIds.length ? scopedGraphData : filteredGraphData, width: windowWidth, height: windowHeight, backgroundColor: theme.colors.gray[visuals.backgroundColor], + warmupTicks: scope.nodeIds.length === 1 ? 100 : scope.nodeIds.length > 1 ? 20 : 0, nodeLabel: (node) => (node as OrgRoamNode).title, nodeColor: (node) => { return getNodeColor(node as OrgRoamNode) @@ -961,9 +1041,9 @@ export const Graph = forwardRef(function (props: GraphProps, graphRef: any) { const isDoubleClick = event.timeStamp - lastNodeClickRef.current < 400 lastNodeClickRef.current = event.timeStamp if (isDoubleClick) { - return handleClick('double', node) + return handleClick('double', node, event) } - return handleClick('click', node) + return handleClick('click', node, event) }, onBackgroundClick: () => { contextMenu.onClose() @@ -989,9 +1069,8 @@ export const Graph = forwardRef(function (props: GraphProps, graphRef: any) { }, onNodeRightClick: (nodeArg, event) => { const node = nodeArg as OrgRoamNode - openContextMenu(node, event) - //handleClick('right', node) + handleClick('right', node, event) }, onNodeDrag: (node) => { contextMenu.onClose() |