fix: persistência na matriz de acessos, sanitização e fallback para WRL e trava de OF no carregador
This commit is contained in:
@@ -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
@@ -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!');
|
||||
|
||||
@@ -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 ? (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user