Skip to content

Commit d5ab023

Browse files
authored
Merge pull request #593 from MetaCell/feature/GEP-200
GEP-200 Create a toolbar component for the 3D canvas that uses toolbar-buttons as children
2 parents de03d0a + 0d449db commit d5ab023

File tree

14 files changed

+874
-75
lines changed

14 files changed

+874
-75
lines changed

examples/vite-3DCanvas-react-app/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<meta charset="UTF-8" />
55
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
78
<title>3D Canvas</title>
89
</head>
910
<body>
Lines changed: 115 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,133 @@
11
import React, { useRef, useState } from "react";
2-
import { Canvas3D } from "@metacell/geppetto-meta-ui/3d-canvas/Canvas3D";
2+
import {
3+
Canvas3D,
4+
Canvas3DRootState,
5+
} from "@metacell/geppetto-meta-ui/3d-canvas/Canvas3D";
36
import { Box } from "@mui/material";
47
import { useFrame } from "@react-three/fiber";
58
import { Mesh } from "three";
9+
import {
10+
Toolbar3D,
11+
Toolbar3DButton,
12+
Toolbar3DSeparator,
13+
} from "@metacell/geppetto-meta-ui/3d-canvas/toolbar/Toolbar3D";
14+
import {
15+
Navigation3D,
16+
EnhancedZoom3DButtons,
17+
Animation3DControls,
18+
} from "@metacell/geppetto-meta-ui/3d-canvas/toolbar/groups";
619
import * as THREE from "three";
720

821
console.log("three.js for 3D view", THREE.REVISION);
922

1023
const classes = {
11-
container: {
12-
height: "100%",
13-
width: "100%",
14-
display: "flex",
15-
alignItems: "stretch",
16-
},
24+
container: {
25+
height: "100%",
26+
width: "100%",
27+
display: "flex",
28+
flexDirection: "column" as const,
29+
position: "relative" as const,
30+
},
31+
canvasContainer: {
32+
flex: 1,
33+
position: "relative" as const,
34+
},
1735
};
1836

1937
function MyRotatingBox() {
20-
const myMesh = useRef<Mesh>();
21-
const [active, setActive] = useState(false);
22-
23-
useFrame(({ clock }) => {
24-
const a = clock.getElapsedTime();
25-
myMesh.current.rotation.x = a;
26-
});
27-
28-
return (
29-
<mesh
30-
scale={active ? 1.5 : 1}
31-
onClick={() => setActive(!active)}
32-
ref={myMesh}
33-
>
34-
<boxGeometry />
35-
<meshPhongMaterial color="royalblue" />
36-
</mesh>
37-
);
38+
const myMesh = useRef<Mesh>();
39+
const [active, setActive] = useState(false);
40+
41+
useFrame(({ clock }) => {
42+
const a = clock.getElapsedTime();
43+
myMesh.current.rotation.x = a;
44+
});
45+
46+
return (
47+
<mesh
48+
scale={active ? 1.5 : 1}
49+
onClick={() => setActive(!active)}
50+
ref={myMesh}
51+
>
52+
<boxGeometry />
53+
<meshPhongMaterial color="royalblue" />
54+
</mesh>
55+
);
3856
}
3957

4058
const Canvas3DExample: React.FC = () => {
41-
return (
42-
<Box className={classes.container} style={classes.container}>
43-
<Canvas3D frameloop={"always"}>
44-
<MyRotatingBox />
45-
</Canvas3D>
46-
</Box>
47-
);
59+
const [activeCamera, setActiveCamera] = useState(false);
60+
61+
const handleCameraClick = ({ controls, camera }: Canvas3DRootState) => {
62+
console.debug("My Controls is", controls);
63+
console.debug("My Camera is", camera);
64+
console.log("Current activeCamera state:", activeCamera);
65+
66+
setActiveCamera(!activeCamera);
67+
68+
if (controls) {
69+
if (!activeCamera) {
70+
console.log("Setting focus position...");
71+
72+
/* prettier-ignore */
73+
controls.setLookAt(
74+
5, 5, 5, // Camera position
75+
0, 0, 0, // Look at center
76+
true // Enable animation
77+
);
78+
} else {
79+
console.log("Resetting camera...");
80+
controls.reset(true);
81+
}
82+
} else {
83+
console.log("CameraControls ref is null!");
84+
}
85+
};
86+
87+
const Scene = () => (
88+
<>
89+
<MyRotatingBox />
90+
<ambientLight intensity={0.5} />
91+
<pointLight position={[10, 10, 10]} />
92+
<gridHelper args={[10, 10]} />
93+
</>
94+
);
95+
96+
return (
97+
<>
98+
<Box style={{ display: "flex", width: "100%" }}>
99+
<Toolbar3D>
100+
<Navigation3D />
101+
<Toolbar3DSeparator />
102+
<EnhancedZoom3DButtons />
103+
<Toolbar3DSeparator />
104+
<Animation3DControls />
105+
<Toolbar3DSeparator />
106+
<Toolbar3DButton
107+
icon={<i className="fas fa-camera" />}
108+
tooltip={activeCamera ? "Reset Camera" : "Focus Camera"}
109+
onClick={handleCameraClick}
110+
active={activeCamera}
111+
/>
112+
</Toolbar3D>
113+
<Box sx={classes.canvasContainer}>
114+
<Canvas3D
115+
frameloop={"always"}
116+
controlsOption={{
117+
minDistance: 2,
118+
maxDistance: 20,
119+
enablePan: true,
120+
enableZoom: true,
121+
enableRotate: true,
122+
}}
123+
defaultLightOff
124+
>
125+
<Scene />
126+
</Canvas3D>
127+
</Box>
128+
</Box>
129+
</>
130+
);
48131
};
49132

