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(
|
export async function uploadIFCToCloud(
|
||||||
file: File,
|
file: File,
|
||||||
@@ -113,9 +113,11 @@ export async function uploadIFCToCloud(
|
|||||||
): Promise<{ publicUrl: string; filePath: string }> {
|
): Promise<{ publicUrl: string; filePath: string }> {
|
||||||
const sanitizedOf = ofNumber.replace(/[^a-zA-Z0-9_-]/g, '_');
|
const sanitizedOf = ofNumber.replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||||
const cleanFileName = file.name.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)
|
// 1. Upload no Supabase Storage (bucket modelos-3d)
|
||||||
const { error: uploadError } = await supabase.storage
|
const { error: uploadError } = await supabase.storage
|
||||||
@@ -126,7 +128,7 @@ export async function uploadIFCToCloud(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (uploadError) {
|
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}`);
|
throw new Error(`Falha ao salvar no storage: ${uploadError.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,7 +153,7 @@ export async function uploadIFCToCloud(
|
|||||||
.eq('num_of', ofNumber);
|
.eq('num_of', ofNumber);
|
||||||
|
|
||||||
if (dbError) {
|
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
|
// 4. Inserir histórico em of_modelos_3d
|
||||||
@@ -168,7 +170,7 @@ export async function uploadIFCToCloud(
|
|||||||
updated_at: new Date().toISOString()
|
updated_at: new Date().toISOString()
|
||||||
});
|
});
|
||||||
} catch (histErr) {
|
} catch (histErr) {
|
||||||
console.debug('[IFC Storage] Registro histórico:', histErr);
|
console.debug('[Storage] Registro histórico:', histErr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Salva no cache local do navegador
|
// 5. Salva no cache local do navegador
|
||||||
@@ -176,14 +178,16 @@ export async function uploadIFCToCloud(
|
|||||||
const buffer = await file.arrayBuffer();
|
const buffer = await file.arrayBuffer();
|
||||||
await cacheIFCModel(ofNumber, publicUrl, buffer);
|
await cacheIFCModel(ofNumber, publicUrl, buffer);
|
||||||
} catch (cErr) {
|
} 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 };
|
return { publicUrl, filePath };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const uploadModel3DToCloud = uploadIFCToCloud;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Baixa e armazena em cache o modelo IFC salvo para a OF.
|
* 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 React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
|
||||||
import { supabase } from '@/integrations/supabase/client';
|
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 { uploadIFCToCloud, fetchSavedIFCModel, removeSavedIFCModel } from '@/lib/ifc/ifcStorageService';
|
||||||
import { ModelViewer3D } from '@/components/viewer3d/ModelViewer3D';
|
import { ModelViewer3D } from '@/components/viewer3d/ModelViewer3D';
|
||||||
import { IFCQualityModal } from '@/components/viewer3d/IFCQualityModal';
|
import { IFCQualityModal } from '@/components/viewer3d/IFCQualityModal';
|
||||||
@@ -58,7 +58,7 @@ export default function Visualizador3D() {
|
|||||||
const [progressoOF, setProgressoOF] = useState<number>(0);
|
const [progressoOF, setProgressoOF] = useState<number>(0);
|
||||||
|
|
||||||
// 3D Model and Quality State
|
// 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 [auditData, setAuditData] = useState<IFCQualityAudit | null>(null);
|
||||||
const [isAuditModalOpen, setIsAuditModalOpen] = useState<boolean>(false);
|
const [isAuditModalOpen, setIsAuditModalOpen] = useState<boolean>(false);
|
||||||
const [loadingStep, setLoadingStep] = useState<string>('');
|
const [loadingStep, setLoadingStep] = useState<string>('');
|
||||||
@@ -258,10 +258,10 @@ export default function Visualizador3D() {
|
|||||||
setLoadingStep(step);
|
setLoadingStep(step);
|
||||||
});
|
});
|
||||||
|
|
||||||
setLoadingStep('Decodificando entidades e montagens IFC...');
|
setLoadingStep('Decodificando entidades e montagens 3D...');
|
||||||
setLoadingPercent(55);
|
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));
|
setLoadingPercent(55 + Math.round(percent * 0.45));
|
||||||
setLoadingStep(step);
|
setLoadingStep(step);
|
||||||
});
|
});
|
||||||
@@ -342,7 +342,7 @@ export default function Visualizador3D() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Processa e audita localmente para renderização imediata
|
// 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));
|
setLoadingPercent(Math.min(percent, 70));
|
||||||
setLoadingStep(step);
|
setLoadingStep(step);
|
||||||
});
|
});
|
||||||
@@ -393,11 +393,11 @@ export default function Visualizador3D() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
toast({
|
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}.`,
|
description: `O arquivo "${file.name}" foi salvo com sucesso e carregará automaticamente ao entrar na OF ${selectedOF}.`,
|
||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Erro ao carregar e salvar IFC:', err);
|
console.error('Erro ao carregar e salvar Modelo 3D:', err);
|
||||||
toast({
|
toast({
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
title: 'Erro no Carregamento',
|
title: 'Erro no Carregamento',
|
||||||
@@ -411,7 +411,7 @@ export default function Visualizador3D() {
|
|||||||
|
|
||||||
// Remover modelo salvo
|
// Remover modelo salvo
|
||||||
const handleRemoveSavedModel = async () => {
|
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 {
|
try {
|
||||||
await removeSavedIFCModel(selectedOF);
|
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" />
|
<Cloud className="w-3.5 h-3.5 text-emerald-700 dark:text-emerald-400" />
|
||||||
<span
|
<span
|
||||||
className="text-emerald-900 dark:text-emerald-300 font-mono font-bold max-w-[170px] truncate"
|
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'}
|
{currentOFInfo?.ifc_filename || 'Modelo Salvo'}
|
||||||
</span>
|
</span>
|
||||||
@@ -585,7 +585,7 @@ export default function Visualizador3D() {
|
|||||||
<input
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
type="file"
|
type="file"
|
||||||
accept=".ifc,.gltf,.glb"
|
accept=".ifc,.wrl,.wrz,.gltf,.glb"
|
||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
className="hidden"
|
className="hidden"
|
||||||
/>
|
/>
|
||||||
@@ -603,7 +603,7 @@ export default function Visualizador3D() {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Upload className="w-4 h-4" />
|
<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>
|
</button>
|
||||||
@@ -687,9 +687,9 @@ export default function Visualizador3D() {
|
|||||||
<Box className="w-16 h-16 stroke-[1.2]" />
|
<Box className="w-16 h-16 stroke-[1.2]" />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1 max-w-md">
|
<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">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { OFAtiva } from '@/hooks/useOFsAtivas';
|
import { OFAtiva } from '@/hooks/useOFsAtivas';
|
||||||
import { supabase } from '@/integrations/supabase/client';
|
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 { fetchSavedIFCModel } from '@/lib/ifc/ifcStorageService';
|
||||||
import { SmartModelViewer3D, ProductionPieceStatus } from '@/components/viewer3d/SmartModelViewer3D';
|
import { SmartModelViewer3D, ProductionPieceStatus } from '@/components/viewer3d/SmartModelViewer3D';
|
||||||
import { smartAudio } from '@/utils/smartAudio';
|
import { smartAudio } from '@/utils/smartAudio';
|
||||||
@@ -17,7 +17,7 @@ export const SmartViewer3DFlow: React.FC<SmartViewer3DFlowProps> = ({
|
|||||||
obra,
|
obra,
|
||||||
onVoltarHub,
|
onVoltarHub,
|
||||||
}) => {
|
}) => {
|
||||||
const [modelData, setModelData] = useState<LoadedIFCResult | null>(null);
|
const [modelData, setModelData] = useState<LoadedModel3DResult | null>(null);
|
||||||
const [phasesList, setPhasesList] = useState<string[]>([]);
|
const [phasesList, setPhasesList] = useState<string[]>([]);
|
||||||
const [productionMap, setProductionMap] = useState<Map<string, ProductionPieceStatus>>(new Map());
|
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...');
|
setLoadingStep('Decodificando montagens e geometrias do modelo...');
|
||||||
setLoadingPercent(68);
|
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));
|
setLoadingPercent(68 + Math.round(pct * 0.32));
|
||||||
setLoadingStep(msg);
|
setLoadingStep(msg);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user