fix: persistência na matriz de acessos, sanitização e fallback para WRL e trava de OF no carregador

This commit is contained in:
2026-09-24 13:51:09 +00:00
parent 79972a5c8b
commit 02eefd1b08
3 changed files with 241 additions and 71 deletions
+5 -51
View File
@@ -256,10 +256,8 @@ export const useMatrizAcessos = () => {
GRUPOS_MODULOS.forEach((g) => {
g.recursos.forEach((r) => {
if (isAdmin) {
permissoesMap[r.key] = 'criar';
} else if (directPerms[r.key]) {
// Prioridade 1: Permissão direta do usuário
if (directPerms[r.key]) {
// Prioridade 1: Permissão direta do usuário configurada na Matriz
const perm = directPerms[r.key];
if (perm === 'can_create_update_delete' || perm === 'can_admin' || perm === 'can_create_only') {
permissoesMap[r.key] = 'criar';
@@ -268,6 +266,9 @@ export const useMatrizAcessos = () => {
} else {
permissoesMap[r.key] = 'nenhum';
}
} else if (isAdmin) {
// Se for admin e não tiver sobreposição direta, padrão é criar
permissoesMap[r.key] = 'criar';
} else if (recursosSet.has(`${r.key}:edit`) || recursosSet.has(`${r.key}_edit`)) {
permissoesMap[r.key] = 'criar';
} else if (recursosSet.has(r.key)) {
@@ -369,29 +370,6 @@ export const useMatrizAcessos = () => {
throw uipError;
}
// 2. Compatibilidade legada com privilégios de grupo (se houver)
if (user.privilegeId) {
// Remover permissões antigas do recurso
await supabase
.from('privilege_interface_resources')
.delete()
.eq('privilege_id', user.privilegeId)
.in('resource_key', [resourceKey, `${resourceKey}:edit`]);
// Gravar novo estado
if (novoNivel === 'visualizar') {
await supabase.from('privilege_interface_resources').insert({
privilege_id: user.privilegeId,
resource_key: resourceKey,
});
} else if (novoNivel === 'criar') {
await supabase.from('privilege_interface_resources').insert([
{ privilege_id: user.privilegeId, resource_key: resourceKey },
{ privilege_id: user.privilegeId, resource_key: `${resourceKey}:edit` },
]);
}
}
const desc = {
nenhum: 'Bloqueado (✕)',
visualizar: 'Apenas Visualização [V]',
@@ -446,30 +424,6 @@ export const useMatrizAcessos = () => {
throw batchError;
}
// 2. Compatibilidade legada com privilégios de grupo
if (user.privilegeId) {
// Limpar recursos atuais do privilégio
await supabase
.from('privilege_interface_resources')
.delete()
.eq('privilege_id', user.privilegeId);
// Inserir os novos recursos do template
const records: { privilege_id: string; resource_key: string }[] = [];
Object.entries(novoMap).forEach(([resKey, nivel]) => {
if (nivel === 'visualizar') {
records.push({ privilege_id: user.privilegeId!, resource_key: resKey });
} else if (nivel === 'criar') {
records.push({ privilege_id: user.privilegeId!, resource_key: resKey });
records.push({ privilege_id: user.privilegeId!, resource_key: `${resKey}:edit` });
}
});
if (records.length > 0) {
await supabase.from('privilege_interface_resources').insert(records);
}
}
toast.success(`Template '${template.nome}' aplicado com sucesso a ${user.fullName}!`);
} catch (err) {
console.error('Erro ao aplicar template:', err);
+212 -17
View File
@@ -39,6 +39,9 @@ function parseWrlNodeName(rawName: string): {
} {
let name = String(rawName || '').trim();
// Remove prefixos de sanitização interna se houver
name = name.replace(/^ID_/, '');
// Remove caracteres especiais de escape ou tags extras
name = name.replace(/^DEF\s+/i, '').replace(/[\{\}\[\]"']/g, '').trim();
@@ -60,7 +63,6 @@ function parseWrlNodeName(rawName: string): {
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)) {
@@ -105,6 +107,192 @@ function parseWrlNodeName(rawName: string): {
};
}
/**
* Sanitiza o texto VRML para compatibilidade com o parser do Three.js:
* 1. Ajusta cabeçalho para #VRML V2.0 utf8.
* 2. Converte identificadores que iniciam com dígitos ou contêm hífens para identificadores válidos.
*/
function sanitizeVrmlForLoader(rawText: string): {
sanitized: string;
nameMap: Map<string, string>;
} {
const nameMap = new Map<string, string>();
let text = rawText.replace(/^\uFEFF/, ''); // remove BOM
// Localiza o início da tag #VRML
const headerIdx = text.search(/#VRML/i);
if (headerIdx > 0) {
text = text.slice(headerIdx);
}
// Normaliza primeira linha para #VRML V2.0 utf8
text = text.replace(/^#VRML[^\r\n]*/i, '#VRML V2.0 utf8');
// Normaliza identificadores em DEF e USE (ex: "DEF 01-V101 Transform" -> "DEF ID_01_V101 Transform")
text = text.replace(/\b(DEF|USE)\s+([^\s\{\[\(\)]+)/g, (_, keyword, rawId) => {
let cleanId = String(rawId).replace(/[^a-zA-Z0-9_]/g, '_');
if (/^[0-9]/.test(cleanId)) {
cleanId = 'ID_' + cleanId;
}
nameMap.set(cleanId, rawId);
return `${keyword} ${cleanId}`;
});
return { sanitized: text, nameMap };
}
/**
* Parser de Fallback Direto e Resiliente para arquivos VRML (.wrl 1.0 ou 2.0).
* Extrai blocos IndexedFaceSet com Coordinate/Coordinate3 diretamente via expressões regulares,
* garantindo renderização mesmo quando o parser Chevrotain do VRMLLoader encontra erros de sintaxe.
*/
function parseVrmlDirectFallback(
vrmlText: string,
nameMap: Map<string, string>
): THREE.Group {
const group = new THREE.Group();
group.name = 'WRL_Direct_Fallback_Root';
// Procura ocorrências de DEF <nome> associadas a IndexedFaceSet
// Suporta tanto VRML 1.0 (Separator, Coordinate3) quanto VRML 2.0 (Shape, Coordinate, Transform)
const defRegex = /DEF\s+([^\s\{\[\(\)]+)[\s\S]*?(?:point\s*\[([\s\S]*?)\])[\s\S]*?(?:coordIndex\s*\[([\s\S]*?)\])/gi;
let match: RegExpExecArray | null;
let count = 0;
while ((match = defRegex.exec(vrmlText)) !== null) {
count++;
const rawDefName = match[1].trim();
const originalName = nameMap.get(rawDefName) || rawDefName;
const pointStr = match[2];
const indexStr = match[3];
// Extrai vértices flutuantes (x, y, z)
const rawCoords = pointStr.trim().split(/[\s,]+/).filter(Boolean).map(Number);
if (rawCoords.length < 9) continue; // mínimo 3 vértices (1 triângulo)
const vertices: [number, number, number][] = [];
for (let i = 0; i < rawCoords.length; i += 3) {
if (!isNaN(rawCoords[i]) && !isNaN(rawCoords[i + 1]) && !isNaN(rawCoords[i + 2])) {
vertices.push([rawCoords[i], rawCoords[i + 1], rawCoords[i + 2]]);
}
}
if (vertices.length === 0) continue;
// Extrai índices de faces separados por -1
const rawIndices = indexStr.trim().split(/[\s,]+/).filter(Boolean).map(Number);
const triangleIndices: number[] = [];
let currentPoly: number[] = [];
for (const idx of rawIndices) {
if (idx === -1) {
if (currentPoly.length >= 3) {
// Triangulação em leque (fan triangulation)
for (let p = 1; p < currentPoly.length - 1; p++) {
triangleIndices.push(currentPoly[0], currentPoly[p], currentPoly[p + 1]);
}
}
currentPoly = [];
} else {
currentPoly.push(idx);
}
}
if (currentPoly.length >= 3) {
for (let p = 1; p < currentPoly.length - 1; p++) {
triangleIndices.push(currentPoly[0], currentPoly[p], currentPoly[p + 1]);
}
}
if (triangleIndices.length === 0) continue;
const positions: number[] = [];
for (const tIdx of triangleIndices) {
const v = vertices[tIdx];
if (v) {
positions.push(v[0], v[1], v[2]);
}
}
if (positions.length === 0) continue;
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
geometry.computeVertexNormals();
const mesh = new THREE.Mesh(geometry);
mesh.name = originalName;
mesh.castShadow = true;
mesh.receiveShadow = true;
group.add(mesh);
}
// Se a busca com DEF não encontrou malhas, busca todos os point/coordIndex sem DEF
if (count === 0) {
const genericRegex = /point\s*\[([\s\S]*?)\][\s\S]*?coordIndex\s*\[([\s\S]*?)\]/gi;
let genMatch: RegExpExecArray | null;
let genIdx = 1;
while ((genMatch = genericRegex.exec(vrmlText)) !== null) {
const pointStr = genMatch[1];
const indexStr = genMatch[2];
const rawCoords = pointStr.trim().split(/[\s,]+/).filter(Boolean).map(Number);
if (rawCoords.length < 9) continue;
const vertices: [number, number, number][] = [];
for (let i = 0; i < rawCoords.length; i += 3) {
if (!isNaN(rawCoords[i]) && !isNaN(rawCoords[i + 1]) && !isNaN(rawCoords[i + 2])) {
vertices.push([rawCoords[i], rawCoords[i + 1], rawCoords[i + 2]]);
}
}
const rawIndices = indexStr.trim().split(/[\s,]+/).filter(Boolean).map(Number);
const triangleIndices: number[] = [];
let currentPoly: number[] = [];
for (const idx of rawIndices) {
if (idx === -1) {
if (currentPoly.length >= 3) {
for (let p = 1; p < currentPoly.length - 1; p++) {
triangleIndices.push(currentPoly[0], currentPoly[p], currentPoly[p + 1]);
}
}
currentPoly = [];
} else {
currentPoly.push(idx);
}
}
if (currentPoly.length >= 3) {
for (let p = 1; p < currentPoly.length - 1; p++) {
triangleIndices.push(currentPoly[0], currentPoly[p], currentPoly[p + 1]);
}
}
const positions: number[] = [];
for (const tIdx of triangleIndices) {
const v = vertices[tIdx];
if (v) positions.push(v[0], v[1], v[2]);
}
if (positions.length > 0) {
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
geometry.computeVertexNormals();
const mesh = new THREE.Mesh(geometry);
mesh.name = `PECA-${genIdx++}`;
mesh.castShadow = true;
mesh.receiveShadow = true;
group.add(mesh);
}
}
}
return group;
}
/**
* Carrega, analisa e gera estrutura idêntica ao IFC para modelos VRML (.wrl / .wrz).
*/
@@ -122,17 +310,23 @@ export async function loadAndAuditWRL(
}
if (onProgress) onProgress(30, 'Descomprimindo e analisando formato VRML...');
const vrmlText = await decompressGzipIfNeeded(arrayBuffer);
const rawVrmlText = await decompressGzipIfNeeded(arrayBuffer);
if (onProgress) onProgress(50, 'Processando geometria 3D com motor VRML...');
if (onProgress) onProgress(45, 'Sanitizando identificadores e sintaxe VRML...');
const { sanitized: vrmlText, nameMap } = sanitizeVrmlForLoader(rawVrmlText);
if (onProgress) onProgress(60, 'Processando geometria 3D com motor VRML...');
const loader = new VRMLLoader();
let vrmlScene: THREE.Scene;
let vrmlScene: THREE.Object3D;
let isFallback = false;
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'}`);
console.warn('[WRL Loader] VRMLLoader padrão falhou, acionando fallback direto resiliente:', parseError?.message);
if (onProgress) onProgress(65, 'Acionando decodificador direto de geometria VRML...');
vrmlScene = parseVrmlDirectFallback(rawVrmlText, nameMap);
isFallback = true;
}
if (onProgress) onProgress(75, 'Mapeando peças estruturais e hierarquia...');
@@ -158,15 +352,16 @@ export async function loadAndAuditWRL(
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();
if (curr.name && !/^(Scene|Root|Group|Transform|Shape|IndexedFaceSet|WRL_Direct_Fallback_Root)$/i.test(curr.name.trim())) {
const mapped = nameMap.get(curr.name.trim()) || curr.name.trim();
return mapped;
}
curr = curr.parent;
}
return mesh.name || `PECA-${idCounter}`;
};
// Percorre todos os nós e malhas gerados pelo VRMLLoader
// Percorre todos os nós e malhas gerados
vrmlScene.traverse((child) => {
if ((child as THREE.Mesh).isMesh) {
const mesh = child as THREE.Mesh;
@@ -181,8 +376,8 @@ export async function loadAndAuditWRL(
mesh.receiveShadow = true;
}
const nodeName = resolveMeaningfulNodeName(mesh);
const parsed = parseWrlNodeName(nodeName);
const rawNodeName = resolveMeaningfulNodeName(mesh);
const parsed = parseWrlNodeName(rawNodeName);
const expressID = idCounter++;
const guid = `wrl-${expressID}-${parsed.cleanMark}`;
@@ -218,7 +413,7 @@ export async function loadAndAuditWRL(
const pieceInfo: PieceInfo = {
expressID,
guid,
name: nodeName,
name: rawNodeName,
type: parsed.detectedType,
pieceMark: parsed.pieceMark,
cleanMark: parsed.cleanMark,
@@ -238,7 +433,7 @@ export async function loadAndAuditWRL(
cleanAssemblyMark: parsed.cleanAssemblyMark,
phase: parsed.phase,
type: parsed.detectedType,
name: nodeName,
name: rawNodeName,
pieceInfo,
originalMaterial: material,
};
@@ -277,7 +472,7 @@ export async function loadAndAuditWRL(
uniqueMarksFound > 0 ? (uniqueMarksFound >= 10 ? 'EXCELENTE' : 'BOM') : 'ATENÇÃO';
const audit: IFCQualityAudit = {
schema: 'VRML 2.0 (WRL)',
schema: isFallback ? 'VRML Direto (WRL Resiliente)' : 'VRML 2.0 (WRL)',
viewDefinition: 'Virtual Reality Modeling Language 3D',
isEM11: false,
totalAssemblies: marksFound.size,
@@ -293,9 +488,9 @@ export async function loadAndAuditWRL(
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,
? `Modelo WRL processado com sucesso. ${totalMeshes} malhas renderizadas e ${uniqueMarksFound} marcas identificadas para apontamento.`
: `Modelo WRL processado com ${totalMeshes} malhas. Nenhuma marca nominal identificada na geometria.`,
fallbackActive: isFallback,
};
if (onProgress) onProgress(100, 'Modelo WRL pronto para visualização!');
+24 -3
View File
@@ -336,6 +336,16 @@ export default function Visualizador3D() {
const file = e.target.files?.[0];
if (!file) return;
if (!selectedOF) {
toast({
title: 'Selecione uma OF',
description: 'Por favor, selecione uma Ordem de Fabricação (OF) antes de carregar o modelo 3D.',
variant: 'destructive',
});
if (fileInputRef.current) fileInputRef.current.value = '';
return;
}
setIsLoadingModel(true);
setLoadingStep('Iniciando processamento e auditoria...');
setLoadingPercent(10);
@@ -591,9 +601,20 @@ export default function Visualizador3D() {
/>
<button
onClick={() => fileInputRef.current?.click()}
disabled={isLoadingModel}
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-cyan-600 to-blue-600 hover:from-cyan-500 hover:to-blue-500 text-white font-bold text-xs rounded-xl shadow-lg shadow-cyan-600/20 transition-all disabled:opacity-50 cursor-pointer"
onClick={() => {
if (!selectedOF) {
toast({
title: 'Selecione uma OF',
description: 'Por favor, selecione uma Ordem de Fabricação (OF) antes de carregar o modelo 3D.',
variant: 'destructive',
});
return;
}
fileInputRef.current?.click();
}}
disabled={isLoadingModel || !selectedOF}
title={!selectedOF ? 'Selecione uma OF acima para habilitar o carregamento do modelo 3D' : undefined}
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-cyan-600 to-blue-600 hover:from-cyan-500 hover:to-blue-500 text-white font-bold text-xs rounded-xl shadow-lg shadow-cyan-600/20 transition-all disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer"
>
{isLoadingModel ? (
<>