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;
processColor?: string;
processOrdem?: number;
perfilPrincipal?: string;
descricao?: string;
}
/**
@@ -67,6 +69,42 @@ function findProductionDataForPiece(
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 {
modelData: LoadedIFCResult | null;
productionData?: Map<string, ProductionPieceStatus>;
@@ -940,17 +978,40 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
const pmark = hitMesh.userData.pieceMark;
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,
guid: hitMesh.userData.guid || '',
name: hitMesh.userData.name || hitMesh.userData.section || 'Peça',
type: hitMesh.userData.type || 'PIECE',
pieceMark: pmark,
cleanMark: hitMesh.userData.cleanMark,
assemblyMark: hitMesh.userData.assemblyMark,
cleanAssemblyMark: hitMesh.userData.cleanAssemblyMark,
section: hitMesh.userData.section || hitMesh.userData.name,
phase: hitMesh.userData.phase,
guid: hitMesh.userData.guid || rawPiece?.guid || '',
name: hitMesh.userData.name || rawPiece?.name || 'Peça',
type: hitMesh.userData.type || rawPiece?.type || 'PIECE',
pieceMark: displayMark,
cleanMark: hitMesh.userData.cleanMark || rawPiece?.cleanMark,
assemblyMark: hitMesh.userData.assemblyMark || rawPiece?.assemblyMark,
cleanAssemblyMark: hitMesh.userData.cleanAssemblyMark || rawPiece?.cleanAssemblyMark,
section: displaySection,
phase,
singlePartMark,
};
setHoveredPiece(piece);
setHoverPosition({ x: clientX, y: clientY });
@@ -1040,18 +1101,40 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
});
}
const pmark = hitMesh.userData.pieceMark;
const piece: PieceInfo = (modelData && modelData.pieceByExpressID.get(targetIfcId)) || {
const rawPiece = 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,
guid: hitMesh.userData.guid || '',
name: hitMesh.userData.name || hitMesh.userData.section || 'Peça',
type: hitMesh.userData.type || 'PIECE',
pieceMark: pmark,
cleanMark: hitMesh.userData.cleanMark,
assemblyMark: hitMesh.userData.assemblyMark,
cleanAssemblyMark: hitMesh.userData.cleanAssemblyMark,
section: hitMesh.userData.section,
phase: hitMesh.userData.phase,
guid: hitMesh.userData.guid || rawPiece?.guid || '',
name: hitMesh.userData.name || rawPiece?.name || 'Peça',
type: hitMesh.userData.type || rawPiece?.type || 'PIECE',
pieceMark: displayMark,
cleanMark: hitMesh.userData.cleanMark || rawPiece?.cleanMark,
assemblyMark: hitMesh.userData.assemblyMark || rawPiece?.assemblyMark,
cleanAssemblyMark: hitMesh.userData.cleanAssemblyMark || rawPiece?.cleanAssemblyMark,
section: displaySection,
phase,
singlePartMark,
};
nextMap.set(targetIfcId, {
@@ -1165,17 +1248,40 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
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,
guid: sampleMesh.userData.guid || '',
name: sampleMesh.userData.name || sampleMesh.userData.section || 'Peça',
type: sampleMesh.userData.type || 'PIECE',
pieceMark: sampleMesh.userData.pieceMark,
cleanMark: sampleMesh.userData.cleanMark,
assemblyMark: sampleMesh.userData.assemblyMark,
cleanAssemblyMark: sampleMesh.userData.cleanAssemblyMark,
section: sampleMesh.userData.section,
phase: sampleMesh.userData.phase || targetPhase,
guid: sampleMesh.userData.guid || rawPiece?.guid || '',
name: sampleMesh.userData.name || rawPiece?.name || 'Peça',
type: sampleMesh.userData.type || rawPiece?.type || 'PIECE',
pieceMark: displayMark,
cleanMark: sampleMesh.userData.cleanMark || rawPiece?.cleanMark,
assemblyMark: sampleMesh.userData.assemblyMark || rawPiece?.assemblyMark,
cleanAssemblyMark: sampleMesh.userData.cleanAssemblyMark || rawPiece?.cleanAssemblyMark,
section: displaySection,
phase,
singlePartMark,
};
nextMap.set(elId, {
@@ -62,6 +62,15 @@ export const PieceHoverTooltip: React.FC<PieceHoverTooltipProps> = ({
</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 && (
<div className="flex items-center justify-between text-slate-300">
<span className="text-slate-400">Componentes:</span>
+56 -28
View File
@@ -1,6 +1,6 @@
import * as THREE from 'three';
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.
@@ -57,6 +57,9 @@ function extractPhaseFromAncestors(ancestors: string[]): string | 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.
*/
@@ -65,10 +68,15 @@ function parseWrlNodeName(
inheritedPhase?: string,
ancestorAssembly?: string
): {
mainMark: string;
cleanMainMark: string;
pieceMark: string;
cleanMark: string;
assemblyMark?: string;
cleanAssemblyMark?: string;
singlePartMark?: string;
profile?: string;
section?: string;
phase?: string;
detectedType: string;
} {
@@ -79,20 +87,22 @@ function parseWrlNodeName(
let assemblyMark = ancestorAssembly ? ancestorAssembly.trim() : undefined;
let pieceMark = name;
let singlePartMark: string | undefined = undefined;
let profile = '';
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])) {
assemblyMark = parts[0];
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")
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];
assemblyMark = parts[0];
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")
else if (parts.length >= 2 && /^[A-Za-z]+\d+$/i.test(parts[0])) {
assemblyMark = parts[0];
pieceMark = parts[0]; // ou parts[1]
pieceMark = parts[0];
profile = parts.slice(1).join('_');
}
// CASO 4: Parafusos / Fixadores
@@ -113,18 +123,32 @@ function parseWrlNodeName(
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();
if (!cleanMark) cleanMark = pieceMark.toUpperCase();
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
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';
} 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';
} else if (searchStr.includes('CHAPA') || searchStr.includes('PLATE') || cleanMark.startsWith('PL') || cleanMark.startsWith('CH')) {
detectedType = 'PLATE';
@@ -145,10 +169,15 @@ function parseWrlNodeName(
}
return {
mainMark: mainMark.toUpperCase(),
cleanMainMark: cleanMainMark.toUpperCase(),
pieceMark: pieceMark.toUpperCase(),
cleanMark,
assemblyMark: assemblyMark ? assemblyMark.toUpperCase() : undefined,
cleanAssemblyMark,
singlePartMark,
profile: profile || undefined,
section: profile || undefined,
phase: inheritedPhase,
detectedType,
};
@@ -503,19 +532,22 @@ export async function loadAndAuditWRL(
}
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 = {
expressID,
guid,
name: rawNodeName,
type: parsed.detectedType,
pieceMark: parsed.pieceMark,
cleanMark: parsed.cleanMark,
assemblyMark: parsed.assemblyMark,
cleanAssemblyMark: parsed.cleanAssemblyMark,
pieceMark: mainMark,
cleanMark: cleanMain,
assemblyMark: parsed.assemblyMark || mainMark,
cleanAssemblyMark: parsed.cleanAssemblyMark || cleanMain,
section: pieceSection,
phase: parsed.phase,
singlePartMark: parsed.singlePartMark,
mesh,
};
@@ -524,10 +556,11 @@ export async function loadAndAuditWRL(
ifcId: expressID, // Identificador primordial compatível com Raycaster, Hover e Seleção do visualizador
expressID,
guid,
pieceMark: parsed.pieceMark,
cleanMark: parsed.cleanMark,
assemblyMark: parsed.assemblyMark,
cleanAssemblyMark: parsed.cleanAssemblyMark,
pieceMark: mainMark, // Peça principal da produção (ex: 1, 8, V101)
cleanMark: cleanMain,
singlePartMark: parsed.singlePartMark, // Componente de corte (ex: 1001)
assemblyMark: parsed.assemblyMark || mainMark,
cleanAssemblyMark: parsed.cleanAssemblyMark || cleanMain,
section: pieceSection,
phase: parsed.phase,
type: parsed.detectedType,
@@ -538,17 +571,12 @@ export async function loadAndAuditWRL(
pieceByExpressID.set(expressID, pieceInfo);
// Agrupa peças por marca
const markKey = parsed.cleanMark || parsed.pieceMark;
if (markKey) {
const list = piecesByMark.get(markKey) || [];
// Agrupa peças por marca de conjunto e por marca de componente
const markKeys = [mainMark, cleanMain, parsed.singlePartMark, parsed.pieceMark, parsed.cleanMark].filter(Boolean) as string[];
for (const mKey of Array.from(new Set(markKeys))) {
const list = piecesByMark.get(mKey) || [];
list.push(pieceInfo);
piecesByMark.set(markKey, list);
}
if (parsed.cleanAssemblyMark && parsed.cleanAssemblyMark !== markKey) {
const listAss = piecesByMark.get(parsed.cleanAssemblyMark) || [];
listAss.push(pieceInfo);
piecesByMark.set(parsed.cleanAssemblyMark, listAss);
piecesByMark.set(mKey, list);
}
}
});
+1
View File
@@ -33,6 +33,7 @@ export interface PieceInfo {
parentAssemblyID?: number;
phase?: string;
ofNumber?: string;
singlePartMark?: string;
section?: string;
span?: number;
material?: string;
+2
View File
@@ -148,6 +148,8 @@ export default function Visualizador3D() {
marca: marcaStr,
fase,
ofNumber: selectedOF,
descricao: p.descricao || '',
perfilPrincipal: p.perfil_principal || p.descricao || '',
totalQtd: Number(p.quantidade || 1),
pesoTotal: Number(p.peso_total || 0),
pesoUnitario: Number(p.peso_unitario || 0),