From f3a7336e5f493bf68900ac616297baa4b0707fec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=B0=D1=81=D1=83=D0=BB?= Date: Wed, 25 Dec 2024 02:13:09 +0300 Subject: [PATCH 1/2] feat: selection v2 --- .../Controls/CameraAnimations/index.ts | 7 +- .../SelectionDataNodes/Connections/index.tsx | 58 ++-- .../Graph/Cubes/SelectionDataNodes/index.tsx | 270 +++++++----------- src/network/fetchGraphData/index.ts | 1 + src/utils/apiUrlFromSwarmHost/index.ts | 2 +- 5 files changed, 139 insertions(+), 199 deletions(-) diff --git a/src/components/Universe/Controls/CameraAnimations/index.ts b/src/components/Universe/Controls/CameraAnimations/index.ts index bc8fca123..25be62c69 100644 --- a/src/components/Universe/Controls/CameraAnimations/index.ts +++ b/src/components/Universe/Controls/CameraAnimations/index.ts @@ -22,9 +22,7 @@ export const useCameraAnimations = ( useAutoNavigate(cameraControlsRef) - const isUserDragging = useControlStore((s) => s.isUserDragging) - - const { graphRadius, disableCameraRotation } = useGraphStore((s) => s) + const { graphRadius } = useGraphStore((s) => s) useEffect(() => { if (!enabled) { @@ -58,6 +56,9 @@ export const useCameraAnimations = ( useFrame((_, delta) => { if (cameraControlsRef.current) { // do camera rotation + const { disableCameraRotation } = useGraphStore.getState() + const { isUserDragging } = useControlStore.getState() + if (!disableCameraRotation && !isUserDragging) { cameraControlsRef.current.azimuthAngle += autoRotateSpeed * delta * MathUtils.DEG2RAD } diff --git a/src/components/Universe/Graph/Cubes/SelectionDataNodes/Connections/index.tsx b/src/components/Universe/Graph/Cubes/SelectionDataNodes/Connections/index.tsx index a58248659..d198f43c8 100644 --- a/src/components/Universe/Graph/Cubes/SelectionDataNodes/Connections/index.tsx +++ b/src/components/Universe/Graph/Cubes/SelectionDataNodes/Connections/index.tsx @@ -1,43 +1,39 @@ import { memo } from 'react' -import { useGraphStore } from '~/stores/useGraphStore' import { Link } from '~/types' import { LinkPosition } from '../../..' import { Connection } from './Connection' type Props = { linksPosition: Map + links: Link[] } -export const Connections = memo(({ linksPosition }: Props) => { - const { selectionGraphData } = useGraphStore((s) => s) +export const Connections = memo(({ linksPosition, links }: Props) => ( + + {links?.map((l: Link) => { + const position = linksPosition.get(l.ref_id) || { + sx: 0, + sy: 0, + sz: 0, + tx: 0, + ty: 0, + tz: 0, + } - return ( - - {selectionGraphData?.links.map((l: Link) => { - const position = linksPosition.get(l.ref_id) || { - sx: 0, - sy: 0, - sz: 0, - tx: 0, - ty: 0, - tz: 0, - } - - return ( - - ) - })} - - ) -}) + return ( + + ) + })} + +)) Connections.displayName = 'Connections' diff --git a/src/components/Universe/Graph/Cubes/SelectionDataNodes/index.tsx b/src/components/Universe/Graph/Cubes/SelectionDataNodes/index.tsx index b8e52c5fe..a648c6f8e 100644 --- a/src/components/Universe/Graph/Cubes/SelectionDataNodes/index.tsx +++ b/src/components/Universe/Graph/Cubes/SelectionDataNodes/index.tsx @@ -1,5 +1,4 @@ import { Html } from '@react-three/drei' -import { forceLink, forceManyBody, forceRadial, forceSimulation } from 'd3-force-3d' import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Box3, Color, Group, Sphere, Vector3 } from 'three' import { Line2 } from 'three-stdlib' @@ -7,56 +6,64 @@ import { useShallow } from 'zustand/react/shallow' import { usePrevious } from '~/hooks/usePrevious' import { fetchNodeEdges } from '~/network/fetchGraphData' import { useDataStore } from '~/stores/useDataStore' -import { useGraphStore, useSelectedNode, useSelectedNodeRelativeIds } from '~/stores/useGraphStore' +import { useGraphStore, useSelectedNode } from '~/stores/useGraphStore' import { useSchemaStore } from '~/stores/useSchemaStore' -import { ForceSimulation } from '~/transformers/forceSimulation' -import { GraphData, Link, Node, NodeExtended } from '~/types' +import { Link, Node, NodeExtended } from '~/types' import { LinkPosition } from '../..' import { Connections } from './Connections' import { Node as GraphNode } from './Node' -const MAX_LENGTH = 6 +const radius = 200 +const MAX_LENGTH = 7 + +type GraphData = { + links: Link[] + nodes: NodeExtended[] +} export const SelectionDataNodes = memo(() => { - const [simulation2d, setSimulation2D] = useState(null) + const [selectionData, setSelectionData] = useState() - const { dataInitial, nodesNormalized, addNewNode } = useDataStore((s) => s) + const { addNewNode } = useDataStore((s) => s) const selectedNode = useSelectedNode() const groupRef = useRef(null) const linksPositionRef = useRef(new Map()) - const selectedNodeRelativeIds = useSelectedNodeRelativeIds().slice(0, MAX_LENGTH) - const prevSelectedNodeId = usePrevious(selectedNode?.ref_id) const { normalizedSchemasByType } = useSchemaStore((s) => s) - const { selectionGraphData, selectionPath, setSelectionData, setSelectedNode, setSelectionGraphRadius } = - useGraphStore(useShallow((s) => s)) + const { setSelectedNode, setSelectionGraphRadius } = useGraphStore(useShallow((s) => s)) + + const graphData: GraphData = useMemo(() => { + if (!selectionData?.nodes.length) { + return { nodes: [], links: [] } + } + + const thetaSpan = (Math.PI * 2) / (selectionData.nodes.length - 1) // Angle between points - const pathNodes = useMemo(() => { - const nodes: NodeExtended[] = selectionPath - .slice(-3, -1) - .filter((id) => !!nodesNormalized.get(id)) - .map((i, index) => { - const node = nodesNormalized.get(i) as unknown as Node + const nodes = selectionData.nodes.map((i, index) => { + const theta = thetaSpan * index + const x = i.ref_id === selectedNode?.ref_id ? 0 : Math.cos(theta) * radius + const y = i.ref_id === selectedNode?.ref_id ? 0 : Math.sin(theta) * radius + const z = i.ref_id === selectedNode?.ref_id ? 0 : 0 - return { ...node, fx: 0, fy: -(index + 1) * 200, fz: 0, x: 0, y: 0, z: 0 } - }) + return { ...i, x, y, z } + }) - return nodes - }, [nodesNormalized, selectionPath]) + return { nodes, links: [] } + }, [selectionData, selectedNode]) useEffect(() => { const init = async () => { if (selectedNode?.ref_id && selectedNode.ref_id !== prevSelectedNodeId) { try { - const data = await fetchNodeEdges(selectedNode.ref_id, 0, 5) + const data = await fetchNodeEdges(selectedNode.ref_id, 0, 5, { useSubGraph: false }) if (data) { const filteredNodes: NodeExtended[] = data.nodes.filter( - (node, index) => node.ref_id !== selectedNode.ref_id && index < 7, + (node, index) => node.ref_id !== selectedNode.ref_id && index < MAX_LENGTH, ) const graphNodes = filteredNodes.map((node: Node) => ({ ...node, x: 0, y: 0, z: 0 })) @@ -73,11 +80,8 @@ export const SelectionDataNodes = memo(() => { ) setSelectionData({ nodes, links: links as unknown as GraphData['links'] }) - setSimulation2D(null) linksPositionRef.current = new Map() - // - addNewNode({ nodes: filteredNodes, edges: links }) } } catch (error) { @@ -92,166 +96,119 @@ export const SelectionDataNodes = memo(() => { }, [addNewNode, prevSelectedNodeId, selectedNode, setSelectionData]) useEffect(() => { - return - - const structuredLinks = structuredClone(dataInitial?.links || []) - - if (prevSelectedNodeId === selectedNode?.ref_id) { - return - } - - const graphNodes: NodeExtended[] = selectedNodeRelativeIds - .filter((id) => !!nodesNormalized.get(id)) - .map((id: string) => { - const node = nodesNormalized.get(id) as unknown as Node - - return { ...node, x: 0, y: 0, z: 0 } - }) - - const nodes: NodeExtended[] = [ - ...graphNodes, - { ...selectedNode, x: 0, y: 0, z: 0, fx: 0, fy: 0, fz: 0 } as NodeExtended, - ] - - if (nodes) { - const links = structuredLinks.filter( - (link: Link) => - nodes.some((node: NodeExtended) => node.ref_id === link.target) && - nodes.some((node: NodeExtended) => node.ref_id === link.source), - ) - - setSelectionData({ nodes, links: links as unknown as GraphData['links'] }) - setSimulation2D(null) - linksPositionRef.current = new Map() - } - }, [dataInitial, selectedNode, selectedNodeRelativeIds, setSelectionData, prevSelectedNodeId, nodesNormalized]) - - useEffect(() => { - if (simulation2d || !selectionGraphData.nodes.length) { + if (!groupRef.current) { return } - const structuredLinks = structuredClone(selectionGraphData.links) - - const simulation = forceSimulation([]) - .numDimensions(2) - .stop() - .nodes(selectionGraphData.nodes) - .force( - 'link', - forceLink() - .links(structuredLinks) - .id((d: NodeExtended) => d.ref_id) - .distance(() => 150), - ) - .force('radial', forceRadial(20, 0, 0, 0).strength(0)) - .force('charge', forceManyBody().strength(-500)) - .alpha(1) - .restart() - - setSimulation2D(simulation) - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectionGraphData, simulation2d]) + const gr = groupRef.current as Group - useEffect(() => { - if (!simulation2d) { - return - } + gr.children.forEach((mesh, index) => { + const simulationNode = graphData.nodes[index] - simulation2d.on('tick', () => { - if (!groupRef.current) { - return + if (simulationNode) { + mesh.position.set(simulationNode.x, simulationNode.y, simulationNode.z) } + }) - const gr = groupRef.current as Group - - gr.children.forEach((mesh, index) => { - const simulationNode = simulation2d.nodes()[index] - - if (simulationNode) { - mesh.position.set(simulationNode.x, simulationNode.y, simulationNode.z) - } - }) - - const grConnections = groupRef.current.getObjectByName('simulation-3d-group__connections') as Group + const grConnections = groupRef.current.getObjectByName('simulation-3d-group__connections') as Group - grConnections.children.forEach((g, i) => { - const r = g.children[0] - const text = g.children[1] + grConnections.children.forEach((g, i) => { + const r = g.children[0] + const text = g.children[1] - if (r instanceof Line2) { - const Line = r as Line2 - const link = selectionGraphData?.links[i] + if (r instanceof Line2) { + const Line = r as Line2 + const link = selectionData?.links[i] - if (link) { - const sourceNode = simulation2d.nodes().find((n: NodeExtended) => n.ref_id === link.source) - const targetNode = simulation2d.nodes().find((n: NodeExtended) => n.ref_id === link.target) + if (link) { + const sourceNode = graphData.nodes.find((n: NodeExtended) => n.ref_id === link.source) + const targetNode = graphData.nodes.find((n: NodeExtended) => n.ref_id === link.target) - if (!sourceNode || !targetNode) { - return - } + if (!sourceNode || !targetNode) { + return + } - const { x: sx, y: sy } = sourceNode - const { x: tx, y: ty } = targetNode + const { x: sx, y: sy } = sourceNode + const { x: tx, y: ty } = targetNode - linksPositionRef.current.set(link.ref_id, { - sx, - sy, - tx, - ty, - sz: 0, - tz: 0, - }) + linksPositionRef.current.set(link.ref_id, { + sx, + sy, + tx, + ty, + sz: 0, + tz: 0, + }) - const midPoint = new Vector3((sx + tx) / 2, (sy + ty) / 2, 0) + const midPoint = new Vector3((sx + tx) / 2, (sy + ty) / 2, 0) - text.position.set(midPoint.x, midPoint.y, 1) + text.position.set(midPoint.x, midPoint.y, 1) - let angle = Math.atan2(ty - sy, tx - sx) + let angle = Math.atan2(ty - sy, tx - sx) - if (tx < sx || (Math.abs(tx - sx) < 0.01 && ty < sy)) { - angle += Math.PI - } + if (tx < sx || (Math.abs(tx - sx) < 0.01 && ty < sy)) { + angle += Math.PI + } - text.rotation.set(0, 0, angle) + text.rotation.set(0, 0, angle) - Line.geometry.setPositions([sx, sy, 0, tx, ty, 0]) + Line.geometry.setPositions([sx, sy, 0, tx, ty, 0]) - const { material } = Line + const { material } = Line - material.color = new Color('white') - } + material.color = new Color('white') } - }) + } }) - simulation2d.on('end', () => { - const nodesVector = simulation2d.nodes().map((i: NodeExtended) => new Vector3(i.x, i.y, i.z)) + const nodesVector = graphData.nodes.map((i: NodeExtended) => new Vector3(i.x, i.y, i.z)) - const boundingBox = new Box3().setFromPoints(nodesVector) + const boundingBox = new Box3().setFromPoints(nodesVector) - const boundingSphere = new Sphere() + const boundingSphere = new Sphere() - boundingBox.getBoundingSphere(boundingSphere) + boundingBox.getBoundingSphere(boundingSphere) - const sphereRadius = Math.min(5000, boundingSphere.radius) + const sphereRadius = Math.min(50, boundingSphere.radius) - setSelectionGraphRadius(sphereRadius) - }) - }, [normalizedSchemasByType, selectionGraphData?.links, simulation2d, setSelectionGraphRadius]) + setSelectionGraphRadius(sphereRadius) + }, [normalizedSchemasByType, selectionData?.links, setSelectionGraphRadius, graphData.nodes]) const handleSelect = useCallback( (node: NodeExtended) => { - setSelectedNode(node) + if (selectedNode) { + const newSelectedNode = graphData.nodes.find((i) => i.ref_id === node.ref_id) + + const nodes = [ + { ...newSelectedNode, x: 0, y: 0, z: 0, fx: 0, fy: 0, fz: 0 }, + { + ...selectedNode, + ...(newSelectedNode?.x !== undefined ? { fx: newSelectedNode?.x, x: newSelectedNode.x } : { x: 0 }), + ...(newSelectedNode?.y !== undefined ? { fy: newSelectedNode?.y, y: newSelectedNode.y } : { y: 0 }), + ...(newSelectedNode?.z !== undefined ? { fz: newSelectedNode?.z, z: newSelectedNode.z } : { z: 0 }), + }, + ] + + const links = graphData.links.filter( + (i) => + (i.target === selectedNode?.ref_id && i.source === node.ref_id) || + (i.source === selectedNode?.ref_id && i.target === node.ref_id), + ) + + setSelectionData({ nodes: nodes as NodeExtended[], links }) + + if (false) { + setSelectedNode(node) + } + } }, - [setSelectedNode], + [graphData.links, graphData.nodes, selectedNode, setSelectedNode], ) return ( <> - {selectionGraphData?.nodes.map((node) => ( + {graphData.nodes?.map((node) => ( { ))} - + - {false && ( - - {pathNodes.map((node) => ( - - - handleSelect(node)} - selected={node.ref_id === selectedNode?.ref_id} - /> - - - ))} - - )} ) }) diff --git a/src/network/fetchGraphData/index.ts b/src/network/fetchGraphData/index.ts index 1c56acf55..ebc73b322 100644 --- a/src/network/fetchGraphData/index.ts +++ b/src/network/fetchGraphData/index.ts @@ -86,6 +86,7 @@ export const fetchNodeEdges = async ( include_properties: includeProperties.toString(), includeContent: includeContent.toString(), depth: depth.toString(), + top_node_count: '5', use_sub_graph: useSubGraph.toString(), ...(nodeType.length > 0 && { node_type: JSON.stringify(nodeType) }), // Add node_type if not empty }).toString() diff --git a/src/utils/apiUrlFromSwarmHost/index.ts b/src/utils/apiUrlFromSwarmHost/index.ts index 763fefd2e..e86beae47 100644 --- a/src/utils/apiUrlFromSwarmHost/index.ts +++ b/src/utils/apiUrlFromSwarmHost/index.ts @@ -22,7 +22,7 @@ export function apiUrlFromSwarmHost(): string | undefined { url = `https://${finalHost}` } } else if (origin.includes('localhost')) { - url = 'https://bitcoin.sphinx.chat' + url = 'https://graphmindset.sphinx.chat' } return `${url}/api` From bd90e83ef6ca4709ca96f9b27a15a288d2cac45f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=B0=D1=81=D1=83=D0=BB?= Date: Wed, 25 Dec 2024 20:00:50 +0300 Subject: [PATCH 2/2] feat: add replacement animation --- .../Controls/CameraAnimations/index.ts | 7 - .../Connections/Connection/index.tsx | 4 +- .../SelectionDataNodes/Connections/index.tsx | 137 +++++++++--- .../Cubes/SelectionDataNodes/Node/index.tsx | 83 +++++-- .../Graph/Cubes/SelectionDataNodes/index.tsx | 210 ++++++++---------- .../SelectionContent/Controls/index.tsx | 8 +- src/stores/useDataStore/index.ts | 30 ++- src/types/index.ts | 2 + src/utils/apiUrlFromSwarmHost/index.ts | 2 +- 9 files changed, 291 insertions(+), 192 deletions(-) diff --git a/src/components/Universe/Controls/CameraAnimations/index.ts b/src/components/Universe/Controls/CameraAnimations/index.ts index 25be62c69..ccc163e6e 100644 --- a/src/components/Universe/Controls/CameraAnimations/index.ts +++ b/src/components/Universe/Controls/CameraAnimations/index.ts @@ -31,13 +31,6 @@ export const useCameraAnimations = ( } }, [enabled]) - // useEffect(() => { - // if (cameraControlsRef.current && graphRadius) { - // cameraControlsRef.current.maxDistance = cameraControlsRef.current.getDistanceToFitSphere(graphRadius + 200) - // cameraControlsRef.current.minDistance = 100 - // } - // }, [graphRadius, cameraControlsRef]) - useEffect(() => { if (!selectedNode && cameraControlsRef.current) { cameraControlsRef.current.setLookAt( diff --git a/src/components/Universe/Graph/Cubes/SelectionDataNodes/Connections/Connection/index.tsx b/src/components/Universe/Graph/Cubes/SelectionDataNodes/Connections/Connection/index.tsx index 8c5f202d5..c6d9b2b61 100644 --- a/src/components/Universe/Graph/Cubes/SelectionDataNodes/Connections/Connection/index.tsx +++ b/src/components/Universe/Graph/Cubes/SelectionDataNodes/Connections/Connection/index.tsx @@ -30,9 +30,9 @@ const _Connection = (props: LineComponentProps) => { points={[sourceX, sourceY, sourceZ, targetX, targetY, targetZ]} /> - + - + {label} diff --git a/src/components/Universe/Graph/Cubes/SelectionDataNodes/Connections/index.tsx b/src/components/Universe/Graph/Cubes/SelectionDataNodes/Connections/index.tsx index d198f43c8..6a6c49a80 100644 --- a/src/components/Universe/Graph/Cubes/SelectionDataNodes/Connections/index.tsx +++ b/src/components/Universe/Graph/Cubes/SelectionDataNodes/Connections/index.tsx @@ -1,39 +1,120 @@ -import { memo } from 'react' -import { Link } from '~/types' +import { memo, useEffect, useRef } from 'react' +import { Box3, Color, Group, Sphere, Vector3 } from 'three' +import { Line2 } from 'three-stdlib' +import { useShallow } from 'zustand/react/shallow' +import { useGraphStore } from '~/stores/useGraphStore' +import { Link, NodeExtended } from '~/types' import { LinkPosition } from '../../..' import { Connection } from './Connection' type Props = { - linksPosition: Map links: Link[] + nodes: NodeExtended[] } -export const Connections = memo(({ linksPosition, links }: Props) => ( - - {links?.map((l: Link) => { - const position = linksPosition.get(l.ref_id) || { - sx: 0, - sy: 0, - sz: 0, - tx: 0, - ty: 0, - tz: 0, +export const Connections = memo(({ links, nodes }: Props) => { + const groupRef = useRef(null) + const linksPositionRef = useRef(new Map()) + const { setSelectionGraphRadius } = useGraphStore(useShallow((s) => s)) + + useEffect(() => { + if (!groupRef.current) { + return + } + + console.log('connection to updated') + + const grConnections = groupRef.current + + grConnections.children.forEach((g, i) => { + const r = g.children[0] + const text = g.children[1] + + if (r instanceof Line2) { + const Line = r as Line2 + const link = links[i] + + if (link) { + const sourceNode = nodes.find((n: NodeExtended) => n.ref_id === link.source) + const targetNode = nodes.find((n: NodeExtended) => n.ref_id === link.target) + + if (!sourceNode || !targetNode) { + return + } + + const { x: sx, y: sy } = sourceNode + const { x: tx, y: ty } = targetNode + + linksPositionRef.current.set(link.ref_id, { + sx, + sy, + tx, + ty, + sz: 0, + tz: 0, + }) + + const midPoint = new Vector3((sx + tx) / 2, (sy + ty) / 2, 0) + + text.position.set(midPoint.x, midPoint.y, 1) + + let angle = Math.atan2(ty - sy, tx - sx) + + if (tx < sx || (Math.abs(tx - sx) < 0.01 && ty < sy)) { + angle += Math.PI + } + + text.rotation.set(0, 0, angle) + + Line.geometry.setPositions([sx, sy, 0, tx, ty, 0]) + + const { material } = Line + + material.color = new Color('white') + } } + }) + + console.log('connection updated') + + const nodesVector = nodes.map((i: NodeExtended) => new Vector3(i.x, i.y, i.z)) + + const boundingBox = new Box3().setFromPoints(nodesVector) + + const boundingSphere = new Sphere() + + boundingBox.getBoundingSphere(boundingSphere) + + setSelectionGraphRadius(boundingSphere.radius) + }, [links, setSelectionGraphRadius, nodes]) + + return ( + + {links?.map((l: Link) => { + const position = linksPositionRef.current?.get(l.ref_id) || { + sx: 0, + sy: 0, + sz: 0, + tx: 0, + ty: 0, + tz: 0, + } - return ( - - ) - })} - -)) + return ( + + ) + })} + + ) +}) Connections.displayName = 'Connections' diff --git a/src/components/Universe/Graph/Cubes/SelectionDataNodes/Node/index.tsx b/src/components/Universe/Graph/Cubes/SelectionDataNodes/Node/index.tsx index 1f07b1446..117288b3e 100644 --- a/src/components/Universe/Graph/Cubes/SelectionDataNodes/Node/index.tsx +++ b/src/components/Universe/Graph/Cubes/SelectionDataNodes/Node/index.tsx @@ -1,4 +1,8 @@ +import { Html } from '@react-three/drei' +import { useFrame } from '@react-three/fiber' +import { useRef } from 'react' import styled from 'styled-components' +import { Mesh, Vector3 } from 'three' import { Flex } from '~/components/common/Flex' import { Icons } from '~/components/Icons' import CloseIcon from '~/components/Icons/CloseIcon' @@ -17,12 +21,25 @@ type Props = { node: NodeExtended rounded?: boolean selected: boolean - onClick: () => void + onClick: (id: string) => void + x: number + y: number + z: number + id: string } -export const Node = ({ onClick, node, selected, rounded = true }: Props) => { +export const Node = ({ onClick, node, selected, rounded = true, x, y, z, id }: Props) => { + const nodeRef = useRef(null) + const { normalizedSchemasByType, getNodeKeysByType } = useSchemaStore((s) => s) const setSelectedNode = useGraphStore((s) => s.setSelectedNode) + const targetPosition = new Vector3(x, y, z) + + useFrame(() => { + if (nodeRef.current) { + nodeRef.current.position.lerp(targetPosition, 0.05) + } + }) const primaryIcon = normalizedSchemasByType[node.node_type]?.icon @@ -34,26 +51,32 @@ export const Node = ({ onClick, node, selected, rounded = true }: Props) => { const titleShortened = title ? truncateText(title, 30) : '' return ( - - <> - {selected ? ( - - setSelectedNode(null)}> - - -
{Icon ? : }
- {titleShortened} -
- ) : ( + + + <> - -
{Icon ? : }
-
- {titleShortened} + {selected ? ( + + setSelectedNode(null)}> + + +
{Icon ? : }
+ {titleShortened} +
+ ) : ( + <> + onClick(id)} rounded={rounded}> + + {!node?.properties?.image_url ? {Icon ? : } : null} + + + {titleShortened} + + )} - )} - -
+
+ +
) } @@ -105,8 +128,8 @@ const IconButton = styled(Flex)` position: absolute; top: -10px; right: -10px; - width: 24px; - height: 24px; + width: 30px; + height: 30px; border-radius: 40px; display: flex; @@ -115,8 +138,22 @@ const IconButton = styled(Flex)` background: black; color: #ffffff; border-radius: 100%; - font-size: 16px; + font-size: 30px; cursor: pointer; transition: opacity 0.4s; box-shadow: 0px 2px 12px rgba(0, 0, 0, 0.5); ` + +type AvatarProps = { + src: string +} + +const Avatar = styled(Flex)` + background-image: ${({ src }) => `url(${src})`}; + background-size: cover; + background-position: center; + background-repeat: no-repeat; + width: 32px; + height: 32px; + border-radius: 50%; +` diff --git a/src/components/Universe/Graph/Cubes/SelectionDataNodes/index.tsx b/src/components/Universe/Graph/Cubes/SelectionDataNodes/index.tsx index a648c6f8e..930036a8a 100644 --- a/src/components/Universe/Graph/Cubes/SelectionDataNodes/index.tsx +++ b/src/components/Universe/Graph/Cubes/SelectionDataNodes/index.tsx @@ -1,20 +1,17 @@ -import { Html } from '@react-three/drei' import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { Box3, Color, Group, Sphere, Vector3 } from 'three' -import { Line2 } from 'three-stdlib' +import { Group } from 'three' import { useShallow } from 'zustand/react/shallow' import { usePrevious } from '~/hooks/usePrevious' import { fetchNodeEdges } from '~/network/fetchGraphData' import { useDataStore } from '~/stores/useDataStore' import { useGraphStore, useSelectedNode } from '~/stores/useGraphStore' -import { useSchemaStore } from '~/stores/useSchemaStore' import { Link, Node, NodeExtended } from '~/types' import { LinkPosition } from '../..' import { Connections } from './Connections' import { Node as GraphNode } from './Node' -const radius = 200 -const MAX_LENGTH = 7 +const radius = 50 +const MAX_LENGTH = 5 type GraphData = { links: Link[] @@ -24,36 +21,82 @@ type GraphData = { export const SelectionDataNodes = memo(() => { const [selectionData, setSelectionData] = useState() - const { addNewNode } = useDataStore((s) => s) + const { addNewNode, nodesNormalized } = useDataStore((s) => s) const selectedNode = useSelectedNode() const groupRef = useRef(null) + const [pathGraph, setPathGraph] = useState(null) + const linksPositionRef = useRef(new Map()) const prevSelectedNodeId = usePrevious(selectedNode?.ref_id) - const { normalizedSchemasByType } = useSchemaStore((s) => s) - - const { setSelectedNode, setSelectionGraphRadius } = useGraphStore(useShallow((s) => s)) + const { setSelectedNode } = useGraphStore(useShallow((s) => s)) - const graphData: GraphData = useMemo(() => { + const newData: GraphData = useMemo(() => { if (!selectionData?.nodes.length) { return { nodes: [], links: [] } } - const thetaSpan = (Math.PI * 2) / (selectionData.nodes.length - 1) // Angle between points + const oldNodes = pathGraph?.nodes || [] + + // Combine `oldNodes` and `selectionData.nodes` for position calculations + const combinedNodes = [...oldNodes, ...selectionData.nodes] + + // Calculate the angular spacing considering both old and new nodes + const totalNodes = combinedNodes.length + const thetaSpan = (Math.PI * 2) / totalNodes // Angle between points + + const nodes = selectionData.nodes.map((node, index) => { + // Check if the node exists in oldNodes + const existingNode = oldNodes.find((oldNode) => oldNode.ref_id === node.ref_id) + + if (existingNode) { + // Retain the old node's position + return existingNode + } - const nodes = selectionData.nodes.map((i, index) => { - const theta = thetaSpan * index - const x = i.ref_id === selectedNode?.ref_id ? 0 : Math.cos(theta) * radius - const y = i.ref_id === selectedNode?.ref_id ? 0 : Math.sin(theta) * radius - const z = i.ref_id === selectedNode?.ref_id ? 0 : 0 + // Calculate new position for nodes not in oldNodes + const theta = thetaSpan * (oldNodes.length + index) // Offset by oldNodes count + const x = node.ref_id === selectedNode?.ref_id ? 0 : Math.cos(theta) * radius + const y = node.ref_id === selectedNode?.ref_id ? 0 : Math.sin(theta) * radius + const z = node.ref_id === selectedNode?.ref_id ? 0 : 0 - return { ...i, x, y, z } + return { ...node, x, y, z } }) - return { nodes, links: [] } - }, [selectionData, selectedNode]) + // Retain old nodes with their original positions + const retainedOldNodes = oldNodes.filter( + (oldNode) => !selectionData.nodes.some((node) => node.ref_id === oldNode.ref_id), + ) + + // Merge the calculated nodes with retained old nodes + const mergedNodes = [...nodes, ...retainedOldNodes] + + return { nodes: mergedNodes, links: selectionData.links } + }, [selectionData, selectedNode, pathGraph?.nodes]) + + const graphData: GraphData = useMemo(() => { + if (newData?.nodes?.length) { + return newData + } + + console.log(pathGraph) + + if (pathGraph) { + return { + nodes: pathGraph.nodes, + links: pathGraph.links, + } + } + + const selected = selectedNode?.ref_id ? nodesNormalized.get(selectedNode?.ref_id || '') : null + + return { + nodes: selected ? [{ ...selected, x: 0, y: 0, z: 0 }] : [], + links: [], + } + }, [newData, pathGraph, selectedNode?.ref_id, nodesNormalized]) useEffect(() => { const init = async () => { @@ -62,16 +105,13 @@ export const SelectionDataNodes = memo(() => { const data = await fetchNodeEdges(selectedNode.ref_id, 0, 5, { useSubGraph: false }) if (data) { - const filteredNodes: NodeExtended[] = data.nodes.filter( + const filteredNodes: Node[] = data.nodes.filter( (node, index) => node.ref_id !== selectedNode.ref_id && index < MAX_LENGTH, ) const graphNodes = filteredNodes.map((node: Node) => ({ ...node, x: 0, y: 0, z: 0 })) - const nodes: NodeExtended[] = [ - ...graphNodes, - { ...selectedNode, x: 0, y: 0, z: 0, fx: 0, fy: 0, fz: 0 } as NodeExtended, - ] + const nodes: NodeExtended[] = [...graphNodes, { ...selectedNode, x: 0, y: 0, z: 0, fx: 0, fy: 0, fz: 0 }] const links = data.edges.filter( (link: Link) => @@ -95,110 +135,33 @@ export const SelectionDataNodes = memo(() => { } }, [addNewNode, prevSelectedNodeId, selectedNode, setSelectionData]) - useEffect(() => { - if (!groupRef.current) { - return - } - - const gr = groupRef.current as Group - - gr.children.forEach((mesh, index) => { - const simulationNode = graphData.nodes[index] - - if (simulationNode) { - mesh.position.set(simulationNode.x, simulationNode.y, simulationNode.z) - } - }) - - const grConnections = groupRef.current.getObjectByName('simulation-3d-group__connections') as Group - - grConnections.children.forEach((g, i) => { - const r = g.children[0] - const text = g.children[1] - - if (r instanceof Line2) { - const Line = r as Line2 - const link = selectionData?.links[i] - - if (link) { - const sourceNode = graphData.nodes.find((n: NodeExtended) => n.ref_id === link.source) - const targetNode = graphData.nodes.find((n: NodeExtended) => n.ref_id === link.target) - - if (!sourceNode || !targetNode) { - return - } - - const { x: sx, y: sy } = sourceNode - const { x: tx, y: ty } = targetNode - - linksPositionRef.current.set(link.ref_id, { - sx, - sy, - tx, - ty, - sz: 0, - tz: 0, - }) - - const midPoint = new Vector3((sx + tx) / 2, (sy + ty) / 2, 0) - - text.position.set(midPoint.x, midPoint.y, 1) - - let angle = Math.atan2(ty - sy, tx - sx) - - if (tx < sx || (Math.abs(tx - sx) < 0.01 && ty < sy)) { - angle += Math.PI - } - - text.rotation.set(0, 0, angle) - - Line.geometry.setPositions([sx, sy, 0, tx, ty, 0]) - - const { material } = Line - - material.color = new Color('white') - } - } - }) - - const nodesVector = graphData.nodes.map((i: NodeExtended) => new Vector3(i.x, i.y, i.z)) - - const boundingBox = new Box3().setFromPoints(nodesVector) - - const boundingSphere = new Sphere() - - boundingBox.getBoundingSphere(boundingSphere) - - const sphereRadius = Math.min(50, boundingSphere.radius) - - setSelectionGraphRadius(sphereRadius) - }, [normalizedSchemasByType, selectionData?.links, setSelectionGraphRadius, graphData.nodes]) - const handleSelect = useCallback( - (node: NodeExtended) => { + (id: string) => { if (selectedNode) { - const newSelectedNode = graphData.nodes.find((i) => i.ref_id === node.ref_id) + const newSelectedNode = graphData.nodes.find((i) => i.ref_id === id) const nodes = [ { ...newSelectedNode, x: 0, y: 0, z: 0, fx: 0, fy: 0, fz: 0 }, { ...selectedNode, - ...(newSelectedNode?.x !== undefined ? { fx: newSelectedNode?.x, x: newSelectedNode.x } : { x: 0 }), - ...(newSelectedNode?.y !== undefined ? { fy: newSelectedNode?.y, y: newSelectedNode.y } : { y: 0 }), + ...(newSelectedNode?.x !== undefined ? { fx: -newSelectedNode.x, x: -newSelectedNode.x } : { x: 0 }), + ...(newSelectedNode?.y !== undefined ? { fy: -newSelectedNode.y, y: -newSelectedNode.y } : { y: 0 }), ...(newSelectedNode?.z !== undefined ? { fz: newSelectedNode?.z, z: newSelectedNode.z } : { z: 0 }), }, ] const links = graphData.links.filter( (i) => - (i.target === selectedNode?.ref_id && i.source === node.ref_id) || - (i.source === selectedNode?.ref_id && i.target === node.ref_id), + (i.target === selectedNode?.ref_id && i.source === id) || + (i.source === selectedNode?.ref_id && i.target === id), ) - setSelectionData({ nodes: nodes as NodeExtended[], links }) + console.log(links, 'here') + setSelectionData(null) + setPathGraph({ nodes: nodes as NodeExtended[], links }) - if (false) { - setSelectedNode(node) + if (newSelectedNode) { + setSelectedNode(newSelectedNode) } } }, @@ -209,17 +172,18 @@ export const SelectionDataNodes = memo(() => { <> {graphData.nodes?.map((node) => ( - - - handleSelect(node)} - selected={node.ref_id === selectedNode?.ref_id} - /> - - + ))} - + ) diff --git a/src/components/Universe/SelectionContent/Controls/index.tsx b/src/components/Universe/SelectionContent/Controls/index.tsx index 898eade77..24b5d61d1 100644 --- a/src/components/Universe/SelectionContent/Controls/index.tsx +++ b/src/components/Universe/SelectionContent/Controls/index.tsx @@ -10,11 +10,15 @@ export const Controls = () => { const [smoothTime] = useState(0.8) useEffect(() => { + console.log(selectionGraphRadius, 'radius') + if (cameraControlsRef.current) { + const distance = cameraControlsRef.current.getDistanceToFitSphere(50 + 5) + cameraControlsRef.current.setLookAt( selectionGraphCameraPosition.x, selectionGraphCameraPosition.y, - selectionGraphRadius * 2, + distance, 0, 0, 0, @@ -31,7 +35,7 @@ export const Controls = () => { boundaryEnclosesCamera makeDefault maxDistance={12000} - minDistance={100} + minDistance={1} polarRotateSpeed={0} smoothTime={smoothTime} /> diff --git a/src/stores/useDataStore/index.ts b/src/stores/useDataStore/index.ts index 2dfb79131..93814fb5e 100644 --- a/src/stores/useDataStore/index.ts +++ b/src/stores/useDataStore/index.ts @@ -23,8 +23,8 @@ export type DataStore = { splashDataLoading: boolean abortRequest: boolean categoryFilter: NodeType | null - dataInitial: { nodes: NodeExtended[]; links: Link[] } | null - dataNew: { nodes: NodeExtended[]; links: Link[] } | null + dataInitial: { nodes: Node[]; links: Link[] } | null + dataNew: { nodes: Node[]; links: Link[] } | null filters: FilterParams isFetching: boolean isLoadingNew: boolean @@ -41,7 +41,7 @@ export type DataStore = { seedQuestions: string[] | null runningProjectId: string runningProjectMessages: string[] - nodesNormalized: Map + nodesNormalized: Map linksNormalized: Map setTrendingTopics: (trendingTopics: Trending[]) => void @@ -137,7 +137,7 @@ const defaultData: Omit< runningProjectId: '', hideNodeDetails: false, nodeTypes: [], - nodesNormalized: new Map(), + nodesNormalized: new Map(), linksNormalized: new Map(), } @@ -243,7 +243,7 @@ export const useDataStore = create()( nodesFilteredByFilters.forEach((node) => { if (!normalizedNodesMap.has(node.ref_id)) { - normalizedNodesMap.set(node.ref_id, node) + normalizedNodesMap.set(node.ref_id, { ...node, sources: [], targets: [] }) newNodes.push(node) } }) @@ -265,6 +265,24 @@ export const useDataStore = create()( ) { normalizedLinksMap.set(link.ref_id, link) newLinks.push(link) + + // Update sources and targets for the respective nodes + const sourceNode = normalizedNodesMap.get(link.source) + const targetNode = normalizedNodesMap.get(link.target) + + if (sourceNode && targetNode) { + if (sourceNode.targets) { + sourceNode.targets.push(link.target) + } else { + sourceNode.targets = [link.target] + } + + if (targetNode.sources) { + targetNode.sources.push(link.source) + } else { + targetNode.sources = [link.source] + } + } } }) @@ -320,7 +338,7 @@ export const useDataStore = create()( dataNew: null, runningProjectId: '', nodeTypes: [], - nodesNormalized: new Map(), + nodesNormalized: new Map(), linksNormalized: new Map(), }) }, diff --git a/src/types/index.ts b/src/types/index.ts index f33828606..3df5b7400 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -153,6 +153,8 @@ export type NodeExtended = Node & { fx?: number fy?: number fz?: number + sources?: string[] + targets?: string[] } export type Link = { diff --git a/src/utils/apiUrlFromSwarmHost/index.ts b/src/utils/apiUrlFromSwarmHost/index.ts index e86beae47..763fefd2e 100644 --- a/src/utils/apiUrlFromSwarmHost/index.ts +++ b/src/utils/apiUrlFromSwarmHost/index.ts @@ -22,7 +22,7 @@ export function apiUrlFromSwarmHost(): string | undefined { url = `https://${finalHost}` } } else if (origin.includes('localhost')) { - url = 'https://graphmindset.sphinx.chat' + url = 'https://bitcoin.sphinx.chat' } return `${url}/api`