merge develop into feat/galerie - resolve model and code conflicts
This commit is contained in:
@@ -0,0 +1,357 @@
|
||||
import React, { useState, useEffect, useRef, useMemo } from "react";
|
||||
import { Canvas, useFrame, useThree } from "@react-three/fiber";
|
||||
import { MapControls, OrthographicCamera, useGLTF } from "@react-three/drei";
|
||||
import * as THREE from "three";
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// 1. Terrain Scene
|
||||
// ----------------------------------------------------------------------------
|
||||
function TerrainScene() {
|
||||
const { scene } = useGLTF("/models/terrain/terrain.glb");
|
||||
return (
|
||||
<group>
|
||||
<ambientLight intensity={1.5} />
|
||||
<directionalLight position={[10, 20, 10]} intensity={2} />
|
||||
<primitive object={scene} />
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// 2. Waypoint Overlay (Debug visualization)
|
||||
// ----------------------------------------------------------------------------
|
||||
function WaypointOverlay({
|
||||
waypoints,
|
||||
visible,
|
||||
}: {
|
||||
waypoints: any[];
|
||||
visible: boolean;
|
||||
}) {
|
||||
if (!visible) return null;
|
||||
return (
|
||||
<group>
|
||||
{waypoints.map((w) => (
|
||||
<mesh key={w.id} position={[w.x, w.y + 1, w.z]}>
|
||||
<sphereGeometry args={[0.3, 16, 16]} />
|
||||
<meshBasicMaterial color="#10b981" />
|
||||
</mesh>
|
||||
))}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// 3. Camera Manager (Handles Orthographic Math & Downloads)
|
||||
// ----------------------------------------------------------------------------
|
||||
function CameraManager({
|
||||
autoBounds,
|
||||
boundsTextRef,
|
||||
}: {
|
||||
autoBounds: any;
|
||||
boundsTextRef: React.RefObject<HTMLPreElement | null>;
|
||||
}) {
|
||||
const { camera, gl, scene } = useThree();
|
||||
const controlsRef = useRef<any>(null);
|
||||
|
||||
// Apply Auto-Bounds function
|
||||
useEffect(() => {
|
||||
const applyAutoBounds = () => {
|
||||
if (camera instanceof THREE.OrthographicCamera && autoBounds) {
|
||||
const width = autoBounds.maxX - autoBounds.minX;
|
||||
const height = autoBounds.maxZ - autoBounds.minZ;
|
||||
const centerX = (autoBounds.minX + autoBounds.maxX) / 2;
|
||||
const centerZ = (autoBounds.minZ + autoBounds.maxZ) / 2;
|
||||
|
||||
camera.position.set(centerX, 200, centerZ);
|
||||
camera.left = -width / 2;
|
||||
camera.right = width / 2;
|
||||
camera.top = height / 2;
|
||||
camera.bottom = -height / 2;
|
||||
camera.zoom = 1;
|
||||
camera.updateProjectionMatrix();
|
||||
|
||||
if (controlsRef.current) {
|
||||
controlsRef.current.target.set(centerX, 0, centerZ);
|
||||
controlsRef.current.update();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
(window as any).applyAutoBounds = applyAutoBounds;
|
||||
// Initial apply
|
||||
applyAutoBounds();
|
||||
|
||||
return () => {
|
||||
delete (window as any).applyAutoBounds;
|
||||
};
|
||||
}, [camera, autoBounds]);
|
||||
|
||||
// Track dynamic bounds without triggering React re-renders!
|
||||
useFrame(() => {
|
||||
if (camera instanceof THREE.OrthographicCamera && boundsTextRef.current) {
|
||||
const width = (camera.right - camera.left) / camera.zoom;
|
||||
const height = (camera.top - camera.bottom) / camera.zoom;
|
||||
const minX = Math.round(camera.position.x - width / 2);
|
||||
const maxX = Math.round(camera.position.x + width / 2);
|
||||
const minZ = Math.round(camera.position.z - height / 2);
|
||||
const maxZ = Math.round(camera.position.z + height / 2);
|
||||
|
||||
// Direct DOM mutation for 60fps performance (prevents WebGL Context Lost!)
|
||||
boundsTextRef.current.innerText = JSON.stringify(
|
||||
{ minX, maxX, minZ, maxZ },
|
||||
null,
|
||||
2,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Attach screenshot capture logic
|
||||
useEffect(() => {
|
||||
(window as any).downloadMapScreenshot = () => {
|
||||
// Force an immediate render frame to ensure no UI overlays are missing
|
||||
gl.render(scene, camera);
|
||||
const dataUrl = gl.domElement.toDataURL("image/png");
|
||||
const a = document.createElement("a");
|
||||
a.href = dataUrl;
|
||||
a.download = "/assets/gps/map_background.png";
|
||||
a.click();
|
||||
};
|
||||
return () => {
|
||||
delete (window as any).downloadMapScreenshot;
|
||||
};
|
||||
}, [gl, camera, scene]);
|
||||
|
||||
return (
|
||||
<MapControls ref={controlsRef} enableRotate={false} dampingFactor={0.05} />
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// 4. Main Page Route Component
|
||||
// ----------------------------------------------------------------------------
|
||||
export function BackgroundMapPage() {
|
||||
const [waypoints, setWaypoints] = useState<any[]>([]);
|
||||
const [showWaypoints, setShowWaypoints] = useState(true);
|
||||
const boundsTextRef = useRef<HTMLPreElement>(null);
|
||||
|
||||
// Load road network waypoints to compute perfect GPS bounds
|
||||
useEffect(() => {
|
||||
const saved = localStorage.getItem("la-fabrik-waypoints");
|
||||
if (saved) {
|
||||
setWaypoints(JSON.parse(saved));
|
||||
} else {
|
||||
fetch("/roadNetwork.json")
|
||||
.then((res) => res.json())
|
||||
.then((data) => setWaypoints(data))
|
||||
.catch(() => {});
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Compute exact bounds that the EbikeGPSMap will use by default
|
||||
const autoBounds = useMemo(() => {
|
||||
if (waypoints.length === 0) return null;
|
||||
const xs = waypoints.map((w) => w.x);
|
||||
const zs = waypoints.map((w) => w.z);
|
||||
const minX = Math.min(...xs);
|
||||
const maxX = Math.max(...xs);
|
||||
const minZ = Math.min(...zs);
|
||||
const maxZ = Math.max(...zs);
|
||||
|
||||
// CRITICAL: We MUST force the camera bounds to be a PERFECT SQUARE.
|
||||
// If the camera is rectangular, the exported PNG will be distorted when drawn
|
||||
// on the EbikeGPSMap's 1024x1024 canvas!
|
||||
const width = maxX - minX;
|
||||
const height = maxZ - minZ;
|
||||
const maxDim = Math.max(width, height);
|
||||
|
||||
const centerX = (minX + maxX) / 2;
|
||||
const centerZ = (minZ + maxZ) / 2;
|
||||
|
||||
const paddedDim = maxDim * 1.15 || 100;
|
||||
|
||||
return {
|
||||
minX: centerX - paddedDim / 2,
|
||||
maxX: centerX + paddedDim / 2,
|
||||
minZ: centerZ - paddedDim / 2,
|
||||
maxZ: centerZ + paddedDim / 2,
|
||||
};
|
||||
}, [waypoints]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: "100vw",
|
||||
height: "100vh",
|
||||
background: "#050505",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{/*
|
||||
CRITICAL: The DOM element MUST be a perfect square so the resulting PNG
|
||||
is exactly 1:1, preventing stretching in the EbikeGPSMap canvas texture!
|
||||
*/}
|
||||
<div
|
||||
style={{
|
||||
width: "min(100vw, 100vh)",
|
||||
height: "min(100vw, 100vh)",
|
||||
background: "#000",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<Canvas
|
||||
gl={{ preserveDrawingBuffer: true, antialias: true, alpha: false }}
|
||||
>
|
||||
<OrthographicCamera
|
||||
makeDefault
|
||||
position={[0, 200, 0]}
|
||||
near={0.1}
|
||||
far={1000}
|
||||
/>
|
||||
<TerrainScene />
|
||||
<WaypointOverlay waypoints={waypoints} visible={showWaypoints} />
|
||||
<CameraManager
|
||||
autoBounds={autoBounds}
|
||||
boundsTextRef={boundsTextRef}
|
||||
/>
|
||||
</Canvas>
|
||||
</div>
|
||||
|
||||
{/* Premium Glassmorphic UI Dashboard */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 24,
|
||||
left: 24,
|
||||
background: "rgba(15, 23, 42, 0.85)",
|
||||
padding: 24,
|
||||
borderRadius: 16,
|
||||
border: "1px solid #334155",
|
||||
color: "white",
|
||||
fontFamily: "system-ui, sans-serif",
|
||||
backdropFilter: "blur(12px)",
|
||||
width: 360,
|
||||
boxShadow: "0 20px 25px -5px rgba(0, 0, 0, 0.5)",
|
||||
}}
|
||||
>
|
||||
<h2
|
||||
style={{ margin: "0 0 16px 0", fontSize: "1.4rem", color: "#38bdf8" }}
|
||||
>
|
||||
GPS Map Generator
|
||||
</h2>
|
||||
|
||||
<p
|
||||
style={{
|
||||
fontSize: "0.9rem",
|
||||
color: "#94a3b8",
|
||||
marginBottom: 20,
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
1. Cadrez votre carte (ou utilisez le <b>Cadrage Automatique</b>).
|
||||
<br />
|
||||
2. Masquez les waypoints (fond visuel seul).
|
||||
<br />
|
||||
3. Cliquez sur <b>Capturer la carte</b>.
|
||||
</p>
|
||||
|
||||
<button
|
||||
onClick={() => setShowWaypoints(!showWaypoints)}
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "12px",
|
||||
marginBottom: 12,
|
||||
background: showWaypoints ? "#1e293b" : "#334155",
|
||||
border: "1px solid #475569",
|
||||
color: "white",
|
||||
borderRadius: 8,
|
||||
cursor: "pointer",
|
||||
fontWeight: 600,
|
||||
transition: "all 0.2s",
|
||||
}}
|
||||
>
|
||||
{showWaypoints ? "👁️ Masquer Waypoints" : "👁️🗨️ Afficher Waypoints"}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
if ((window as any).applyAutoBounds)
|
||||
(window as any).applyAutoBounds();
|
||||
}}
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "12px",
|
||||
marginBottom: 16,
|
||||
background: "#1e293b",
|
||||
border: "1px solid #475569",
|
||||
color: "#10b981",
|
||||
borderRadius: 8,
|
||||
cursor: "pointer",
|
||||
fontWeight: 600,
|
||||
transition: "all 0.2s",
|
||||
}}
|
||||
>
|
||||
🎯 Cadrage Automatique
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
if ((window as any).downloadMapScreenshot)
|
||||
(window as any).downloadMapScreenshot();
|
||||
}}
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "14px",
|
||||
background: "#0ea5e9",
|
||||
border: "none",
|
||||
color: "white",
|
||||
borderRadius: 8,
|
||||
cursor: "pointer",
|
||||
fontWeight: "bold",
|
||||
fontSize: "1rem",
|
||||
boxShadow: "0 4px 6px -1px rgba(14, 165, 233, 0.4)",
|
||||
}}
|
||||
>
|
||||
📸 Capturer la carte (.png)
|
||||
</button>
|
||||
|
||||
<div
|
||||
style={{
|
||||
marginTop: 24,
|
||||
padding: 16,
|
||||
background: "#020617",
|
||||
borderRadius: 10,
|
||||
fontSize: "0.85rem",
|
||||
}}
|
||||
>
|
||||
<div style={{ color: "#64748b", marginBottom: 8, fontWeight: 600 }}>
|
||||
Limites Actuelles (worldBounds):
|
||||
</div>
|
||||
<pre
|
||||
ref={boundsTextRef}
|
||||
style={{ margin: 0, color: "#10b981", fontFamily: "monospace" }}
|
||||
>
|
||||
Calcul...
|
||||
</pre>
|
||||
<div
|
||||
style={{
|
||||
color: "#ef4444",
|
||||
marginTop: 12,
|
||||
fontSize: "0.75rem",
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
>
|
||||
*Si vous décadrez à la souris, vous devrez copier ces valeurs
|
||||
exactes dans la prop <code>worldBounds</code> de votre composant{" "}
|
||||
<b>EbikeGPSMap</b> !
|
||||
<br />
|
||||
<br />
|
||||
Astuce : Utilisez le <b>Cadrage Automatique</b> pour ne rien avoir à
|
||||
configurer.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,7 +5,6 @@ export function DocsAnimationPage(): React.JSX.Element {
|
||||
return (
|
||||
<DocsDocument
|
||||
content={animation}
|
||||
frContent={animation}
|
||||
meta="15"
|
||||
title="Animation & 3D Model System"
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,6 @@ export function DocsArchitecturePage(): React.JSX.Element {
|
||||
return (
|
||||
<DocsDocument
|
||||
content={architecture}
|
||||
frContent={architecture}
|
||||
meta="02"
|
||||
title="Current Architecture"
|
||||
/>
|
||||
|
||||
@@ -3,11 +3,6 @@ import { DocsDocument } from "@/components/docs/DocsDocument";
|
||||
|
||||
export function DocsAudioPage(): React.JSX.Element {
|
||||
return (
|
||||
<DocsDocument
|
||||
content={audio}
|
||||
frContent={audio}
|
||||
meta="08"
|
||||
title="Audio Technical Notes"
|
||||
/>
|
||||
<DocsDocument content={audio} meta="08" title="Audio Technical Notes" />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,11 +3,6 @@ import { DocsDocument } from "@/components/docs/DocsDocument";
|
||||
|
||||
export function DocsCodeReviewPage(): React.JSX.Element {
|
||||
return (
|
||||
<DocsDocument
|
||||
content={codeReview}
|
||||
frContent={codeReview}
|
||||
meta="16"
|
||||
title="Code Review Prep"
|
||||
/>
|
||||
<DocsDocument content={codeReview} meta="16" title="Code Review Prep" />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,12 +2,5 @@ import editor from "../../../../docs/user/editor.md?raw";
|
||||
import { DocsDocument } from "@/components/docs/DocsDocument";
|
||||
|
||||
export function DocsEditorPage(): React.JSX.Element {
|
||||
return (
|
||||
<DocsDocument
|
||||
content={editor}
|
||||
frContent={editor}
|
||||
meta="14"
|
||||
title="Editor User Guide"
|
||||
/>
|
||||
);
|
||||
return <DocsDocument content={editor} meta="14" title="Editor User Guide" />;
|
||||
}
|
||||
|
||||
@@ -2,12 +2,5 @@ import features from "../../../../docs/user/features.md?raw";
|
||||
import { DocsDocument } from "@/components/docs/DocsDocument";
|
||||
|
||||
export function DocsFeaturesPage(): React.JSX.Element {
|
||||
return (
|
||||
<DocsDocument
|
||||
content={features}
|
||||
frContent={features}
|
||||
meta="12"
|
||||
title="Features"
|
||||
/>
|
||||
);
|
||||
return <DocsDocument content={features} meta="12" title="Features" />;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ export function DocsHandTrackingPage(): React.JSX.Element {
|
||||
return (
|
||||
<DocsDocument
|
||||
content={handTracking}
|
||||
frContent={handTracking}
|
||||
meta="09"
|
||||
title="Hand Tracking Technical Notes"
|
||||
/>
|
||||
|
||||
@@ -3,11 +3,6 @@ import { DocsDocument } from "@/components/docs/DocsDocument";
|
||||
|
||||
export function DocsInteractionPage(): React.JSX.Element {
|
||||
return (
|
||||
<DocsDocument
|
||||
content={interaction}
|
||||
frContent={interaction}
|
||||
meta="05"
|
||||
title="Interaction System"
|
||||
/>
|
||||
<DocsDocument content={interaction} meta="05" title="Interaction System" />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,12 +2,5 @@ import mainFeature from "../../../../docs/user/main-feature.md?raw";
|
||||
import { DocsDocument } from "@/components/docs/DocsDocument";
|
||||
|
||||
export function DocsMainFeaturePage(): React.JSX.Element {
|
||||
return (
|
||||
<DocsDocument
|
||||
content={mainFeature}
|
||||
frContent={mainFeature}
|
||||
meta="13"
|
||||
title="Main Feature"
|
||||
/>
|
||||
);
|
||||
return <DocsDocument content={mainFeature} meta="13" title="Main Feature" />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import mapPerformance from "../../../../docs/technical/map-performance.md?raw";
|
||||
import { DocsDocument } from "@/components/docs/DocsDocument";
|
||||
|
||||
export function DocsMapPerformancePage(): React.JSX.Element {
|
||||
return (
|
||||
<DocsDocument content={mapPerformance} meta="12" title="Map Performance" />
|
||||
);
|
||||
}
|
||||
@@ -2,12 +2,5 @@ import readme from "../../../README.md?raw";
|
||||
import { DocsDocument } from "@/components/docs/DocsDocument";
|
||||
|
||||
export function DocsReadmePage(): React.JSX.Element {
|
||||
return (
|
||||
<DocsDocument
|
||||
content={readme}
|
||||
frContent={readme}
|
||||
meta="01"
|
||||
title="README"
|
||||
/>
|
||||
);
|
||||
return <DocsDocument content={readme} meta="01" title="README" />;
|
||||
}
|
||||
|
||||
@@ -2,12 +2,5 @@ import repairGame from "../../../../docs/technical/repair-game.md?raw";
|
||||
import { DocsDocument } from "@/components/docs/DocsDocument";
|
||||
|
||||
export function DocsRepairGamePage(): React.JSX.Element {
|
||||
return (
|
||||
<DocsDocument
|
||||
content={repairGame}
|
||||
frContent={repairGame}
|
||||
meta="04"
|
||||
title="Repair Game"
|
||||
/>
|
||||
);
|
||||
return <DocsDocument content={repairGame} meta="04" title="Repair Game" />;
|
||||
}
|
||||
|
||||
@@ -3,11 +3,6 @@ import { DocsDocument } from "@/components/docs/DocsDocument";
|
||||
|
||||
export function DocsSceneRuntimePage(): React.JSX.Element {
|
||||
return (
|
||||
<DocsDocument
|
||||
content={sceneRuntime}
|
||||
frContent={sceneRuntime}
|
||||
meta="03"
|
||||
title="Scene Runtime"
|
||||
/>
|
||||
<DocsDocument content={sceneRuntime} meta="03" title="Scene Runtime" />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ export function DocsTargetArchitecturePage(): React.JSX.Element {
|
||||
return (
|
||||
<DocsDocument
|
||||
content={targetArchitecture}
|
||||
frContent={targetArchitecture}
|
||||
meta="06"
|
||||
title="Target Architecture"
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,6 @@ export function DocsTechnicalEditorPage(): React.JSX.Element {
|
||||
return (
|
||||
<DocsDocument
|
||||
content={technicalEditor}
|
||||
frContent={technicalEditor}
|
||||
meta="07"
|
||||
title="Editor Technical Notes"
|
||||
/>
|
||||
|
||||
@@ -3,11 +3,6 @@ import { DocsDocument } from "@/components/docs/DocsDocument";
|
||||
|
||||
export function DocsThreeDebuggingPage(): React.JSX.Element {
|
||||
return (
|
||||
<DocsDocument
|
||||
content={threeDebugging}
|
||||
frContent={threeDebugging}
|
||||
meta="11"
|
||||
title="Three Debugging"
|
||||
/>
|
||||
<DocsDocument content={threeDebugging} meta="11" title="Three Debugging" />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,12 +2,5 @@ import zustand from "../../../../docs/technical/zustand.md?raw";
|
||||
import { DocsDocument } from "@/components/docs/DocsDocument";
|
||||
|
||||
export function DocsZustandPage(): React.JSX.Element {
|
||||
return (
|
||||
<DocsDocument
|
||||
content={zustand}
|
||||
frContent={zustand}
|
||||
meta="10"
|
||||
title="Zustand Stores"
|
||||
/>
|
||||
);
|
||||
return <DocsDocument content={zustand} meta="10" title="Zustand Stores" />;
|
||||
}
|
||||
|
||||
+300
-86
@@ -1,52 +1,56 @@
|
||||
import { Suspense, useCallback, useEffect, useState } from "react";
|
||||
import { Canvas } from "@react-three/fiber";
|
||||
import { useProgress } from "@react-three/drei";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Canvas, useThree } from "@react-three/fiber";
|
||||
import { EditorControls } from "@/components/editor/EditorControls";
|
||||
import { EditorScene } from "@/components/editor/scene/EditorScene";
|
||||
import type { EditorCinematicPreviewRequest } from "@/components/editor/scene/EditorScene";
|
||||
import { SceneLoadingOverlay } from "@/components/ui/SceneLoadingOverlay";
|
||||
import { Subtitles } from "@/components/ui/Subtitles";
|
||||
import { useEditorHistory } from "@/hooks/editor/useEditorHistory";
|
||||
import type { CinematicDefinition } from "@/types/cinematics/cinematics";
|
||||
import { useEditorSceneData } from "@/hooks/editor/useEditorSceneData";
|
||||
import type { MapNode, SceneData, TransformMode } from "@/types/editor/editor";
|
||||
import type {
|
||||
EditorCinematicPreviewRequest,
|
||||
MapNode,
|
||||
TransformMode,
|
||||
} from "@/types/editor/editor";
|
||||
import type { SceneLoadingState } from "@/types/world/sceneLoading";
|
||||
import { logger } from "@/utils/core/Logger";
|
||||
import {
|
||||
INITIAL_SCENE_LOADING_STATE,
|
||||
type SceneLoadingChangeHandler,
|
||||
type SceneLoadingState,
|
||||
} from "@/types/world/sceneLoading";
|
||||
addTreeNode,
|
||||
createNewMapNode,
|
||||
mergeFlatNodeTransformsIntoTree,
|
||||
removeEditorMetadata,
|
||||
removeTreeNodeAtPath,
|
||||
serializeMapNodes,
|
||||
updateSceneDataTree,
|
||||
updateTreeNodeAtPath,
|
||||
} from "@/utils/editor/editorMapTree";
|
||||
|
||||
const SAVE_ERROR_MESSAGE = "Erreur lors de l'enregistrement";
|
||||
const DEFAULT_NEW_NODE_NAME = "new-model";
|
||||
|
||||
interface EditorSceneLoadingTrackerProps {
|
||||
onLoadingStateChange: SceneLoadingChangeHandler;
|
||||
}
|
||||
|
||||
function serializeMapNodes(sceneData: SceneData): string {
|
||||
return JSON.stringify(sceneData.mapNodes, null, 2);
|
||||
}
|
||||
|
||||
function EditorSceneLoadingTracker({
|
||||
onLoadingStateChange,
|
||||
}: EditorSceneLoadingTrackerProps): null {
|
||||
const { active, progress } = useProgress();
|
||||
function EditorWebGLContextLogger(): null {
|
||||
const gl = useThree((state) => state.gl);
|
||||
|
||||
useEffect(() => {
|
||||
if (active) {
|
||||
onLoadingStateChange({
|
||||
currentStep: "Importation des models",
|
||||
progress: 0.2 + (progress / 100) * 0.7,
|
||||
status: "loading",
|
||||
});
|
||||
return;
|
||||
}
|
||||
gl.setClearColor("#050505");
|
||||
|
||||
onLoadingStateChange({
|
||||
currentStep: "Gameplay prêt",
|
||||
progress: 1,
|
||||
status: "ready",
|
||||
});
|
||||
}, [active, onLoadingStateChange, progress]);
|
||||
const canvas = gl.domElement;
|
||||
const handleContextLost = (event: Event) => {
|
||||
event.preventDefault();
|
||||
logger.error("WebGL", "Context lost - GPU resources exhausted");
|
||||
};
|
||||
const handleContextRestored = () => {
|
||||
logger.info("WebGL", "Context restored");
|
||||
};
|
||||
|
||||
canvas.addEventListener("webglcontextlost", handleContextLost);
|
||||
canvas.addEventListener("webglcontextrestored", handleContextRestored);
|
||||
|
||||
return () => {
|
||||
canvas.removeEventListener("webglcontextlost", handleContextLost);
|
||||
canvas.removeEventListener("webglcontextrestored", handleContextRestored);
|
||||
};
|
||||
}, [gl]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -63,40 +67,33 @@ export function EditorPage(): React.JSX.Element {
|
||||
const [selectedNodeIndex, setSelectedNodeIndex] = useState<number | null>(
|
||||
null,
|
||||
);
|
||||
const [selectedNodeIndexes, setSelectedNodeIndexes] = useState<number[]>([]);
|
||||
const [hoveredNodeIndex, setHoveredNodeIndex] = useState<number | null>(null);
|
||||
const [transformMode, setTransformMode] =
|
||||
useState<TransformMode>("translate");
|
||||
const [isPlayerMode, setIsPlayerMode] = useState(false);
|
||||
const [isSelectionLocked, setIsSelectionLocked] = useState(false);
|
||||
const [sceneLoadingState, setSceneLoadingState] = useState<SceneLoadingState>(
|
||||
{
|
||||
...INITIAL_SCENE_LOADING_STATE,
|
||||
currentStep: "Montage progressif des models",
|
||||
progress: 0.2,
|
||||
},
|
||||
const [snapToTerrain, setSnapToTerrain] = useState(true);
|
||||
const [newNodeName, setNewNodeName] = useState(DEFAULT_NEW_NODE_NAME);
|
||||
const [lockTerrainSelection, setLockTerrainSelection] = useState(true);
|
||||
const [resetCameraRequest, setResetCameraRequest] = useState(0);
|
||||
const [snapAllToTerrainRequest, setSnapAllToTerrainRequest] = useState(0);
|
||||
const [focusSelectedCameraRequest, setFocusSelectedCameraRequest] =
|
||||
useState(0);
|
||||
const [cameraViewMode, setCameraViewMode] = useState<"home" | "object">(
|
||||
"home",
|
||||
);
|
||||
const handleSceneLoadingStateChange = useCallback(
|
||||
(nextState: SceneLoadingState) => {
|
||||
setSceneLoadingState((currentState) => {
|
||||
const shouldRestartProgress = currentState.status === "ready";
|
||||
|
||||
return {
|
||||
...nextState,
|
||||
progress: shouldRestartProgress
|
||||
? nextState.progress
|
||||
: Math.max(currentState.progress, nextState.progress),
|
||||
};
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
const editorLoadingState = isMapLoading
|
||||
const editorLoadingState: SceneLoadingState = isMapLoading
|
||||
? {
|
||||
currentStep: "Récupération blocking",
|
||||
currentStep: "Chargement de la carte",
|
||||
progress: 0.08,
|
||||
status: "loading" as const,
|
||||
}
|
||||
: sceneLoadingState;
|
||||
: {
|
||||
currentStep: "Gameplay prêt",
|
||||
progress: 1,
|
||||
status: "ready" as const,
|
||||
};
|
||||
const [cinematicPreviewRequest, setCinematicPreviewRequest] =
|
||||
useState<EditorCinematicPreviewRequest | null>(null);
|
||||
|
||||
@@ -111,16 +108,96 @@ export function EditorPage(): React.JSX.Element {
|
||||
|
||||
const handleSelectNode = useCallback((index: number | null) => {
|
||||
setSelectedNodeIndex(index);
|
||||
setSelectedNodeIndexes(index === null ? [] : [index]);
|
||||
|
||||
if (index !== null) {
|
||||
setCameraViewMode("object");
|
||||
return;
|
||||
}
|
||||
|
||||
setCameraViewMode("home");
|
||||
setResetCameraRequest((request) => request + 1);
|
||||
}, []);
|
||||
|
||||
const handleToggleNodeSelection = useCallback(
|
||||
(index: number) => {
|
||||
const isSelected = selectedNodeIndexes.includes(index);
|
||||
const nextIndexes = isSelected
|
||||
? selectedNodeIndexes.filter((item) => item !== index)
|
||||
: [...selectedNodeIndexes, index];
|
||||
|
||||
setSelectedNodeIndexes(nextIndexes);
|
||||
setSelectedNodeIndex(nextIndexes.at(-1) ?? null);
|
||||
if (nextIndexes.length > 0) {
|
||||
setCameraViewMode("object");
|
||||
} else {
|
||||
setCameraViewMode("home");
|
||||
setResetCameraRequest((request) => request + 1);
|
||||
}
|
||||
},
|
||||
[selectedNodeIndexes],
|
||||
);
|
||||
|
||||
const handleClearSelection = useCallback(() => {
|
||||
setSelectedNodeIndex(null);
|
||||
setSelectedNodeIndexes([]);
|
||||
setCameraViewMode("home");
|
||||
setResetCameraRequest((request) => request + 1);
|
||||
}, []);
|
||||
|
||||
const handleSelectionLockToggle = useCallback(() => {
|
||||
setIsSelectionLocked((locked) => !locked);
|
||||
}, []);
|
||||
|
||||
const handleSnapToTerrainToggle = useCallback(() => {
|
||||
setSnapToTerrain((enabled) => !enabled);
|
||||
}, []);
|
||||
|
||||
const handleSnapAllToTerrainRequest = useCallback(() => {
|
||||
setSnapAllToTerrainRequest((request) => request + 1);
|
||||
}, []);
|
||||
|
||||
const handleSnapAllToTerrain = useCallback(
|
||||
(mapNodes: MapNode[]) => {
|
||||
setSceneData((prev) => {
|
||||
if (!prev) return null;
|
||||
|
||||
const nextSceneData = { ...prev, mapNodes };
|
||||
if (!prev.mapTree) return nextSceneData;
|
||||
|
||||
const mapTree = mergeFlatNodeTransformsIntoTree(nextSceneData);
|
||||
return updateSceneDataTree(nextSceneData, mapTree);
|
||||
});
|
||||
},
|
||||
[setSceneData],
|
||||
);
|
||||
|
||||
const handleNewNodeNameChange = useCallback((value: string) => {
|
||||
setNewNodeName(value);
|
||||
}, []);
|
||||
|
||||
const handleTerrainSelectionLockChange = useCallback(
|
||||
(locked: boolean) => {
|
||||
setLockTerrainSelection(locked);
|
||||
|
||||
if (!locked) return;
|
||||
|
||||
const nextIndexes = selectedNodeIndexes.filter(
|
||||
(index) => sceneData?.mapNodes[index]?.name !== "terrain",
|
||||
);
|
||||
const selectedNode =
|
||||
selectedNodeIndex !== null
|
||||
? sceneData?.mapNodes[selectedNodeIndex]
|
||||
: null;
|
||||
|
||||
setSelectedNodeIndexes(nextIndexes);
|
||||
setSelectedNodeIndex(
|
||||
selectedNode?.name === "terrain" ? null : selectedNodeIndex,
|
||||
);
|
||||
},
|
||||
[sceneData, selectedNodeIndex, selectedNodeIndexes],
|
||||
);
|
||||
|
||||
const handleHoverNode = useCallback((index: number | null) => {
|
||||
setHoveredNodeIndex(index);
|
||||
}, []);
|
||||
@@ -167,6 +244,17 @@ export function EditorPage(): React.JSX.Element {
|
||||
setIsPlayerMode((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
const handleCameraAction = useCallback(() => {
|
||||
if (selectedNodeIndex !== null && cameraViewMode === "home") {
|
||||
setFocusSelectedCameraRequest((request) => request + 1);
|
||||
setCameraViewMode("object");
|
||||
return;
|
||||
}
|
||||
|
||||
setResetCameraRequest((request) => request + 1);
|
||||
setCameraViewMode("home");
|
||||
}, [cameraViewMode, selectedNodeIndex]);
|
||||
|
||||
const handlePreviewCinematic = useCallback(
|
||||
(cinematic: CinematicDefinition) => {
|
||||
setCinematicPreviewRequest({
|
||||
@@ -185,14 +273,112 @@ export function EditorPage(): React.JSX.Element {
|
||||
(nodeIndex: number, updatedNode: MapNode) => {
|
||||
setSceneData((prev) => {
|
||||
if (!prev) return null;
|
||||
const newMapNodes = [...prev.mapNodes];
|
||||
newMapNodes[nodeIndex] = updatedNode;
|
||||
return { ...prev, mapNodes: newMapNodes };
|
||||
const currentNode = prev.mapNodes[nodeIndex];
|
||||
if (!currentNode) return prev;
|
||||
|
||||
if (!prev.mapTree || !currentNode.sourcePath) {
|
||||
const mapNodes = [...prev.mapNodes];
|
||||
mapNodes[nodeIndex] = updatedNode;
|
||||
return { ...prev, mapNodes };
|
||||
}
|
||||
|
||||
const mapTree = updateTreeNodeAtPath(
|
||||
prev.mapTree,
|
||||
currentNode.sourcePath,
|
||||
(node) => ({
|
||||
...node,
|
||||
position: updatedNode.position,
|
||||
rotation: updatedNode.rotation,
|
||||
scale: updatedNode.scale,
|
||||
}),
|
||||
);
|
||||
return updateSceneDataTree(prev, mapTree);
|
||||
});
|
||||
},
|
||||
[setSceneData],
|
||||
);
|
||||
|
||||
const handleSelectedScaleChange = useCallback(
|
||||
(axis: 0 | 1 | 2, value: number) => {
|
||||
if (selectedNodeIndex === null || Number.isNaN(value)) return;
|
||||
|
||||
setSceneData((prev) => {
|
||||
if (!prev) return null;
|
||||
const currentNode = prev.mapNodes[selectedNodeIndex];
|
||||
if (!currentNode) return prev;
|
||||
|
||||
const nextScale = [...currentNode.scale] as [number, number, number];
|
||||
nextScale[axis] = value;
|
||||
|
||||
if (!prev.mapTree || !currentNode.sourcePath) {
|
||||
const mapNodes = [...prev.mapNodes];
|
||||
mapNodes[selectedNodeIndex] = { ...currentNode, scale: nextScale };
|
||||
return { ...prev, mapNodes };
|
||||
}
|
||||
|
||||
const mapTree = updateTreeNodeAtPath(
|
||||
prev.mapTree,
|
||||
currentNode.sourcePath,
|
||||
(node) => ({ ...node, scale: nextScale }),
|
||||
);
|
||||
|
||||
return updateSceneDataTree(prev, mapTree);
|
||||
});
|
||||
},
|
||||
[selectedNodeIndex, setSceneData],
|
||||
);
|
||||
|
||||
const handleAddNode = useCallback(() => {
|
||||
if (!sceneData) return;
|
||||
|
||||
if (!sceneData.mapTree) {
|
||||
const newNode = createNewMapNode(newNodeName);
|
||||
const mapNodes = [...sceneData.mapNodes, removeEditorMetadata(newNode)];
|
||||
const selectedIndex = mapNodes.length - 1;
|
||||
|
||||
setSceneData({ ...sceneData, mapNodes });
|
||||
setSelectedNodeIndex(selectedIndex);
|
||||
setSelectedNodeIndexes([selectedIndex]);
|
||||
return;
|
||||
}
|
||||
|
||||
const mapTree = addTreeNode(
|
||||
sceneData.mapTree,
|
||||
createNewMapNode(newNodeName),
|
||||
);
|
||||
const nextSceneData = updateSceneDataTree(sceneData, mapTree);
|
||||
const selectedIndex = nextSceneData.mapNodes.length - 1;
|
||||
|
||||
setSceneData(nextSceneData);
|
||||
setSelectedNodeIndex(selectedIndex);
|
||||
setSelectedNodeIndexes([selectedIndex]);
|
||||
}, [newNodeName, sceneData, setSceneData]);
|
||||
|
||||
const handleDeleteSelectedNode = useCallback(() => {
|
||||
if (!sceneData || selectedNodeIndex === null) return;
|
||||
|
||||
const currentNode = sceneData.mapNodes[selectedNodeIndex];
|
||||
if (!currentNode) return;
|
||||
|
||||
if (!sceneData.mapTree || !currentNode.sourcePath) {
|
||||
setSceneData({
|
||||
...sceneData,
|
||||
mapNodes: sceneData.mapNodes.filter(
|
||||
(_node, index) => index !== selectedNodeIndex,
|
||||
),
|
||||
});
|
||||
} else {
|
||||
const mapTree = removeTreeNodeAtPath(
|
||||
sceneData.mapTree,
|
||||
currentNode.sourcePath,
|
||||
);
|
||||
setSceneData(updateSceneDataTree(sceneData, mapTree));
|
||||
}
|
||||
|
||||
setSelectedNodeIndex(null);
|
||||
setSelectedNodeIndexes([]);
|
||||
}, [sceneData, selectedNodeIndex, setSceneData]);
|
||||
|
||||
if (isMapLoading) {
|
||||
return (
|
||||
<div className="editor-container">
|
||||
@@ -243,33 +429,39 @@ export function EditorPage(): React.JSX.Element {
|
||||
<Canvas
|
||||
camera={{ position: [0, 50, 100], fov: 50 }}
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
onCreated={({ gl }) => {
|
||||
gl.setClearColor("#050505");
|
||||
gl={{
|
||||
powerPreference: "high-performance",
|
||||
antialias: true,
|
||||
stencil: false,
|
||||
}}
|
||||
>
|
||||
<EditorSceneLoadingTracker
|
||||
onLoadingStateChange={handleSceneLoadingStateChange}
|
||||
<EditorWebGLContextLogger />
|
||||
<EditorScene
|
||||
sceneData={sceneData!}
|
||||
selectedNodeIndex={selectedNodeIndex}
|
||||
selectedNodeIndexes={selectedNodeIndexes}
|
||||
onSelectNode={handleSelectNode}
|
||||
onToggleNodeSelection={handleToggleNodeSelection}
|
||||
isSelectionLocked={isSelectionLocked}
|
||||
hoveredNodeIndex={hoveredNodeIndex}
|
||||
onHoverNode={handleHoverNode}
|
||||
transformMode={transformMode}
|
||||
snapToTerrain={snapToTerrain}
|
||||
lockTerrainSelection={lockTerrainSelection}
|
||||
onTransformModeChange={handleTransformModeChange}
|
||||
onTransformStart={handleTransformStart}
|
||||
onTransformEnd={handleTransformEnd}
|
||||
onNodeTransform={handleNodeTransform}
|
||||
snapAllToTerrainRequest={snapAllToTerrainRequest}
|
||||
onSnapAllToTerrain={handleSnapAllToTerrain}
|
||||
onUndo={handleUndo}
|
||||
onRedo={handleRedo}
|
||||
resetCameraRequest={resetCameraRequest}
|
||||
focusSelectedCameraRequest={focusSelectedCameraRequest}
|
||||
isPlayerMode={isPlayerMode}
|
||||
cinematicPreviewRequest={cinematicPreviewRequest}
|
||||
onCinematicPreviewComplete={handleCinematicPreviewComplete}
|
||||
/>
|
||||
<Suspense fallback={null}>
|
||||
<EditorScene
|
||||
sceneData={sceneData!}
|
||||
selectedNodeIndex={selectedNodeIndex}
|
||||
onSelectNode={handleSelectNode}
|
||||
isSelectionLocked={isSelectionLocked}
|
||||
hoveredNodeIndex={hoveredNodeIndex}
|
||||
onHoverNode={handleHoverNode}
|
||||
transformMode={transformMode}
|
||||
onTransformModeChange={handleTransformModeChange}
|
||||
onTransformStart={handleTransformStart}
|
||||
onTransformEnd={handleTransformEnd}
|
||||
onNodeTransform={handleNodeTransform}
|
||||
onUndo={handleUndo}
|
||||
onRedo={handleRedo}
|
||||
isPlayerMode={isPlayerMode}
|
||||
cinematicPreviewRequest={cinematicPreviewRequest}
|
||||
onCinematicPreviewComplete={handleCinematicPreviewComplete}
|
||||
/>
|
||||
</Suspense>
|
||||
</Canvas>
|
||||
|
||||
<SceneLoadingOverlay state={editorLoadingState} />
|
||||
@@ -279,6 +471,7 @@ export function EditorPage(): React.JSX.Element {
|
||||
transformMode={transformMode}
|
||||
onTransformModeChange={handleTransformModeChange}
|
||||
selectedNodeIndex={selectedNodeIndex}
|
||||
selectedNodeIndexes={selectedNodeIndexes}
|
||||
mapNodes={sceneData.mapNodes}
|
||||
nodesCount={sceneData.mapNodes.length}
|
||||
selectedNodeName={
|
||||
@@ -286,13 +479,34 @@ export function EditorPage(): React.JSX.Element {
|
||||
? sceneData.mapNodes[selectedNodeIndex].name || null
|
||||
: null
|
||||
}
|
||||
selectedNodeScale={
|
||||
selectedNodeIndex !== null && sceneData.mapNodes[selectedNodeIndex]
|
||||
? sceneData.mapNodes[selectedNodeIndex].scale
|
||||
: null
|
||||
}
|
||||
lockTerrainSelection={lockTerrainSelection}
|
||||
onLockTerrainSelectionChange={handleTerrainSelectionLockChange}
|
||||
isSelectionLocked={isSelectionLocked}
|
||||
onSelectionLockToggle={handleSelectionLockToggle}
|
||||
onClearSelection={handleClearSelection}
|
||||
snapToTerrain={snapToTerrain}
|
||||
onSnapToTerrainToggle={handleSnapToTerrainToggle}
|
||||
onSnapAllToTerrain={handleSnapAllToTerrainRequest}
|
||||
newNodeName={newNodeName}
|
||||
onNewNodeNameChange={handleNewNodeNameChange}
|
||||
onAddNode={handleAddNode}
|
||||
onDeleteSelectedNode={handleDeleteSelectedNode}
|
||||
onSelectedScaleChange={handleSelectedScaleChange}
|
||||
undoCount={undoCount}
|
||||
redoCount={redoCount}
|
||||
onUndo={handleUndo}
|
||||
onRedo={handleRedo}
|
||||
cameraActionLabel={
|
||||
selectedNodeIndex !== null && cameraViewMode === "home"
|
||||
? "Center on object"
|
||||
: "Reset camera"
|
||||
}
|
||||
onCameraAction={handleCameraAction}
|
||||
onExportJson={handleExportJson}
|
||||
onSaveToServer={import.meta.env.DEV ? handleSaveToServer : undefined}
|
||||
onPlayerMode={handlePlayerMode}
|
||||
|
||||
@@ -33,12 +33,10 @@ import {
|
||||
AMBIENT_INTENSITY_MAX,
|
||||
AMBIENT_INTENSITY_MIN,
|
||||
AMBIENT_INTENSITY_STEP,
|
||||
AMBIENT_LIGHT_COLOR,
|
||||
LIGHTING_DEFAULTS,
|
||||
SUN_INTENSITY_MAX,
|
||||
SUN_INTENSITY_MIN,
|
||||
SUN_INTENSITY_STEP,
|
||||
SUN_LIGHT_COLOR,
|
||||
SUN_X_MAX,
|
||||
SUN_X_MIN,
|
||||
SUN_X_STEP,
|
||||
@@ -50,8 +48,8 @@ import {
|
||||
SUN_Z_STEP,
|
||||
} from "@/data/world/lightingConfig";
|
||||
import {
|
||||
GAME_SCENE_FALLBACK_SKY_MODEL_PATH,
|
||||
GAME_SCENE_FALLBACK_SKY_MODEL_SCALE,
|
||||
GAME_SCENE_SKY_FALLBACK_MODEL_PATH,
|
||||
GAME_SCENE_SKY_FALLBACK_MODEL_SCALE,
|
||||
GAME_SCENE_SKY_MODEL_PATH,
|
||||
GAME_SCENE_SKY_MODEL_SCALE,
|
||||
} from "@/data/world/environmentConfig";
|
||||
@@ -337,8 +335,8 @@ function GalleryScene({
|
||||
return (
|
||||
<>
|
||||
<SkyModel
|
||||
fallbackModelPath={GAME_SCENE_FALLBACK_SKY_MODEL_PATH}
|
||||
fallbackScale={GAME_SCENE_FALLBACK_SKY_MODEL_SCALE}
|
||||
fallbackModelPath={GAME_SCENE_SKY_FALLBACK_MODEL_PATH}
|
||||
fallbackScale={GAME_SCENE_SKY_FALLBACK_MODEL_SCALE}
|
||||
materialSide={THREE.DoubleSide}
|
||||
modelPath={GAME_SCENE_SKY_MODEL_PATH}
|
||||
scale={GAME_SCENE_SKY_MODEL_SCALE}
|
||||
@@ -374,12 +372,12 @@ function GalleryLighting({
|
||||
<>
|
||||
<ambientLight
|
||||
intensity={lighting.ambientIntensity}
|
||||
color={AMBIENT_LIGHT_COLOR}
|
||||
color={LIGHTING_DEFAULTS.ambientColor}
|
||||
/>
|
||||
<directionalLight
|
||||
position={[lighting.sunX, lighting.sunY, lighting.sunZ]}
|
||||
intensity={lighting.sunIntensity}
|
||||
color={SUN_LIGHT_COLOR}
|
||||
color={LIGHTING_DEFAULTS.sunColor}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
+35
-4
@@ -6,12 +6,11 @@ import { DialogMessage } from "@/components/ui/DialogMessage";
|
||||
import { GameUI } from "@/components/ui/GameUI";
|
||||
import { BienvenueDisplay, IntroUI } from "@/components/ui/IntroUI";
|
||||
import { SceneLoadingOverlay } from "@/components/ui/SceneLoadingOverlay";
|
||||
import { INITIAL_SCENE_LOADING_STATE } from "@/data/world/sceneLoadingConfig";
|
||||
import { useGameStore } from "@/managers/stores/useGameStore";
|
||||
import { HandTrackingProvider } from "@/providers/gameplay/HandTrackingProvider";
|
||||
import {
|
||||
INITIAL_SCENE_LOADING_STATE,
|
||||
type SceneLoadingState,
|
||||
} from "@/types/world/sceneLoading";
|
||||
import type { SceneLoadingState } from "@/types/world/sceneLoading";
|
||||
import { logger } from "@/utils/core/Logger";
|
||||
import { World } from "@/world/World";
|
||||
|
||||
export function HomePage(): React.JSX.Element {
|
||||
@@ -51,11 +50,43 @@ export function HomePage(): React.JSX.Element {
|
||||
[],
|
||||
);
|
||||
|
||||
const handleCanvasCreated = useCallback(
|
||||
({ gl }: { gl: THREE.WebGLRenderer }) => {
|
||||
const canvas = gl.domElement;
|
||||
|
||||
gl.shadowMap.enabled = true;
|
||||
gl.shadowMap.type = THREE.PCFShadowMap;
|
||||
gl.shadowMap.autoUpdate = true;
|
||||
|
||||
const handleContextLost = (event: Event) => {
|
||||
event.preventDefault();
|
||||
logger.error("WebGL", "Context lost - GPU resources exhausted");
|
||||
};
|
||||
|
||||
const handleContextRestored = () => {
|
||||
gl.shadowMap.enabled = true;
|
||||
gl.shadowMap.type = THREE.PCFShadowMap;
|
||||
gl.shadowMap.autoUpdate = true;
|
||||
logger.info("WebGL", "Context restored");
|
||||
};
|
||||
|
||||
canvas.addEventListener("webglcontextlost", handleContextLost);
|
||||
canvas.addEventListener("webglcontextrestored", handleContextRestored);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<HandTrackingProvider>
|
||||
<Canvas
|
||||
camera={{ position: [85, 60, 85], fov: 42 }}
|
||||
shadows={{ type: THREE.PCFShadowMap }}
|
||||
gl={{
|
||||
powerPreference: "high-performance",
|
||||
antialias: true,
|
||||
stencil: false,
|
||||
}}
|
||||
onCreated={handleCanvasCreated}
|
||||
>
|
||||
<Suspense fallback={null}>
|
||||
<World onLoadingStateChange={handleSceneLoadingStateChange} />
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user