feat(3d-viewer): replica selecao e hover para modelos WRL com destaque magenta e apontamentos
This commit is contained in:
@@ -20,6 +20,53 @@ interface ProductionPieceStatus {
|
||||
processOrdem?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Busca de apontamento e status fabril com máxima tolerância e amostragem de chaves para IFC e WRL.
|
||||
*/
|
||||
function findProductionDataForPiece(
|
||||
productionData?: Map<string, ProductionPieceStatus>,
|
||||
piece?: {
|
||||
pieceMark?: string;
|
||||
cleanMark?: string;
|
||||
assemblyMark?: string;
|
||||
cleanAssemblyMark?: string;
|
||||
phase?: string;
|
||||
} | null,
|
||||
selectedOF?: string
|
||||
): ProductionPieceStatus | undefined {
|
||||
if (!productionData || !piece) return undefined;
|
||||
|
||||
const pmark = String(piece.pieceMark || '').trim().toUpperCase();
|
||||
const cleanMark = String(piece.cleanMark || pmark).trim().toUpperCase();
|
||||
const assMark = String(piece.assemblyMark || '').trim().toUpperCase();
|
||||
const cleanAssMark = String(piece.cleanAssemblyMark || cleanMark).trim().toUpperCase();
|
||||
const elementPhase = piece.phase ? String(piece.phase).trim() : '';
|
||||
|
||||
const searchKeys = [
|
||||
elementPhase && selectedOF ? `${selectedOF}-${elementPhase}-${cleanAssMark}` : '',
|
||||
elementPhase ? `${elementPhase}-${cleanAssMark}` : '',
|
||||
elementPhase && selectedOF ? `${selectedOF}-${elementPhase}-${cleanMark}` : '',
|
||||
elementPhase ? `${elementPhase}-${cleanMark}` : '',
|
||||
selectedOF ? `${selectedOF}-${cleanAssMark}` : '',
|
||||
selectedOF ? `${selectedOF}-${cleanMark}` : '',
|
||||
assMark,
|
||||
pmark,
|
||||
cleanAssMark,
|
||||
cleanMark,
|
||||
].filter(Boolean).map((k) => k.toUpperCase());
|
||||
|
||||
for (const key of searchKeys) {
|
||||
const match = productionData.get(key);
|
||||
if (match) {
|
||||
if (!elementPhase || !match.fase || String(match.fase) === String(elementPhase)) {
|
||||
return match;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
interface ModelViewer3DProps {
|
||||
modelData: LoadedIFCResult | null;
|
||||
productionData?: Map<string, ProductionPieceStatus>;
|
||||
@@ -721,7 +768,7 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
||||
}
|
||||
|
||||
// Se a peça física pertencer à seleção permanente, preserva a cor magenta
|
||||
const ifcId = mesh.userData.ifcId;
|
||||
const ifcId = mesh.userData.ifcId ?? mesh.userData.expressID;
|
||||
if (ifcId !== undefined && selectedElementsMapRef.current.has(ifcId)) {
|
||||
mesh.userData.originalMaterial = mesh.material;
|
||||
mesh.material = magentaMaterialRef.current;
|
||||
@@ -889,17 +936,21 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
||||
}
|
||||
|
||||
if (hitMesh) {
|
||||
const expressID = hitMesh.userData.ifcId;
|
||||
const expressID = hitMesh.userData.ifcId ?? hitMesh.userData.expressID;
|
||||
const pmark = hitMesh.userData.pieceMark;
|
||||
|
||||
if (modelData && expressID !== undefined) {
|
||||
const piece = modelData.pieceByExpressID.get(expressID) || {
|
||||
expressID,
|
||||
guid: '',
|
||||
name: hitMesh.userData.section || 'Peça',
|
||||
type: 'PIECE',
|
||||
guid: hitMesh.userData.guid || '',
|
||||
name: hitMesh.userData.name || hitMesh.userData.section || 'Peça',
|
||||
type: hitMesh.userData.type || 'PIECE',
|
||||
pieceMark: pmark,
|
||||
section: hitMesh.userData.section,
|
||||
cleanMark: hitMesh.userData.cleanMark,
|
||||
assemblyMark: hitMesh.userData.assemblyMark,
|
||||
cleanAssemblyMark: hitMesh.userData.cleanAssemblyMark,
|
||||
section: hitMesh.userData.section || hitMesh.userData.name,
|
||||
phase: hitMesh.userData.phase,
|
||||
};
|
||||
setHoveredPiece(piece);
|
||||
setHoverPosition({ x: clientX, y: clientY });
|
||||
@@ -956,7 +1007,7 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
||||
}
|
||||
|
||||
if (hitMesh && hitMesh.visible) {
|
||||
const targetIfcId = hitMesh.userData.ifcId;
|
||||
const targetIfcId = hitMesh.userData.ifcId ?? hitMesh.userData.expressID;
|
||||
if (targetIfcId === undefined) return;
|
||||
|
||||
const nextMap = new Map(selectedElementsMapRef.current);
|
||||
@@ -972,16 +1023,19 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
||||
});
|
||||
nextMap.delete(targetIfcId);
|
||||
} else {
|
||||
// Marcar peça física inteira: localiza e pinta todas as sub-malhas do mesmo ifcId
|
||||
// Marcar peça física inteira: localiza e pinta todas as sub-malhas do mesmo targetIfcId
|
||||
const relatedMeshes: THREE.Mesh[] = [];
|
||||
if (modelGroupRef.current) {
|
||||
modelGroupRef.current.traverse((child) => {
|
||||
if ((child as THREE.Mesh).isMesh && child.userData.ifcId === targetIfcId) {
|
||||
const m = child as THREE.Mesh;
|
||||
m.userData.originalMaterial = m.material;
|
||||
m.userData.isMagentaSelected = true;
|
||||
m.material = magentaMaterialRef.current;
|
||||
relatedMeshes.push(m);
|
||||
if ((child as THREE.Mesh).isMesh) {
|
||||
const childId = child.userData.ifcId ?? child.userData.expressID;
|
||||
if (childId === targetIfcId) {
|
||||
const m = child as THREE.Mesh;
|
||||
m.userData.originalMaterial = m.material;
|
||||
m.userData.isMagentaSelected = true;
|
||||
m.material = magentaMaterialRef.current;
|
||||
relatedMeshes.push(m);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -989,10 +1043,13 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
||||
const pmark = hitMesh.userData.pieceMark;
|
||||
const piece: PieceInfo = (modelData && modelData.pieceByExpressID.get(targetIfcId)) || {
|
||||
expressID: targetIfcId,
|
||||
guid: '',
|
||||
name: hitMesh.userData.section || 'Peça',
|
||||
type: 'PIECE',
|
||||
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,
|
||||
};
|
||||
@@ -1018,8 +1075,32 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
||||
const mouseX = ((e.clientX - rect.left) / rect.width) * 2 - 1;
|
||||
const mouseY = -((e.clientY - rect.top) / rect.height) * 2 + 1;
|
||||
|
||||
raycasterRef.current.setFromCamera(new THREE.Vector2(mouseX, mouseY), activeCameraRef.current);
|
||||
const intersects = raycasterRef.current.intersectObjects(modelGroupRef.current.children, true);
|
||||
const raycaster = raycasterRef.current;
|
||||
raycaster.params.Line = { threshold: 4.0 };
|
||||
raycaster.params.Points = { threshold: 4.0 };
|
||||
raycaster.setFromCamera(new THREE.Vector2(mouseX, mouseY), activeCameraRef.current);
|
||||
let intersects = raycaster.intersectObjects(modelGroupRef.current.children, true);
|
||||
|
||||
// Amostragem circular magnética de tolerância para peças esbeltas/finas
|
||||
if (intersects.length === 0) {
|
||||
const radiusOffsetsPx = [6, 12, 18];
|
||||
const angles = [0, Math.PI / 4, Math.PI / 2, (3 * Math.PI) / 4, Math.PI, (5 * Math.PI) / 4, (3 * Math.PI) / 2, (7 * Math.PI) / 4];
|
||||
|
||||
for (const rPx of radiusOffsetsPx) {
|
||||
if (intersects.length > 0) break;
|
||||
for (const angle of angles) {
|
||||
const dx = ((rPx * Math.cos(angle)) / rect.width) * 2;
|
||||
const dy = -((rPx * Math.sin(angle)) / rect.height) * 2;
|
||||
const samplePos = new THREE.Vector2(mouseX + dx, mouseY + dy);
|
||||
raycaster.setFromCamera(samplePos, activeCameraRef.current);
|
||||
const sampleIntersects = raycaster.intersectObjects(modelGroupRef.current.children, true);
|
||||
if (sampleIntersects.length > 0) {
|
||||
intersects = sampleIntersects;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (intersects.length > 0) {
|
||||
const hitPoint = intersects[0].point;
|
||||
@@ -1033,36 +1114,49 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
||||
|
||||
if (hitMesh) {
|
||||
const targetPieceMark = String(hitMesh.userData.pieceMark || '').trim().toUpperCase();
|
||||
const targetCleanMark = String(hitMesh.userData.cleanMark || targetPieceMark).trim().toUpperCase();
|
||||
const targetAssemblyMark = String(hitMesh.userData.assemblyMark || '').trim().toUpperCase();
|
||||
const targetCleanAssMark = String(hitMesh.userData.cleanAssemblyMark || targetCleanMark).trim().toUpperCase();
|
||||
const targetPhase = hitMesh.userData.phase ? String(hitMesh.userData.phase).trim() : '';
|
||||
|
||||
if (targetPieceMark) {
|
||||
if (targetPieceMark || targetCleanMark) {
|
||||
const nextMap = new Map(selectedElementsMapRef.current);
|
||||
|
||||
// Agrupa todas as sub-malhas por elemento físico (ifcId único)
|
||||
const meshesByIfcId = new Map<number, { meshes: THREE.Mesh[]; sampleMesh: THREE.Mesh }>();
|
||||
// Agrupa todas as sub-malhas por elemento físico único (ifcId ?? expressID)
|
||||
const meshesByElementId = new Map<number, { meshes: THREE.Mesh[]; sampleMesh: THREE.Mesh }>();
|
||||
|
||||
modelGroupRef.current.traverse((child) => {
|
||||
if ((child as THREE.Mesh).isMesh && child.visible) {
|
||||
const mesh = child as THREE.Mesh;
|
||||
const pmark = String(mesh.userData.pieceMark || '').trim().toUpperCase();
|
||||
const cmark = String(mesh.userData.cleanMark || pmark).trim().toUpperCase();
|
||||
const amark = String(mesh.userData.assemblyMark || '').trim().toUpperCase();
|
||||
const cleanAm = String(mesh.userData.cleanAssemblyMark || cmark).trim().toUpperCase();
|
||||
const phase = mesh.userData.phase ? String(mesh.userData.phase).trim() : '';
|
||||
|
||||
// Mesma marca e mesma fase
|
||||
const isMatch = pmark === targetPieceMark && (!targetPhase || !phase || phase === targetPhase);
|
||||
if (isMatch) {
|
||||
const ifcId = mesh.userData.ifcId;
|
||||
if (ifcId !== undefined) {
|
||||
if (!meshesByIfcId.has(ifcId)) {
|
||||
meshesByIfcId.set(ifcId, { meshes: [], sampleMesh: mesh });
|
||||
// Mesma marca (peça, conjunto ou limpa) e mesma fase (se especificada)
|
||||
const isMarkMatch =
|
||||
(pmark && pmark === targetPieceMark) ||
|
||||
(cmark && cmark === targetCleanMark) ||
|
||||
(targetAssemblyMark && amark === targetAssemblyMark) ||
|
||||
(targetCleanAssMark && cleanAm === targetCleanAssMark);
|
||||
|
||||
const isPhaseMatch = !targetPhase || !phase || phase === targetPhase;
|
||||
|
||||
if (isMarkMatch && isPhaseMatch) {
|
||||
const elId = mesh.userData.ifcId ?? mesh.userData.expressID;
|
||||
if (elId !== undefined) {
|
||||
if (!meshesByElementId.has(elId)) {
|
||||
meshesByElementId.set(elId, { meshes: [], sampleMesh: mesh });
|
||||
}
|
||||
meshesByIfcId.get(ifcId)!.meshes.push(mesh);
|
||||
meshesByElementId.get(elId)!.meshes.push(mesh);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Pinta e adiciona cada peça física real como 1 unidade
|
||||
meshesByIfcId.forEach(({ meshes, sampleMesh }, ifcId) => {
|
||||
meshesByElementId.forEach(({ meshes, sampleMesh }, elId) => {
|
||||
meshes.forEach((m) => {
|
||||
if (!m.userData.isMagentaSelected) {
|
||||
m.userData.originalMaterial = m.material;
|
||||
@@ -1071,18 +1165,21 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
||||
m.material = magentaMaterialRef.current;
|
||||
});
|
||||
|
||||
const piece: PieceInfo = (modelData && modelData.pieceByExpressID.get(ifcId)) || {
|
||||
expressID: ifcId,
|
||||
guid: '',
|
||||
name: sampleMesh.userData.section || 'Peça',
|
||||
type: 'PIECE',
|
||||
const piece: PieceInfo = (modelData && modelData.pieceByExpressID.get(elId)) || {
|
||||
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,
|
||||
};
|
||||
|
||||
nextMap.set(ifcId, {
|
||||
expressID: ifcId,
|
||||
nextMap.set(elId, {
|
||||
expressID: elId,
|
||||
piece,
|
||||
meshes,
|
||||
});
|
||||
@@ -1118,7 +1215,7 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
||||
const selectedPiecesList: SelectedPieceItem[] = Array.from(selectedElementsMap.values()).map(
|
||||
({ expressID, piece }) => {
|
||||
const pmark = piece.pieceMark || 'Sem Marca';
|
||||
const prod = productionData?.get(pmark);
|
||||
const prod = findProductionDataForPiece(productionData, piece, selectedOF);
|
||||
return {
|
||||
id: String(expressID),
|
||||
pieceMark: pmark,
|
||||
@@ -1338,16 +1435,17 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
||||
<PieceHoverTooltip
|
||||
piece={hoveredPiece}
|
||||
position={hoverPosition}
|
||||
productionInfo={
|
||||
hoveredPiece?.pieceMark && productionData
|
||||
productionInfo={(() => {
|
||||
const prod = findProductionDataForPiece(productionData, hoveredPiece, selectedOF);
|
||||
return prod
|
||||
? {
|
||||
pointedQty: productionData.get(hoveredPiece.pieceMark)?.pointedQtd,
|
||||
totalQty: productionData.get(hoveredPiece.pieceMark)?.totalQtd,
|
||||
currentStage: productionData.get(hoveredPiece.pieceMark)?.currentProcessName,
|
||||
stageColor: productionData.get(hoveredPiece.pieceMark)?.processColor,
|
||||
pointedQty: prod.pointedQtd,
|
||||
totalQty: prod.totalQtd,
|
||||
currentStage: prod.currentProcessName,
|
||||
stageColor: prod.processColor,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
: undefined;
|
||||
})()}
|
||||
/>
|
||||
|
||||
{/* Modal de Lista e Exportação da Seleção 3D */}
|
||||
|
||||
@@ -557,7 +557,7 @@ export const SmartModelViewer3D: React.FC<SmartModelViewer3DProps> = ({
|
||||
}
|
||||
|
||||
// 4.3. Preserva o destaque magenta se a peça estiver selecionada pelo operador
|
||||
const ifcId = mesh.userData.ifcId;
|
||||
const ifcId = mesh.userData.ifcId ?? mesh.userData.expressID;
|
||||
if (ifcId !== undefined && selectedElementsMapRef.current.has(ifcId)) {
|
||||
mesh.userData.originalMaterial = mesh.material;
|
||||
mesh.material = magentaMaterialRef.current;
|
||||
@@ -620,7 +620,7 @@ export const SmartModelViewer3D: React.FC<SmartModelViewer3DProps> = ({
|
||||
else if (hitObj.parent && (hitObj.parent as THREE.Mesh).isMesh) hitMesh = hitObj.parent as THREE.Mesh;
|
||||
|
||||
if (hitMesh && hitMesh.visible) {
|
||||
const targetIfcId = hitMesh.userData.ifcId;
|
||||
const targetIfcId = hitMesh.userData.ifcId ?? hitMesh.userData.expressID;
|
||||
if (targetIfcId === undefined) return;
|
||||
|
||||
const nextMap = new Map(selectedElementsMapRef.current);
|
||||
@@ -644,11 +644,14 @@ export const SmartModelViewer3D: React.FC<SmartModelViewer3DProps> = ({
|
||||
// Marca a peça física inteira em Magenta permanente
|
||||
const relatedMeshes: THREE.Mesh[] = [];
|
||||
modelGroupRef.current.traverse((child) => {
|
||||
if ((child as THREE.Mesh).isMesh && child.userData.ifcId === targetIfcId) {
|
||||
const m = child as THREE.Mesh;
|
||||
m.userData.originalMaterial = m.material;
|
||||
m.material = magentaMaterialRef.current;
|
||||
relatedMeshes.push(m);
|
||||
if ((child as THREE.Mesh).isMesh) {
|
||||
const childId = child.userData.ifcId ?? child.userData.expressID;
|
||||
if (childId === targetIfcId) {
|
||||
const m = child as THREE.Mesh;
|
||||
m.userData.originalMaterial = m.material;
|
||||
m.material = magentaMaterialRef.current;
|
||||
relatedMeshes.push(m);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -503,6 +503,8 @@ export async function loadAndAuditWRL(
|
||||
}
|
||||
mesh.material = material;
|
||||
|
||||
const pieceSection = parsed.cleanMark || parsed.pieceMark || 'Perfil';
|
||||
|
||||
const pieceInfo: PieceInfo = {
|
||||
expressID,
|
||||
guid,
|
||||
@@ -512,18 +514,21 @@ export async function loadAndAuditWRL(
|
||||
cleanMark: parsed.cleanMark,
|
||||
assemblyMark: parsed.assemblyMark,
|
||||
cleanAssemblyMark: parsed.cleanAssemblyMark,
|
||||
section: pieceSection,
|
||||
phase: parsed.phase,
|
||||
mesh,
|
||||
};
|
||||
|
||||
// Injeta metadados essenciais no userData do Mesh
|
||||
// Injeta metadados essenciais no userData do Mesh (com total compatibilidade IFC / Three.js)
|
||||
mesh.userData = {
|
||||
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,
|
||||
section: pieceSection,
|
||||
phase: parsed.phase,
|
||||
type: parsed.detectedType,
|
||||
name: rawNodeName,
|
||||
|
||||
Reference in New Issue
Block a user