diff --git a/package.json b/package.json
index be6f25b18..38f368a86 100644
--- a/package.json
+++ b/package.json
@@ -181,6 +181,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 ? (
+
+ ) : (
+
+ );
+
+ 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 8d8df016c..af42b227b 100644
--- a/src/pages/robot/Brain/Brain.tsx
+++ b/src/pages/robot/Brain/Brain.tsx
@@ -7,12 +7,14 @@ 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 useGraphLimit from './useGraphLimit';
enum TabsKey {
graph3d = 'graph3d',
graph = 'graph',
list = 'list',
+ vr = 'vr',
}
function Brain() {
@@ -51,6 +53,10 @@ function Brain() {
to: './graph',
text: '2d graph',
},
+ {
+ key: TabsKey.vr,
+ to: './vr',
+ },
{
key: TabsKey.list,
to: './list',
@@ -69,6 +75,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 b7c64097e..014970f16 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -11085,6 +11085,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"
@@ -18985,7 +18999,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==
@@ -25042,6 +25056,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"
@@ -31262,6 +31286,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"
@@ -31304,6 +31337,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"