50133
export default Canvas3DExample;

examples/vite-3DCanvas-react-app/yarn.lock

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1395,9 +1395,9 @@ camera-controls@^3.1.0:
13951395
integrity sha512-w5oULNpijgTRH0ARFJJ0R5ct1nUM3R3WP7/b8A6j9uTGpRfnsypc/RBMPQV8JQDPayUe37p/TZZY1PcUr4czOQ==
13961396

13971397
caniuse-lite@^1.0.30001741:
1398-
version "1.0.30001743"
1399-
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001743.tgz#50ff91a991220a1ee2df5af00650dd5c308ea7cd"
1400-
integrity sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==
1398+
version "1.0.30001745"
1399+
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001745.tgz#ab2a36e3b6ed5bfb268adc002c476aab6513f859"
1400+
integrity sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==
14011401

14021402
chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2:
14031403
version "4.1.2"
@@ -1578,9 +1578,9 @@ dunder-proto@^1.0.1:
15781578
gopd "^1.2.0"
15791579

15801580
electron-to-chromium@^1.5.218:
1581-
version "1.5.222"
1582-
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.222.tgz#965c93783ad989116b74593ae3068b9466fdb237"
1583-
integrity sha512-gA7psSwSwQRE60CEoLz6JBCQPIxNeuzB2nL8vE03GK/OHxlvykbLyeiumQy1iH5C2f3YbRAZpGCMT12a/9ih9w==
1581+
version "1.5.223"
1582+
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.223.tgz#cf9b1aebba1c8ee5e50d1c9e198229e15bc87b28"
1583+
integrity sha512-qKm55ic6nbEmagFlTFczML33rF90aU+WtrJ9MdTCThrcvDNdUHN4p6QfVN78U06ZmguqXIyMPyYhw2TrbDUwPQ==
15841584

15851585
emoji-regex@^8.0.0:
15861586
version "8.0.0"
@@ -2923,9 +2923,9 @@ run-parallel@^1.1.9:
29232923
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
29242924

29252925
sass@^1.32.12:
2926-
version "1.93.1"
2927-
resolved "https://registry.yarnpkg.com/sass/-/sass-1.93.1.tgz#94bf77eeef5fba022c7316515327e275e46b654e"
2928-
integrity sha512-wLAeLB7IksO2u+cCfhHqcy7/2ZUMPp/X2oV6+LjmweTqgjhOKrkaE/Q1wljxtco5EcOcupZ4c981X0gpk5Tiag==
2926+
version "1.93.2"
2927+
resolved "https://registry.yarnpkg.com/sass/-/sass-1.93.2.tgz#e97d225d60f59a3b3dbb6d2ae3c1b955fd1f2cd1"
2928+
integrity sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==
29292929
dependencies:
29302930
chokidar "^4.0.0"
29312931
immutable "^5.0.2"

geppetto.js/geppetto-ui/src/3d-canvas/Canvas3D.tsx

Lines changed: 98 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,25 @@
1-
import { Canvas, CanvasProps } from "@react-three/fiber";
2-
import React, { useEffect, useRef } from "react";
1+
import { Canvas, CanvasProps, useThree } from "@react-three/fiber";
2+
import React, { forwardRef, useEffect, useRef } from "react";
33

4-
import { CameraControls } from "@react-three/drei";
4+
import { RootState } from "@react-three/fiber";
5+
import { CameraControls, CameraControlsProps } from "@react-three/drei";
56
import { useState } from "react";
67
import { Loader } from "three";
7-
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
8+
import create from "zustand";
89

10+
export type FiberRootState = RootState;
911
type Canvas3DBaseProps = {
12+
id?: string;
1013
children?;
14+
ref?;
1115
defaultLightOff?: boolean;
1216
nonInteractive?: boolean;
17+
controlsOption?: CameraControlsProps;
1318
};
1419

