diff --git a/src/components/three/ModelViewer.tsx b/src/components/three/ModelViewer.tsx index ca3b726..1ac27b2 100644 --- a/src/components/three/ModelViewer.tsx +++ b/src/components/three/ModelViewer.tsx @@ -9,6 +9,19 @@ import { registerModelLocalGroup, unregisterModelLocalGroup, getModelWorldScaleF import { parseIFCtoThree } from '@/lib/convertIFC'; import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; import { mainCameraRef, mainControlsRef, viewAnim, calibration, pushModelFaceNormal, computeCalibrationQuaternion, subscribeCalibration } from './viewCubeBus'; +import { toast } from 'sonner'; + +function getColorForMaterialName(name: string): string { + if (!name) return '#a1a1aa'; + let hash = 0; + for (let i = 0; i < name.length; i++) { + hash = name.charCodeAt(i) + ((hash << 5) - hash); + } + const h = Math.abs(hash) % 360; + const s = 65 + (Math.abs(hash >> 8) % 15); + const l = 45 + (Math.abs(hash >> 16) % 10); + return `hsl(${h}, ${s}%, ${l}%)`; +} interface ModelViewerProps { url?: string; // legacy, ignored — uses store.models @@ -195,6 +208,7 @@ function GLBModel({ sceneModel, isActive }: { sceneModel: SceneModel; isActive: const setActive = useModelStore((s) => s.setActiveModel); const selectionMode = useModelStore((s) => s.selectionMode); const measureMode = useModelStore((s) => s.measureMode); + const ifcMaterialColors = useModelStore((s) => s.ifcMaterialColors); const fineTuning = sceneModel.fineTuning; // Re-render when calibration state changes (so calQuat resets to identity during the flow). @@ -261,8 +275,14 @@ function GLBModel({ sceneModel, isActive }: { sceneModel: SceneModel; isActive: } else if (allApproved) { mat.color.setHSL(0.38, 0.7, 0.45); } else { - // Tint with the per-model color (subtle) - mat.color.set(sceneModel.color); + const root = findElementRoot(child); + const matName = root?.userData?.materialName; + if (ifcMaterialColors && matName) { + mat.color.set(getColorForMaterialName(matName)); + } else { + // Tint with the per-model color (subtle) + mat.color.set(sceneModel.color); + } } } mat.needsUpdate = true; @@ -284,7 +304,7 @@ function GLBModel({ sceneModel, isActive }: { sceneModel: SceneModel; isActive: } } }); - }, [scene, opacity, renderMode, checklist, wireframeColor, wireframeThickness, edgeThresholdAngle, sceneModel.color, measureMode]); + }, [scene, opacity, renderMode, checklist, wireframeColor, wireframeThickness, edgeThresholdAngle, sceneModel.color, measureMode, ifcMaterialColors]); const rotXRad = (fineTuning.rotX * Math.PI) / 180; const rotYRad = (fineTuning.rotY * Math.PI) / 180; @@ -716,6 +736,7 @@ function HoverDetector() { const hoverTimer = useRef | null>(null); const hideTimer = useRef | null>(null); const lastHitKey = useRef(''); + const lastHitExpressId = useRef(0); useEffect(() => { const clearTimers = () => { @@ -741,11 +762,59 @@ function HoverDetector() { if (!hit || !(hit.object instanceof THREE.Mesh)) { lastHitKey.current = ''; + lastHitExpressId.current = 0; setHoverInfo(null); clearTimers(); + toast.dismiss('ifc-hover-info'); return; } + // Detecção de propriedades do IFC de forma ágil baseada em mudança de elemento + const elementRoot = findElementRoot(hit.object); + const expressID = elementRoot?.userData?.ifcId ?? 0; + + if (expressID !== lastHitExpressId.current) { + lastHitExpressId.current = expressID; + const props = elementRoot?.userData?.properties; + + if (props && (props.name || props.material || props.tag)) { + const infoLines: string[] = []; + if (props.tag || props.name) { + infoLines.push(`Peça: ${props.tag || props.name}`); + } + if (props.material) { + infoLines.push(`Material: ${props.material}`); + } + + const lengthKey = Object.keys(props).find(k => { + const kl = k.toLowerCase(); + return kl.includes('length') || kl.includes('comprimento') || kl.includes('lengthvalue'); + }); + + if (lengthKey && props[lengthKey]) { + infoLines.push(`Comprimento: ${props[lengthKey]}`); + } else { + const commonLengthKeys = ['Length', 'Comprimento', 'LengthValue', 'height', 'comprimento']; + for (const key of commonLengthKeys) { + if (props[key]) { + infoLines.push(`Comprimento: ${props[key]}`); + break; + } + } + } + + if (infoLines.length > 0) { + toast(infoLines.join(' | '), { + id: 'ifc-hover-info', + duration: 8000, + position: 'bottom-center', + }); + } + } else { + toast.dismiss('ifc-hover-info'); + } + } + // Stability check – same approximate position for debounce (~3 mm) const key = `${hit.point.x.toFixed(3)},${hit.point.y.toFixed(3)},${hit.point.z.toFixed(3)}`; if (key === lastHitKey.current) return; // timer already running @@ -808,8 +877,10 @@ function HoverDetector() { const onLeave = () => { lastHitKey.current = ''; + lastHitExpressId.current = 0; setHoverInfo(null); clearTimers(); + toast.dismiss('ifc-hover-info'); }; gl.domElement.addEventListener('pointermove', onMove); @@ -818,6 +889,7 @@ function HoverDetector() { gl.domElement.removeEventListener('pointermove', onMove); gl.domElement.removeEventListener('pointerleave', onLeave); clearTimers(); + toast.dismiss('ifc-hover-info'); }; }, [camera, scene, gl, raycaster, mouse, setHoverInfo]); diff --git a/src/lib/convertIFC.ts b/src/lib/convertIFC.ts index 6f4ebf1..bfdb696 100644 --- a/src/lib/convertIFC.ts +++ b/src/lib/convertIFC.ts @@ -7,6 +7,53 @@ const WASM_PATH = 'https://unpkg.com/web-ifc@0.0.57/'; /** * Convert an IFC file (ArrayBuffer) into a GLB Blob using web-ifc + GLTFExporter. */ +function getMaterialName(ifcApi: WebIFC.IfcAPI, modelID: number, matRef: any): string | null { + if (!matRef) return null; + const matId = matRef.value; + if (!matId) return null; + try { + const matLine = ifcApi.GetLine(modelID, matId); + if (!matLine) return null; + + if (matLine.Name && matLine.Name.value) { + return matLine.Name.value; + } + + if (matLine.Materials) { + for (const mRef of matLine.Materials) { + const name = getMaterialName(ifcApi, modelID, mRef); + if (name) return name; + } + } + + if (matLine.MaterialConstituents) { + for (const mcRef of matLine.MaterialConstituents) { + const mcLine = ifcApi.GetLine(modelID, mcRef.value); + if (mcLine && mcLine.Material) { + const name = getMaterialName(ifcApi, modelID, mcLine.Material); + if (name) return name; + } + } + } + + if (matLine.MaterialProfileSet) { + return getMaterialName(ifcApi, modelID, matLine.MaterialProfileSet); + } + if (matLine.MaterialProfiles) { + for (const mpRef of matLine.MaterialProfiles) { + const mpLine = ifcApi.GetLine(modelID, mpRef.value); + if (mpLine && mpLine.Material) { + const name = getMaterialName(ifcApi, modelID, mpLine.Material); + if (name) return name; + } + } + } + } catch (err) { + // Silencia erros de atributos inexistentes + } + return null; +} + export async function parseIFCtoThree(buffer: ArrayBuffer): Promise { const ifcApi = new WebIFC.IfcAPI(); ifcApi.SetWasmPath(WASM_PATH, true); @@ -15,6 +62,87 @@ export async function parseIFCtoThree(buffer: ArrayBuffer): Promise const data = new Uint8Array(buffer); const modelID = ifcApi.OpenModel(data); + // Mapeamento de materiais + const elementMaterialMap = new Map(); + try { + const rels = ifcApi.GetLineIDsWithType(modelID, WebIFC.IFCRELASSOCIATESMATERIAL); + for (let i = 0; i < rels.size(); i++) { + const relId = rels.get(i); + const rel = ifcApi.GetLine(modelID, relId); + if (rel && rel.RelatedObjects && rel.RelatingMaterial) { + const matName = getMaterialName(ifcApi, modelID, rel.RelatingMaterial); + if (matName) { + for (const objRef of rel.RelatedObjects) { + elementMaterialMap.set(objRef.value, matName); + } + } + } + } + } catch (err) { + console.warn('[IFC Parser] Falha ao mapear materiais:', err); + } + + // Mapeamento de propriedades adicionais + const elementPropertiesMap = new Map>(); + try { + const relsProp = ifcApi.GetLineIDsWithType(modelID, WebIFC.IFCRELDEFINESBYPROPERTIES); + for (let i = 0; i < relsProp.size(); i++) { + const relId = relsProp.get(i); + const rel = ifcApi.GetLine(modelID, relId); + if (rel && rel.RelatedObjects && rel.RelatingPropertyDefinition) { + const propDefId = rel.RelatingPropertyDefinition.value; + const propDef = ifcApi.GetLine(modelID, propDefId); + + if (propDef) { + const props: Record = {}; + + if (propDef.HasProperties) { + for (const pRef of propDef.HasProperties) { + const pLine = ifcApi.GetLine(modelID, pRef.value); + if (pLine && pLine.Name) { + const name = pLine.Name.value; + let value = ''; + if (pLine.NominalValue) { + value = String(pLine.NominalValue.value); + } + if (name && value) { + props[name] = value; + } + } + } + } + + if (propDef.Quantities) { + for (const qRef of propDef.Quantities) { + const qLine = ifcApi.GetLine(modelID, qRef.value); + if (qLine && qLine.Name) { + const name = qLine.Name.value; + let value = ''; + if (qLine.LengthValue !== undefined) value = `${qLine.LengthValue.toFixed(1)} mm`; + else if (qLine.AreaValue !== undefined) value = `${qLine.AreaValue.toFixed(1)} m²`; + else if (qLine.VolumeValue !== undefined) value = `${qLine.VolumeValue.toFixed(2)} m³`; + else if (qLine.NominalValue) value = String(qLine.NominalValue.value); + + if (name && value) { + props[name] = value; + } + } + } + } + + if (Object.keys(props).length > 0) { + for (const objRef of rel.RelatedObjects) { + const existing = elementPropertiesMap.get(objRef.value) ?? {}; + elementPropertiesMap.set(objRef.value, { ...existing, ...props }); + } + } + } + } + } + } catch (err) { + console.warn('[IFC Parser] Falha ao mapear propriedades:', err); + } + const scene = new THREE.Scene(); const materials: Map = new Map(); @@ -22,11 +150,38 @@ export async function parseIFCtoThree(buffer: ArrayBuffer): Promise const placedGeometries = mesh.geometries; const expressID = (mesh as unknown as { expressID?: number }).expressID ?? 0; - // One Group per IfcProduct → represents a single "element" (beam, plate…). - // userData is preserved by GLTFExporter as `extras` and survives reload. + let name = ''; + let tag = ''; + let objectType = ''; + let description = ''; + try { + const elementLine = ifcApi.GetLine(modelID, expressID); + if (elementLine) { + name = elementLine.Name?.value ?? ''; + tag = elementLine.Tag?.value ?? ''; + objectType = elementLine.ObjectType?.value ?? ''; + description = elementLine.Description?.value ?? ''; + } + } catch (e) {} + + const materialName = elementMaterialMap.get(expressID) ?? ''; + const extraProps = elementPropertiesMap.get(expressID) ?? {}; + const elementGroup = new THREE.Group(); elementGroup.name = `ifc_${expressID}`; - elementGroup.userData = { ifcElement: true, ifcId: expressID }; + elementGroup.userData = { + ifcElement: true, + ifcId: expressID, + materialName, + properties: { + name, + tag, + objectType, + description, + material: materialName, + ...extraProps + } + }; for (let i = 0; i < placedGeometries.size(); i++) { const placedGeometry = placedGeometries.get(i); diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 4aad18a..39facad 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -24,7 +24,7 @@ const Index = () => { const [showLanding, setShowLanding] = useState(false); const [converting, setConverting] = useState(false); - const { model, addModel, models, maxModels, xrSupported, setXrSupported } = useModelStore(); + const { model, addModel, models, maxModels, xrSupported, setXrSupported, ifcMaterialColors, setIfcMaterialColors } = useModelStore(); // --- Painel de Logs Remotos em Tempo Real --- const [debugLogs, setDebugLogs] = useState<{ level: 'info' | 'warn' | 'error'; message: string; timestamp: string; data?: unknown }[]>([]); @@ -265,6 +265,23 @@ const Index = () => { /> + {/* Toggle IFC Material Colors */} +
+
+ +

+ {ifcMaterialColors ? 'Cores baseadas no material do IFC' : 'Cores padrão da cena'} +

+
+ +
+ {/* Demo buttons */}
diff --git a/src/stores/useModelStore.ts b/src/stores/useModelStore.ts index 3bc1eef..e6ff940 100644 --- a/src/stores/useModelStore.ts +++ b/src/stores/useModelStore.ts @@ -94,6 +94,18 @@ function loadVisibility(fileName: string): { hidden: Set; isolated: Set< return { hidden: new Set(v.hidden ?? []), isolated: v.isolated ? new Set(v.isolated) : null }; } +const IFC_MAT_COLORS_KEY = 'tsxr_ifc_mat_colors_v1'; +function loadIfcMaterialColors(): boolean { + try { + const raw = localStorage.getItem(IFC_MAT_COLORS_KEY); + return raw !== 'false'; + } catch (e) { /* ignore */ } + return true; +} +function saveIfcMaterialColors(enabled: boolean) { + try { localStorage.setItem(IFC_MAT_COLORS_KEY, String(enabled)); } catch (e) { /* ignore */ } +} + export interface ScaleRatio { @@ -354,6 +366,9 @@ interface ModelStore { xrFeatures: XRFeatureFlags; setXRFeature: (key: keyof XRFeatureFlags, value: boolean) => void; resetXRFeatures: () => void; + + ifcMaterialColors: boolean; + setIfcMaterialColors: (enabled: boolean) => void; } /** Sync top-level legacy `model` / `fineTuning` from active SceneModel. */ @@ -785,4 +800,9 @@ export const useModelStore = create((set, get) => ({ saveXRFeatures(next); set({ xrFeatures: next }); }, + ifcMaterialColors: loadIfcMaterialColors(), + setIfcMaterialColors: (enabled: boolean) => { + saveIfcMaterialColors(enabled); + set({ ifcMaterialColors: enabled }); + }, }));