diff --git a/src/components/viewer3d/ModelViewer3D.tsx b/src/components/viewer3d/ModelViewer3D.tsx index e6a7fdf..c920829 100644 --- a/src/components/viewer3d/ModelViewer3D.tsx +++ b/src/components/viewer3d/ModelViewer3D.tsx @@ -46,7 +46,7 @@ export const ModelViewer3D: React.FC = ({ const [isOrthographic, setIsOrthographic] = useState(false); const [isWireframe, setIsWireframe] = useState(false); const [navMode, setNavMode] = useState<'orbit' | 'walk'>('orbit'); - const [colorMode, setColorMode] = useState<'description' | 'production'>('description'); + const [colorMode, setColorMode] = useState<'description' | 'production'>('production'); const [hasSectionPlanes, setHasSectionPlanes] = useState(false); const [isMeasuring, setIsMeasuring] = useState(false); @@ -55,16 +55,39 @@ export const ModelViewer3D: React.FC = ({ const [hoveredPiece, setHoveredPiece] = useState(null); const [hoverPosition, setHoverPosition] = useState<{ x: number; y: number } | null>(null); - // Highlight Material (Light Green #4ade80) - const highlightMaterialRef = useRef( - new THREE.MeshStandardMaterial({ - color: 0x4ade80, - emissive: 0x22c55e, - emissiveIntensity: 0.35, - metalness: 0.3, - roughness: 0.4, - side: THREE.DoubleSide, - }) + // Smart Matcher: Matches any mark format (e.g. "B135-3-21", "3-21", "21", "B135-21") against production data + const getPieceProductionStatus = useCallback( + (rawMark: string): ProductionPieceStatus | undefined => { + if (!productionData || !rawMark) return undefined; + + const clean = rawMark.trim(); + // 1. Direct map lookup + if (productionData.has(clean)) return productionData.get(clean); + + // 2. Tokenize by -, /, ., _, space + const tokens = clean.split(/[-/._\s]+/).filter(Boolean); + + // Suffix combinations e.g. "3-21" + if (tokens.length >= 2) { + const lastTwo = `${tokens[tokens.length - 2]}-${tokens[tokens.length - 1]}`; + if (productionData.has(lastTwo)) return productionData.get(lastTwo); + } + + // Last token is the specific piece mark (e.g. "21" in "B135-3-21") + if (tokens.length > 0) { + const lastTok = tokens[tokens.length - 1]; + if (productionData.has(lastTok)) return productionData.get(lastTok); + } + + // 3. Fallback scan across map values + for (const [key, val] of productionData.entries()) { + if (val.marca === clean) return val; + if (tokens.length > 0 && val.marca === tokens[tokens.length - 1]) return val; + } + + return undefined; + }, + [productionData] ); // Initialize Scene @@ -119,7 +142,7 @@ export const ModelViewer3D: React.FC = ({ controlsRef.current = controls; // 5. Lights - const ambientLight = new THREE.AmbientLight(0xffffff, 0.75); + const ambientLight = new THREE.AmbientLight(0xffffff, 0.85); scene.add(ambientLight); const dirLight1 = new THREE.DirectionalLight(0xffffff, 1.2); @@ -127,11 +150,11 @@ export const ModelViewer3D: React.FC = ({ dirLight1.castShadow = true; scene.add(dirLight1); - const dirLight2 = new THREE.DirectionalLight(0x94a3b8, 0.6); + const dirLight2 = new THREE.DirectionalLight(0x94a3b8, 0.7); dirLight2.position.set(-100, -50, -100); scene.add(dirLight2); - const hemiLight = new THREE.HemisphereLight(0xffffff, 0x1e293b, 0.5); + const hemiLight = new THREE.HemisphereLight(0xffffff, 0x1e293b, 0.6); scene.add(hemiLight); // 6. Floor Grid @@ -229,59 +252,116 @@ export const ModelViewer3D: React.FC = ({ if (!modelData || !modelGroupRef.current) return; const group = modelGroupRef.current; + const allMeshes: THREE.Mesh[] = []; group.traverse((child) => { if ((child as THREE.Mesh).isMesh) { - const mesh = child as THREE.Mesh; - const pmark = mesh.userData.pieceMark || ''; - - if (colorMode === 'production' && productionData) { - // Look up production pointing status - const prod = productionData.get(pmark); - if (prod && prod.pointedQtd > 0) { - // Apply stage color (e.g., Light Green #4ade80 for pointed) - const stageHex = prod.processColor || '#4ade80'; - mesh.material = new THREE.MeshStandardMaterial({ - color: new THREE.Color(stageHex), - metalness: 0.35, - roughness: 0.45, - side: THREE.DoubleSide, - }); - } else { - // Unpointed / Pending (Default Gray) - mesh.material = new THREE.MeshStandardMaterial({ - color: 0x64748b, - metalness: 0.3, - roughness: 0.6, - transparent: opacity < 100, - opacity: opacity / 100, - side: THREE.DoubleSide, - }); - } - } else { - // Color Mode "Descrição" (Palette by Profile/Section) - const colorKey = mesh.userData.colorKey; - const cachedMat = modelData.materialsByColor.get(colorKey); - if (cachedMat) { - mesh.material = cachedMat; - } - } - - // Apply Opacity & Wireframe - if (Array.isArray(mesh.material)) { - mesh.material.forEach((m) => { - m.transparent = opacity < 100; - m.opacity = opacity / 100; - if ('wireframe' in m) (m as any).wireframe = isWireframe; - }); - } else if (mesh.material) { - mesh.material.transparent = opacity < 100; - mesh.material.opacity = opacity / 100; - if ('wireframe' in mesh.material) (mesh.material as any).wireframe = isWireframe; - } + allMeshes.push(child as THREE.Mesh); } }); - }, [modelData, colorMode, productionData, opacity, isWireframe]); + + if (colorMode === 'production') { + // Group meshes by their resolved piece/assembly mark + const meshesByMark = new Map(); + + allMeshes.forEach((mesh) => { + let pmark = mesh.userData.pieceMark || ''; + if (!pmark && mesh.userData.expressID && modelData.pieceByExpressID) { + const pInfo = modelData.pieceByExpressID.get(mesh.userData.expressID); + pmark = pInfo?.pieceMark || ''; + } + + const prod = getPieceProductionStatus(pmark); + const groupKey = prod ? `${prod.fase}-${prod.marca}` : (pmark || `mesh-${mesh.id}`); + + if (!meshesByMark.has(groupKey)) { + meshesByMark.set(groupKey, []); + } + meshesByMark.get(groupKey)!.push(mesh); + }); + + // Apply proportional coloring for each mark + meshesByMark.forEach((meshes, groupKey) => { + const sampleMark = meshes[0]?.userData.pieceMark || groupKey; + const prod = getPieceProductionStatus(sampleMark); + + if (prod && prod.pointedQtd > 0) { + // If pointed, calculate how many mesh instances to highlight green + // Example: 2 of 2 un. -> all 2 green. 2 of 4 un. -> 2 green, 2 gray. + const totalQtd = Math.max(prod.totalQtd || 1, 1); + const pointedQtd = Math.max(prod.pointedQtd, 0); + + // If there are multiple meshes, highlight proportional count + const ratio = Math.min(pointedQtd / totalQtd, 1.0); + const countToHighlight = Math.max(1, Math.min(meshes.length, Math.round(meshes.length * ratio))); + + meshes.forEach((mesh, idx) => { + const isPointed = idx < countToHighlight; + if (isPointed) { + // Vibrant Light Green (#4ade80) for pointed pieces + mesh.material = new THREE.MeshStandardMaterial({ + color: 0x4ade80, + emissive: 0x16a34a, + emissiveIntensity: 0.28, + metalness: 0.35, + roughness: 0.4, + side: THREE.DoubleSide, + }); + } else { + // Sleek Metallic Light Gray for unpointed + mesh.material = new THREE.MeshStandardMaterial({ + color: 0xb0bec5, + metalness: 0.58, + roughness: 0.38, + side: THREE.DoubleSide, + }); + } + }); + } else { + // Unpointed / Pending -> Sleek Metallic Light Gray (Steel) + meshes.forEach((mesh) => { + mesh.material = new THREE.MeshStandardMaterial({ + color: 0xb0bec5, + metalness: 0.58, + roughness: 0.38, + side: THREE.DoubleSide, + }); + }); + } + }); + } else { + // Color Mode "Descrição" (Palette by Profile/Section) + allMeshes.forEach((mesh) => { + const colorKey = mesh.userData.colorKey; + const cachedMat = modelData.materialsByColor.get(colorKey); + if (cachedMat) { + mesh.material = cachedMat; + } else { + mesh.material = new THREE.MeshStandardMaterial({ + color: 0xb0bec5, + metalness: 0.58, + roughness: 0.38, + side: THREE.DoubleSide, + }); + } + }); + } + + // Apply Opacity & Wireframe to all meshes + allMeshes.forEach((mesh) => { + if (Array.isArray(mesh.material)) { + mesh.material.forEach((m) => { + m.transparent = opacity < 100; + m.opacity = opacity / 100; + if ('wireframe' in m) (m as any).wireframe = isWireframe; + }); + } else if (mesh.material) { + mesh.material.transparent = opacity < 100; + mesh.material.opacity = opacity / 100; + if ('wireframe' in mesh.material) (mesh.material as any).wireframe = isWireframe; + } + }); + }, [modelData, colorMode, productionData, opacity, isWireframe, getPieceProductionStatus]); // Camera Toggle (Ortogonal / Perspectiva) const handleToggleCamera = useCallback(() => { @@ -353,6 +433,39 @@ export const ModelViewer3D: React.FC = ({ } }, []); + // Click Selection + const handleClick = (e: React.MouseEvent) => { + if (!containerRef.current || !activeCameraRef.current || !modelGroupRef.current) return; + + const rect = containerRef.current.getBoundingClientRect(); + const mouseX = ((e.clientX - rect.left) / rect.width) * 2 - 1; + const mouseY = -((e.clientY - rect.top) / rect.height) * 2 + 1; + const clickPos = new THREE.Vector2(mouseX, mouseY); + + raycasterRef.current.setFromCamera(clickPos, activeCameraRef.current); + const intersects = raycasterRef.current.intersectObjects(modelGroupRef.current.children, true); + + if (intersects.length > 0) { + const hitMesh = intersects[0].object as THREE.Mesh; + const expressID = hitMesh.userData.expressID; + const pmark = hitMesh.userData.pieceMark; + + if (modelData && expressID) { + const piece = modelData.pieceByExpressID.get(expressID) || { + expressID, + guid: '', + name: hitMesh.userData.section || 'Peça', + type: 'PIECE', + pieceMark: pmark, + section: hitMesh.userData.section, + }; + onSelectPiece?.(piece); + } + } else { + onSelectPiece?.(null); + } + }; + // 1.5-Second Hover Detection const handlePointerMove = (e: React.PointerEvent) => { if (!containerRef.current || !activeCameraRef.current || !modelGroupRef.current) return; @@ -411,6 +524,7 @@ export const ModelViewer3D: React.FC = ({ return (
= ({ position={hoverPosition} productionInfo={ hoveredPiece?.pieceMark && productionData - ? { - pointedQty: productionData.get(hoveredPiece.pieceMark)?.pointedQtd, - totalQty: productionData.get(hoveredPiece.pieceMark)?.totalQtd, - currentStage: productionData.get(hoveredPiece.pieceMark)?.currentProcessName, - stageColor: productionData.get(hoveredPiece.pieceMark)?.processColor, - } + ? (() => { + const prod = getPieceProductionStatus(hoveredPiece.pieceMark); + return prod + ? { + pointedQty: prod.pointedQtd, + totalQty: prod.totalQtd, + currentStage: prod.currentProcessName, + stageColor: prod.processColor, + } + : undefined; + })() : undefined } /> diff --git a/src/lib/ifc/ifcLoaderService.ts b/src/lib/ifc/ifcLoaderService.ts index e80c190..15874ca 100644 --- a/src/lib/ifc/ifcLoaderService.ts +++ b/src/lib/ifc/ifcLoaderService.ts @@ -160,14 +160,23 @@ export async function loadAndAuditIFC( // 4.1 Native Aggregates mapping (IFCRELAGGREGATES) const aggregates = ifcApi.GetLineIDsWithType(modelID, WebIFC.IFCRELAGGREGATES); const assemblyToChildren = new Map(); + const childToAssemblyMap = new Map(); for (let i = 0; i < aggregates.size(); i++) { const relID = aggregates.get(i); try { const rel = ifcApi.GetLine(modelID, relID); if (rel && rel.RelatingObject && rel.RelatedObjects) { - const parentID = rel.RelatingObject.value; - const childIDs = (rel.RelatedObjects as any[]).map((r: any) => r.value); + const parentID = typeof rel.RelatingObject === 'number' ? rel.RelatingObject : rel.RelatingObject.value; + const childObjs = Array.isArray(rel.RelatedObjects) ? rel.RelatedObjects : [rel.RelatedObjects]; + const childIDs: number[] = []; + for (const c of childObjs) { + const cId = typeof c === 'number' ? c : c?.value; + if (typeof cId === 'number' && cId > 0) { + childIDs.push(cId); + childToAssemblyMap.set(cId, parentID); + } + } assemblyToChildren.set(parentID, childIDs); } } catch { @@ -240,31 +249,35 @@ export async function loadAndAuditIFC( if (relId && relId > 0) { const rel = ifcApi.GetLine(modelID, relId); if (rel && rel.RelatedObjects && rel.RelatingPropertyDefinition) { - const propDefId = rel.RelatingPropertyDefinition.value; + const propDefId = typeof rel.RelatingPropertyDefinition === 'number' + ? rel.RelatingPropertyDefinition + : rel.RelatingPropertyDefinition.value; + if (propDefId && typeof propDefId === 'number' && propDefId > 0) { const propDef = ifcApi.GetLine(modelID, propDefId); if (propDef && propDef.HasProperties) { const props: Record = {}; for (const pRef of propDef.HasProperties) { - const pId = pRef?.value; + const pId = typeof pRef === 'number' ? pRef : pRef?.value; if (pId && typeof pId === 'number' && pId > 0) { const pLine = ifcApi.GetLine(modelID, pId); if (pLine && pLine.Name) { - const name = pLine.Name.value; + const name = pLine.Name.value || pLine.Name; let value = ''; if (pLine.NominalValue) { - value = String(pLine.NominalValue.value); + value = String(pLine.NominalValue.value !== undefined ? pLine.NominalValue.value : pLine.NominalValue); } if (name && value) { - props[name] = value; + props[String(name)] = value; } } } } + const relatedObjs = Array.isArray(rel.RelatedObjects) ? rel.RelatedObjects : [rel.RelatedObjects]; for (const objRef of relatedObjs) { - const objId = objRef?.value || objRef; - if (objId && typeof objId === 'number') { + const objId = typeof objRef === 'number' ? objRef : objRef?.value; + if (objId && typeof objId === 'number' && objId > 0) { const existing = elementPropertiesMap.get(objId) || {}; elementPropertiesMap.set(objId, { ...existing, ...props }); } @@ -292,7 +305,8 @@ export async function loadAndAuditIFC( const assObj = ifcApi.GetLine(modelID, assID); const props = elementPropertiesMap.get(assID) || {}; const rawProps = getSyncProperties(ifcApi, modelID, assID); - const pmark = props.PieceMark || props.Reference || props.Mark || props.Tag || rawProps.Name || 'indefinido'; + let pmark = props.PieceMark || props.Reference || props.Mark || props.Tag || rawProps.Name || 'indefinido'; + if (pmark === 'indefinido') pmark = ''; const children = assemblyToChildren.get(assID) || []; const piece: PieceInfo = { @@ -308,7 +322,7 @@ export async function loadAndAuditIFC( ofNumber: extractOFFromMark(pmark) }; - if (pmark !== 'indefinido') { + if (pmark) { uniqueMarksSet.add(pmark); if (!piecesByMark.has(pmark)) piecesByMark.set(pmark, []); piecesByMark.get(pmark)!.push(piece); @@ -324,10 +338,11 @@ export async function loadAndAuditIFC( const sceneGroup = new THREE.Group(); sceneGroup.name = 'IFC_ROOT_MODEL'; - const defaultGrayMat = new THREE.MeshStandardMaterial({ - color: 0x94a3b8, - metalness: 0.35, - roughness: 0.55, + // Base metallic light gray material + const defaultSteelMat = new THREE.MeshStandardMaterial({ + color: 0xb0bec5, // Metallic Light Gray + metalness: 0.58, + roughness: 0.38, side: THREE.DoubleSide }); @@ -342,7 +357,34 @@ export async function loadAndAuditIFC( // Check properties for this element const props = elementPropertiesMap.get(expressID) || {}; const rawProps = getSyncProperties(ifcApi, modelID, expressID); - const pmark = props.PieceMark || props.Reference || props.Mark || props.Tag || rawProps.Name || ''; + + // Look up mark from element properties OR parent assembly OR weld group peers + let pmark = props.PieceMark || props.Reference || props.Mark || props.Tag || rawProps.Name || ''; + if (pmark === 'indefinido') pmark = ''; + + if (!pmark) { + const parentID = childToAssemblyMap.get(expressID); + if (parentID) { + const parentProps = elementPropertiesMap.get(parentID) || {}; + const parentRaw = getSyncProperties(ifcApi, modelID, parentID); + pmark = parentProps.PieceMark || parentProps.Reference || parentProps.Mark || parentProps.Tag || parentRaw.Name || ''; + if (pmark === 'indefinido') pmark = ''; + } + } + + const connectedGroup = weldedGroupMap.get(expressID) || [expressID]; + if (!pmark) { + for (const peerID of connectedGroup) { + const peerProps = elementPropertiesMap.get(peerID) || {}; + const peerRaw = getSyncProperties(ifcApi, modelID, peerID); + const peerMark = peerProps.PieceMark || peerProps.Reference || peerProps.Mark || peerProps.Tag || peerRaw.Name || ''; + if (peerMark && peerMark !== 'indefinido') { + pmark = peerMark; + break; + } + } + } + const section = props.Section || props.Profile || props.Description || rawProps.Name || ''; // Determine section color @@ -364,10 +406,7 @@ export async function loadAndAuditIFC( }) ); } - const mat = materialCache.get(hexColor) || defaultGrayMat; - - // Get welded connected peers (Fallback topology) - const connectedGroup = weldedGroupMap.get(expressID) || [expressID]; + const mat = defaultSteelMat; for (let g = 0; g < placedGeometries.size(); g++) { const placedGeo = placedGeometries.get(g); diff --git a/src/pages/Visualizador3D.tsx b/src/pages/Visualizador3D.tsx index 6e61266..53578fe 100644 --- a/src/pages/Visualizador3D.tsx +++ b/src/pages/Visualizador3D.tsx @@ -99,17 +99,25 @@ export default function Visualizador3D() { .select('id, of_number, etapa_fase, marca, descricao, quantidade, peso_unitario, peso_total, perfil_principal, material') .eq('of_number', selectedOF); - // Fetch apontamentos com join de peca e processo + // Fetch apontamentos const { data: apontamentosData } = await supabase .from('apontamentos_producao' as any) - .select(` - id, of_number, peca_id, quantidade_produzida, - peca:pecas(marca, etapa_fase), - processo:processos_fabricacao(nome, cor, ordem) - `) + .select('id, of_number, peca_id, processo_id, quantidade_produzida') .eq('of_number', selectedOF); - // Build phases list + // Fetch processos de fabricação + const { data: processosData } = await supabase + .from('processos_fabricacao' as any) + .select('id, nome, cor, ordem'); + + const processoById = new Map(); + if (processosData) { + processosData.forEach((pr: any) => { + processoById.set(pr.id, pr); + }); + } + + const pecaById = new Map(); const phases = new Set(); const prodMap = new Map(); let totalPecas = 0; @@ -117,40 +125,57 @@ export default function Visualizador3D() { if (pecasData) { pecasData.forEach((p: any) => { + pecaById.set(p.id, p); const fase = String(p.etapa_fase || '1'); + const marca = String(p.marca || '').trim(); phases.add(fase); totalPecas += Number(p.quantidade || 0); totalPeso += Number(p.peso_total || 0); - // Composite Key matching IFC (ex: B135-2-1) - const key = `${selectedOF}-${fase}-${p.marca}`; - prodMap.set(key, { + const pieceStatus = { pecaId: p.id, - marca: p.marca, + marca, fase, + of_number: selectedOF, totalQtd: Number(p.quantidade || 1), pointedQtd: 0, currentProcessName: 'Pendente', processColor: '#64748b' - }); + }; + + // Register multiple lookup keys for resilient matching: + // 1. OF-Fase-Marca: e.g. "B135-3-21" + prodMap.set(`${selectedOF}-${fase}-${marca}`, pieceStatus); + // 2. Fase-Marca: e.g. "3-21" + prodMap.set(`${fase}-${marca}`, pieceStatus); + // 3. Marca only: e.g. "21" + prodMap.set(marca, pieceStatus); + // 4. OF-Marca: e.g. "B135-21" + prodMap.set(`${selectedOF}-${marca}`, pieceStatus); }); } let pointedTotal = 0; if (apontamentosData) { apontamentosData.forEach((ap: any) => { - const marca = ap.peca?.marca; - const fase = String(ap.peca?.etapa_fase || '1'); - if (marca) { + const peca = pecaById.get(ap.peca_id); + if (peca) { + const marca = String(peca.marca || '').trim(); + const fase = String(peca.etapa_fase || '1'); const key = `${selectedOF}-${fase}-${marca}`; const existing = prodMap.get(key); if (existing) { const qty = Number(ap.quantidade_produzida || 0); existing.pointedQtd += qty; pointedTotal += qty; - if (ap.processo?.nome) { - existing.currentProcessName = ap.processo.nome; - existing.processColor = ap.processo.cor || PROCESS_COLORS[ap.processo.nome] || '#10b981'; + + const proc = processoById.get(ap.processo_id); + if (proc) { + existing.currentProcessName = proc.nome; + existing.processColor = proc.cor || PROCESS_COLORS[proc.nome] || '#22c55e'; + } else { + existing.currentProcessName = 'Apontado'; + existing.processColor = '#22c55e'; } } }