refactor: clean architecture and remove unused code
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { useMemo, useRef, useEffect, useState } from "react";
|
||||
import { useRef, useEffect, useState } from "react";
|
||||
import { Grid, TransformControls, useGLTF } from "@react-three/drei";
|
||||
import type { ThreeEvent } from "@react-three/fiber";
|
||||
import * as THREE from "three";
|
||||
|
||||
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
||||
import type { SceneData, MapNode, TransformMode } from "@/types/editor/editor";
|
||||
|
||||
interface EditorMapProps {
|
||||
@@ -138,7 +139,7 @@ export function EditorMap({
|
||||
const objectsMapRef = useRef<Map<number, THREE.Object3D>>(new Map());
|
||||
|
||||
const handleTransformMouseDown = () => {
|
||||
onTransformStart?.();
|
||||
onTransformStart();
|
||||
};
|
||||
|
||||
const handleTransformMouseUp = () => {
|
||||
@@ -153,10 +154,10 @@ export function EditorMap({
|
||||
rotation: [obj.rotation.x, obj.rotation.y, obj.rotation.z],
|
||||
scale: [obj.scale.x, obj.scale.y, obj.scale.z],
|
||||
};
|
||||
onNodeTransform?.(selectedNodeIndex, updatedNode);
|
||||
onNodeTransform(selectedNodeIndex, updatedNode);
|
||||
}
|
||||
}
|
||||
onTransformEnd?.();
|
||||
onTransformEnd();
|
||||
};
|
||||
|
||||
const [selectedObject, setSelectedObject] = useState<THREE.Object3D | null>(
|
||||
@@ -258,8 +259,7 @@ function EditorModelNode({
|
||||
new Map<THREE.Mesh, THREE.Material | THREE.Material[]>(),
|
||||
);
|
||||
const { scene } = useGLTF(modelUrl);
|
||||
|
||||
const sceneInstance = useMemo(() => scene.clone(true), [scene]);
|
||||
const sceneInstance = useClonedObject(scene);
|
||||
const pointerHandlers = createEditorNodePointerHandlers(
|
||||
index,
|
||||
onSelectNode,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useGLTF } from "@react-three/drei";
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import gsap from "gsap";
|
||||
@@ -15,14 +15,13 @@ import {
|
||||
REPAIR_CASE_ROTATION_AMPLITUDE_DEGREES,
|
||||
REPAIR_CASE_ROTATION_RESET_SPEED,
|
||||
} from "@/data/gameplay/repairCaseConfig";
|
||||
import type { Vector3Tuple } from "@/types/three/three";
|
||||
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
||||
import type { ModelTransformProps } from "@/types/three/three";
|
||||
import { toVector3Scale } from "@/utils/three/scale";
|
||||
|
||||
interface RepairCaseModelProps {
|
||||
interface RepairCaseModelProps extends ModelTransformProps {
|
||||
modelPath: string;
|
||||
open: boolean;
|
||||
position?: Vector3Tuple;
|
||||
rotation?: Vector3Tuple;
|
||||
scale?: number | Vector3Tuple;
|
||||
}
|
||||
|
||||
const CASE_CLOSED_ROTATION_OFFSET_Z = THREE.MathUtils.degToRad(
|
||||
@@ -44,7 +43,7 @@ export function RepairCaseModel({
|
||||
}: RepairCaseModelProps): React.JSX.Element {
|
||||
const camera = useThree((state) => state.camera);
|
||||
const { scene } = useGLTF(modelPath);
|
||||
const model = useMemo(() => scene.clone(true), [scene]);
|
||||
const model = useClonedObject(scene);
|
||||
const groupRef = useRef<THREE.Group>(null);
|
||||
const lidRef = useRef<THREE.Object3D | null>(null);
|
||||
const worldPosition = useRef(new THREE.Vector3());
|
||||
@@ -53,8 +52,7 @@ export function RepairCaseModel({
|
||||
const phase = useRef({ x: 0, y: 0, z: 0 });
|
||||
const initialOpen = useRef(open);
|
||||
const openedRotationZ = useRef(0);
|
||||
const parsedScale =
|
||||
typeof scale === "number" ? ([scale, scale, scale] as Vector3Tuple) : scale;
|
||||
const parsedScale = toVector3Scale(scale);
|
||||
|
||||
useEffect(() => {
|
||||
phase.current = {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useGLTF } from "@react-three/drei";
|
||||
import { RigidBody } from "@react-three/rapier";
|
||||
import { InteractableObject } from "@/components/three/interaction/InteractableObject";
|
||||
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
||||
import {
|
||||
TRIGGER_DEFAULT_COLLIDERS,
|
||||
TRIGGER_DEFAULT_LABEL,
|
||||
@@ -38,7 +39,7 @@ function SpawnedModelInstance({
|
||||
position: Vector3Tuple;
|
||||
}): React.JSX.Element {
|
||||
const { scene } = useGLTF(path);
|
||||
const model = useMemo(() => scene.clone(true), [scene]);
|
||||
const model = useClonedObject(scene);
|
||||
|
||||
return <primitive object={model} position={position} />;
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@ import type { ReactNode } from "react";
|
||||
import { Component, useEffect, useMemo } from "react";
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import { useGLTF } from "@react-three/drei";
|
||||
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
||||
import { ExplodedModel } from "@/utils/three/ExplodedModel";
|
||||
import type { Vector3Tuple } from "@/types/three/three";
|
||||
import type { ModelTransformProps, Vector3Tuple } from "@/types/three/three";
|
||||
import { toVector3Scale } from "@/utils/three/scale";
|
||||
|
||||
interface ModelErrorBoundaryProps {
|
||||
children: ReactNode;
|
||||
fallback?: ReactNode;
|
||||
position?: Vector3Tuple | undefined;
|
||||
}
|
||||
|
||||
interface ModelErrorBoundaryState {
|
||||
@@ -32,17 +34,17 @@ class ModelErrorBoundary extends Component<
|
||||
}
|
||||
|
||||
render(): ReactNode {
|
||||
if (this.state.hasError) return this.props.fallback ?? null;
|
||||
if (this.state.hasError) {
|
||||
return <MissingModelFallback position={this.props.position} />;
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
interface ExplodableModelInnerProps {
|
||||
interface ExplodableModelInnerProps extends ModelTransformProps {
|
||||
modelPath: string;
|
||||
split: boolean;
|
||||
position?: Vector3Tuple;
|
||||
rotation?: Vector3Tuple;
|
||||
scale?: number | Vector3Tuple;
|
||||
splitDistance?: number;
|
||||
}
|
||||
|
||||
@@ -50,10 +52,7 @@ export function ExplodableModel(
|
||||
props: ExplodableModelInnerProps,
|
||||
): React.JSX.Element {
|
||||
return (
|
||||
<ModelErrorBoundary
|
||||
key={props.modelPath}
|
||||
fallback={<MissingModelFallback position={props.position ?? [0, 0, 0]} />}
|
||||
>
|
||||
<ModelErrorBoundary key={props.modelPath} position={props.position}>
|
||||
<ExplodableModelInner {...props} />
|
||||
</ModelErrorBoundary>
|
||||
);
|
||||
@@ -68,13 +67,12 @@ function ExplodableModelInner({
|
||||
splitDistance = 1.2,
|
||||
}: ExplodableModelInnerProps): React.JSX.Element {
|
||||
const { scene } = useGLTF(modelPath);
|
||||
const model = useMemo(() => scene.clone(true), [scene]);
|
||||
const model = useClonedObject(scene);
|
||||
const explodedModel = useMemo(
|
||||
() => new ExplodedModel(model, { distance: splitDistance }),
|
||||
[model, splitDistance],
|
||||
);
|
||||
const parsedScale =
|
||||
typeof scale === "number" ? ([scale, scale, scale] as Vector3Tuple) : scale;
|
||||
const parsedScale = toVector3Scale(scale);
|
||||
|
||||
useEffect(() => {
|
||||
explodedModel.setSplit(split);
|
||||
@@ -94,7 +92,7 @@ function ExplodableModelInner({
|
||||
function MissingModelFallback({
|
||||
position = [0, 0, 0],
|
||||
}: {
|
||||
position?: Vector3Tuple;
|
||||
position?: Vector3Tuple | undefined;
|
||||
}): React.JSX.Element {
|
||||
return (
|
||||
<mesh position={position}>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import { useGLTF } from "@react-three/drei";
|
||||
import { useMemo, useRef } from "react";
|
||||
import { useRef } from "react";
|
||||
import * as THREE from "three";
|
||||
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
||||
|
||||
interface SkyModelProps {
|
||||
modelPath: string;
|
||||
@@ -13,7 +14,7 @@ export function SkyModel({ modelPath }: SkyModelProps): React.JSX.Element {
|
||||
const camera = useThree((state) => state.camera);
|
||||
const groupRef = useRef<THREE.Group>(null);
|
||||
const { scene } = useGLTF(modelPath);
|
||||
const model = useMemo(() => scene.clone(true), [scene]);
|
||||
const model = useClonedObject(scene);
|
||||
|
||||
useFrame(() => {
|
||||
groupRef.current?.position.copy(camera.position);
|
||||
|
||||
@@ -24,7 +24,6 @@ Construit avec React, Three.js et Vite. Fonctionne dans le navigateur, sans inst
|
||||
| [@react-three/fiber](https://docs.pmnd.rs/react-three-fiber/getting-started/introduction) |
|
||||
| [@react-three/drei](https://pmndrs.github.io/drei) |
|
||||
| [@react-three/rapier](https://rapier.rs/docs/) |
|
||||
| [@react-three/postprocessing](https://github.com/pmndrs/postprocessing) |
|
||||
| [GSAP](https://gsap.com/docs/v3/Installation/) |
|
||||
|
||||
### Performance et effets
|
||||
@@ -48,17 +47,17 @@ la-fabrik/
|
||||
│ └── sounds/
|
||||
│
|
||||
└── src/
|
||||
├── world/ # Monde 3D persistant
|
||||
│ ├── World.tsx # Composition principale de la scène
|
||||
│ ├── Map.tsx # Carte de base, toujours montée
|
||||
├── world/ # Composition du monde 3D persistant
|
||||
│ ├── World.tsx # Composition de la scène active
|
||||
│ ├── GameMap.tsx # Chargement de carte et collision octree
|
||||
│ ├── Lighting.tsx # Lumières ambiante, directionnelle et ponctuelles
|
||||
│ ├── Environment.tsx # HDRI, brouillard, ciel
|
||||
│ ├── PostFX.tsx # Bloom, SSAO, aberration chromatique
|
||||
│ ├── zones/ # Zones spatiales, LOD par zone
|
||||
│ ├── Environment.tsx # Arrière-plan et modèle de ciel
|
||||
│ ├── GameMusic.tsx # Cycle de vie de la musique de jeu
|
||||
│ ├── debug/ # Scène de test debug
|
||||
│ └── player/ # Contrôleur joueur et caméra
|
||||
│
|
||||
├── components/
|
||||
│ ├── 3d/ # Éléments 3D réutilisables
|
||||
│ ├── three/ # Composants R3F par domaine
|
||||
│ └── ui/ # Overlays HTML hors Canvas
|
||||
│
|
||||
├── managers/ # Logique, état et orchestration
|
||||
@@ -142,7 +141,7 @@ Ce document décrit l'architecture visée à moyen terme pour le projet.
|
||||
## Relation avec le code actuel
|
||||
|
||||
- \`docs/technical/architecture.md\` reste la source de vérité de ce qui existe maintenant.
|
||||
- Ce document est volontairement aspirational.
|
||||
- Ce document décrit une direction d'architecture, pas un comportement implémenté.
|
||||
- Si ce document contredit l'implémentation actuelle, l'implémentation actuelle gagne.
|
||||
|
||||
## Objectifs
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const HAND_TRACKING_LOCAL_WS_URL = "ws://localhost:8000/ws";
|
||||
export const HAND_TRACKING_PROD_WS_URL = "wss://handtracking.la-fabrik.fr/ws";
|
||||
const HAND_TRACKING_LOCAL_WS_URL = "ws://localhost:8000/ws";
|
||||
const HAND_TRACKING_PROD_WS_URL = "wss://handtracking.la-fabrik.fr/ws";
|
||||
|
||||
export const HAND_TRACKING_FRAME_WIDTH = 320;
|
||||
export const HAND_TRACKING_FRAME_HEIGHT = 240;
|
||||
|
||||
@@ -15,6 +15,13 @@ export function useModelSelection(
|
||||
): UseModelSelectionResult {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const firstModel = models[0];
|
||||
|
||||
if (!firstModel) {
|
||||
throw new Error("useModelSelection requires at least one model");
|
||||
}
|
||||
|
||||
const selectedModel = models[selectedIndex] ?? firstModel;
|
||||
|
||||
const close = useCallback(() => setIsOpen(false), []);
|
||||
const open = useCallback(() => setIsOpen(true), []);
|
||||
@@ -42,7 +49,7 @@ export function useModelSelection(
|
||||
}
|
||||
|
||||
if (key === "e" || key === "enter") {
|
||||
onSelect(models[selectedIndex]);
|
||||
onSelect(selectedModel);
|
||||
close();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -60,12 +67,12 @@ export function useModelSelection(
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown, { capture: true });
|
||||
};
|
||||
}, [close, isOpen, models, onSelect, selectedIndex]);
|
||||
}, [close, isOpen, models, onSelect, selectedModel]);
|
||||
|
||||
return {
|
||||
isOpen,
|
||||
selectedIndex,
|
||||
selectedModel: models[selectedIndex],
|
||||
selectedModel,
|
||||
open,
|
||||
close,
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "@/data/handTrackingConfig";
|
||||
import type {
|
||||
HandTrackingFrameMessage,
|
||||
HandTrackingHand,
|
||||
HandTrackingServerMessage,
|
||||
HandTrackingSnapshot,
|
||||
} from "@/types/handTracking/handTracking";
|
||||
@@ -31,6 +32,58 @@ function getBase64Payload(dataUrl: string): string {
|
||||
return dataUrl.slice(dataUrl.indexOf(",") + 1);
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function isFiniteNumber(value: unknown): value is number {
|
||||
return typeof value === "number" && Number.isFinite(value);
|
||||
}
|
||||
|
||||
function isHandTrackingLandmark(value: unknown): boolean {
|
||||
return (
|
||||
isRecord(value) &&
|
||||
isFiniteNumber(value.x) &&
|
||||
isFiniteNumber(value.y) &&
|
||||
isFiniteNumber(value.z)
|
||||
);
|
||||
}
|
||||
|
||||
function isHandTrackingHand(value: unknown): value is HandTrackingHand {
|
||||
return (
|
||||
isRecord(value) &&
|
||||
isFiniteNumber(value.x) &&
|
||||
isFiniteNumber(value.y) &&
|
||||
isFiniteNumber(value.z) &&
|
||||
Array.isArray(value.landmarks) &&
|
||||
value.landmarks.every(isHandTrackingLandmark) &&
|
||||
typeof value.handedness === "string" &&
|
||||
typeof value.isFist === "boolean" &&
|
||||
isFiniteNumber(value.score)
|
||||
);
|
||||
}
|
||||
|
||||
function isHandTrackingServerMessage(
|
||||
value: unknown,
|
||||
): value is HandTrackingServerMessage {
|
||||
if (!isRecord(value) || !isFiniteNumber(value.timestamp)) return false;
|
||||
|
||||
if (value.type === "hands") {
|
||||
return Array.isArray(value.hands) && value.hands.every(isHandTrackingHand);
|
||||
}
|
||||
|
||||
if (value.type === "status") {
|
||||
return typeof value.status === "string";
|
||||
}
|
||||
|
||||
return (
|
||||
value.type === "error" &&
|
||||
Array.isArray(value.hands) &&
|
||||
value.hands.every(isHandTrackingHand) &&
|
||||
typeof value.message === "string"
|
||||
);
|
||||
}
|
||||
|
||||
function getCameraStreamWithTimeout(
|
||||
constraints: MediaStreamConstraints,
|
||||
): Promise<MediaStream> {
|
||||
@@ -106,6 +159,16 @@ export function useRemoteHandTracking({
|
||||
clearResponseTimeout();
|
||||
};
|
||||
|
||||
const markInvalidResponse = (): void => {
|
||||
setSnapshot((current) => ({
|
||||
...current,
|
||||
hands: [],
|
||||
status: "error",
|
||||
usageStatus: "inactive",
|
||||
error: "Invalid hand tracking response",
|
||||
}));
|
||||
};
|
||||
|
||||
const sendFrame = (): void => {
|
||||
const ws = wsRef.current;
|
||||
const video = videoRef.current;
|
||||
@@ -201,7 +264,23 @@ export function useRemoteHandTracking({
|
||||
};
|
||||
ws.onmessage = (event) => {
|
||||
markResponseReceived();
|
||||
const data = JSON.parse(event.data) as HandTrackingServerMessage;
|
||||
if (typeof event.data !== "string") {
|
||||
markInvalidResponse();
|
||||
return;
|
||||
}
|
||||
|
||||
let data: unknown;
|
||||
try {
|
||||
data = JSON.parse(event.data);
|
||||
} catch {
|
||||
markInvalidResponse();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isHandTrackingServerMessage(data)) {
|
||||
markInvalidResponse();
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.type === "hands") {
|
||||
setSnapshot((current) => ({
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { useMemo } from "react";
|
||||
import type * as THREE from "three";
|
||||
|
||||
export function useClonedObject<T extends THREE.Object3D>(object: T): T {
|
||||
return useMemo(() => object.clone(true) as T, [object]);
|
||||
}
|
||||
+59
-99
@@ -1,141 +1,101 @@
|
||||
import { Suspense, lazy } from "react";
|
||||
|
||||
const LazyDocsLayout = lazy(() =>
|
||||
import("@/components/docs/DocsLayout").then((module) => ({
|
||||
default: module.DocsLayout,
|
||||
})),
|
||||
);
|
||||
function lazyNamed<T extends Record<string, React.ComponentType>>(
|
||||
loader: () => Promise<T>,
|
||||
exportName: keyof T,
|
||||
): React.LazyExoticComponent<T[keyof T]> {
|
||||
return lazy(() =>
|
||||
loader().then((module) => ({ default: module[exportName] })),
|
||||
);
|
||||
}
|
||||
|
||||
const LazyDocsReadmePage = lazy(() =>
|
||||
import("@/pages/docs/page").then((module) => ({
|
||||
default: module.DocsReadmePage,
|
||||
})),
|
||||
);
|
||||
function withDocsSuspense(
|
||||
Component: React.LazyExoticComponent<React.ComponentType>,
|
||||
): React.JSX.Element {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<Component />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
const LazyDocsArchitecturePage = lazy(() =>
|
||||
import("@/pages/docs/architecture/page").then((module) => ({
|
||||
default: module.DocsArchitecturePage,
|
||||
})),
|
||||
const LazyDocsLayout = lazyNamed(
|
||||
() => import("@/components/docs/DocsLayout"),
|
||||
"DocsLayout",
|
||||
);
|
||||
|
||||
const LazyDocsTargetArchitecturePage = lazy(() =>
|
||||
import("@/pages/docs/target-architecture/page").then((module) => ({
|
||||
default: module.DocsTargetArchitecturePage,
|
||||
})),
|
||||
const LazyDocsReadmePage = lazyNamed(
|
||||
() => import("@/pages/docs/page"),
|
||||
"DocsReadmePage",
|
||||
);
|
||||
|
||||
const LazyDocsTechnicalEditorPage = lazy(() =>
|
||||
import("@/pages/docs/technical-editor/page").then((module) => ({
|
||||
default: module.DocsTechnicalEditorPage,
|
||||
})),
|
||||
const LazyDocsArchitecturePage = lazyNamed(
|
||||
() => import("@/pages/docs/architecture/page"),
|
||||
"DocsArchitecturePage",
|
||||
);
|
||||
|
||||
const LazyDocsHandTrackingPage = lazy(() =>
|
||||
import("@/pages/docs/hand-tracking/page").then((module) => ({
|
||||
default: module.DocsHandTrackingPage,
|
||||
})),
|
||||
const LazyDocsTargetArchitecturePage = lazyNamed(
|
||||
() => import("@/pages/docs/target-architecture/page"),
|
||||
"DocsTargetArchitecturePage",
|
||||
);
|
||||
|
||||
const LazyDocsFeaturesPage = lazy(() =>
|
||||
import("@/pages/docs/features/page").then((module) => ({
|
||||
default: module.DocsFeaturesPage,
|
||||
})),
|
||||
const LazyDocsTechnicalEditorPage = lazyNamed(
|
||||
() => import("@/pages/docs/technical-editor/page"),
|
||||
"DocsTechnicalEditorPage",
|
||||
);
|
||||
|
||||
const LazyDocsMainFeaturePage = lazy(() =>
|
||||
import("@/pages/docs/main-feature/page").then((module) => ({
|
||||
default: module.DocsMainFeaturePage,
|
||||
})),
|
||||
const LazyDocsHandTrackingPage = lazyNamed(
|
||||
() => import("@/pages/docs/hand-tracking/page"),
|
||||
"DocsHandTrackingPage",
|
||||
);
|
||||
|
||||
const LazyDocsEditorPage = lazy(() =>
|
||||
import("@/pages/docs/editor/page").then((module) => ({
|
||||
default: module.DocsEditorPage,
|
||||
})),
|
||||
const LazyDocsFeaturesPage = lazyNamed(
|
||||
() => import("@/pages/docs/features/page"),
|
||||
"DocsFeaturesPage",
|
||||
);
|
||||
|
||||
const LazyDocsAnimationPage = lazy(() =>
|
||||
import("@/pages/docs/animation/page").then((module) => ({
|
||||
default: module.DocsAnimationPage,
|
||||
})),
|
||||
const LazyDocsMainFeaturePage = lazyNamed(
|
||||
() => import("@/pages/docs/main-feature/page"),
|
||||
"DocsMainFeaturePage",
|
||||
);
|
||||
const LazyDocsEditorPage = lazyNamed(
|
||||
() => import("@/pages/docs/editor/page"),
|
||||
"DocsEditorPage",
|
||||
);
|
||||
const LazyDocsAnimationPage = lazyNamed(
|
||||
() => import("@/pages/docs/animation/page"),
|
||||
"DocsAnimationPage",
|
||||
);
|
||||
|
||||
export function DocsLayoutRoute(): React.JSX.Element {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<LazyDocsLayout />
|
||||
</Suspense>
|
||||
);
|
||||
return withDocsSuspense(LazyDocsLayout);
|
||||
}
|
||||
|
||||
export function DocsReadmeRoute(): React.JSX.Element {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<LazyDocsReadmePage />
|
||||
</Suspense>
|
||||
);
|
||||
return withDocsSuspense(LazyDocsReadmePage);
|
||||
}
|
||||
|
||||
export function DocsArchitectureRoute(): React.JSX.Element {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<LazyDocsArchitecturePage />
|
||||
</Suspense>
|
||||
);
|
||||
return withDocsSuspense(LazyDocsArchitecturePage);
|
||||
}
|
||||
|
||||
export function DocsTargetArchitectureRoute(): React.JSX.Element {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<LazyDocsTargetArchitecturePage />
|
||||
</Suspense>
|
||||
);
|
||||
return withDocsSuspense(LazyDocsTargetArchitecturePage);
|
||||
}
|
||||
|
||||
export function DocsTechnicalEditorRoute(): React.JSX.Element {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<LazyDocsTechnicalEditorPage />
|
||||
</Suspense>
|
||||
);
|
||||
return withDocsSuspense(LazyDocsTechnicalEditorPage);
|
||||
}
|
||||
|
||||
export function DocsHandTrackingRoute(): React.JSX.Element {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<LazyDocsHandTrackingPage />
|
||||
</Suspense>
|
||||
);
|
||||
return withDocsSuspense(LazyDocsHandTrackingPage);
|
||||
}
|
||||
|
||||
export function DocsFeaturesRoute(): React.JSX.Element {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<LazyDocsFeaturesPage />
|
||||
</Suspense>
|
||||
);
|
||||
return withDocsSuspense(LazyDocsFeaturesPage);
|
||||
}
|
||||
|
||||
export function DocsMainFeatureRoute(): React.JSX.Element {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<LazyDocsMainFeaturePage />
|
||||
</Suspense>
|
||||
);
|
||||
return withDocsSuspense(LazyDocsMainFeaturePage);
|
||||
}
|
||||
|
||||
export function DocsEditorRoute(): React.JSX.Element {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<LazyDocsEditorPage />
|
||||
</Suspense>
|
||||
);
|
||||
return withDocsSuspense(LazyDocsEditorPage);
|
||||
}
|
||||
|
||||
export function DocsAnimationRoute(): React.JSX.Element {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<LazyDocsAnimationPage />
|
||||
</Suspense>
|
||||
);
|
||||
return withDocsSuspense(LazyDocsAnimationPage);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Vector3Tuple } from "@/types/three/three";
|
||||
import type { Vector3Tuple } from "../three/three";
|
||||
|
||||
export interface MapNode {
|
||||
name: string;
|
||||
|
||||
@@ -14,7 +14,7 @@ export interface HandTrackingHand {
|
||||
score: number;
|
||||
}
|
||||
|
||||
export type HandTrackingUsageStatus = "inactive" | "available" | "active";
|
||||
type HandTrackingUsageStatus = "inactive" | "available" | "active";
|
||||
|
||||
export type HandTrackingStatus =
|
||||
| "idle"
|
||||
@@ -42,19 +42,19 @@ export interface HandTrackingFrameMessage {
|
||||
image: string;
|
||||
}
|
||||
|
||||
export interface HandTrackingHandsMessage {
|
||||
interface HandTrackingHandsMessage {
|
||||
type: "hands";
|
||||
timestamp: number;
|
||||
hands: HandTrackingHand[];
|
||||
}
|
||||
|
||||
export interface HandTrackingStatusMessage {
|
||||
interface HandTrackingStatusMessage {
|
||||
type: "status";
|
||||
timestamp: number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface HandTrackingErrorMessage {
|
||||
interface HandTrackingErrorMessage {
|
||||
type: "error";
|
||||
timestamp: number;
|
||||
hands: HandTrackingHand[];
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
export type InteractableKind = "grab" | "trigger";
|
||||
|
||||
interface TriggerInteractableHandle {
|
||||
kind: "trigger";
|
||||
label: string;
|
||||
|
||||
@@ -2,6 +2,14 @@ import type { Octree } from "three/addons/math/Octree.js";
|
||||
|
||||
export type Vector3Tuple = [number, number, number];
|
||||
|
||||
export type Vector3Scale = Vector3Tuple | number;
|
||||
|
||||
export interface ModelTransformProps {
|
||||
position?: Vector3Tuple;
|
||||
rotation?: Vector3Tuple;
|
||||
scale?: Vector3Scale;
|
||||
}
|
||||
|
||||
export type ColliderShape = "cuboid" | "ball" | "hull";
|
||||
|
||||
export type OctreeReadyHandler = (octree: Octree) => void;
|
||||
|
||||
@@ -9,6 +9,10 @@ interface StoredDebugControls {
|
||||
sceneMode: SceneMode;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function isCameraMode(value: unknown): value is CameraMode {
|
||||
return value === "player" || value === "debug";
|
||||
}
|
||||
@@ -22,7 +26,8 @@ function getStoredDebugControls(): Partial<StoredDebugControls> {
|
||||
const rawValue = window.localStorage.getItem(DEBUG_CONTROLS_STORAGE_KEY);
|
||||
if (!rawValue) return {};
|
||||
|
||||
const parsedValue = JSON.parse(rawValue) as Partial<StoredDebugControls>;
|
||||
const parsedValue: unknown = JSON.parse(rawValue);
|
||||
if (!isRecord(parsedValue)) return {};
|
||||
|
||||
return {
|
||||
...(isCameraMode(parsedValue.cameraMode)
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import type { MapNode } from "@/types/editor/editor";
|
||||
import type { MapNode } from "../../types/editor/editor";
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function isVector3Tuple(value: unknown): value is [number, number, number] {
|
||||
return (
|
||||
@@ -8,18 +12,17 @@ function isVector3Tuple(value: unknown): value is [number, number, number] {
|
||||
);
|
||||
}
|
||||
|
||||
export function isMapNode(value: unknown): value is MapNode {
|
||||
if (typeof value !== "object" || value === null) {
|
||||
function isMapNode(value: unknown): value is MapNode {
|
||||
if (!isRecord(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const node = value as Record<string, unknown>;
|
||||
return (
|
||||
typeof node.name === "string" &&
|
||||
typeof node.type === "string" &&
|
||||
isVector3Tuple(node.position) &&
|
||||
isVector3Tuple(node.rotation) &&
|
||||
isVector3Tuple(node.scale)
|
||||
typeof value.name === "string" &&
|
||||
typeof value.type === "string" &&
|
||||
isVector3Tuple(value.position) &&
|
||||
isVector3Tuple(value.rotation) &&
|
||||
isVector3Tuple(value.scale)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,10 @@ export class ExplodedModel {
|
||||
}
|
||||
|
||||
private createParts(model: THREE.Object3D): ExplodedPart[] {
|
||||
const root = model.children.length === 1 ? model.children[0] : model;
|
||||
const root =
|
||||
model.children.length === 1 && model.children[0]
|
||||
? model.children[0]
|
||||
: model;
|
||||
const directChildren = root.children.filter((child) => hasMesh(child));
|
||||
const sourceObjects =
|
||||
directChildren.length > 1 ? directChildren : getMeshes(root);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { Vector3Scale, Vector3Tuple } from "@/types/three/three";
|
||||
|
||||
export function toVector3Scale(scale: Vector3Scale): Vector3Tuple {
|
||||
return typeof scale === "number" ? [scale, scale, scale] : scale;
|
||||
}
|
||||
+9
-24
@@ -1,7 +1,8 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { Component, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Component, useEffect, useRef, useState } from "react";
|
||||
import { useGLTF } from "@react-three/drei";
|
||||
import * as THREE from "three";
|
||||
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
||||
import { useOctreeGraphNode } from "@/hooks/three/useOctreeGraphNode";
|
||||
import { loadMapSceneData } from "@/utils/map/loadMapSceneData";
|
||||
import type { MapNode } from "@/types/editor/editor";
|
||||
@@ -14,7 +15,6 @@ interface LoadedMapNode {
|
||||
|
||||
interface ErrorBoundaryProps {
|
||||
children: ReactNode;
|
||||
fallback?: ReactNode;
|
||||
}
|
||||
|
||||
interface ErrorBoundaryState {
|
||||
@@ -40,7 +40,7 @@ class ModelErrorBoundary extends Component<
|
||||
|
||||
render(): ReactNode {
|
||||
if (this.state.hasError) {
|
||||
return this.props.fallback ?? null;
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
@@ -53,7 +53,6 @@ interface GameMapProps {
|
||||
|
||||
export function GameMap({ onOctreeReady }: GameMapProps): React.JSX.Element {
|
||||
const [mapNodes, setMapNodes] = useState<LoadedMapNode[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const groupRef = useRef<THREE.Group>(null);
|
||||
|
||||
useOctreeGraphNode(groupRef, onOctreeReady, mapNodes.length);
|
||||
@@ -64,7 +63,6 @@ export function GameMap({ onOctreeReady }: GameMapProps): React.JSX.Element {
|
||||
const sceneData = await loadMapSceneData();
|
||||
if (!sceneData) {
|
||||
console.warn("map.json not found");
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -84,8 +82,6 @@ export function GameMap({ onOctreeReady }: GameMapProps): React.JSX.Element {
|
||||
setMapNodes(loadedMapNodes);
|
||||
} catch (error) {
|
||||
console.error("Error loading map:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -94,12 +90,11 @@ export function GameMap({ onOctreeReady }: GameMapProps): React.JSX.Element {
|
||||
|
||||
return (
|
||||
<group ref={groupRef}>
|
||||
{!isLoading &&
|
||||
mapNodes.map((mapNode, index) => (
|
||||
<ModelErrorBoundary key={index}>
|
||||
<ModelInstance node={mapNode.node} modelUrl={mapNode.modelUrl} />
|
||||
</ModelErrorBoundary>
|
||||
))}
|
||||
{mapNodes.map((mapNode, index) => (
|
||||
<ModelErrorBoundary key={index}>
|
||||
<ModelInstance node={mapNode.node} modelUrl={mapNode.modelUrl} />
|
||||
</ModelErrorBoundary>
|
||||
))}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
@@ -111,22 +106,12 @@ function ModelInstance({
|
||||
node: MapNode;
|
||||
modelUrl: string;
|
||||
}): React.JSX.Element {
|
||||
const groupRef = useRef<THREE.Group>(null);
|
||||
const { scene } = useGLTF(modelUrl);
|
||||
const sceneInstance = useMemo(() => scene.clone(true), [scene]);
|
||||
const sceneInstance = useClonedObject(scene);
|
||||
const { position, rotation, scale } = node;
|
||||
|
||||
useEffect(() => {
|
||||
if (groupRef.current) {
|
||||
groupRef.current.position.set(...position);
|
||||
groupRef.current.rotation.set(...rotation);
|
||||
groupRef.current.scale.set(...scale);
|
||||
}
|
||||
}, [position, rotation, scale]);
|
||||
|
||||
return (
|
||||
<primitive
|
||||
ref={groupRef}
|
||||
object={sceneInstance}
|
||||
position={position}
|
||||
rotation={rotation}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useRef } from "react";
|
||||
import * as THREE from "three";
|
||||
import { Physics, RigidBody, CuboidCollider } from "@react-three/rapier";
|
||||
import { RepairGameZone } from "@/components/three/gameplay/repairGame/RepairGameZone";
|
||||
import { RepairGameZone } from "@/components/three/gameplay/RepairGameZone";
|
||||
import { GrabbableObject } from "@/components/three/interaction/GrabbableObject";
|
||||
import { TriggerObject } from "@/components/three/interaction/TriggerObject";
|
||||
import {
|
||||
|
||||
Reference in New Issue
Block a user