From b9010ad86feb0cd61a42ea3da04d2bb859544004 Mon Sep 17 00:00:00 2001 From: C H Date: Mon, 9 Sep 2024 13:07:42 +0800 Subject: [PATCH] feat(brain): experiment with vr brain --- package.json | 1 + .../CyberlinksGraphContainerVR.tsx | 68 ++++++ .../CyberlinksGraph/CyberlinksGraphVR.tsx | 222 ++++++++++++++++++ .../GraphHoverInfo/GraphHoverInfoVR.tsx | 80 +++++++ src/pages/robot/Brain/Brain.tsx | 13 + src/pages/robot/Brain/ui/GraphViewVR.tsx | 16 ++ yarn.lock | 40 +++- 7 files changed, 439 insertions(+), 1 deletion(-) create mode 100644 src/features/cyberlinks/CyberlinksGraph/CyberlinksGraphContainerVR.tsx create mode 100644 src/features/cyberlinks/CyberlinksGraph/CyberlinksGraphVR.tsx create mode 100644 src/features/cyberlinks/CyberlinksGraph/GraphHoverInfo/GraphHoverInfoVR.tsx create mode 100644 src/pages/robot/Brain/ui/GraphViewVR.tsx diff --git a/package.json b/package.json index 6ee83e5e9..f5333a2d5 100644 --- a/package.json +++ b/package.json @@ -180,6 +180,7 @@ "@types/react-router-dom": "^5.3.3", "@uniswap/sdk": "^3.0.3", "@xenova/transformers": "^2.17.0", + "aframe": "^1.6.0", "apollo-boost": "^0.4.7", "bech32": "^1.1.3", "big.js": "^5.2.2", diff --git a/src/features/cyberlinks/CyberlinksGraph/CyberlinksGraphContainerVR.tsx b/src/features/cyberlinks/CyberlinksGraph/CyberlinksGraphContainerVR.tsx new file mode 100644 index 000000000..eb4e7b83d --- /dev/null +++ b/src/features/cyberlinks/CyberlinksGraph/CyberlinksGraphContainerVR.tsx @@ -0,0 +1,68 @@ +import { createPortal } from 'react-dom'; +import { Loading } from 'src/components'; +import { useAppSelector } from 'src/redux/hooks'; +import { selectCurrentAddress } from 'src/redux/features/pocket'; +import useCyberlinks from './useCyberlinks'; +import { PORTAL_ID } from '../../../containers/application/App'; +import LinksGraphVR from './CyberlinksGraphVR'; + +type Props = { + address?: string; + toPortal?: boolean; + size?: number; + limit?: number; + data?: any; +}; + +function CyberlinksGraphContainer({ + address, + toPortal, + size, + limit, + data, +}: Props) { + const { data: fetchData, loading } = useCyberlinks( + { address }, + { + limit, + skip: !!data, + } + ); + + const currentAddress = useAppSelector(selectCurrentAddress); + + const content = loading ? ( +
+ +

+ loading... +

+
+ ) : ( + + ); + + const portalEl = document.getElementById(PORTAL_ID); + + return toPortal ? portalEl && createPortal(content, portalEl) : content; +} + +export default CyberlinksGraphContainer; diff --git a/src/features/cyberlinks/CyberlinksGraph/CyberlinksGraphVR.tsx b/src/features/cyberlinks/CyberlinksGraph/CyberlinksGraphVR.tsx new file mode 100644 index 000000000..0acb90a55 --- /dev/null +++ b/src/features/cyberlinks/CyberlinksGraph/CyberlinksGraphVR.tsx @@ -0,0 +1,222 @@ +import { useEffect, useState, useRef, useCallback } from 'react'; +import { ForceGraphVR } from 'react-force-graph'; +import GraphHoverInfoVR from './GraphHoverInfo/GraphHoverInfoVR'; + +import styles from './CyberlinksGraph.module.scss'; + +type Props = { + data: any; + // currentAddress?: string; + size?: number; +}; + +// before zoom in +const INITIAL_CAMERA_DISTANCE = 2500; +const DEFAULT_CAMERA_DISTANCE = 1300; +const CAMERA_ZOOM_IN_EFFECT_DURATION = 5000; +const CAMERA_ZOOM_IN_EFFECT_DELAY = 500; + +function CyberlinksGraph({ data, size }: Props) { + const [isRendering, setRendering] = useState(true); + const [touched, setTouched] = useState(false); + const [hoverNode, setHoverNode] = useState(null); + + const fgRef = useRef(); + + // debug, remove later + useEffect(() => { + if (isRendering) { + console.time('rendering'); + } else { + console.timeEnd('rendering'); + } + }, [isRendering]); + + // initial camera position, didn't find via props + useEffect(() => { + if (!fgRef.current) { + return; + } + // fgRef.current.cameraPosition({ z: INITIAL_CAMERA_DISTANCE }); + }, [fgRef]); + + // initial loading camera zoom effect + useEffect(() => { + // if (!fgRef.current || isRendering) { + // return; + // } + + // setTimeout(() => { + // if (!fgRef.current) { + // return; + // } + + // fgRef.current.cameraPosition( + // { z: DEFAULT_CAMERA_DISTANCE }, + // null, + // CAMERA_ZOOM_IN_EFFECT_DURATION + // ); + // }, CAMERA_ZOOM_IN_EFFECT_DELAY); + }, [fgRef, isRendering]); + + useEffect(() => { + if (!fgRef.current) { + return; + } + + function onTouch() { + setTouched(true); + } + + // fgRef.current.controls().addEventListener('start', onTouch); + + // return () => { + // if (fgRef.current) { + // fgRef.current.controls().removeEventListener('start', onTouch); + // } + // }; + }, [fgRef]); + + // orbit camera + useEffect(() => { + // if (!fgRef.current || touched || isRendering) { + // return; + // } + + // let angle = 0; + + // let interval = null; + + // const timeout = setTimeout(() => { + // interval = setInterval(() => { + // fgRef.current.cameraPosition({ + // x: DEFAULT_CAMERA_DISTANCE * Math.sin(angle), + // z: DEFAULT_CAMERA_DISTANCE * Math.cos(angle), + // }); + // angle += Math.PI / 3000; + // }, 10); + // }, CAMERA_ZOOM_IN_EFFECT_DURATION + CAMERA_ZOOM_IN_EFFECT_DELAY); + + // return () => { + // clearTimeout(timeout); + // clearInterval(interval); + // }; + }, [fgRef, touched, isRendering]); + + const handleNodeClick = useCallback( + (node) => { + if (!fgRef.current) { + return; + } + + const distance = 300; + const distRatio = 1 + distance / Math.hypot(node.x, node.y, node.z); + + // fgRef.current.cameraPosition( + // { x: node.x * distRatio, y: node.y * distRatio, z: node.z * distRatio }, + // node, + // 5000 + // ); + }, + [fgRef] + ); + + const handleLinkClick = useCallback( + (link) => { + if (!fgRef.current) { + return; + } + + const node = link.target; + const distance = 300; + const distRatio = 1 + distance / Math.hypot(node.x, node.y, node.z); + + // fgRef.current.cameraPosition( + // { x: node.x * distRatio, y: node.y * distRatio, z: node.z * distRatio }, + // node, + // 5000 + // ); + }, + [fgRef] + ); + + const handleNodeRightClick = useCallback((node) => { + window.open(`${window.location.origin}/ipfs/${node.id}`, '_blank'); + }, []); + + const handleLinkRightClick = useCallback((link) => { + window.open( + `${window.location.origin}/network/bostrom/tx/${link.name}`, + '_blank' + ); + }, []); + + const handleEngineStop = useCallback(() => { + console.log('ForceGraph3D engine stopped!'); + setRendering(false); + }, []); + + return ( +
+ {isRendering && ( +
rendering data...
+ )} + + 'rgba(0,100,235,1)'} + nodeOpacity={1.0} + nodeRelSize={8} + onNodeHover={setHoverNode} + linkColor={ + // not working + (link) => + // link.subject && link.subject === currentAddress + // ? 'red' + 'rgba(9,255,13,1)' + } + linkLabel="" + linkWidth={4} + linkCurvature={0.2} + linkOpacity={0.7} + linkDirectionalParticles={1} + linkDirectionalParticleColor={() => 'rgba(9,255,13,1)'} + linkDirectionalParticleWidth={4} + linkDirectionalParticleSpeed={0.015} + // linkDirectionalArrowRelPos={1} + // linkDirectionalArrowLength={10} + // linkDirectionalArrowColor={() => 'rgba(9,255,13,1)'} + + onNodeClick={handleNodeRightClick} + onNodeRightClick={handleNodeClick} + onLinkClick={handleLinkRightClick} + onLinkRightClick={handleLinkClick} + onEngineStop={handleEngineStop} + /> + + {/* */} +
+ ); +} + +export default CyberlinksGraph; diff --git a/src/features/cyberlinks/CyberlinksGraph/GraphHoverInfo/GraphHoverInfoVR.tsx b/src/features/cyberlinks/CyberlinksGraph/GraphHoverInfo/GraphHoverInfoVR.tsx new file mode 100644 index 000000000..df93a5ebe --- /dev/null +++ b/src/features/cyberlinks/CyberlinksGraph/GraphHoverInfo/GraphHoverInfoVR.tsx @@ -0,0 +1,80 @@ +import React, { useEffect, useRef } from 'react'; +import 'aframe'; + +declare global { + namespace JSX { + interface IntrinsicElements { + 'a-entity': any; + 'a-plane': any; + 'a-text': any; + 'a-image': any; + } + } +} + +type Props = { + node: any; + camera: any; + size: number; + imageUrl?: string; +}; + +function HoverInfo({ node, camera, size, imageUrl }: Props) { + const hoverRef = useRef(null); + +// useEffect(() => { +// if (!node || !camera || !hoverRef.current) return; + +// const hoverEl = hoverRef.current; + +// // Position the hover info in 3D space +// hoverEl.setAttribute('position', `${node.x} ${node.y} ${node.z}`); + +// // Make the hover info always face the camera +// hoverEl.setAttribute('look-at', '[camera]'); + +// // Update content +// const textEl = hoverEl.querySelector('[text]'); +// if (textEl) { +// textEl.setAttribute('text', 'value', node.id); +// } + +// // Show/hide based on distance from camera +// const updateVisibility = () => { +// const distance = hoverEl.object3D.position.distanceTo(camera.position); +// if (hoverEl.object3D) { +// hoverEl.object3D.visible = distance < size; +// } +// }; + +// // Add this to the render loop +// (hoverEl as any).sceneEl.addBehavior({ +// tick: updateVisibility +// }); + +// }, [node, camera, size]); + + return ( + + + {imageUrl && ( + + )} + + + + ); +} + +export default HoverInfo; diff --git a/src/pages/robot/Brain/Brain.tsx b/src/pages/robot/Brain/Brain.tsx index 983e97243..4feba5a43 100644 --- a/src/pages/robot/Brain/Brain.tsx +++ b/src/pages/robot/Brain/Brain.tsx @@ -6,11 +6,13 @@ import { useRobotContext } from '../robot.context'; import TreedView from './ui/TreedView'; import styles from './Brain.module.scss'; import GraphView from './ui/GraphView'; +import GraphViewVR from './ui/GraphViewVR'; import { LIMIT_GRAPH } from './utils'; enum TabsKey { list = 'list', graph = 'graph', + vr = 'vr', } function Brain() { @@ -43,6 +45,10 @@ function Brain() { key: TabsKey.graph, to: './graph', }, + { + key: TabsKey.vr, + to: './vr', + }, { key: TabsKey.list, to: './list', @@ -60,6 +66,13 @@ function Brain() { element={} /> ))} + {['/', 'vr'].map((path) => ( + } + /> + ))} } /> diff --git a/src/pages/robot/Brain/ui/GraphViewVR.tsx b/src/pages/robot/Brain/ui/GraphViewVR.tsx new file mode 100644 index 000000000..6eb7a6936 --- /dev/null +++ b/src/pages/robot/Brain/ui/GraphViewVR.tsx @@ -0,0 +1,16 @@ +import useCyberlinks from 'src/features/cyberlinks/CyberlinksGraph/useCyberlinks'; +import CyberlinksGraphContainerVR from 'src/features/cyberlinks/CyberlinksGraph/CyberlinksGraphContainerVR'; +import { LIMIT_GRAPH } from '../utils'; + +function GraphView({ address }: { address?: string }) { + const { data: fetchData, loading } = useCyberlinks( + { address }, + { + limit: LIMIT_GRAPH, + } + ); + + return ; +} + +export default GraphView; diff --git a/yarn.lock b/yarn.lock index d866dd2cf..231cc69be 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11108,6 +11108,20 @@ aframe@^1.4: three-bmfont-text dmarcos/three-bmfont-text#21d017046216e318362c48abd1a48bddfb6e0733 webvr-polyfill "^0.10.12" +aframe@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/aframe/-/aframe-1.6.0.tgz#7f17461b36e08f3548e23d6d6bf8fbc0386c586f" + integrity sha512-+P1n2xKGZQbCNW4lTwfue9in2KmfAwYD/BZOU5uXKrJCTegPyUZZX/haJRR9Rb33ij+KPj3vFdwT5ALaucXTNA== + dependencies: + buffer "^6.0.3" + debug "^4.3.4" + deep-assign "^2.0.0" + load-bmfont "^1.2.3" + super-animejs "^3.1.0" + three "npm:super-three@0.164.0" + three-bmfont-text dmarcos/three-bmfont-text#eed4878795be9b3e38cf6aec6b903f56acd1f695 + webvr-polyfill "^0.10.12" + agent-base@5: version "5.1.1" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c" @@ -19019,7 +19033,7 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -got@9.6.0: +got@9.6.0, got@^9.2.2: version "9.6.0" resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q== @@ -25076,6 +25090,16 @@ nice-color-palettes@^1.0.1: new-array "^1.0.0" xhr-request "^1.0.1" +nice-color-palettes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/nice-color-palettes/-/nice-color-palettes-3.0.0.tgz#1ec31927cfc4ce8a51822b6bab2c64845d181abb" + integrity sha512-lL4AjabAAFi313tjrtmgm/bxCRzp4l3vCshojfV/ij3IPdtnRqv6Chcw+SqJUhbe7g3o3BecaqCJYUNLswGBhQ== + dependencies: + got "^9.2.2" + map-limit "0.0.1" + minimist "^1.2.0" + new-array "^1.0.0" + nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" @@ -31289,6 +31313,15 @@ three-bmfont-text@dmarcos/three-bmfont-text#21d017046216e318362c48abd1a48bddfb6e quad-indices "^2.0.1" three-buffer-vertex-data dmarcos/three-buffer-vertex-data#69378fc58daf27d3b1d930df9f233473e4a4818c +three-bmfont-text@dmarcos/three-bmfont-text#eed4878795be9b3e38cf6aec6b903f56acd1f695: + version "3.0.0" + resolved "https://codeload.github.com/dmarcos/three-bmfont-text/tar.gz/eed4878795be9b3e38cf6aec6b903f56acd1f695" + dependencies: + array-shuffle "^1.0.1" + layout-bmfont-text "^1.2.0" + nice-color-palettes "^3.0.0" + quad-indices "^2.0.1" + three-buffer-vertex-data@dmarcos/three-buffer-vertex-data#69378fc58daf27d3b1d930df9f233473e4a4818c: version "1.1.0" resolved "https://codeload.github.com/dmarcos/three-buffer-vertex-data/tar.gz/69378fc58daf27d3b1d930df9f233473e4a4818c" @@ -31331,6 +31364,11 @@ three-render-objects@^1.28: resolved "https://registry.yarnpkg.com/three/-/three-0.150.1.tgz#870d324a4d2daf1c7d55be97f3f73d83783e28be" integrity sha512-5C1MqKUWaHYo13BX0Q64qcdwImgnnjSOFgBscOzAo8MYCzEtqfQqorEKMcajnA3FHy1yVlIe9AmaMQ0OQracNA== +"three@npm:super-three@0.164.0": + version "0.164.0" + resolved "https://registry.yarnpkg.com/super-three/-/super-three-0.164.0.tgz#2aaac4e551a1c54ff0522a41b81800b7aadad93a" + integrity sha512-yMtOkw2hSXfIvGlwcghCbhHGsKRAmh8ksDeOo/0HI7KlEVoIYKHiYLYe9GF6QBViNwzKGpMIz77XUDRveZ4XJg== + throat@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a"