1520
type Canvas3DProps = Canvas3DBaseProps &
1621
Omit<CanvasProps, keyof Canvas3DBaseProps>;
1722

18-
/**
19-
* Wraps @react-three/fiber’s <Canvas /> and handles lights and if it has to be interactive or not.
20-
* Beside the defined parameters, the additional props are passed to the three/fiber <Canvas /> component.
21-
*
22-
* By default, the fameloop is set to "demand" to optimize performance, and can be overridden via props.
23-
* By default, ambient and directional lights are added, but can be disabled via props.
24-
*
25-
* @param defaultLightOff Disables default ambient + directional lights (default: false)
26-
* @param nonInteractive If true, disables default camera controls (default: false)
27-
*/
28-
export const Canvas3D: React.FC<Canvas3DProps> = ({
29-
children,
30-
defaultLightOff = false,
31-
nonInteractive = false,
32-
...canvasProps
33-
}) => {
34-
return (
35-
<Canvas frameloop={"demand"} {...canvasProps}>
36-
{!defaultLightOff && (
37-
<>
38-
<ambientLight intensity={0.5} />
39-
<directionalLight />
40-
</>
41-
)}
42-
{children}
43-
{!nonInteractive && <CameraControls />}
44-
</Canvas>
45-
);
46-
};
47-
4823
/**
4924
* Hook that lets you load multiple 3D objects in parallel using a specified loader.
5025
*
@@ -158,3 +133,92 @@ export const useParallelLoader = <T,>(
158133
}
159134
return objects;
160135
};
136+
137+
/**
138+
* Wraps @react-three/fiber’s <Canvas /> and handles lights and if it has to be interactive or not.
139+
* Beside the defined parameters, the additional props are passed to the three/fiber <Canvas /> component.
140+
*
141+
* By default, the fameloop is set to "demand" to optimize performance, and can be overridden via props.
142+
* By default, ambient and directional lights are added, but can be disabled via props.
143+
*
144+
* @param defaultLightOff Disables default ambient + directional lights (default: false)
145+
* @param nonInteractive If true, disables default camera controls (default: false)
146+
*/
147+
export const Canvas3D: React.FC<Canvas3DProps> = forwardRef<
148+
HTMLCanvasElement,
149+
Canvas3DBaseProps
150+
>(
151+
(
152+
{
153+
id = undefined,
154+
children,
155+
defaultLightOff = false,
156+
nonInteractive = false,
157+
controlsOption,
158+
...canvasProps
159+
},
160+
ref
161+
) => {
162+
return (
163+
<Canvas ref={ref} frameloop={"demand"} {...canvasProps}>
164+
<FiberBridge storeId={id} />
165+
{!defaultLightOff && (
166+
<>
167+
<ambientLight intensity={0.5} />
168+
<directionalLight />
169+
</>
170+
)}
171+
{children}
172+
{!nonInteractive && <CameraControls makeDefault {...controlsOption} />}
173+
</Canvas>
174+
);
175+
}
176+
);
177+
178+
export type Canvas3DRootState = Omit<RootState, "controls"> & {
179+
controls: CameraControls;
180+
};
181+
182+
type FiberStore = {
183+
rootStates: Record<string, Canvas3DRootState | null>;
184+
setRootState: (id: string, state: Canvas3DRootState) => void;
185+
clearRootState: (id: string) => void;
186+
};
187+
188+
export const useFiberStore = create<FiberStore>((set) => ({
189+
rootStates: {},
190+
191+
setRootState: (id, state) =>
192+
set((prev) => ({
193+
rootStates: {
194+
...prev.rootStates,
195+
[id]: state,
196+
},
197+
})),
198+
199+
clearRootState: (id) =>
200+
set((prev) => {
201+
const { [id]: _, ...rest } = prev.rootStates;
202+
return { rootStates: rest };
203+
}),
204+
}));
205+
206+
export const useFiber = (id: string) =>
207+
useFiberStore((s) => s.rootStates[id] ?? null);
208+
209+
const FiberBridge: React.FC<{ storeId?: string }> = ({ storeId }) => {
210+
const state = useThree();
211+
const id = storeId ?? "default";
212+
const setRootState = useFiberStore((s) => s.setRootState);
213+
const clearRootState = useFiberStore((s) => s.clearRootState);
214+
215+
React.useEffect(() => {
216+
// @ts-expect-error
217+
setRootState(id, state);
218+
return () => {
219+
clearRootState(id);
220+
};
221+
}, [id, state, setRootState, clearRootState]);
222+
223+
return null;
224+
};

0 commit comments

Comments
 (0)