forked from hazem/yslootahrobotics
feat: add AttireErrorBoundary component for improved error handling in attire model loading
This commit is contained in:
parent
ed6ebcc8af
commit
6dc705b332
@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useRef, useMemo, useEffect, Suspense, useState } from 'react';
|
import { useRef, useMemo, useEffect, Suspense, useState, Component, type ReactNode } from 'react';
|
||||||
import { useGLTF } from '@react-three/drei';
|
import { useGLTF } from '@react-three/drei';
|
||||||
import { useFrame } from '@react-three/fiber';
|
import { useFrame } from '@react-three/fiber';
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
@ -24,6 +24,30 @@ function buildAttireGlbMap(personas: { id: string; modelPath?: string }[]): Reco
|
|||||||
return { ...STATIC_ATTIRE_GLB, ...dynamic }; // uploaded GLBs override static
|
return { ...STATIC_ATTIRE_GLB, ...dynamic }; // uploaded GLBs override static
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AttireErrorBoundaryProps {
|
||||||
|
children: ReactNode;
|
||||||
|
onError: () => void;
|
||||||
|
}
|
||||||
|
interface AttireErrorBoundaryState { hasError: boolean; }
|
||||||
|
|
||||||
|
class AttireErrorBoundary extends Component<AttireErrorBoundaryProps, AttireErrorBoundaryState> {
|
||||||
|
constructor(props: AttireErrorBoundaryProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = { hasError: false };
|
||||||
|
}
|
||||||
|
static getDerivedStateFromError(): AttireErrorBoundaryState {
|
||||||
|
return { hasError: true };
|
||||||
|
}
|
||||||
|
componentDidCatch(error: Error) {
|
||||||
|
console.warn('[AttireModel] Failed to load GLB, falling back to base robot:', error.message);
|
||||||
|
this.props.onError();
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) return null;
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function easeInOutCubic(t: number): number {
|
function easeInOutCubic(t: number): number {
|
||||||
return t < 0.5
|
return t < 0.5
|
||||||
? 4 * t * t * t
|
? 4 * t * t * t
|
||||||
@ -205,13 +229,14 @@ export function RobotModel({ onError }: RobotModelProps) {
|
|||||||
<primitive object={processedScene} />
|
<primitive object={processedScene} />
|
||||||
|
|
||||||
{attireGlbPath && (
|
{attireGlbPath && (
|
||||||
|
<AttireErrorBoundary key={attireGlbPath} onError={() => { setDisplayedAttire('none'); setAttireReady(false); }}>
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<AttireModel
|
<AttireModel
|
||||||
key={attireGlbPath}
|
|
||||||
glbPath={attireGlbPath}
|
glbPath={attireGlbPath}
|
||||||
onLoaded={handleAttireLoaded}
|
onLoaded={handleAttireLoaded}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
</AttireErrorBoundary>
|
||||||
)}
|
)}
|
||||||
</group>
|
</group>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -49,18 +49,6 @@ export const DEFAULT_PERSONAS: PersonaOption[] = [
|
|||||||
description: 'Professional navy suit',
|
description: 'Professional navy suit',
|
||||||
colors: { torso: '#1e293b', legs: '#1e293b' },
|
colors: { torso: '#1e293b', legs: '#1e293b' },
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'robot-doctor',
|
|
||||||
label: 'Robot Doctor',
|
|
||||||
description: 'Medical doctor attire',
|
|
||||||
colors: { torso: '#ffffff', legs: '#ffffff' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'security-guard',
|
|
||||||
label: 'Security Guard',
|
|
||||||
description: 'Security personnel uniform',
|
|
||||||
colors: { torso: '#1c1c1c', legs: '#1c1c1c' },
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const STORAGE_KEY = 'lootah-personas';
|
const STORAGE_KEY = 'lootah-personas';
|
||||||
@ -138,11 +126,13 @@ export const personaStore = createStore<PersonaStore>((set, get) => ({
|
|||||||
hydrate: () => {
|
hydrate: () => {
|
||||||
const stored = loadFromStorage();
|
const stored = loadFromStorage();
|
||||||
if (stored && stored.length > 0) {
|
if (stored && stored.length > 0) {
|
||||||
|
// Only re-inject truly built-in personas (those still in DEFAULT_PERSONAS) if missing.
|
||||||
|
// Dynamic/uploaded personas that were deleted via the dashboard must NOT be re-added.
|
||||||
const storedIds = new Set(stored.map((s) => s.id));
|
const storedIds = new Set(stored.map((s) => s.id));
|
||||||
const missing = DEFAULT_PERSONAS.filter((d) => !storedIds.has(d.id));
|
const missing = DEFAULT_PERSONAS.filter((d) => !storedIds.has(d.id));
|
||||||
set({ personas: [...stored, ...missing], isHydrated: true });
|
set({ personas: [...stored, ...missing], isHydrated: true });
|
||||||
} else {
|
} else {
|
||||||
set({ isHydrated: true });
|
set({ personas: [...DEFAULT_PERSONAS], isHydrated: true });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user