first
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
'use client';
|
||||
|
||||
import React, { Suspense, useRef, useCallback, useState } from 'react';
|
||||
import { Canvas } from '@react-three/fiber';
|
||||
import {
|
||||
Environment,
|
||||
ContactShadows,
|
||||
OrbitControls,
|
||||
useProgress,
|
||||
Html,
|
||||
} from '@react-three/drei';
|
||||
import { useThree } from '@react-three/fiber';
|
||||
import { RobotModel } from './RobotModel';
|
||||
import type { WebGLRenderer, Scene, Camera } from 'three';
|
||||
|
||||
function Loader() {
|
||||
const { progress } = useProgress();
|
||||
return (
|
||||
<Html center>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.75rem' }}>
|
||||
<div style={{
|
||||
width: '48px', height: '48px',
|
||||
border: '3px solid rgba(59, 130, 246, 0.15)',
|
||||
borderTopColor: '#3b82f6',
|
||||
borderRadius: '50%',
|
||||
animation: 'spin 1s linear infinite',
|
||||
}} />
|
||||
<p style={{ fontSize: '0.875rem', color: '#64748b', fontFamily: 'system-ui, sans-serif' }}>
|
||||
{progress.toFixed(0)}% loaded
|
||||
</p>
|
||||
</div>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
|
||||
function SceneCapture({ onCapture }: { onCapture: (gl: WebGLRenderer, scene: Scene, camera: Camera) => void }) {
|
||||
const { gl, scene, camera } = useThree();
|
||||
React.useEffect(() => {
|
||||
onCapture(gl, scene, camera);
|
||||
}, [gl, scene, camera, onCapture]);
|
||||
return null;
|
||||
}
|
||||
|
||||
function SceneContent({ onCapture }: { onCapture: (gl: WebGLRenderer, scene: Scene, camera: Camera) => void }) {
|
||||
return (
|
||||
<>
|
||||
<SceneCapture onCapture={onCapture} />
|
||||
<Environment preset="city" />
|
||||
<ambientLight intensity={0.8} />
|
||||
<directionalLight position={[5, 5, 5]} intensity={1.8} color="#ffffff" castShadow shadow-mapSize={[1024, 1024]} />
|
||||
<directionalLight position={[-4, 3, 2]} intensity={0.8} color="#e8f0ff" />
|
||||
<directionalLight position={[0, 3, -5]} intensity={0.8} color="#94a3b8" />
|
||||
<spotLight position={[0, 8, 0]} intensity={1.0} angle={0.6} penumbra={0.5} color="#ffffff" />
|
||||
<directionalLight position={[0, 2, 5]} intensity={0.7} color="#ffffff" />
|
||||
<RobotModel />
|
||||
<OrbitControls enablePan enableZoom enableRotate minDistance={2} maxDistance={10} minPolarAngle={0} maxPolarAngle={Math.PI} dampingFactor={0.05} enableDamping />
|
||||
<ContactShadows position={[0, -1, 0]} opacity={0.25} scale={10} blur={2} far={4} resolution={256} color="#000000" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function RobotCanvas() {
|
||||
const glRef = useRef<WebGLRenderer | null>(null);
|
||||
const sceneRef = useRef<Scene | null>(null);
|
||||
const cameraRef = useRef<Camera | null>(null);
|
||||
const [isCapturing, setIsCapturing] = useState(false);
|
||||
const [shareStatus, setShareStatus] = useState<'idle' | 'copied' | 'failed'>('idle');
|
||||
|
||||
const handleCapture = useCallback((gl: WebGLRenderer, scene: Scene, camera: Camera) => {
|
||||
glRef.current = gl;
|
||||
sceneRef.current = scene;
|
||||
cameraRef.current = camera;
|
||||
}, []);
|
||||
|
||||
const handleShare = useCallback(async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(window.location.href);
|
||||
setShareStatus('copied');
|
||||
setTimeout(() => setShareStatus('idle'), 2000);
|
||||
} catch {
|
||||
setShareStatus('failed');
|
||||
setTimeout(() => setShareStatus('idle'), 2000);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSnapshot = useCallback(() => {
|
||||
if (!glRef.current || !sceneRef.current || !cameraRef.current) return;
|
||||
setIsCapturing(true);
|
||||
try {
|
||||
glRef.current.render(sceneRef.current, cameraRef.current);
|
||||
const dataUrl = glRef.current.domElement.toDataURL('image/png');
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const link = document.createElement('a');
|
||||
link.download = `g1-robot-${timestamp}.png`;
|
||||
link.href = dataUrl;
|
||||
link.click();
|
||||
} catch (error) {
|
||||
console.error('Failed to capture snapshot:', error);
|
||||
} finally {
|
||||
setIsCapturing(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const btnBase: React.CSSProperties = {
|
||||
position: 'absolute',
|
||||
top: '1rem',
|
||||
padding: '0.5rem 1rem',
|
||||
backdropFilter: 'blur(8px)',
|
||||
borderRadius: '0.5rem',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.8rem',
|
||||
fontWeight: 500,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
transition: 'all 0.2s ease',
|
||||
zIndex: 10,
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
|
||||
<Canvas
|
||||
dpr={[1, 2]}
|
||||
camera={{ position: [0, 1, 5], fov: 50 }}
|
||||
gl={{ antialias: true, powerPreference: 'high-performance' }}
|
||||
style={{ background: 'linear-gradient(180deg, #e8e8e4 0%, #f0f0ec 50%, #e8e8e4 100%)' }}
|
||||
>
|
||||
<Suspense fallback={<Loader />}>
|
||||
<SceneContent onCapture={handleCapture} />
|
||||
</Suspense>
|
||||
</Canvas>
|
||||
|
||||
<button
|
||||
onClick={handleSnapshot}
|
||||
disabled={isCapturing}
|
||||
style={{ ...btnBase, left: '1rem', backgroundColor: 'rgba(255,255,255,0.8)', border: '1px solid #e2e8f0', color: '#1a1a2e' }}
|
||||
aria-label="Capture 3D scene snapshot"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" /><circle cx="8.5" cy="8.5" r="1.5" /><polyline points="21 15 16 10 5 21" />
|
||||
</svg>
|
||||
{isCapturing ? 'Capturing...' : 'Snapshot'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleShare}
|
||||
style={{
|
||||
...btnBase,
|
||||
left: '8.5rem',
|
||||
backgroundColor: shareStatus === 'copied' ? 'rgba(34,197,94,0.1)' : 'rgba(255,255,255,0.8)',
|
||||
border: `1px solid ${shareStatus === 'copied' ? '#86efac' : '#e2e8f0'}`,
|
||||
color: shareStatus === 'copied' ? '#16a34a' : '#1a1a2e',
|
||||
}}
|
||||
aria-label="Share configuration link"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<circle cx="18" cy="5" r="3" /><circle cx="6" cy="12" r="3" /><circle cx="18" cy="19" r="3" /><line x1="8.59" y1="13.51" x2="15.42" y2="17.49" /><line x1="15.41" y1="6.51" x2="8.59" y2="10.49" />
|
||||
</svg>
|
||||
{shareStatus === 'copied' ? 'Copied!' : 'Share'}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user