feat: suporte a modelos 3D WRL (VRML) com extrator de pecas e apontamentos

This commit is contained in:
2026-09-24 11:51:52 +00:00
parent ebaf820e5c
commit 3abc47e3c4
5 changed files with 404 additions and 24 deletions
+63
View File
@@ -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);
}
+313
View File
@@ -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(),
};
}
+12 -8
View File
@@ -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.
*/
+13 -13
View File
@@ -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);
});