studio3d / src /components /Viewport.tsx
varunm2004's picture
Update src/components/Viewport.tsx
96c6832 verified
import React, { Suspense, useRef, useEffect } from 'react';
import { Canvas, useThree } from '@react-three/fiber';
import {
OrbitControls, Grid, GizmoHelper,
GizmoViewport, Sky, Stars, TransformControls,
} from '@react-three/drei';
import * as THREE from 'three';
import { useStudioStore } from '../store/useStudioStore';
import { Model, ModelHandle } from './Model';
import './Viewport.css';
// โ”€โ”€ One model + its optional TransformControls โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const ModelWithGizmo: React.FC<{
obj: any;
isSelected: boolean;
gizmoMode: 'translate' | 'rotate' | 'scale';
onOrbitEnable: (v: boolean) => void;
}> = ({ obj, isSelected, gizmoMode, onOrbitEnable }) => {
const modelRef = useRef<ModelHandle>(null);
const { updateObject, setSelectedId } = useStudioStore();
const syncTransform = () => {
const g = modelRef.current?.group;
if (!g) return;
updateObject(obj.id, {
position: [+g.position.x.toFixed(3), +g.position.y.toFixed(3), +g.position.z.toFixed(3)],
rotation: [
+THREE.MathUtils.radToDeg(g.rotation.x).toFixed(2),
+THREE.MathUtils.radToDeg(g.rotation.y).toFixed(2),
+THREE.MathUtils.radToDeg(g.rotation.z).toFixed(2),
],
scale: [+g.scale.x.toFixed(3), +g.scale.y.toFixed(3), +g.scale.z.toFixed(3)],
});
};
return (
<>
<Model
ref={modelRef}
obj={obj}
isSelected={isSelected}
onClick={() => setSelectedId(obj.id)}
/>
{isSelected && modelRef.current?.group && (
<TransformControls
object={modelRef.current.group}
mode={gizmoMode}
size={0.9}
onMouseDown={() => onOrbitEnable(false)}
onMouseUp={() => { onOrbitEnable(true); syncTransform(); }}
onChange={syncTransform}
/>
)}
</>
);
};
// โ”€โ”€ HDR / equirectangular skybox โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const SkyboxHDR: React.FC<{ url: string }> = ({ url }) => {
const { scene } = useThree();
useEffect(() => {
new THREE.TextureLoader().load(url, (tex) => {
tex.mapping = THREE.EquirectangularReflectionMapping;
scene.background = tex;
scene.environment = tex;
});
}, [url, scene]);
return null;
};
// โ”€โ”€ Main scene content โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const SceneContent: React.FC = () => {
const {
objects, selectedId,
ambientIntensity, directionalIntensity,
showGrid, showAxes,
skyboxType, skyboxUrl, bgColor,
mode, gizmoMode,
} = useStudioStore();
const { scene } = useThree();
const orbitRef = useRef<any>(null);
useEffect(() => {
if (skyboxType !== 'uploaded')
scene.background = new THREE.Color(bgColor);
}, [bgColor, skyboxType, scene]);
const setOrbit = (v: boolean) => {
if (orbitRef.current) orbitRef.current.enabled = v;
};
return (
<>
<ambientLight intensity={ambientIntensity} />
<directionalLight
position={[5, 10, 5]}
intensity={directionalIntensity}
castShadow
shadow-mapSize={[2048, 2048]}
/>
<directionalLight position={[-5, 5, -5]} intensity={directionalIntensity * 0.3} />
{skyboxType === 'gradient' && (
<Sky sunPosition={[100, 20, 100]} turbidity={8} rayleigh={2} />
)}
{skyboxType === 'uploaded' && skyboxUrl && <SkyboxHDR url={skyboxUrl} />}
{mode === 'render' && (
<Stars radius={100} depth={50} count={3000} factor={4} />
)}
<Suspense fallback={null}>
{objects.map((obj) => (
<ModelWithGizmo
key={obj.id}
obj={obj}
isSelected={selectedId === obj.id}
gizmoMode={gizmoMode}
onOrbitEnable={setOrbit}
/>
))}
</Suspense>
{showGrid && (
<Grid
position={[0, -0.01, 0]}
args={[20, 20]}
cellSize={1}
cellThickness={0.5}
cellColor="#444466"
sectionSize={5}
sectionThickness={1}
sectionColor="#6666aa"
fadeDistance={30}
infiniteGrid
/>
)}
{showAxes && <axesHelper args={[5]} />}
<OrbitControls ref={orbitRef} makeDefault />
<GizmoHelper alignment="bottom-right" margin={[80, 80]}>
<GizmoViewport
axisColors={['#f06292', '#66bb6a', '#42a5f5']}
labelColor="white"
/>
</GizmoHelper>
</>
);
};
// โ”€โ”€ Viewport root โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const Viewport: React.FC = () => {
const { gizmoMode, setGizmoMode, mode } = useStudioStore();
return (
<div className="viewport">
{/* Floating gizmo mode toolbar โ€” only in MODEL mode */}
{mode === 'model' && (
<div className="gizmo-toolbar">
{([
{ id: 'translate', icon: 'โœ›', label: 'Move', key: 'G' },
{ id: 'rotate', icon: 'โ†ป', label: 'Rotate', key: 'R' },
{ id: 'scale', icon: 'โคข', label: 'Scale', key: 'S' },
] as const).map((g) => (
<button
key={g.id}
className={`gizmo-btn ${gizmoMode === g.id ? 'active' : ''}`}
onClick={() => setGizmoMode(g.id)}
title={`${g.label} (${g.key})`}
>
<span className="gizmo-icon">{g.icon}</span>
<span>{g.label}</span>
<span className="gizmo-key">{g.key}</span>
</button>
))}
</div>
)}
<Canvas
shadows
camera={{ position: [5, 5, 5], fov: 50 }}
gl={{
antialias: true,
toneMapping: THREE.ACESFilmicToneMapping,
toneMappingExposure: 1,
preserveDrawingBuffer: true, // required for video recording
}}
onPointerMissed={() => useStudioStore.getState().setSelectedId(null)}
>
<SceneContent />
</Canvas>
</div>
);
};
export default Viewport;