feat: suporte a modelos 3D WRL (VRML) com extrator de pecas e apontamentos
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import { loadAndAuditIFC, LoadedIFCResult, PieceInfo, IFCQualityAudit, getColorForMaterialName, isValidMark } from '@/lib/ifc/ifcLoaderService';
|
||||
import { loadAndAuditWRL } from '@/lib/3d/wrlLoaderService';
|
||||
|
||||
export type { LoadedIFCResult, PieceInfo, IFCQualityAudit };
|
||||
export { getColorForMaterialName, isValidMark };
|
||||
|
||||
// Tipo genérico para o resultado de qualquer modelo 3D carregado (IFC ou WRL)
|
||||
export type LoadedModel3DResult = LoadedIFCResult;
|
||||
|
||||
/**
|
||||
* Detecta se os bytes iniciais ou a extensão correspondem a um arquivo VRML / WRL.
|
||||
*/
|
||||
function isWrlFormat(fileName?: string, buffer?: ArrayBuffer): boolean {
|
||||
if (fileName) {
|
||||
const lower = fileName.toLowerCase();
|
||||
if (lower.endsWith('.wrl') || lower.endsWith('.wrz') || lower.endsWith('.vrml')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (buffer && buffer.byteLength >= 10) {
|
||||
const bytes = new Uint8Array(buffer.slice(0, 100));
|
||||
|
||||
// Checa assinatura mágica GZIP (0x1f 0x8b) que arquivos .wrz frequentemente usam
|
||||
if (bytes[0] === 0x1f && bytes[1] === 0x8b) {
|
||||
// É gzip, em arquivos 3D industriais costuma ser VRML comprimido (.wrz)
|
||||
return true;
|
||||
}
|
||||
|
||||
// Checa cabeçalho ASCII VRML (ex: "#VRML V2.0 utf8" ou "#VRML V1.0 ascii")
|
||||
const headerStr = new TextDecoder('utf-8', { fatal: false }).decode(bytes);
|
||||
if (headerStr.includes('#VRML')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Função unificada para carregar e auditar qualquer modelo 3D suportado (IFC ou WRL/VRML).
|
||||
*/
|
||||
export async function loadAndAuditModel3D(
|
||||
fileOrBuffer: File | ArrayBuffer,
|
||||
fileNameHint?: string,
|
||||
onProgress?: (percent: number, step: string) => void
|
||||
): Promise<LoadedModel3DResult> {
|
||||
const fileName = fileOrBuffer instanceof File ? fileOrBuffer.name : fileNameHint;
|
||||
let bufferSample: ArrayBuffer | undefined = undefined;
|
||||
|
||||
if (fileOrBuffer instanceof ArrayBuffer) {
|
||||
bufferSample = fileOrBuffer;
|
||||
}
|
||||
|
||||
const isWrl = isWrlFormat(fileName, bufferSample);
|
||||
|
||||
if (isWrl) {
|
||||
return await loadAndAuditWRL(fileOrBuffer, onProgress);
|
||||
}
|
||||
|
||||
// Padrão: IFC
|
||||
return await loadAndAuditIFC(fileOrBuffer, onProgress);
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
import * as THREE from 'three';
|
||||
import { VRMLLoader } from 'three-stdlib';
|
||||
import { PieceInfo, LoadedIFCResult, IFCQualityAudit, getColorForMaterialName, isValidMark } from '@/lib/ifc/ifcLoaderService';
|
||||
|
||||
/**
|
||||
* Função utilitária para descompactar arquivos gzip (como .wrz ou .wrl comprimidos) no navegador.
|
||||
*/
|
||||
async function decompressGzipIfNeeded(buffer: ArrayBuffer): Promise<string> {
|
||||
const bytes = new Uint8Array(buffer);
|
||||
// Assinatura mágica do GZIP: 0x1F, 0x8B
|
||||
if (bytes.length > 2 && bytes[0] === 0x1f && bytes[1] === 0x8b) {
|
||||
if (typeof DecompressionStream !== 'undefined') {
|
||||
try {
|
||||
const stream = new Blob([buffer]).stream();
|
||||
const decompressedStream = stream.pipeThrough(new DecompressionStream('gzip'));
|
||||
const response = new Response(decompressedStream);
|
||||
const decompressedBuffer = await response.arrayBuffer();
|
||||
return new TextDecoder('utf-8', { fatal: false }).decode(decompressedBuffer);
|
||||
} catch (err) {
|
||||
console.warn('[WRL Loader] Falha ao descomprimir GZIP nativo:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new TextDecoder('utf-8', { fatal: false }).decode(buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Heurística avançada para extrair e normalizar a marca da peça a partir do nó VRML (DEF).
|
||||
* Softwares como Tekla, Advance Steel, Bocad e outros exportam identificadores variados:
|
||||
* Ex: "DEF V101 Transform", "DEF Part_V101_1 Transform", "DEF C1_V101 Transform", "DEF 01-V101 Transform"
|
||||
*/
|
||||
function parseWrlNodeName(rawName: string): {
|
||||
pieceMark: string;
|
||||
cleanMark: string;
|
||||
assemblyMark?: string;
|
||||
cleanAssemblyMark?: string;
|
||||
phase?: string;
|
||||
detectedType: string;
|
||||
} {
|
||||
let name = String(rawName || '').trim();
|
||||
|
||||
// Remove caracteres especiais de escape ou tags extras
|
||||
name = name.replace(/^DEF\s+/i, '').replace(/[\{\}\[\]"']/g, '').trim();
|
||||
|
||||
let phase: string | undefined = undefined;
|
||||
let assemblyMark: string | undefined = undefined;
|
||||
let pieceMark = name;
|
||||
|
||||
// 1. Detecção de Fase no início (ex: "01-V101", "F1_V101", "B138-01-V101")
|
||||
const phaseMatch = name.match(/^(?:[A-Za-z0-9]+-)?(?:Fase|F|Etapa)?(\d+)[-_](.+)$/i);
|
||||
if (phaseMatch) {
|
||||
phase = phaseMatch[1];
|
||||
pieceMark = phaseMatch[2];
|
||||
}
|
||||
|
||||
// 2. Remoção de prefixos comuns como "Part_", "Piece_", "Peca_", "Elem_", "Assembly_", "Conjunto_"
|
||||
pieceMark = pieceMark.replace(/^(?:Part|Piece|Peca|Elem|Item|Member|Profile|Mesh)[-_]/i, '');
|
||||
|
||||
// 3. Detecção de Conjunto e Peça (ex: "C1_V101", "ASSEMBLY1-V101", "V101-PL1")
|
||||
if (pieceMark.includes('_') || pieceMark.includes('-')) {
|
||||
const parts = pieceMark.split(/[-_]/);
|
||||
if (parts.length >= 2) {
|
||||
// Se a primeira parte parecer um conjunto (ex: C1, M1, CONJ1)
|
||||
const first = parts[0].trim();
|
||||
const second = parts.slice(1).join('-').trim();
|
||||
if (/^(?:C|M|CONJ|ASS|G)\d+$/i.test(first)) {
|
||||
assemblyMark = first;
|
||||
pieceMark = second;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Marca limpa (remove sufixo de instância numérica gerada por exportadores, ex: "V101_1", "V101-2", "V101.1", "V101#1")
|
||||
let cleanMark = pieceMark.replace(/[-_.#]\d+$/, '').trim().toUpperCase();
|
||||
if (!cleanMark) {
|
||||
cleanMark = pieceMark.toUpperCase();
|
||||
}
|
||||
|
||||
const cleanAssemblyMark = assemblyMark ? assemblyMark.replace(/[-_.#]\d+$/, '').trim().toUpperCase() : cleanMark;
|
||||
|
||||
// 5. Detecção de Tipo de Elemento Estrutural com base na marca
|
||||
let detectedType = 'ELEMENT';
|
||||
const upper = cleanMark.toUpperCase();
|
||||
if (/^(?:V|VIGA|BEAM|VIG)/.test(upper)) {
|
||||
detectedType = 'BEAM';
|
||||
} else if (/^(?:P|PILAR|COL|COLUMN|PIL)/.test(upper)) {
|
||||
detectedType = 'COLUMN';
|
||||
} else if (/^(?:PL|CH|CHAPA|PLATE|GUSSET|BASE|FLANGE)/.test(upper)) {
|
||||
detectedType = 'PLATE';
|
||||
} else if (/^(?:CV|CONTRA|BRACE|TIE|DIAG)/.test(upper)) {
|
||||
detectedType = 'BRACE';
|
||||
} else if (/^(?:TER|TERCA|PURLIN)/.test(upper)) {
|
||||
detectedType = 'PURLIN';
|
||||
} else if (/^(?:T|TRENA|TIE|TIRANTE)/.test(upper)) {
|
||||
detectedType = 'TIE';
|
||||
}
|
||||
|
||||
return {
|
||||
pieceMark: pieceMark.toUpperCase(),
|
||||
cleanMark,
|
||||
assemblyMark: assemblyMark ? assemblyMark.toUpperCase() : undefined,
|
||||
cleanAssemblyMark,
|
||||
phase,
|
||||
detectedType,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Carrega, analisa e gera estrutura idêntica ao IFC para modelos VRML (.wrl / .wrz).
|
||||
*/
|
||||
export async function loadAndAuditWRL(
|
||||
fileOrBuffer: File | ArrayBuffer,
|
||||
onProgress?: (percent: number, step: string) => void
|
||||
): Promise<LoadedIFCResult> {
|
||||
if (onProgress) onProgress(15, 'Lendo arquivo WRL (VRML)...');
|
||||
|
||||
let arrayBuffer: ArrayBuffer;
|
||||
if (fileOrBuffer instanceof File) {
|
||||
arrayBuffer = await fileOrBuffer.arrayBuffer();
|
||||
} else {
|
||||
arrayBuffer = fileOrBuffer;
|
||||
}
|
||||
|
||||
if (onProgress) onProgress(30, 'Descomprimindo e analisando formato VRML...');
|
||||
const vrmlText = await decompressGzipIfNeeded(arrayBuffer);
|
||||
|
||||
if (onProgress) onProgress(50, 'Processando geometria 3D com motor VRML...');
|
||||
const loader = new VRMLLoader();
|
||||
let vrmlScene: THREE.Scene;
|
||||
|
||||
try {
|
||||
vrmlScene = loader.parse(vrmlText, '');
|
||||
} catch (parseError: any) {
|
||||
console.error('[WRL Loader] Erro ao analisar VRML:', parseError);
|
||||
throw new Error(`Falha ao decodificar arquivo WRL: ${parseError.message || 'Formato VRML inválido'}`);
|
||||
}
|
||||
|
||||
if (onProgress) onProgress(75, 'Mapeando peças estruturais e hierarquia...');
|
||||
|
||||
const sceneGroup = new THREE.Group();
|
||||
sceneGroup.name = 'WRL_Root_Model';
|
||||
|
||||
const piecesByMark = new Map<string, PieceInfo[]>();
|
||||
const pieceByExpressID = new Map<number, PieceInfo>();
|
||||
const weldedGroupMap = new Map<number, number[]>();
|
||||
const materialsByColor = new Map<string, THREE.Material>();
|
||||
const detectedPhasesSet = new Set<string>();
|
||||
|
||||
let idCounter = 1;
|
||||
let totalMeshes = 0;
|
||||
let totalBeams = 0;
|
||||
let totalColumns = 0;
|
||||
let totalPlates = 0;
|
||||
let totalMembers = 0;
|
||||
const marksFound = new Set<string>();
|
||||
|
||||
// Helper para buscar o nome do nó mais relevante na árvore
|
||||
const resolveMeaningfulNodeName = (mesh: THREE.Mesh): string => {
|
||||
let curr: THREE.Object3D | null = mesh;
|
||||
while (curr) {
|
||||
if (curr.name && !/^(Scene|Root|Group|Transform|Shape|IndexedFaceSet)$/i.test(curr.name.trim())) {
|
||||
return curr.name.trim();
|
||||
}
|
||||
curr = curr.parent;
|
||||
}
|
||||
return mesh.name || `PECA-${idCounter}`;
|
||||
};
|
||||
|
||||
// Percorre todos os nós e malhas gerados pelo VRMLLoader
|
||||
vrmlScene.traverse((child) => {
|
||||
if ((child as THREE.Mesh).isMesh) {
|
||||
const mesh = child as THREE.Mesh;
|
||||
totalMeshes++;
|
||||
|
||||
// Garante normais de vértices para iluminação perfeita
|
||||
if (mesh.geometry) {
|
||||
if (!mesh.geometry.attributes.normal) {
|
||||
mesh.geometry.computeVertexNormals();
|
||||
}
|
||||
mesh.castShadow = true;
|
||||
mesh.receiveShadow = true;
|
||||
}
|
||||
|
||||
const nodeName = resolveMeaningfulNodeName(mesh);
|
||||
const parsed = parseWrlNodeName(nodeName);
|
||||
|
||||
const expressID = idCounter++;
|
||||
const guid = `wrl-${expressID}-${parsed.cleanMark}`;
|
||||
|
||||
if (parsed.phase) {
|
||||
detectedPhasesSet.add(parsed.phase);
|
||||
}
|
||||
|
||||
if (parsed.cleanMark && isValidMark(parsed.cleanMark)) {
|
||||
marksFound.add(parsed.cleanMark);
|
||||
}
|
||||
|
||||
// Contabiliza tipos
|
||||
if (parsed.detectedType === 'BEAM') totalBeams++;
|
||||
else if (parsed.detectedType === 'COLUMN') totalColumns++;
|
||||
else if (parsed.detectedType === 'PLATE') totalPlates++;
|
||||
else totalMembers++;
|
||||
|
||||
// Aplica material inicial elegante baseado na marca/perfil
|
||||
const baseColorHex = getColorForMaterialName(parsed.cleanMark || parsed.pieceMark);
|
||||
let material = materialsByColor.get(baseColorHex);
|
||||
if (!material) {
|
||||
material = new THREE.MeshStandardMaterial({
|
||||
color: new THREE.Color(baseColorHex),
|
||||
roughness: 0.45,
|
||||
metalness: 0.15,
|
||||
side: THREE.DoubleSide,
|
||||
});
|
||||
materialsByColor.set(baseColorHex, material);
|
||||
}
|
||||
mesh.material = material;
|
||||
|
||||
const pieceInfo: PieceInfo = {
|
||||
expressID,
|
||||
guid,
|
||||
name: nodeName,
|
||||
type: parsed.detectedType,
|
||||
pieceMark: parsed.pieceMark,
|
||||
cleanMark: parsed.cleanMark,
|
||||
assemblyMark: parsed.assemblyMark,
|
||||
cleanAssemblyMark: parsed.cleanAssemblyMark,
|
||||
phase: parsed.phase,
|
||||
mesh,
|
||||
};
|
||||
|
||||
// Injeta metadados essenciais no userData do Mesh (exatamente o que o ModelViewer3D espera)
|
||||
mesh.userData = {
|
||||
expressID,
|
||||
guid,
|
||||
pieceMark: parsed.pieceMark,
|
||||
cleanMark: parsed.cleanMark,
|
||||
assemblyMark: parsed.assemblyMark,
|
||||
cleanAssemblyMark: parsed.cleanAssemblyMark,
|
||||
phase: parsed.phase,
|
||||
type: parsed.detectedType,
|
||||
name: nodeName,
|
||||
pieceInfo,
|
||||
originalMaterial: material,
|
||||
};
|
||||
|
||||
pieceByExpressID.set(expressID, pieceInfo);
|
||||
|
||||
// Agrupa peças por marca
|
||||
const markKey = parsed.cleanMark || parsed.pieceMark;
|
||||
if (markKey) {
|
||||
const list = piecesByMark.get(markKey) || [];
|
||||
list.push(pieceInfo);
|
||||
piecesByMark.set(markKey, list);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Adiciona a cena VRML ao grupo final
|
||||
sceneGroup.add(vrmlScene);
|
||||
|
||||
// Normaliza dimensões e orientação: se o modelo estiver em milímetros (típico CAD metálico > 1000 unidades), converte para metros
|
||||
const bbox = new THREE.Box3().setFromObject(sceneGroup);
|
||||
const size = bbox.getSize(new THREE.Vector3());
|
||||
const maxDim = Math.max(size.x, size.y, size.z);
|
||||
|
||||
// Se o modelo estiver em milímetros (ex: 20 metros = 20000mm)
|
||||
if (maxDim > 500) {
|
||||
const scaleFactor = 0.001;
|
||||
sceneGroup.scale.set(scaleFactor, scaleFactor, scaleFactor);
|
||||
sceneGroup.updateMatrixWorld(true);
|
||||
}
|
||||
|
||||
// Gera auditoria de qualidade compatível
|
||||
const uniqueMarksFound = marksFound.size;
|
||||
const marksList = Array.from(marksFound).sort();
|
||||
const qualityScore: 'EXCELENTE' | 'BOM' | 'ATENÇÃO' =
|
||||
uniqueMarksFound > 0 ? (uniqueMarksFound >= 10 ? 'EXCELENTE' : 'BOM') : 'ATENÇÃO';
|
||||
|
||||
const audit: IFCQualityAudit = {
|
||||
schema: 'VRML 2.0 (WRL)',
|
||||
viewDefinition: 'Virtual Reality Modeling Language 3D',
|
||||
isEM11: false,
|
||||
totalAssemblies: marksFound.size,
|
||||
totalBeams,
|
||||
totalColumns,
|
||||
totalMembers,
|
||||
totalPlates,
|
||||
totalFasteners: 0,
|
||||
totalStructuralElements: totalMeshes,
|
||||
totalWeldedGroups: 0,
|
||||
uniqueMarksFound,
|
||||
marksList,
|
||||
qualityScore,
|
||||
qualityMessage:
|
||||
uniqueMarksFound > 0
|
||||
? `Modelo WRL carregado com sucesso. ${totalMeshes} malhas processadas e ${uniqueMarksFound} marcas identificadas para apontamento.`
|
||||
: `Modelo WRL carregado com ${totalMeshes} malhas. Nenhuma marca nominal identificada na geometria.`,
|
||||
fallbackActive: false,
|
||||
};
|
||||
|
||||
if (onProgress) onProgress(100, 'Modelo WRL pronto para visualização!');
|
||||
|
||||
return {
|
||||
sceneGroup,
|
||||
audit,
|
||||
piecesByMark,
|
||||
pieceByExpressID,
|
||||
weldedGroupMap,
|
||||
materialsByColor,
|
||||
totalMeshes,
|
||||
detectedPhases: Array.from(detectedPhasesSet).sort(),
|
||||
};
|
||||
}
|
||||
@@ -103,7 +103,7 @@ export async function clearCachedIFCModel(ofNumber: string): Promise<void> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Realiza o upload do arquivo IFC para o Supabase Storage e salva o vínculo na OF.
|
||||
* Realiza o upload do arquivo de modelo 3D (IFC ou WRL) para o Supabase Storage e salva o vínculo na OF.
|
||||
*/
|
||||
export async function uploadIFCToCloud(
|
||||
file: File,
|
||||
@@ -113,9 +113,11 @@ export async function uploadIFCToCloud(
|
||||
): Promise<{ publicUrl: string; filePath: string }> {
|
||||
const sanitizedOf = ofNumber.replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||
const cleanFileName = file.name.replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||
const filePath = `ifc/${sanitizedOf}/${cleanFileName}`;
|
||||
const ext = file.name.split('.').pop()?.toLowerCase();
|
||||
const folder = (ext === 'wrl' || ext === 'wrz') ? 'wrl' : 'ifc';
|
||||
const filePath = `${folder}/${sanitizedOf}/${cleanFileName}`;
|
||||
|
||||
if (onProgress) onProgress('Enviando arquivo IFC para a nuvem...', 20);
|
||||
if (onProgress) onProgress('Enviando modelo 3D para a nuvem...', 20);
|
||||
|
||||
// 1. Upload no Supabase Storage (bucket modelos-3d)
|
||||
const { error: uploadError } = await supabase.storage
|
||||
@@ -126,7 +128,7 @@ export async function uploadIFCToCloud(
|
||||
});
|
||||
|
||||
if (uploadError) {
|
||||
console.error('[IFC Storage] Erro no upload:', uploadError);
|
||||
console.error('[Storage] Erro no upload:', uploadError);
|
||||
throw new Error(`Falha ao salvar no storage: ${uploadError.message}`);
|
||||
}
|
||||
|
||||
@@ -151,7 +153,7 @@ export async function uploadIFCToCloud(
|
||||
.eq('num_of', ofNumber);
|
||||
|
||||
if (dbError) {
|
||||
console.warn('[IFC Storage] Aviso ao atualizar ordens_fabricacao:', dbError);
|
||||
console.warn('[Storage] Aviso ao atualizar ordens_fabricacao:', dbError);
|
||||
}
|
||||
|
||||
// 4. Inserir histórico em of_modelos_3d
|
||||
@@ -168,7 +170,7 @@ export async function uploadIFCToCloud(
|
||||
updated_at: new Date().toISOString()
|
||||
});
|
||||
} catch (histErr) {
|
||||
console.debug('[IFC Storage] Registro histórico:', histErr);
|
||||
console.debug('[Storage] Registro histórico:', histErr);
|
||||
}
|
||||
|
||||
// 5. Salva no cache local do navegador
|
||||
@@ -176,14 +178,16 @@ export async function uploadIFCToCloud(
|
||||
const buffer = await file.arrayBuffer();
|
||||
await cacheIFCModel(ofNumber, publicUrl, buffer);
|
||||
} catch (cErr) {
|
||||
console.debug('[IFC Storage] Cache local:', cErr);
|
||||
console.debug('[Storage] Cache local:', cErr);
|
||||
}
|
||||
|
||||
if (onProgress) onProgress('Modelo IFC salvo com sucesso!', 100);
|
||||
if (onProgress) onProgress('Modelo 3D salvo com sucesso!', 100);
|
||||
|
||||
return { publicUrl, filePath };
|
||||
}
|
||||
|
||||
export const uploadModel3DToCloud = uploadIFCToCloud;
|
||||
|
||||
/**
|
||||
* Baixa e armazena em cache o modelo IFC salvo para a OF.
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { loadAndAuditIFC, LoadedIFCResult, IFCQualityAudit } from '@/lib/ifc/ifcLoaderService';
|
||||
import { loadAndAuditModel3D, LoadedModel3DResult, IFCQualityAudit } from '@/lib/3d/modelLoaderService';
|
||||
import { uploadIFCToCloud, fetchSavedIFCModel, removeSavedIFCModel } from '@/lib/ifc/ifcStorageService';
|
||||
import { ModelViewer3D } from '@/components/viewer3d/ModelViewer3D';
|
||||
import { IFCQualityModal } from '@/components/viewer3d/IFCQualityModal';
|
||||
@@ -58,7 +58,7 @@ export default function Visualizador3D() {
|
||||
const [progressoOF, setProgressoOF] = useState<number>(0);
|
||||
|
||||
// 3D Model and Quality State
|
||||
const [modelData, setModelData] = useState<LoadedIFCResult | null>(null);
|
||||
const [modelData, setModelData] = useState<LoadedModel3DResult | null>(null);
|
||||
const [auditData, setAuditData] = useState<IFCQualityAudit | null>(null);
|
||||
const [isAuditModalOpen, setIsAuditModalOpen] = useState<boolean>(false);
|
||||
const [loadingStep, setLoadingStep] = useState<string>('');
|
||||
@@ -258,10 +258,10 @@ export default function Visualizador3D() {
|
||||
setLoadingStep(step);
|
||||
});
|
||||
|
||||
setLoadingStep('Decodificando entidades e montagens IFC...');
|
||||
setLoadingStep('Decodificando entidades e montagens 3D...');
|
||||
setLoadingPercent(55);
|
||||
|
||||
const result = await loadAndAuditIFC(buffer, (percent, step) => {
|
||||
const result = await loadAndAuditModel3D(buffer, filename || undefined, (percent, step) => {
|
||||
setLoadingPercent(55 + Math.round(percent * 0.45));
|
||||
setLoadingStep(step);
|
||||
});
|
||||
@@ -342,7 +342,7 @@ export default function Visualizador3D() {
|
||||
|
||||
try {
|
||||
// 1. Processa e audita localmente para renderização imediata
|
||||
const result = await loadAndAuditIFC(file, (percent, step) => {
|
||||
const result = await loadAndAuditModel3D(file, file.name, (percent, step) => {
|
||||
setLoadingPercent(Math.min(percent, 70));
|
||||
setLoadingStep(step);
|
||||
});
|
||||
@@ -393,11 +393,11 @@ export default function Visualizador3D() {
|
||||
);
|
||||
|
||||
toast({
|
||||
title: 'Modelo IFC Gravado no Banco!',
|
||||
title: 'Modelo 3D Gravado no Banco!',
|
||||
description: `O arquivo "${file.name}" foi salvo com sucesso e carregará automaticamente ao entrar na OF ${selectedOF}.`,
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error('Erro ao carregar e salvar IFC:', err);
|
||||
console.error('Erro ao carregar e salvar Modelo 3D:', err);
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: 'Erro no Carregamento',
|
||||
@@ -411,7 +411,7 @@ export default function Visualizador3D() {
|
||||
|
||||
// Remover modelo salvo
|
||||
const handleRemoveSavedModel = async () => {
|
||||
if (!confirm(`Deseja remover o modelo IFC vinculado à OF ${selectedOF}?`)) return;
|
||||
if (!confirm(`Deseja remover o modelo 3D vinculado à OF ${selectedOF}?`)) return;
|
||||
|
||||
try {
|
||||
await removeSavedIFCModel(selectedOF);
|
||||
@@ -524,7 +524,7 @@ export default function Visualizador3D() {
|
||||
<Cloud className="w-3.5 h-3.5 text-emerald-700 dark:text-emerald-400" />
|
||||
<span
|
||||
className="text-emerald-900 dark:text-emerald-300 font-mono font-bold max-w-[170px] truncate"
|
||||
title={currentOFInfo?.ifc_filename || 'Modelo IFC Vinculado'}
|
||||
title={currentOFInfo?.ifc_filename || 'Modelo 3D Vinculado'}
|
||||
>
|
||||
{currentOFInfo?.ifc_filename || 'Modelo Salvo'}
|
||||
</span>
|
||||
@@ -585,7 +585,7 @@ export default function Visualizador3D() {
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".ifc,.gltf,.glb"
|
||||
accept=".ifc,.wrl,.wrz,.gltf,.glb"
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
/>
|
||||
@@ -603,7 +603,7 @@ export default function Visualizador3D() {
|
||||
) : (
|
||||
<>
|
||||
<Upload className="w-4 h-4" />
|
||||
<span>{hasSavedModel ? 'Substituir Modelo IFC' : 'Carregar Modelo IFC'}</span>
|
||||
<span>{hasSavedModel ? 'Substituir Modelo 3D' : 'Carregar Modelo 3D (IFC/WRL)'}</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
@@ -687,9 +687,9 @@ export default function Visualizador3D() {
|
||||
<Box className="w-16 h-16 stroke-[1.2]" />
|
||||
</div>
|
||||
<div className="space-y-1 max-w-md">
|
||||
<h2 className="text-lg font-bold text-slate-900 dark:text-white">Nenhum modelo IFC carregado para {selectedOF}</h2>
|
||||
<h2 className="text-lg font-bold text-slate-900 dark:text-white">Nenhum modelo 3D carregado para {selectedOF}</h2>
|
||||
<p className="text-xs text-slate-700 font-bold dark:text-slate-300 dark:font-medium leading-relaxed">
|
||||
Clique no botão <span className="text-cyan-600 dark:text-cyan-300 font-semibold">"Carregar Modelo IFC"</span> acima para importar o arquivo de fabricação desta obra. Ele será gravado no banco de dados e recarregado automaticamente nas próximas visitas.
|
||||
Clique no botão <span className="text-cyan-600 dark:text-cyan-300 font-semibold">"Carregar Modelo 3D (IFC/WRL)"</span> acima para importar o arquivo de fabricação desta obra. Ele será gravado no banco de dados e recarregado automaticamente nas próximas visitas.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { OFAtiva } from '@/hooks/useOFsAtivas';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { loadAndAuditIFC, LoadedIFCResult } from '@/lib/ifc/ifcLoaderService';
|
||||
import { loadAndAuditModel3D, LoadedModel3DResult } from '@/lib/3d/modelLoaderService';
|
||||
import { fetchSavedIFCModel } from '@/lib/ifc/ifcStorageService';
|
||||
import { SmartModelViewer3D, ProductionPieceStatus } from '@/components/viewer3d/SmartModelViewer3D';
|
||||
import { smartAudio } from '@/utils/smartAudio';
|
||||
@@ -17,7 +17,7 @@ export const SmartViewer3DFlow: React.FC<SmartViewer3DFlowProps> = ({
|
||||
obra,
|
||||
onVoltarHub,
|
||||
}) => {
|
||||
const [modelData, setModelData] = useState<LoadedIFCResult | null>(null);
|
||||
const [modelData, setModelData] = useState<LoadedModel3DResult | null>(null);
|
||||
const [phasesList, setPhasesList] = useState<string[]>([]);
|
||||
const [productionMap, setProductionMap] = useState<Map<string, ProductionPieceStatus>>(new Map());
|
||||
|
||||
@@ -186,7 +186,7 @@ export const SmartViewer3DFlow: React.FC<SmartViewer3DFlowProps> = ({
|
||||
setLoadingStep('Decodificando montagens e geometrias do modelo...');
|
||||
setLoadingPercent(68);
|
||||
|
||||
const result = await loadAndAuditIFC(buffer, (pct, msg) => {
|
||||
const result = await loadAndAuditModel3D(buffer, ifcFilename || undefined, (pct, msg) => {
|
||||
setLoadingPercent(68 + Math.round(pct * 0.32));
|
||||
setLoadingStep(msg);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user