feat(3d-viewer): enriquecer identificacao de pecas WRL com marca principal e perfil oficial

This commit is contained in:
2026-10-15 16:05:27 -03:00
parent a5e82bf440
commit 16414ca4ba
5 changed files with 205 additions and 59 deletions
+137 -31
View File
@@ -18,6 +18,8 @@ interface ProductionPieceStatus {
currentProcessName?: string; currentProcessName?: string;
processColor?: string; processColor?: string;
processOrdem?: number; processOrdem?: number;
perfilPrincipal?: string;
descricao?: string;
} }
/** /**
@@ -67,6 +69,42 @@ function findProductionDataForPiece(
return undefined; return undefined;
} }
/**
* Helper para formatar a Marca Principal da Peça no padrão de Engenharia (OF-Fase-Marca, ex: B138-2-1 / B135-2-8)
* e obter o Perfil/Bitola oficial a partir do banco de dados ou do modelo 3D.
*/
function resolvePieceEngineeringDetails(
rawPieceMark: string,
rawSection: string | undefined,
phase: string | undefined,
singlePartMark: string | undefined,
prod: ProductionPieceStatus | undefined,
selectedOF: string | undefined
): {
displayMark: string;
displaySection: string;
} {
const baseMark = prod?.marca || rawPieceMark || 'Peça';
let displayMark = baseMark;
// Se tivermos OF e Fase e a marca for pura (ex: "1" ou "8"), formata como "B138-2-1" idêntico ao Print 2 "B135-2-8"
if (selectedOF && phase && !baseMark.toUpperCase().startsWith(selectedOF.toUpperCase())) {
displayMark = `${selectedOF}-${phase}-${baseMark}`;
}
// Perfil / Bitola oficial: prioriza banco de produção (TS_ERP.pecas.perfil_principal), seguido de section limpo do modelo
let displaySection = prod?.perfilPrincipal || '';
if (!displaySection || displaySection === '1001' || displaySection === 'Perfil') {
if (rawSection && rawSection !== '1001' && rawSection !== 'Perfil') {
displaySection = rawSection;
} else {
displaySection = prod?.descricao || 'Perfil';
}
}
return { displayMark, displaySection };
}
interface ModelViewer3DProps { interface ModelViewer3DProps {
modelData: LoadedIFCResult | null; modelData: LoadedIFCResult | null;
productionData?: Map<string, ProductionPieceStatus>; productionData?: Map<string, ProductionPieceStatus>;
@@ -940,17 +978,40 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
const pmark = hitMesh.userData.pieceMark; const pmark = hitMesh.userData.pieceMark;
if (modelData && expressID !== undefined) { if (modelData && expressID !== undefined) {
const piece = modelData.pieceByExpressID.get(expressID) || { const rawPiece = modelData.pieceByExpressID.get(expressID);
const rawMark = hitMesh.userData.pieceMark || rawPiece?.pieceMark || '';
const phase = hitMesh.userData.phase || rawPiece?.phase;
const singlePartMark = hitMesh.userData.singlePartMark || rawPiece?.singlePartMark;
const prod = findProductionDataForPiece(productionData, {
pieceMark: rawMark,
cleanMark: hitMesh.userData.cleanMark || rawPiece?.cleanMark,
assemblyMark: hitMesh.userData.assemblyMark || rawPiece?.assemblyMark,
cleanAssemblyMark: hitMesh.userData.cleanAssemblyMark || rawPiece?.cleanAssemblyMark,
phase,
}, selectedOF);
const { displayMark, displaySection } = resolvePieceEngineeringDetails(
rawMark,
hitMesh.userData.section || rawPiece?.section,
phase,
singlePartMark,
prod,
selectedOF
);
const piece: PieceInfo = {
expressID, expressID,
guid: hitMesh.userData.guid || '', guid: hitMesh.userData.guid || rawPiece?.guid || '',
name: hitMesh.userData.name || hitMesh.userData.section || 'Peça', name: hitMesh.userData.name || rawPiece?.name || 'Peça',
type: hitMesh.userData.type || 'PIECE', type: hitMesh.userData.type || rawPiece?.type || 'PIECE',
pieceMark: pmark, pieceMark: displayMark,
cleanMark: hitMesh.userData.cleanMark, cleanMark: hitMesh.userData.cleanMark || rawPiece?.cleanMark,
assemblyMark: hitMesh.userData.assemblyMark, assemblyMark: hitMesh.userData.assemblyMark || rawPiece?.assemblyMark,
cleanAssemblyMark: hitMesh.userData.cleanAssemblyMark, cleanAssemblyMark: hitMesh.userData.cleanAssemblyMark || rawPiece?.cleanAssemblyMark,
section: hitMesh.userData.section || hitMesh.userData.name, section: displaySection,
phase: hitMesh.userData.phase, phase,
singlePartMark,
}; };
setHoveredPiece(piece); setHoveredPiece(piece);
setHoverPosition({ x: clientX, y: clientY }); setHoverPosition({ x: clientX, y: clientY });
@@ -1040,18 +1101,40 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
}); });
} }
const pmark = hitMesh.userData.pieceMark; const rawPiece = modelData?.pieceByExpressID.get(targetIfcId);
const piece: PieceInfo = (modelData && modelData.pieceByExpressID.get(targetIfcId)) || { const rawMark = hitMesh.userData.pieceMark || rawPiece?.pieceMark || '';
const phase = hitMesh.userData.phase || rawPiece?.phase;
const singlePartMark = hitMesh.userData.singlePartMark || rawPiece?.singlePartMark;
const prod = findProductionDataForPiece(productionData, {
pieceMark: rawMark,
cleanMark: hitMesh.userData.cleanMark || rawPiece?.cleanMark,
assemblyMark: hitMesh.userData.assemblyMark || rawPiece?.assemblyMark,
cleanAssemblyMark: hitMesh.userData.cleanAssemblyMark || rawPiece?.cleanAssemblyMark,
phase,
}, selectedOF);
const { displayMark, displaySection } = resolvePieceEngineeringDetails(
rawMark,
hitMesh.userData.section || rawPiece?.section,
phase,
singlePartMark,
prod,
selectedOF
);
const piece: PieceInfo = {
expressID: targetIfcId, expressID: targetIfcId,
guid: hitMesh.userData.guid || '', guid: hitMesh.userData.guid || rawPiece?.guid || '',
name: hitMesh.userData.name || hitMesh.userData.section || 'Peça', name: hitMesh.userData.name || rawPiece?.name || 'Peça',
type: hitMesh.userData.type || 'PIECE', type: hitMesh.userData.type || rawPiece?.type || 'PIECE',
pieceMark: pmark, pieceMark: displayMark,
cleanMark: hitMesh.userData.cleanMark, cleanMark: hitMesh.userData.cleanMark || rawPiece?.cleanMark,
assemblyMark: hitMesh.userData.assemblyMark, assemblyMark: hitMesh.userData.assemblyMark || rawPiece?.assemblyMark,
cleanAssemblyMark: hitMesh.userData.cleanAssemblyMark, cleanAssemblyMark: hitMesh.userData.cleanAssemblyMark || rawPiece?.cleanAssemblyMark,
section: hitMesh.userData.section, section: displaySection,
phase: hitMesh.userData.phase, phase,
singlePartMark,
}; };
nextMap.set(targetIfcId, { nextMap.set(targetIfcId, {
@@ -1165,17 +1248,40 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
m.material = magentaMaterialRef.current; m.material = magentaMaterialRef.current;
}); });
const piece: PieceInfo = (modelData && modelData.pieceByExpressID.get(elId)) || { const rawPiece = modelData?.pieceByExpressID.get(elId);
const rawMark = sampleMesh.userData.pieceMark || rawPiece?.pieceMark || '';
const phase = sampleMesh.userData.phase || rawPiece?.phase || targetPhase;
const singlePartMark = sampleMesh.userData.singlePartMark || rawPiece?.singlePartMark;
const prod = findProductionDataForPiece(productionData, {
pieceMark: rawMark,
cleanMark: sampleMesh.userData.cleanMark || rawPiece?.cleanMark,
assemblyMark: sampleMesh.userData.assemblyMark || rawPiece?.assemblyMark,
cleanAssemblyMark: sampleMesh.userData.cleanAssemblyMark || rawPiece?.cleanAssemblyMark,
phase,
}, selectedOF);
const { displayMark, displaySection } = resolvePieceEngineeringDetails(
rawMark,
sampleMesh.userData.section || rawPiece?.section,
phase,
singlePartMark,
prod,
selectedOF
);
const piece: PieceInfo = {
expressID: elId, expressID: elId,
guid: sampleMesh.userData.guid || '', guid: sampleMesh.userData.guid || rawPiece?.guid || '',
name: sampleMesh.userData.name || sampleMesh.userData.section || 'Peça', name: sampleMesh.userData.name || rawPiece?.name || 'Peça',
type: sampleMesh.userData.type || 'PIECE', type: sampleMesh.userData.type || rawPiece?.type || 'PIECE',
pieceMark: sampleMesh.userData.pieceMark, pieceMark: displayMark,
cleanMark: sampleMesh.userData.cleanMark, cleanMark: sampleMesh.userData.cleanMark || rawPiece?.cleanMark,
assemblyMark: sampleMesh.userData.assemblyMark, assemblyMark: sampleMesh.userData.assemblyMark || rawPiece?.assemblyMark,
cleanAssemblyMark: sampleMesh.userData.cleanAssemblyMark, cleanAssemblyMark: sampleMesh.userData.cleanAssemblyMark || rawPiece?.cleanAssemblyMark,
section: sampleMesh.userData.section, section: displaySection,
phase: sampleMesh.userData.phase || targetPhase, phase,
singlePartMark,
}; };
nextMap.set(elId, { nextMap.set(elId, {
@@ -62,6 +62,15 @@ export const PieceHoverTooltip: React.FC<PieceHoverTooltipProps> = ({
</div> </div>
)} )}
{piece.singlePartMark && piece.singlePartMark !== piece.pieceMark && (
<div className="flex items-center justify-between text-slate-300">
<span className="text-slate-400">Sub-peça / Corte:</span>
<span className="font-semibold text-slate-200 font-mono bg-slate-950/60 px-2 py-0.5 rounded border border-slate-800">
Pos. {piece.singlePartMark}
</span>
</div>
)}
{piece.childrenIDs && piece.childrenIDs.length > 0 && ( {piece.childrenIDs && piece.childrenIDs.length > 0 && (
<div className="flex items-center justify-between text-slate-300"> <div className="flex items-center justify-between text-slate-300">
<span className="text-slate-400">Componentes:</span> <span className="text-slate-400">Componentes:</span>
+56 -28
View File
@@ -1,6 +1,6 @@
import * as THREE from 'three'; import * as THREE from 'three';
import { VRMLLoader } from 'three-stdlib'; import { VRMLLoader } from 'three-stdlib';
import { PieceInfo, LoadedIFCResult, IFCQualityAudit, getColorForMaterialName, isValidMark } from '@/lib/ifc/ifcLoaderService'; import { PieceInfo, LoadedIFCResult, IFCQualityAudit, getColorForMaterialName, isValidMark, isSinglePartCutNumber } from '@/lib/ifc/ifcLoaderService';
/** /**
* Função utilitária para descompactar arquivos gzip (como .wrz ou .wrl comprimidos) no navegador. * Função utilitária para descompactar arquivos gzip (como .wrz ou .wrl comprimidos) no navegador.
@@ -57,6 +57,9 @@ function extractPhaseFromAncestors(ancestors: string[]): string | undefined {
return undefined; return undefined;
} }
/**
* Analisa e normaliza a marca da peça, marca do conjunto e tipo estrutural com suporte completo a Tekla / Bocad.
*/
/** /**
* Analisa e normaliza a marca da peça, marca do conjunto e tipo estrutural com suporte completo a Tekla / Bocad. * Analisa e normaliza a marca da peça, marca do conjunto e tipo estrutural com suporte completo a Tekla / Bocad.
*/ */
@@ -65,10 +68,15 @@ function parseWrlNodeName(
inheritedPhase?: string, inheritedPhase?: string,
ancestorAssembly?: string ancestorAssembly?: string
): { ): {
mainMark: string;
cleanMainMark: string;
pieceMark: string; pieceMark: string;
cleanMark: string; cleanMark: string;
assemblyMark?: string; assemblyMark?: string;
cleanAssemblyMark?: string; cleanAssemblyMark?: string;
singlePartMark?: string;
profile?: string;
section?: string;
phase?: string; phase?: string;
detectedType: string; detectedType: string;
} { } {
@@ -79,20 +87,22 @@ function parseWrlNodeName(
let assemblyMark = ancestorAssembly ? ancestorAssembly.trim() : undefined; let assemblyMark = ancestorAssembly ? ancestorAssembly.trim() : undefined;
let pieceMark = name; let pieceMark = name;
let singlePartMark: string | undefined = undefined;
let profile = ''; let profile = '';
const parts = name.split('_'); const parts = name.split('_');
// CASO 1: Tekla composto com 2 números no início: [Conjunto]_[Peca]_[Perfil] (ex: "121_1127_TUBO44x45X3_121293") // CASO 1: Tekla composto com 2 números no início: [Conjunto]_[Peca]_[Perfil] (ex: "1_1001_W360X44_0", "121_1127_TUBO44x45X3_121293")
if (parts.length >= 3 && /^\d+$/.test(parts[0]) && /^\d+$/.test(parts[1])) { if (parts.length >= 3 && /^\d+$/.test(parts[0]) && /^\d+$/.test(parts[1])) {
assemblyMark = parts[0]; assemblyMark = parts[0];
pieceMark = parts[1]; pieceMark = parts[1];
profile = parts[2]; singlePartMark = parts[1];
let rest = parts.slice(2).join('_');
rest = rest.replace(/_\d{5,}$/, '');
profile = rest;
} }
// CASO 2: Tekla padrão [MarcaPeca]_[Perfil] (ex: "5_W360X44_0", "6_W360X32_9", "1_W360X44_0", "6_VIGA") // CASO 2: Tekla padrão [MarcaPeca]_[Perfil] (ex: "5_W360X44_0", "6_W360X32_9", "1_W360X44_0", "6_VIGA")
else if (parts.length >= 2 && /^\d+$/.test(parts[0])) { else if (parts.length >= 2 && /^\d+$/.test(parts[0])) {
// Se o primeiro segmento é numérico e o segundo não é outro número (é perfil ou descrição),
// a marca real da peça é o primeiro segmento (ex: "5", "6", "1")!
pieceMark = parts[0]; pieceMark = parts[0];
assemblyMark = parts[0]; assemblyMark = parts[0];
profile = parts.slice(1).join('_'); profile = parts.slice(1).join('_');
@@ -100,7 +110,7 @@ function parseWrlNodeName(
// CASO 3: Prefixo de conjunto alfanumérico (ex: "V101_1_CHAPA", "P1_2_CANTON") // CASO 3: Prefixo de conjunto alfanumérico (ex: "V101_1_CHAPA", "P1_2_CANTON")
else if (parts.length >= 2 && /^[A-Za-z]+\d+$/i.test(parts[0])) { else if (parts.length >= 2 && /^[A-Za-z]+\d+$/i.test(parts[0])) {
assemblyMark = parts[0]; assemblyMark = parts[0];
pieceMark = parts[0]; // ou parts[1] pieceMark = parts[0];
profile = parts.slice(1).join('_'); profile = parts.slice(1).join('_');
} }
// CASO 4: Parafusos / Fixadores // CASO 4: Parafusos / Fixadores
@@ -113,18 +123,32 @@ function parseWrlNodeName(
pieceMark = pieceMark.replace(/^(?:Part|Piece|Peca|Elem|Item|Member|Profile|Mesh)[-_]/i, ''); pieceMark = pieceMark.replace(/^(?:Part|Piece|Peca|Elem|Item|Member|Profile|Mesh)[-_]/i, '');
} }
// Normalização do Perfil / Bitola (ex: W360X44_0 -> W360X44.0, W360X32_9 -> W360X32.9)
if (profile) {
profile = profile.replace(/_(\d+)$/, '.$1').replace(/_/g, ' ').trim();
}
let cleanMark = pieceMark.replace(/[-_.#]\d+$/, '').trim().toUpperCase(); let cleanMark = pieceMark.replace(/[-_.#]\d+$/, '').trim().toUpperCase();
if (!cleanMark) cleanMark = pieceMark.toUpperCase(); if (!cleanMark) cleanMark = pieceMark.toUpperCase();
const cleanAssemblyMark = assemblyMark ? assemblyMark.replace(/[-_.#]\d+$/, '').trim().toUpperCase() : cleanMark; const cleanAssemblyMark = assemblyMark ? assemblyMark.replace(/[-_.#]\d+$/, '').trim().toUpperCase() : cleanMark;
// Em estruturas metálicas, a marca da peça apontada na produção é a marca do conjunto (Assembly Mark).
// Se a sub-peça for um número de corte (>= 1000) e houver conjunto pai, a marca principal é o conjunto!
let mainMark = pieceMark;
let cleanMainMark = cleanMark;
if (assemblyMark && (isSinglePartCutNumber(pieceMark) || !mainMark || mainMark === 'indefinido')) {
mainMark = assemblyMark;
cleanMainMark = cleanAssemblyMark;
}
// Detecção de tipo de elemento estrutural // Detecção de tipo de elemento estrutural
let detectedType = 'ELEMENT'; let detectedType = 'ELEMENT';
const searchStr = `${profile} ${name} ${cleanMark} ${cleanAssemblyMark}`.toUpperCase(); const searchStr = `${profile} ${name} ${cleanMark} ${cleanAssemblyMark} ${mainMark}`.toUpperCase();
if (searchStr.includes('VIGA') || searchStr.includes('BEAM') || cleanMark.startsWith('V')) { if (searchStr.includes('VIGA') || searchStr.includes('BEAM') || cleanMainMark.startsWith('V')) {
detectedType = 'BEAM'; detectedType = 'BEAM';
} else if (searchStr.includes('COLUNA') || searchStr.includes('PILAR') || searchStr.includes('COLUMN') || cleanMark.startsWith('P')) { } else if (searchStr.includes('COLUNA') || searchStr.includes('PILAR') || searchStr.includes('COLUMN') || cleanMainMark.startsWith('P')) {
detectedType = 'COLUMN'; detectedType = 'COLUMN';
} else if (searchStr.includes('CHAPA') || searchStr.includes('PLATE') || cleanMark.startsWith('PL') || cleanMark.startsWith('CH')) { } else if (searchStr.includes('CHAPA') || searchStr.includes('PLATE') || cleanMark.startsWith('PL') || cleanMark.startsWith('CH')) {
detectedType = 'PLATE'; detectedType = 'PLATE';
@@ -145,10 +169,15 @@ function parseWrlNodeName(
} }
return { return {
mainMark: mainMark.toUpperCase(),
cleanMainMark: cleanMainMark.toUpperCase(),
pieceMark: pieceMark.toUpperCase(), pieceMark: pieceMark.toUpperCase(),
cleanMark, cleanMark,
assemblyMark: assemblyMark ? assemblyMark.toUpperCase() : undefined, assemblyMark: assemblyMark ? assemblyMark.toUpperCase() : undefined,
cleanAssemblyMark, cleanAssemblyMark,
singlePartMark,
profile: profile || undefined,
section: profile || undefined,
phase: inheritedPhase, phase: inheritedPhase,
detectedType, detectedType,
}; };
@@ -503,19 +532,22 @@ export async function loadAndAuditWRL(
} }
mesh.material = material; mesh.material = material;
const pieceSection = parsed.cleanMark || parsed.pieceMark || 'Perfil'; const pieceSection = parsed.profile || parsed.section || parsed.cleanMainMark || 'Perfil';
const mainMark = parsed.mainMark || parsed.cleanMainMark || parsed.pieceMark;
const cleanMain = parsed.cleanMainMark || parsed.cleanMark;
const pieceInfo: PieceInfo = { const pieceInfo: PieceInfo = {
expressID, expressID,
guid, guid,
name: rawNodeName, name: rawNodeName,
type: parsed.detectedType, type: parsed.detectedType,
pieceMark: parsed.pieceMark, pieceMark: mainMark,
cleanMark: parsed.cleanMark, cleanMark: cleanMain,
assemblyMark: parsed.assemblyMark, assemblyMark: parsed.assemblyMark || mainMark,
cleanAssemblyMark: parsed.cleanAssemblyMark, cleanAssemblyMark: parsed.cleanAssemblyMark || cleanMain,
section: pieceSection, section: pieceSection,
phase: parsed.phase, phase: parsed.phase,
singlePartMark: parsed.singlePartMark,
mesh, mesh,
}; };
@@ -524,10 +556,11 @@ export async function loadAndAuditWRL(
ifcId: expressID, // Identificador primordial compatível com Raycaster, Hover e Seleção do visualizador ifcId: expressID, // Identificador primordial compatível com Raycaster, Hover e Seleção do visualizador
expressID, expressID,
guid, guid,
pieceMark: parsed.pieceMark, pieceMark: mainMark, // Peça principal da produção (ex: 1, 8, V101)
cleanMark: parsed.cleanMark, cleanMark: cleanMain,
assemblyMark: parsed.assemblyMark, singlePartMark: parsed.singlePartMark, // Componente de corte (ex: 1001)
cleanAssemblyMark: parsed.cleanAssemblyMark, assemblyMark: parsed.assemblyMark || mainMark,
cleanAssemblyMark: parsed.cleanAssemblyMark || cleanMain,
section: pieceSection, section: pieceSection,
phase: parsed.phase, phase: parsed.phase,
type: parsed.detectedType, type: parsed.detectedType,
@@ -538,17 +571,12 @@ export async function loadAndAuditWRL(
pieceByExpressID.set(expressID, pieceInfo); pieceByExpressID.set(expressID, pieceInfo);
// Agrupa peças por marca // Agrupa peças por marca de conjunto e por marca de componente
const markKey = parsed.cleanMark || parsed.pieceMark; const markKeys = [mainMark, cleanMain, parsed.singlePartMark, parsed.pieceMark, parsed.cleanMark].filter(Boolean) as string[];
if (markKey) { for (const mKey of Array.from(new Set(markKeys))) {
const list = piecesByMark.get(markKey) || []; const list = piecesByMark.get(mKey) || [];
list.push(pieceInfo); list.push(pieceInfo);
piecesByMark.set(markKey, list); piecesByMark.set(mKey, list);
}
if (parsed.cleanAssemblyMark && parsed.cleanAssemblyMark !== markKey) {
const listAss = piecesByMark.get(parsed.cleanAssemblyMark) || [];
listAss.push(pieceInfo);
piecesByMark.set(parsed.cleanAssemblyMark, listAss);
} }
} }
}); });
+1
View File
@@ -33,6 +33,7 @@ export interface PieceInfo {
parentAssemblyID?: number; parentAssemblyID?: number;
phase?: string; phase?: string;
ofNumber?: string; ofNumber?: string;
singlePartMark?: string;
section?: string; section?: string;
span?: number; span?: number;
material?: string; material?: string;
+2
View File
@@ -148,6 +148,8 @@ export default function Visualizador3D() {
marca: marcaStr, marca: marcaStr,
fase, fase,
ofNumber: selectedOF, ofNumber: selectedOF,
descricao: p.descricao || '',
perfilPrincipal: p.perfil_principal || p.descricao || '',
totalQtd: Number(p.quantidade || 1), totalQtd: Number(p.quantidade || 1),
pesoTotal: Number(p.peso_total || 0), pesoTotal: Number(p.peso_total || 0),
pesoUnitario: Number(p.peso_unitario || 0), pesoUnitario: Number(p.peso_unitario || 0),