diff --git a/src/components/Universe/Graph/Cubes/NodeWrapper/index.tsx b/src/components/Universe/Graph/Cubes/NodeWrapper/index.tsx new file mode 100644 index 000000000..9364e0fb7 --- /dev/null +++ b/src/components/Universe/Graph/Cubes/NodeWrapper/index.tsx @@ -0,0 +1,94 @@ +import { useFrame } from '@react-three/fiber' +import gsap from 'gsap' +import { memo, useRef } from 'react' +import { Mesh, Vector3 } from 'three' +import { useGraphStore } from '~/stores/useGraphStore' +import { NodeExtended } from '~/types' +import { TextNode } from '../Text/index' + +type Props = { + node: NodeExtended + color: string + scale: number + index: number +} + +const offset = { x: 20, y: 20 } + +export const NodeWrapper = memo( + (props: Props) => { + const { node, color, index } = props + const simulation = useGraphStore((s) => s.simulation) + + const finishedSimulationCircle = useRef(false) + + const wrapperRef = useRef(null) + + useFrame(({ camera, size }) => { + if (wrapperRef.current && simulation) { + const simulationNode = simulation.nodes()[index] + + if (!simulationNode) { + return + } + + if (!finishedSimulationCircle.current) { + // Define the NDC coordinates for the fixed position + const ndc = new Vector3( + -1 + (offset.x * 2) / size.width, // Adjust for left offset + 1 - (offset.y * 2) / size.height, // Adjust for top offset + 0, // Near clipping plane + ) + + // Convert NDC to world space + const worldPosition = ndc.unproject(camera) + + // Maintain a fixed distance from the camera + const distanceFromCamera = 5 + const direction = worldPosition.sub(camera.position).normalize() + const fixedPosition = camera.position.clone().add(direction.multiplyScalar(distanceFromCamera)) + + wrapperRef.current.position.copy(fixedPosition) + + // Store the largest dimension as the "size" of the mesh + + wrapperRef.current.scale.set(0.1, 0.1, 0.1) + + wrapperRef.current.visible = false + } + + if (simulationNode.fx && !finishedSimulationCircle.current) { + wrapperRef.current.visible = true + finishedSimulationCircle.current = true + + gsap.to(wrapperRef.current.position, { + x: simulationNode.fx, // Destination X coordinate + y: simulationNode.fy, // Destination Y coordinate + z: simulationNode.fz, // Destination Z coordinate + duration: 4, // Animation duration in seconds + ease: 'power2.in', // Easing function + }) + + gsap.to(wrapperRef.current.scale, { + x: 1, // Destination X coordinate + y: 1, // Destination Y coordinate + z: 1, // Destination Z coordinate + duration: 4.5, // Animation duration in seconds + ease: 'power2.in', // Easing function + }) + } + } + }) + + return ( + + + + + + ) + }, + (prevProps, nextProps) => prevProps.node.ref_id === nextProps.node.ref_id, +) + +NodeWrapper.displayName = 'NodeWrapper' diff --git a/src/components/Universe/Graph/Cubes/SelectionDataNodes/index.tsx b/src/components/Universe/Graph/Cubes/SelectionDataNodes/index.tsx index 99fe4c07b..a6199bfdb 100644 --- a/src/components/Universe/Graph/Cubes/SelectionDataNodes/index.tsx +++ b/src/components/Universe/Graph/Cubes/SelectionDataNodes/index.tsx @@ -116,17 +116,9 @@ export const SelectionDataNodes = memo(() => { return ( <> - {selectionGraphData?.nodes.map((node, index) => ( + {selectionGraphData?.nodes.map((node) => ( - + ))} diff --git a/src/components/Universe/Graph/Cubes/Text/index.tsx b/src/components/Universe/Graph/Cubes/Text/index.tsx index f0c06a83a..522795a5a 100644 --- a/src/components/Universe/Graph/Cubes/Text/index.tsx +++ b/src/components/Universe/Graph/Cubes/Text/index.tsx @@ -1,8 +1,7 @@ import { Billboard, Plane, Svg, Text } from '@react-three/drei' -import { useFrame, useThree } from '@react-three/fiber' -import gsap from 'gsap' +import { useFrame } from '@react-three/fiber' import { memo, useRef } from 'react' -import { Mesh, MeshBasicMaterial, Vector3 } from 'three' +import { DoubleSide, Mesh, MeshBasicMaterial, Vector3 } from 'three' import { Icons } from '~/components/Icons' import { useTraceUpdate } from '~/hooks/useTraceUpdate' import { useGraphStore } from '~/stores/useGraphStore' @@ -19,7 +18,6 @@ type Props = { hide?: boolean ignoreDistance: boolean scale: number - index: number } function splitStringIntoThreeParts(text: string): string { @@ -40,22 +38,13 @@ function splitStringIntoThreeParts(text: string): string { return `${firstPart}\n${secondPart}\n${thirdPart}` } -const offset = { x: 20, y: 20 } - export const TextNode = memo( (props: Props) => { - const { node, hide, ignoreDistance, color, index } = props - const simulation = useGraphStore((s) => s.simulation) - const positionAnimationRef = useRef(null) - const scaleAnimationRef = useRef(null) - - const finishedSimulationCircle = useRef(false) + const { node, hide, ignoreDistance, color } = props const svgRef = useRef(null) - const ringRef = useRef(null) + const nodeRef = useRef(null) const circleRef = useRef(null) - const wrapperRef = useRef(null) - const { camera: perspectiveCamera, size } = useThree() useTraceUpdate(props) @@ -69,10 +58,10 @@ export const TextNode = memo( const { selectedNode, hoveredNode, activeEdge } = useGraphStore.getState() const checkDistance = () => { - const nodePosition = nodePositionRef.current.setFromMatrixPosition(ringRef.current!.matrixWorld) + const nodePosition = nodePositionRef.current.setFromMatrixPosition(nodeRef.current!.matrixWorld) - if (ringRef.current) { - ringRef.current.visible = ignoreDistance ? true : nodePosition.distanceTo(camera.position) < 1500 + if (nodeRef.current) { + nodeRef.current.visible = ignoreDistance ? true : nodePosition.distanceTo(camera.position) < 1500 } // Set visibility based on distance @@ -85,83 +74,37 @@ export const TextNode = memo( activeEdge?.source === node.ref_id if (isActive) { - if (ringRef.current) { - ringRef.current.visible = true + if (nodeRef.current) { + nodeRef.current.visible = true } - const scale = 3 + 0.2 * Math.sin(clock.getElapsedTime() * 2) // Adjust frequency and amplitude + const elapsedTime = clock.getElapsedTime() + const cycleTime = 2 // Duration of one cycle (in seconds) + + const t = (elapsedTime % cycleTime) / cycleTime // Normalize time to a 0-1 range within the cycle + const scale = 1 + t * 2 // Grow from scale 3 to 8 + const opacity = 1 - t // Fade out as it grows if (circleRef.current) { circleRef.current.visible = true circleRef.current.scale.set(scale, scale, scale) + + // Ensure the material is of the correct type and set the opacity + const { material } = circleRef.current + + if (material instanceof MeshBasicMaterial) { + material.opacity = opacity + } } return } if (circleRef.current) { - circleRef.current.visible = true + circleRef.current.visible = false } - if (finishedSimulationCircle.current) { - checkDistance() - } - - if (wrapperRef.current && simulation) { - const simulationNode = simulation.nodes()[index] - - if (!simulationNode) { - return - } - - if (!finishedSimulationCircle.current) { - // Define the NDC coordinates for the fixed position - const ndc = new Vector3( - -1 + (offset.x * 2) / size.width, // Adjust for left offset - 1 - (offset.y * 2) / size.height, // Adjust for top offset - 0, // Near clipping plane - ) - - // Convert NDC to world space - const worldPosition = ndc.unproject(perspectiveCamera) - - // Maintain a fixed distance from the camera - const distanceFromCamera = 5 - const direction = worldPosition.sub(perspectiveCamera.position).normalize() - const fixedPosition = perspectiveCamera.position.clone().add(direction.multiplyScalar(distanceFromCamera)) - - wrapperRef.current.position.copy(fixedPosition) - - // Store the largest dimension as the "size" of the mesh - - wrapperRef.current.scale.set(0.1, 0.1, 0.1) - - wrapperRef.current.visible = false - } - - if (simulationNode.fx && !finishedSimulationCircle.current) { - wrapperRef.current.visible = true - finishedSimulationCircle.current = true - positionAnimationRef?.current?.kill() - scaleAnimationRef?.current?.kill() - - positionAnimationRef.current = gsap.to(wrapperRef.current.position, { - x: simulationNode.fx, // Destination X coordinate - y: simulationNode.fy, // Destination Y coordinate - z: simulationNode.fz, // Destination Z coordinate - duration: 4, // Animation duration in seconds - ease: 'power2.in', // Easing function - }) - - scaleAnimationRef.current = gsap.to(wrapperRef.current.scale, { - x: 1, // Destination X coordinate - y: 1, // Destination Y coordinate - z: 1, // Destination Z coordinate - duration: 4.5, // Animation duration in seconds - ease: 'power2.in', // Easing function - }) - } - } + checkDistance() }) const primaryColor = normalizedSchemasByType[node.node_type]?.primary_color @@ -182,20 +125,22 @@ export const TextNode = memo( } return ( - - - - - - - - - - - {node.properties?.image_url && texture ? ( - - + + + {/* Inner radius, Outer radius, Segments */} + + + + {node.properties?.image_url && texture ? ( + + - - ) : ( - { - svg.traverse((child) => { - if (child instanceof Mesh) { - // Apply dynamic color to meshes - // eslint-disable-next-line no-param-reassign - child.material = new MeshBasicMaterial({ color }) - } - }) - }} - position={[-15, 15, 0]} - scale={2} - src={`svg-icons/${iconName}.svg`} - strokeMaterial={{ color: 'yellow' }} - userData={node} /> - )} - - {sanitizedNodeName && ( - - {splitStringIntoThreeParts(sanitizedNodeName)} - - )} - - - + + ) : ( + { + svg.traverse((child) => { + if (child instanceof Mesh) { + // Apply dynamic color to meshes + // eslint-disable-next-line no-param-reassign + child.material = new MeshBasicMaterial({ color }) + } + }) + }} + position={[-15, 15, 0]} + scale={2} + src={`svg-icons/${iconName}.svg`} + strokeMaterial={{ color: 'yellow' }} + userData={node} + /> + )} + + {sanitizedNodeName && ( + + {splitStringIntoThreeParts(sanitizedNodeName)} + + )} + + ) }, (prevProps, nextProps) => diff --git a/src/components/Universe/Graph/Cubes/index.tsx b/src/components/Universe/Graph/Cubes/index.tsx index 4da4b98be..b2e117bd9 100644 --- a/src/components/Universe/Graph/Cubes/index.tsx +++ b/src/components/Universe/Graph/Cubes/index.tsx @@ -4,13 +4,13 @@ import { memo, useCallback, useRef } from 'react' import { Object3D } from 'three' import { useAppStore } from '~/stores/useAppStore' import { useDataStore, useNodeTypes } from '~/stores/useDataStore' -import { useGraphStore, useHoveredNode, useSelectedNode, useSelectedNodeRelativeIds } from '~/stores/useGraphStore' +import { useGraphStore, useHoveredNode, useSelectedNode } from '~/stores/useGraphStore' import { NodeExtended } from '~/types' import { colors } from '~/utils' import { NodePoints } from './NodePoints' +import { NodeWrapper } from './NodeWrapper' import { RelevanceBadges } from './RelevanceBadges' import { SelectionDataNodes } from './SelectionDataNodes' -import { TextNode } from './Text' const POINTER_IN_DELAY = 200 @@ -48,7 +48,6 @@ export const Cubes = memo(() => { const selectedNode = useSelectedNode() const hoveredNode = useHoveredNode() - const relativeIds = useSelectedNodeRelativeIds() const { selectionGraphData, showSelectionGraph, setHoveredNode, setIsHovering } = useGraphStore((s) => s) const nodeTypes = useNodeTypes() @@ -134,37 +133,28 @@ export const Cubes = memo(() => { const hideUniverse = showSelectionGraph && !!selectedNode return ( - selected.filter((f) => !!f.userData?.ref_id)} + onChange={handleSelect} + onPointerOut={onPointerOut} + onPointerOver={onPointerIn} + > + + {data?.nodes.map((node: NodeExtended, index: number) => { + const color = COLORS_MAP[nodeTypes.indexOf(node.node_type)] || colors.white + + return + })} + + + + + + {hideUniverse && } + - - {data?.nodes.map((node: NodeExtended, index) => { - const hide = !!selectedNode && (relativeIds.includes(node.ref_id) || selectedNode.ref_id === node.ref_id) - const color = COLORS_MAP[nodeTypes.indexOf(node.node_type)] || colors.white - - return ( - - ) - })} - - - - - - {hideUniverse && } - + ) })