🚀 Auto-deploy: melhoria no snap e medição AR em 22/05/2026 20:51:44
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "vite_react_shadcn_ts",
|
"name": "vite_react_shadcn_ts",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.2",
|
"version": "1.0.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Suspense, useRef, useMemo, useEffect, type ReactNode } from 'react';
|
import { Suspense, useRef, useMemo, useEffect, type ReactNode, useState } from 'react';
|
||||||
import { Canvas, useThree, useFrame } from '@react-three/fiber';
|
import { Canvas, useThree, useFrame } from '@react-three/fiber';
|
||||||
import { OrbitControls, useGLTF, Grid, Html, Line } from '@react-three/drei';
|
import { OrbitControls, useGLTF, Grid, Html, Line } from '@react-three/drei';
|
||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
@@ -6,6 +6,8 @@ import * as THREE from 'three';
|
|||||||
import { useModelStore, type SceneModel } from '@/stores/useModelStore';
|
import { useModelStore, type SceneModel } from '@/stores/useModelStore';
|
||||||
import { findNearestVertex, detectHoleAtFace, findNearestEdgeSegment, detectCircularEdgeAtPoint } from './SmartMeasure';
|
import { findNearestVertex, detectHoleAtFace, findNearestEdgeSegment, detectCircularEdgeAtPoint } from './SmartMeasure';
|
||||||
import { registerModelLocalGroup, unregisterModelLocalGroup } from '@/lib/modelTransforms';
|
import { registerModelLocalGroup, unregisterModelLocalGroup } from '@/lib/modelTransforms';
|
||||||
|
import { parseIFCtoThree } from '@/lib/convertIFC';
|
||||||
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
||||||
|
|
||||||
interface ModelViewerProps {
|
interface ModelViewerProps {
|
||||||
url?: string; // legacy, ignored — uses store.models
|
url?: string; // legacy, ignored — uses store.models
|
||||||
@@ -73,9 +75,36 @@ function LoadingFallback() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function GLBModel({ sceneModel, isActive }: { sceneModel: SceneModel; isActive: boolean }) {
|
function GLBModel({ sceneModel, isActive }: { sceneModel: SceneModel; isActive: boolean }) {
|
||||||
const { scene: rawScene } = useGLTF(sceneModel.url);
|
const [rawScene, setRawScene] = useState<THREE.Object3D | null>(null);
|
||||||
// Clone scene per instance so multiple GLBModels with same url don't share materials/transforms
|
const scene = useMemo(() => rawScene ? rawScene.clone(true) : null, [rawScene]);
|
||||||
const scene = useMemo(() => rawScene.clone(true), [rawScene]);
|
|
||||||
|
useEffect(() => {
|
||||||
|
let active = true;
|
||||||
|
const isIfc = sceneModel.fileName.toLowerCase().endsWith('.ifc');
|
||||||
|
if (isIfc) {
|
||||||
|
fetch(sceneModel.url)
|
||||||
|
.then((res) => res.arrayBuffer())
|
||||||
|
.then((buf) => parseIFCtoThree(buf))
|
||||||
|
.then((threeScene) => {
|
||||||
|
if (active) setRawScene(threeScene);
|
||||||
|
})
|
||||||
|
.catch((err) => console.error('[GLBModel] IFC parsing error', err));
|
||||||
|
} else {
|
||||||
|
const loader = new GLTFLoader();
|
||||||
|
loader.load(
|
||||||
|
sceneModel.url,
|
||||||
|
(gltf) => {
|
||||||
|
if (active) setRawScene(gltf.scene);
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
(err) => console.error('[GLBModel] GLTF loading error', err)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
|
}, [sceneModel.url, sceneModel.fileName]);
|
||||||
|
|
||||||
const ref = useRef<THREE.Group>(null);
|
const ref = useRef<THREE.Group>(null);
|
||||||
const opacity = useModelStore((s) => s.opacity);
|
const opacity = useModelStore((s) => s.opacity);
|
||||||
const renderMode = useModelStore((s) => s.renderMode);
|
const renderMode = useModelStore((s) => s.renderMode);
|
||||||
@@ -88,6 +117,7 @@ function GLBModel({ sceneModel, isActive }: { sceneModel: SceneModel; isActive:
|
|||||||
const fineTuning = sceneModel.fineTuning;
|
const fineTuning = sceneModel.fineTuning;
|
||||||
|
|
||||||
const modelInfo = useMemo(() => {
|
const modelInfo = useMemo(() => {
|
||||||
|
if (!scene) return { size: new THREE.Vector3(), center: new THREE.Vector3() };
|
||||||
const box = new THREE.Box3().setFromObject(scene);
|
const box = new THREE.Box3().setFromObject(scene);
|
||||||
const size = new THREE.Vector3();
|
const size = new THREE.Vector3();
|
||||||
const center = new THREE.Vector3();
|
const center = new THREE.Vector3();
|
||||||
@@ -99,6 +129,7 @@ function GLBModel({ sceneModel, isActive }: { sceneModel: SceneModel; isActive:
|
|||||||
const originalColors = useRef<Map<THREE.Material, THREE.Color>>(new Map());
|
const originalColors = useRef<Map<THREE.Material, THREE.Color>>(new Map());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!scene) return;
|
||||||
const hasRejected = checklist.some(i => i.status === 'rejected');
|
const hasRejected = checklist.some(i => i.status === 'rejected');
|
||||||
const allApproved = checklist.every(i => i.status === 'approved');
|
const allApproved = checklist.every(i => i.status === 'approved');
|
||||||
|
|
||||||
@@ -174,6 +205,7 @@ function GLBModel({ sceneModel, isActive }: { sceneModel: SceneModel; isActive:
|
|||||||
const renderFactor = scaleRatio?.factor ?? 1;
|
const renderFactor = scaleRatio?.factor ?? 1;
|
||||||
|
|
||||||
if (!sceneModel.visible) return null;
|
if (!sceneModel.visible) return null;
|
||||||
|
if (!scene) return null;
|
||||||
|
|
||||||
// Determine dominant local axis of the model (longest bbox side) for axial roll
|
// Determine dominant local axis of the model (longest bbox side) for axial roll
|
||||||
const dominantAxis: 'x' | 'y' | 'z' =
|
const dominantAxis: 'x' | 'y' | 'z' =
|
||||||
|
|||||||
@@ -7,10 +7,7 @@ const WASM_PATH = 'https://unpkg.com/web-ifc@0.0.57/';
|
|||||||
/**
|
/**
|
||||||
* Convert an IFC file (ArrayBuffer) into a GLB Blob using web-ifc + GLTFExporter.
|
* Convert an IFC file (ArrayBuffer) into a GLB Blob using web-ifc + GLTFExporter.
|
||||||
*/
|
*/
|
||||||
export async function convertIFCtoGLB(
|
export async function parseIFCtoThree(buffer: ArrayBuffer): Promise<THREE.Scene> {
|
||||||
buffer: ArrayBuffer,
|
|
||||||
fileName: string
|
|
||||||
): Promise<{ blob: Blob; fileName: string; fileSize: number }> {
|
|
||||||
const ifcApi = new WebIFC.IfcAPI();
|
const ifcApi = new WebIFC.IfcAPI();
|
||||||
ifcApi.SetWasmPath(WASM_PATH, true);
|
ifcApi.SetWasmPath(WASM_PATH, true);
|
||||||
await ifcApi.Init();
|
await ifcApi.Init();
|
||||||
@@ -92,6 +89,14 @@ export async function convertIFCtoGLB(
|
|||||||
});
|
});
|
||||||
|
|
||||||
ifcApi.CloseModel(modelID);
|
ifcApi.CloseModel(modelID);
|
||||||
|
return scene;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function convertIFCtoGLB(
|
||||||
|
buffer: ArrayBuffer,
|
||||||
|
fileName: string
|
||||||
|
): Promise<{ blob: Blob; fileName: string; fileSize: number }> {
|
||||||
|
const scene = await parseIFCtoThree(buffer);
|
||||||
|
|
||||||
// Export to GLB
|
// Export to GLB
|
||||||
const exporter = new GLTFExporter();
|
const exporter = new GLTFExporter();
|
||||||
@@ -108,7 +113,6 @@ export async function convertIFCtoGLB(
|
|||||||
const blob = new Blob([glb], { type: 'model/gltf-binary' });
|
const blob = new Blob([glb], { type: 'model/gltf-binary' });
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
materials.forEach((m) => m.dispose());
|
|
||||||
scene.traverse((child) => {
|
scene.traverse((child) => {
|
||||||
if (child instanceof THREE.Mesh) {
|
if (child instanceof THREE.Mesh) {
|
||||||
child.geometry.dispose();
|
child.geometry.dispose();
|
||||||
|
|||||||
+64
-8
@@ -11,6 +11,8 @@ import { CloudLoader } from '@/components/CloudLoader';
|
|||||||
import { SceneModelList } from '@/components/SceneModelList';
|
import { SceneModelList } from '@/components/SceneModelList';
|
||||||
import { RecentFilesList } from '@/components/RecentFilesList';
|
import { RecentFilesList } from '@/components/RecentFilesList';
|
||||||
import { addRecentFile } from '@/lib/recentFiles';
|
import { addRecentFile } from '@/lib/recentFiles';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
|
||||||
const Index = () => {
|
const Index = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -21,6 +23,16 @@ const Index = () => {
|
|||||||
const [converting, setConverting] = useState(false);
|
const [converting, setConverting] = useState(false);
|
||||||
const { model, addModel, models, maxModels, xrSupported, setXrSupported } = useModelStore();
|
const { model, addModel, models, maxModels, xrSupported, setXrSupported } = useModelStore();
|
||||||
|
|
||||||
|
const [ifcConvert, setIfcConvert] = useState(() => {
|
||||||
|
const saved = localStorage.getItem('tsxr_ifc_convert');
|
||||||
|
return saved !== 'false';
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleToggleIfcConvert = (checked: boolean) => {
|
||||||
|
setIfcConvert(checked);
|
||||||
|
localStorage.setItem('tsxr_ifc_convert', checked ? 'true' : 'false');
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (navigator.xr) {
|
if (navigator.xr) {
|
||||||
navigator.xr.isSessionSupported('immersive-ar').then(setXrSupported).catch(() => setXrSupported(false));
|
navigator.xr.isSessionSupported('immersive-ar').then(setXrSupported).catch(() => setXrSupported(false));
|
||||||
@@ -74,8 +86,19 @@ const Index = () => {
|
|||||||
if (!sigCheck.ok) {
|
if (!sigCheck.ok) {
|
||||||
toast.error(sigCheck.reason!);
|
toast.error(sigCheck.reason!);
|
||||||
if (sigCheck.detail) console.warn('[validateModelFile]', sigCheck.detail);
|
if (sigCheck.detail) console.warn('[validateModelFile]', sigCheck.detail);
|
||||||
|
setConverting(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ext === 'ifc' && !ifcConvert) {
|
||||||
|
const url = URL.createObjectURL(file);
|
||||||
|
addModel({ fileName: file.name, fileSize: file.size, url });
|
||||||
|
addRecentFile(file.name, file).catch(() => {});
|
||||||
|
toast.success(`Modelo "${file.name}" carregado de forma nativa!`);
|
||||||
|
setConverting(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let result;
|
let result;
|
||||||
if (ext === 'ifc') {
|
if (ext === 'ifc') {
|
||||||
result = await convertIFCtoGLB(buffer, file.name);
|
result = await convertIFCtoGLB(buffer, file.name);
|
||||||
@@ -93,7 +116,7 @@ const Index = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setConverting(false);
|
setConverting(false);
|
||||||
}
|
}
|
||||||
}, [addModel]);
|
}, [addModel, ifcConvert]);
|
||||||
|
|
||||||
// Carregar da Nuvem agora é encapsulado em <CloudLoader />
|
// Carregar da Nuvem agora é encapsulado em <CloudLoader />
|
||||||
|
|
||||||
@@ -102,6 +125,14 @@ const Index = () => {
|
|||||||
try {
|
try {
|
||||||
const response = await fetch('/models/demo250hp.ifc');
|
const response = await fetch('/models/demo250hp.ifc');
|
||||||
const buffer = await response.arrayBuffer();
|
const buffer = await response.arrayBuffer();
|
||||||
|
if (!ifcConvert) {
|
||||||
|
const blob = new Blob([buffer], { type: 'application/octet-stream' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
addModel({ fileName: 'demo250hp.ifc', fileSize: blob.size, url });
|
||||||
|
addRecentFile('demo250hp.ifc', blob).catch(() => {});
|
||||||
|
toast.success('Modelo demo "250 HP" (IFC nativo) carregado!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const result = await convertIFCtoGLB(buffer, 'demo250hp.ifc');
|
const result = await convertIFCtoGLB(buffer, 'demo250hp.ifc');
|
||||||
const url = URL.createObjectURL(result.blob);
|
const url = URL.createObjectURL(result.blob);
|
||||||
addModel({ fileName: result.fileName, fileSize: result.fileSize, url });
|
addModel({ fileName: result.fileName, fileSize: result.fileSize, url });
|
||||||
@@ -109,17 +140,25 @@ const Index = () => {
|
|||||||
toast.success('Modelo demo "250 HP" (IFC) carregado!');
|
toast.success('Modelo demo "250 HP" (IFC) carregado!');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
toast.error('Falha ao converter modelo demo IFC');
|
toast.error('Falha ao processar modelo demo IFC');
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingDemoIFC(false);
|
setLoadingDemoIFC(false);
|
||||||
}
|
}
|
||||||
}, [addModel]);
|
}, [addModel, ifcConvert]);
|
||||||
|
|
||||||
const handleLoadDemoCotovelo = useCallback(async () => {
|
const handleLoadDemoCotovelo = useCallback(async () => {
|
||||||
setLoadingDemoCotovelo(true);
|
setLoadingDemoCotovelo(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/models/B126-COTOVELO.ifc');
|
const response = await fetch('/models/B126-COTOVELO.ifc');
|
||||||
const buffer = await response.arrayBuffer();
|
const buffer = await response.arrayBuffer();
|
||||||
|
if (!ifcConvert) {
|
||||||
|
const blob = new Blob([buffer], { type: 'application/octet-stream' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
addModel({ fileName: 'B126-COTOVELO.ifc', fileSize: blob.size, url });
|
||||||
|
addRecentFile('B126-COTOVELO.ifc', blob).catch(() => {});
|
||||||
|
toast.success('Modelo demo "B126 Cotovelo" (IFC nativo) carregado!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const result = await convertIFCtoGLB(buffer, 'B126-COTOVELO.ifc');
|
const result = await convertIFCtoGLB(buffer, 'B126-COTOVELO.ifc');
|
||||||
const url = URL.createObjectURL(result.blob);
|
const url = URL.createObjectURL(result.blob);
|
||||||
addModel({ fileName: result.fileName, fileSize: result.fileSize, url });
|
addModel({ fileName: result.fileName, fileSize: result.fileSize, url });
|
||||||
@@ -127,15 +166,15 @@ const Index = () => {
|
|||||||
toast.success('Modelo demo "B126 Cotovelo" (IFC) carregado!');
|
toast.success('Modelo demo "B126 Cotovelo" (IFC) carregado!');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
toast.error('Falha ao converter modelo demo Cotovelo');
|
toast.error('Falha ao processar modelo demo Cotovelo');
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingDemoCotovelo(false);
|
setLoadingDemoCotovelo(false);
|
||||||
}
|
}
|
||||||
}, [addModel]);
|
}, [addModel, ifcConvert]);
|
||||||
|
|
||||||
const handleEnterViewer = () => {
|
const handleEnterViewer = () => {
|
||||||
if (!model) {
|
if (!model) {
|
||||||
toast.error('Importe um modelo GLB primeiro');
|
toast.error('Importe um modelo 3D primeiro');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
navigate('/viewer');
|
navigate('/viewer');
|
||||||
@@ -175,11 +214,28 @@ const Index = () => {
|
|||||||
|
|
||||||
{converting ? <Loader2 className="h-8 w-8 animate-spin" /> : <Upload className="h-8 w-8" />}
|
{converting ? <Loader2 className="h-8 w-8 animate-spin" /> : <Upload className="h-8 w-8" />}
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-sm font-semibold">{converting ? 'Convertendo modelo…' : 'Importar Modelo 3D'}</p>
|
<p className="text-sm font-semibold">{converting ? 'Carregando modelo…' : 'Importar Modelo 3D'}</p>
|
||||||
<p className="text-xs text-muted-foreground">GLB · OBJ · STL · IFC — Escala 1:1</p>
|
<p className="text-xs text-muted-foreground">GLB · OBJ · STL · IFC — Escala 1:1</p>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{/* Toggle IFC Conversion */}
|
||||||
|
<div className="flex items-center justify-between rounded-lg border border-border bg-card/50 p-3.5">
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
|
<Label htmlFor="ifc-convert-toggle" className="text-xs font-semibold cursor-pointer text-foreground">
|
||||||
|
Conversão IFC ➔ GLB
|
||||||
|
</Label>
|
||||||
|
<p className="text-[10px] text-muted-foreground">
|
||||||
|
{ifcConvert ? 'Otimizado para AR (conversão automática)' : 'Modo nativo (carregamento bruto IFC)'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="ifc-convert-toggle"
|
||||||
|
checked={ifcConvert}
|
||||||
|
onCheckedChange={handleToggleIfcConvert}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Demo buttons */}
|
{/* Demo buttons */}
|
||||||
<div className="grid grid-cols-3 gap-2">
|
<div className="grid grid-cols-3 gap-2">
|
||||||
<CloudLoader />
|
<CloudLoader />
|
||||||
@@ -254,7 +310,7 @@ const Index = () => {
|
|||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<p className="mt-16 text-center font-mono text-xs text-muted-foreground/50">
|
<p className="mt-16 text-center font-mono text-xs text-muted-foreground/50">
|
||||||
TrackSteelXR v1.02 — Q.C. Inspection
|
TrackSteelXR v1.03 — Q.C. Inspection
|
||||||
</p>
|
</p>
|
||||||
</div>);
|
</div>);
|
||||||
|
|
||||||
|
|||||||
+35
-2
@@ -23,6 +23,8 @@ import { useDevKit } from '@/devkit/useDevKit';
|
|||||||
import { DevPanel } from '@/devkit/DevPanel';
|
import { DevPanel } from '@/devkit/DevPanel';
|
||||||
import { FakeControllers } from '@/devkit/FakeControllers';
|
import { FakeControllers } from '@/devkit/FakeControllers';
|
||||||
import { VisibilityApplier } from '@/components/three/ModelViewer';
|
import { VisibilityApplier } from '@/components/three/ModelViewer';
|
||||||
|
import { parseIFCtoThree } from '@/lib/convertIFC';
|
||||||
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
||||||
|
|
||||||
// --- Diagnóstico XR ---
|
// --- Diagnóstico XR ---
|
||||||
console.log('[XR] Inicializando store...');
|
console.log('[XR] Inicializando store...');
|
||||||
@@ -66,8 +68,36 @@ const store = createXRStore({
|
|||||||
|
|
||||||
// ─── XRModel ───────────────────────────────────────────
|
// ─── XRModel ───────────────────────────────────────────
|
||||||
function XRModel({ sceneModel }: { sceneModel: import('@/stores/useModelStore').SceneModel }) {
|
function XRModel({ sceneModel }: { sceneModel: import('@/stores/useModelStore').SceneModel }) {
|
||||||
const { scene: rawScene } = useGLTF(sceneModel.url);
|
const [rawScene, setRawScene] = useState<THREE.Object3D | null>(null);
|
||||||
const scene = useMemo(() => rawScene.clone(true), [rawScene]);
|
const scene = useMemo(() => rawScene ? rawScene.clone(true) : null, [rawScene]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let active = true;
|
||||||
|
const isIfc = sceneModel.fileName.toLowerCase().endsWith('.ifc');
|
||||||
|
if (isIfc) {
|
||||||
|
fetch(sceneModel.url)
|
||||||
|
.then((res) => res.arrayBuffer())
|
||||||
|
.then((buf) => parseIFCtoThree(buf))
|
||||||
|
.then((threeScene) => {
|
||||||
|
if (active) setRawScene(threeScene);
|
||||||
|
})
|
||||||
|
.catch((err) => console.error('[XRModel] IFC parsing error', err));
|
||||||
|
} else {
|
||||||
|
const loader = new GLTFLoader();
|
||||||
|
loader.load(
|
||||||
|
sceneModel.url,
|
||||||
|
(gltf) => {
|
||||||
|
if (active) setRawScene(gltf.scene);
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
(err) => console.error('[XRModel] GLTF loading error', err)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
|
}, [sceneModel.url, sceneModel.fileName]);
|
||||||
|
|
||||||
const ref = useRef<THREE.Group>(null);
|
const ref = useRef<THREE.Group>(null);
|
||||||
const fineTuning = sceneModel.fineTuning;
|
const fineTuning = sceneModel.fineTuning;
|
||||||
const opacity = useModelStore((s) => s.opacity);
|
const opacity = useModelStore((s) => s.opacity);
|
||||||
@@ -78,6 +108,7 @@ function XRModel({ sceneModel }: { sceneModel: import('@/stores/useModelStore').
|
|||||||
const checklist = useModelStore((s) => s.checklist);
|
const checklist = useModelStore((s) => s.checklist);
|
||||||
|
|
||||||
const modelInfo = useMemo(() => {
|
const modelInfo = useMemo(() => {
|
||||||
|
if (!scene) return { size: new THREE.Vector3(), center: new THREE.Vector3() };
|
||||||
const box = new THREE.Box3().setFromObject(scene);
|
const box = new THREE.Box3().setFromObject(scene);
|
||||||
const size = new THREE.Vector3();
|
const size = new THREE.Vector3();
|
||||||
const center = new THREE.Vector3();
|
const center = new THREE.Vector3();
|
||||||
@@ -87,6 +118,7 @@ function XRModel({ sceneModel }: { sceneModel: import('@/stores/useModelStore').
|
|||||||
}, [scene]);
|
}, [scene]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!scene) return;
|
||||||
const hasRejected = checklist.some(i => i.status === 'rejected');
|
const hasRejected = checklist.some(i => i.status === 'rejected');
|
||||||
const allApproved = checklist.every(i => i.status === 'approved');
|
const allApproved = checklist.every(i => i.status === 'approved');
|
||||||
|
|
||||||
@@ -154,6 +186,7 @@ function XRModel({ sceneModel }: { sceneModel: import('@/stores/useModelStore').
|
|||||||
const renderFactor = scaleRatio?.factor ?? 1;
|
const renderFactor = scaleRatio?.factor ?? 1;
|
||||||
|
|
||||||
if (!sceneModel.visible) return null;
|
if (!sceneModel.visible) return null;
|
||||||
|
if (!scene) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group scale={[renderFactor, renderFactor, renderFactor]}>
|
<group scale={[renderFactor, renderFactor, renderFactor]}>
|
||||||
|
|||||||
Reference in New Issue
Block a user