import { Suspense, useRef, useMemo, useEffect, type ReactNode } from 'react';
import { Canvas, useThree, useFrame } from '@react-three/fiber';
import { OrbitControls, useGLTF, Grid, Html, Line } from '@react-three/drei';
import { Loader2 } from 'lucide-react';
import * as THREE from 'three';
import { useModelStore, type SceneModel } from '@/stores/useModelStore';
import { findNearestVertex, detectHoleAtFace, findNearestEdgeSegment, detectCircularEdgeAtPoint } from './SmartMeasure';
import { registerModelLocalGroup, unregisterModelLocalGroup } from '@/lib/modelTransforms';
interface ModelViewerProps {
url?: string; // legacy, ignored — uses store.models
}
function isPickableModelMesh(obj: THREE.Object3D): obj is THREE.Mesh {
if (!(obj instanceof THREE.Mesh)) return false;
if (obj.userData.__edgeLine) return false;
if (obj.geometry instanceof THREE.SphereGeometry) return false;
if (obj.geometry instanceof THREE.RingGeometry) return false;
if (obj.geometry instanceof THREE.PlaneGeometry) return false;
return true;
}
/** Walk up the parent chain until we find a node tagged as `ifcElement`.
* Falls back to the clicked mesh so older/demo GLBs without IFC metadata
* still support selection/highlight. */
export function findElementRoot(obj: THREE.Object3D | null): THREE.Object3D | null {
const fallback = isPickableModelMesh(obj as THREE.Object3D) ? obj : null;
let cur: THREE.Object3D | null = obj;
while (cur) {
if (cur.userData?.ifcElement) return cur;
cur = cur.parent;
}
return fallback;
}
function hasIfcAncestor(obj: THREE.Object3D): boolean {
let cur = obj.parent;
while (cur) {
if (cur.userData?.ifcElement) return true;
cur = cur.parent;
}
return false;
}
/** Stable key for an element across reloads: prefers ifcId, falls back to name. */
export function elementKey(modelId: string, el: THREE.Object3D): string {
const id = el.userData?.ifcId ?? el.name ?? el.uuid;
return `${modelId}:${id}`;
}
/** Globally accessible ref to the active R3F scene (set by SceneRefCapture). */
export const sceneRef: { current: THREE.Scene | null } = { current: null };
function SceneRefCapture() {
const { scene } = useThree();
useEffect(() => {
sceneRef.current = scene;
return () => { if (sceneRef.current === scene) sceneRef.current = null; };
}, [scene]);
return null;
}
function LoadingFallback() {
return (
Carregando modelo…
);
}
function GLBModel({ sceneModel, isActive }: { sceneModel: SceneModel; isActive: boolean }) {
const { scene: rawScene } = useGLTF(sceneModel.url);
// Clone scene per instance so multiple GLBModels with same url don't share materials/transforms
const scene = useMemo(() => rawScene.clone(true), [rawScene]);
const ref = useRef(null);
const opacity = useModelStore((s) => s.opacity);
const renderMode = useModelStore((s) => s.renderMode);
const wireframeColor = useModelStore((s) => s.wireframeColor);
const wireframeThickness = useModelStore((s) => s.wireframeThickness);
const edgeThresholdAngle = useModelStore((s) => s.edgeThresholdAngle);
const checklist = useModelStore((s) => s.checklist);
const setActive = useModelStore((s) => s.setActiveModel);
const selectionMode = useModelStore((s) => s.selectionMode);
const fineTuning = sceneModel.fineTuning;
const modelInfo = useMemo(() => {
const box = new THREE.Box3().setFromObject(scene);
const size = new THREE.Vector3();
const center = new THREE.Vector3();
box.getSize(size);
box.getCenter(center);
return { size, center };
}, [scene]);
const originalColors = useRef