Skip to content

Commit 8278bda

Browse files
leonardcserbchapuis
authored andcommitted
refactor(web): GLTF viewer
1 parent c58aa43 commit 8278bda

9 files changed

Lines changed: 704 additions & 641 deletions

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { useThree } from "@react-three/fiber";
2+
import { useEffect, useRef } from "react";
3+
4+
import type { CameraControllerProps } from "./types";
5+
import { positionCameraForScene } from "./utils";
6+
7+
// Hook to monitor scene ref and position camera when available
8+
function useSceneCamera(
9+
sceneRef: React.RefObject<import("three").Group | null>,
10+
trigger: number,
11+
viewportAspect: number = 1.0
12+
) {
13+
const { camera, size } = useThree();
14+
const lastSceneUUID = useRef<string | null>(null);
15+
16+
useEffect(() => {
17+
const scene = sceneRef.current;
18+
19+
// Calculate actual aspect ratio from Three.js size if available, fallback to provided aspect
20+
const actualAspect =
21+
size.width && size.height ? size.width / size.height : viewportAspect;
22+
23+
if (!scene || !camera) {
24+
return;
25+
}
26+
27+
// Only position camera if scene has changed (different UUID)
28+
if (scene.uuid === lastSceneUUID.current) {
29+
return;
30+
}
31+
lastSceneUUID.current = scene.uuid;
32+
positionCameraForScene(scene, camera as any, actualAspect);
33+
}, [trigger, viewportAspect, size.width, size.height, camera, sceneRef]); // Re-run when trigger, aspect ratio, or size changes
34+
35+
return null;
36+
}
37+
38+
// Simple camera controller that monitors scene ref
39+
export function CameraController({
40+
sceneRef,
41+
trigger,
42+
viewportAspect,
43+
}: CameraControllerProps) {
44+
useSceneCamera(sceneRef, trigger, viewportAspect);
45+
return null;
46+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { Component, type ErrorInfo, type ReactNode } from "react";
2+
3+
interface GltfViewerErrorBoundaryProps {
4+
children: ReactNode;
5+
onError: (error: Error, errorInfo: ErrorInfo) => void;
6+
}
7+
8+
interface GltfViewerErrorBoundaryState {
9+
hasError: boolean;
10+
}
11+
12+
export class GltfViewerErrorBoundary extends Component<
13+
GltfViewerErrorBoundaryProps,
14+
GltfViewerErrorBoundaryState
15+
> {
16+
constructor(props: GltfViewerErrorBoundaryProps) {
17+
super(props);
18+
this.state = { hasError: false };
19+
}
20+
21+
static getDerivedStateFromError(): GltfViewerErrorBoundaryState {
22+
return { hasError: true };
23+
}
24+
25+
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
26+
console.error("=== Enhanced 3D Error Boundary ===");
27+
console.error("Error message:", error.message);
28+
console.error("Error stack:", error.stack);
29+
console.error("Component stack:", errorInfo.componentStack);
30+
console.error("Error name:", error.name);
31+
console.error("Full error object:", error);
32+
console.error("Full errorInfo object:", errorInfo);
33+
34+
// Check for specific Three.js/glTF related errors
35+
const isThreeJSError =
36+
error.message.includes("THREE") ||
37+
error.message.includes("glTF") ||
38+
error.message.includes("GLTFLoader") ||
39+
error.stack?.includes("three");
40+
console.error("Is Three.js related error:", isThreeJSError);
41+
42+
this.props.onError(error, errorInfo);
43+
}
44+
45+
render() {
46+
if (this.state.hasError) {
47+
return (
48+
<div className="flex items-center justify-center w-full h-full bg-red-50 dark:bg-red-900 rounded border border-red-200 dark:border-red-800">
49+
<div className="flex flex-col items-center space-y-2 text-center p-4">
50+
<div className="w-8 h-8 text-red-500 dark:text-red-400">
51+
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor">
52+
<path
53+
strokeLinecap="round"
54+
strokeLinejoin="round"
55+
strokeWidth={2}
56+
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.962-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"
57+
/>
58+
</svg>
59+
</div>
60+
<span className="text-sm text-red-600 dark:text-red-400">
61+
3D Rendering Error
62+
</span>
63+
<span className="text-xs text-red-500 dark:text-red-300">
64+
WebGL may not be supported in your browser
65+
</span>
66+
</div>
67+
</div>
68+
);
69+
}
70+
71+
return this.props.children;
72+
}
73+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { useGLTF } from "@react-three/drei";
2+
import { useMemo } from "react";
3+
import { LoadingManager } from "three";
4+
5+
// Custom hook for authenticated GLTF loading
6+
export function useAuthenticatedGLTF(url: string) {
7+
const extendLoader = useMemo(() => {
8+
return (loader: any) => {
9+
console.log("Extending GLTFLoader for authenticated requests");
10+
11+
// Set withCredentials to include cookies/auth headers
12+
if (loader.manager && loader.manager.itemStart) {
13+
// Create a custom loading manager that includes credentials
14+
const manager = new LoadingManager();
15+
16+
// Override the manager's load method to add credentials
17+
const originalResolveURL = manager.resolveURL?.bind(manager);
18+
if (originalResolveURL) {
19+
manager.resolveURL = function (url: string) {
20+
return originalResolveURL(url);
21+
};
22+
}
23+
24+
loader.manager = manager;
25+
}
26+
27+
// If the loader has a setWithCredentials method, use it
28+
if (typeof loader.setWithCredentials === "function") {
29+
loader.setWithCredentials(true);
30+
console.log("Set withCredentials=true on GLTFLoader");
31+
}
32+
33+
// If the loader supports request headers (some loaders do)
34+
if (typeof loader.setRequestHeader === "function") {
35+
// Add any additional headers if needed
36+
console.log("GLTFLoader supports setRequestHeader");
37+
}
38+
39+
// Override the load method to ensure credentials
40+
const originalLoad = loader.load.bind(loader);
41+
loader.load = function (
42+
url: string,
43+
onLoad?: any,
44+
onProgress?: any,
45+
onError?: any
46+
) {
47+
console.log("Loading glTF with authentication:", url);
48+
49+
// Try to use fetch directly with credentials for better control
50+
fetch(url, {
51+
credentials: "include",
52+
method: "GET",
53+
})
54+
.then((response) => {
55+
if (!response.ok) {
56+
throw new Error(
57+
`HTTP ${response.status}: ${response.statusText}`
58+
);
59+
}
60+
return response.arrayBuffer();
61+
})
62+
.then((buffer) => {
63+
// Create a blob URL for the Three.js loader to use
64+
const blob = new Blob([buffer], { type: "model/gltf-binary" });
65+
const blobUrl = URL.createObjectURL(blob);
66+
67+
// Load using the blob URL (no auth needed)
68+
return originalLoad(
69+
blobUrl,
70+
(gltf: any) => {
71+
URL.revokeObjectURL(blobUrl); // Clean up
72+
if (onLoad) onLoad(gltf);
73+
},
74+
onProgress,
75+
onError
76+
);
77+
})
78+
.catch((error) => {
79+
console.error("Authenticated glTF fetch failed:", error);
80+
if (onError) onError(error);
81+
});
82+
};
83+
};
84+
}, []);
85+
86+
return useGLTF(url, false, false, extendLoader);
87+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { useEffect, useMemo, useRef } from "react";
2+
import type { Group } from "three";
3+
import * as THREE from "three";
4+
5+
import { useAuthenticatedGLTF } from "./gltf-loader";
6+
import type { GltfModelProps } from "./types";
7+
import { cloneSceneWithMaterials } from "./utils";
8+
9+
export function GltfModel({ url, onSceneLoad, wireframeMode }: GltfModelProps) {
10+
const groupRef = useRef<Group>(null);
11+
const { scene } = useAuthenticatedGLTF(url);
12+
13+
// Clone the scene once per loaded source to isolate materials/textures
14+
const clonedScene = useMemo(() => {
15+
if (!scene) return null;
16+
return cloneSceneWithMaterials(scene as any);
17+
}, [scene]);
18+
19+
useEffect(() => {
20+
if (clonedScene) {
21+
// Fix material issues
22+
clonedScene.traverse((child) => {
23+
if ((child as any).isMesh) {
24+
const mesh = child as any;
25+
if (mesh.material) {
26+
const mat = mesh.material as THREE.Material;
27+
28+
mat.visible = true;
29+
(mat as any).transparent = true;
30+
31+
if ("wireframe" in mat) {
32+
(mat as any).wireframe = wireframeMode || false;
33+
}
34+
35+
if (mat.type === "MeshStandardMaterial") {
36+
const stdMat = mat as THREE.MeshStandardMaterial;
37+
stdMat.needsUpdate = true;
38+
}
39+
}
40+
}
41+
});
42+
43+
if (onSceneLoad) {
44+
onSceneLoad(clonedScene as any);
45+
}
46+
}
47+
}, [clonedScene, onSceneLoad, wireframeMode]);
48+
49+
if (!clonedScene) {
50+
return null;
51+
}
52+
53+
return <primitive ref={groupRef} object={clonedScene} />;
54+
}

0 commit comments

Comments
 (0)