diff --git a/conversor_relatorio_estruturas.html b/conversor_relatorio_estruturas.html index 8e97b2c..6d86461 100644 --- a/conversor_relatorio_estruturas.html +++ b/conversor_relatorio_estruturas.html @@ -386,190 +386,267 @@ } function processPdfLines(items) { - items.sort((a, b) => { - if (a.page !== b.page) return a.page - b.page; - if (Math.abs(b.y - a.y) > 3) return b.y - a.y; - return a.x - b.x; + // 1. Agrupamento robusto de linhas por Clustering de Coordenada Y em cada página + const pagesMap = new Map(); + items.forEach((it) => { + if (!pagesMap.has(it.page)) pagesMap.set(it.page, []); + pagesMap.get(it.page).push(it); }); const lines = []; - let currentLine = []; - let lastY = null; - let lastPage = null; + const Y_TOLERANCE = 6.0; - items.forEach(item => { - if (lastPage === null || item.page !== lastPage || Math.abs(item.y - lastY) > 3.5) { - if (currentLine.length > 0) { - lines.push(currentLine); + const pageNumbers = Array.from(pagesMap.keys()).sort((a, b) => a - b); + for (const pNum of pageNumbers) { + const pageItems = pagesMap.get(pNum); + const clusters = []; + + pageItems.sort((a, b) => b.y - a.y); + + for (const item of pageItems) { + let bestCluster = null; + let minDiff = Infinity; + + for (const cluster of clusters) { + const diff = Math.abs(item.y - cluster.avgY); + if (diff <= Y_TOLERANCE && diff < minDiff) { + minDiff = diff; + bestCluster = cluster; + } + } + + if (bestCluster) { + bestCluster.items.push(item); + bestCluster.ySum += item.y; + bestCluster.count += 1; + bestCluster.avgY = bestCluster.ySum / bestCluster.count; + } else { + clusters.push({ + ySum: item.y, + count: 1, + avgY: item.y, + items: [item] + }); } - currentLine = [item]; - lastY = item.y; - lastPage = item.page; - } else { - currentLine.push(item); } - }); - if (currentLine.length > 0) lines.push(currentLine); + + clusters.sort((a, b) => b.avgY - a.avgY); + + for (const cluster of clusters) { + cluster.items.sort((a, b) => a.x - b.x); + lines.push(cluster.items); + } + } log(`Total de linhas identificadas: ${lines.length}`); let defaultOf = ""; for (const line of lines) { const lineText = line.map(i => i.text).join(' '); - const matchTrabalho = lineText.match(/Trabalho:\s*([A-Za-z0-9\-]+)/i); + const matchTrabalho = lineText.match(/Trabalho:\s*([A-Za-z0-9\-]+)/i) || lineText.match(/Projeto:\s*([A-Za-z0-9\-]+)/i); if (matchTrabalho) { - defaultOf = matchTrabalho[1].replace('-', ''); + defaultOf = matchTrabalho[1].replace(/-/g, ''); log(`OF / Trabalho identificado no cabeçalho: ${defaultOf}`); break; } } + const KNOWN_MATERIALS = [ + 'A572-GR 50', 'A572-GR50', 'A572-GR.50', 'A572 GR50', 'A572GR50', 'A572GR5', 'A572', + 'A36', 'ASTM A36', 'ASTM A572', '10.9', '8.8', 'SAE 1020', 'SAE 1045', 'INOX', '304', '316' + ]; + + function normalizeLineText(lineStr) { + let s = lineStr.trim(); + s = s.replace(/([A-Za-z0-9]+)\s*-\s*(\d+)\s*-\s*(\d+)/g, '$1-$2-$3'); + s = s.replace(/([A-Za-z0-9]+)\s*-\s*(\d+)/g, '$1-$2'); + s = s.replace(/A572\s*-\s*GR\s*50/gi, 'A572-GR 50'); + s = s.replace(/A572\s*-\s*GR/gi, 'A572-GR'); + return s; + } + const assemblies = []; let currentAssembly = null; - // Filtro estrito para evitar linhas duplicadas/ruídos de componentes repetidos na listagem - const processedMarks = new Set(); - for (let i = 0; i < lines.length; i++) { const line = lines[i]; - const lineText = line.map(i => i.text).join(' '); + const rawLineText = line.map(it => it.text).join(' ').trim(); + const lineText = normalizeLineText(rawLineText); - if (lineText.includes('Lista de Peças') || lineText.includes('Cliente:') || lineText.includes('Marca Quant Nome') || lineText.includes('Superfície')) { + if ( + lineText.includes('LISTA DE PEÇAS') || + lineText.includes('LISTA ESTRUTURADA') || + lineText.includes('Lista de Peças') || + lineText.includes('DetailDwgExtract') || + lineText.includes('Cliente:') || + lineText.includes('Desenhado') || + lineText.includes('Superfície') || + lineText.includes('Trabalho:') || + lineText.includes('Kg/piece') || + lineText.includes('( mm)') || + lineText.toLowerCase().includes('peso total:') || + (lineText.toLowerCase().includes('marca') && lineText.toLowerCase().includes('nome')) + ) { continue; } - const firstToken = line[0].text; - const markMatch = firstToken.match(/^([A-Za-z0-9]+)-(\d+)-(\d+)$/) || firstToken.match(/^([A-Za-z0-9]+)-(\d+)$/); - - if (markMatch) { - const parts = firstToken.split('-'); - let ofCode = ""; - let phaseCode = "0"; - let pieceNumber = ""; - - if (parts.length === 3) { - ofCode = parts[0]; - phaseCode = parts[1]; - pieceNumber = parts[2]; - } else if (parts.length === 2) { - ofCode = parts[0]; - phaseCode = "0"; - pieceNumber = parts[1]; - } - + // 1. Linha Mestre de Peça Principal (ex: "B134-13-1 90 W 150x13.0" ou "B134-13-2 72 W 150x13.0") + const masterMatch = lineText.match(/^([A-Za-z0-9]+(?:-\d+)+)(?:\s+(\d+))?\s+([A-Za-z].*)$/); + if (masterMatch) { + const fullMark = masterMatch[1]; + const quantFromLine = masterMatch[2] ? parseInt(masterMatch[2], 10) : 1; + const descCandidate = masterMatch[3].trim(); + const parts = fullMark.split('-'); + const pieceNumber = parts[parts.length - 1]; const pieceNumInt = parseInt(pieceNumber, 10); - const isComponent = !isNaN(pieceNumInt) && pieceNumInt >= 1000; - if (!isComponent) { - // Evitar duplicidade caso a mesma marca principal apareça mais de uma vez agrupada - if (processedMarks.has(firstToken)) { + if (pieceNumInt < 1000) { + const hasMaterial = KNOWN_MATERIALS.some(m => descCandidate.includes(m)); + + if (!hasMaterial) { + const ofCode = parts.length >= 2 ? parts[0] : defaultOf; + const phaseCode = parts.length >= 3 ? parts[1] : (parts.length === 2 ? parts[0] : '1'); + + currentAssembly = { + rawMark: fullMark, + of: ofCode || defaultOf, + fase: phaseCode || '1', + marca: pieceNumber, + numeroPeca: pieceNumber, + descricao: descCandidate || 'ESTRUTURA', + quantidade: quantFromLine > 0 ? quantFromLine : 1, + material: '', + comprimento: 0, + pesoUnitario: 0, + pesoTotal: 0, + subtotalPeso: 0, + isComposed: 'NÃO', + components: [] + }; + assemblies.push(currentAssembly); continue; } - processedMarks.add(firstToken); + } + } - let mainQuant = 1; - let mainDesc = ""; - - if (line.length >= 2) { - const qVal = parseInt(line[1].text, 10); - if (!isNaN(qVal)) { - mainQuant = qVal; - // CORREÇÃO 2: Garantir que a descrição seja única, pegando apenas o primeiro token descritivo limpo (ex: W 310x38.7) - let rawDescTokens = line.slice(2).map(item => item.text); - if (rawDescTokens.length > 0) { - mainDesc = rawDescTokens[0]; // Nome único/perfil inicial da peça principal - // Se o perfil tiver complemento separado por espaço (ex: W 310x38.7), agrupar se necessário ou manter limpo - if (rawDescTokens.length > 1 && (rawDescTokens[1].includes('x') || rawDescTokens[1].includes('Pl') || !isNaN(parseFloat(rawDescTokens[1])))) { - mainDesc = rawDescTokens.slice(0, 2).join(' '); - } - } - } else { - let rawDescTokens = line.slice(1).map(item => item.text); - mainDesc = rawDescTokens.length > 0 ? rawDescTokens[0] : 'ESTRUTURA'; - } - } - - currentAssembly = { - rawMark: firstToken, - of: ofCode || defaultOf, - fase: phaseCode, - marca: pieceNumber, // CORREÇÃO 1: Apenas o número da peça (última numeração) - numeroPeca: pieceNumber, - descricao: mainDesc || 'ESTRUTURA', - quantidade: mainQuant, - pesoUnitario: 0, - pesoTotal: 0, - components: [] - }; - assemblies.push(currentAssembly); - } else { - // Componente subordinado - if (currentAssembly) { - let compQuant = 1; - let compProfile = ""; - let compMaterial = "A36"; - let compLength = 0; - let compWeightTotal = 0; - - let compTokens = line.map(it => it.text); - if (compTokens.length >= 2) { - const qTest = parseInt(compTokens[1], 10); - if (!isNaN(qTest)) compQuant = qTest; - } - - let matIndex = -1; - for (let k = 1; k < compTokens.length; k++) { - const tok = compTokens[k].toUpperCase(); - if (tok.includes('A572') || tok.includes('A36') || tok.includes('A500') || tok.includes('A106') || tok.includes('GR') || tok.includes('INOX') || tok.includes('SAE')) { - matIndex = k; - compMaterial = compTokens[k]; - if (k + 1 < compTokens.length && (compTokens[k+1].toUpperCase().includes('GR') || compTokens[k+1] === '50')) { - compMaterial += ' ' + compTokens[k+1]; - } - break; - } - } - - if (matIndex > 2) { - compProfile = compTokens.slice(2, matIndex).join(' '); - } else if (compTokens.length >= 3) { - compProfile = compTokens[2]; - } - - const afterMatTokens = matIndex !== -1 ? compTokens.slice(matIndex + 1) : compTokens.slice(3); - const numericValues = []; - afterMatTokens.forEach(t => { - const cleanT = t.replace(/[()]/g, ''); - if (/[0-9]/.test(cleanT) && !cleanT.toUpperCase().includes('GR')) { - numericValues.push(cleanT); - } - }); - - if (numericValues.length >= 1) { - compLength = parseNumeric(numericValues[0]); - } - // O peso total do componente geralmente é o último ou penúltimo valor numérico da linha de detalhe - if (numericValues.length >= 3) { - compWeightTotal = parseNumeric(numericValues[numericValues.length - 1]); - } else if (numericValues.length === 2) { - compWeightTotal = parseNumeric(numericValues[1]); - } - - currentAssembly.components.push({ - quantidade: compQuant, - perfil: compProfile, - material: compMaterial, - comprimento: compLength, - pesoTotal: compWeightTotal - }); + // 2. Linha com Subtotal da Marca + const tokens = lineText.split(/\s+/); + const allTokensNumeric = tokens.length > 0 && tokens.every(tok => /^[0-9.,]+$/.test(tok)); + if (allTokensNumeric && tokens.length <= 3) { + if (currentAssembly && (!currentAssembly.subtotalPeso || currentAssembly.subtotalPeso === 0)) { + const subP = parseNumeric(tokens[0]); + if (subP > 0) { + currentAssembly.subtotalPeso = subP; } } + continue; + } + + // 3. Linha de Detalhe ou Componente + let itemMark = ''; + let itemQuant = 1; + let remaining = lineText; + + const mMark = remaining.match(/^([A-Za-z0-9]+(?:-\d+)+|-)\s+(\d+)\s+/); + if (mMark) { + itemMark = mMark[1]; + itemQuant = parseInt(mMark[2], 10); + remaining = remaining.substring(mMark[0].length).trim(); + } else if (remaining.startsWith('-')) { + itemMark = '-'; + remaining = remaining.substring(1).trim(); + const mQ = remaining.match(/^(\d+)\s+/); + if (mQ) { + itemQuant = parseInt(mQ[1], 10); + remaining = remaining.substring(mQ[0].length).trim(); + } + } + + let detectedMaterial = ''; + let matIdx = -1; + let matLen = 0; + + for (const mat of KNOWN_MATERIALS) { + const idx = remaining.indexOf(mat); + if (idx !== -1 && idx > matIdx) { + detectedMaterial = mat; + matIdx = idx; + matLen = mat.length; + } + } + + let itemDesc = ''; + let afterMat = ''; + + if (matIdx !== -1) { + itemDesc = remaining.substring(0, matIdx).trim(); + afterMat = remaining.substring(matIdx + matLen).trim(); } else { - // Linha de subtotal ou peso geral da peça principal - if (currentAssembly && line.length <= 3) { - const possibleTotal = parseNumeric(line[0].text); - if (possibleTotal > 10 && currentAssembly.pesoTotal === 0) { - currentAssembly.pesoTotal = possibleTotal; + const parts = remaining.split(/\s+/); + const firstNumIdx = parts.findIndex(p => /^[0-9.,]+$/.test(p)); + if (firstNumIdx > 0) { + itemDesc = parts.slice(0, firstNumIdx).join(' '); + afterMat = parts.slice(firstNumIdx).join(' '); + } else { + itemDesc = remaining; + } + } + + const numTokens = afterMat.split(/\s+/).filter(tok => /^[0-9.,]+$/.test(tok)); + const nums = numTokens.map(parseNumeric); + + let itemComp = 0; + let itemPUn = 0; + let itemPesoTot = 0; + + if (nums.length >= 1) { + itemComp = nums[0]; + } + + if (nums.length === 3) { + itemPUn = nums[1]; + itemPesoTot = nums[2]; + } else if (nums.length === 4) { + itemPUn = nums[1]; + itemPesoTot = nums[2]; + } else if (nums.length === 5) { + itemPUn = nums[1]; + itemPesoTot = nums[2]; + } else if (nums.length >= 6) { + itemPUn = nums[2]; + itemPesoTot = nums[3]; + } + + if (currentAssembly) { + const isSubComp = + itemMark === '-' || + itemMark.includes('-100') || + itemMark.includes('-10') || + (itemMark.match(/-\d+$/) && parseInt(itemMark.match(/-(\d+)$/)[1], 10) >= 1000); + + if (isSubComp) { + currentAssembly.isComposed = 'SIM'; + currentAssembly.components.push({ + quantidade: itemQuant, + perfil: itemDesc || currentAssembly.descricao, + material: detectedMaterial || 'A36', + comprimento: itemComp, + pesoUnitario: itemPUn, + pesoTotal: itemPesoTot + }); + } else { + // Monopeça + if (!currentAssembly.material && detectedMaterial) { + currentAssembly.material = detectedMaterial; + } + if (itemComp > 0 && (!currentAssembly.comprimento || currentAssembly.comprimento === 0)) { + currentAssembly.comprimento = itemComp; + } + if (itemPUn > 0 && (!currentAssembly.pesoUnitario || currentAssembly.pesoUnitario === 0)) { + currentAssembly.pesoUnitario = itemPUn; + } + if (itemPesoTot > 0 && (!currentAssembly.pesoTotal || currentAssembly.pesoTotal === 0)) { + currentAssembly.pesoTotal = itemPesoTot; } } } @@ -578,45 +655,73 @@ log(`Total de Peças Principais processadas: ${assemblies.length}`); extractedData = assemblies.map(asm => { - const hasComponents = asm.components.length > 0; - + const isComposed = asm.isComposed === 'SIM' && asm.components.length > 0; let perfilPrincipal = asm.descricao; - let materialPrincipal = "A36"; - let maxComp = 0; - let sumComponentsWeight = 0; + let materialPrincipal = 'A36'; + let maxComp = asm.comprimento || 0; + let totalPeso = asm.subtotalPeso > 0 ? asm.subtotalPeso : asm.pesoTotal; - if (hasComponents) { + if (isComposed) { + let sumComponentsWeight = 0; + let sumUnitComponentsWeight = 0; let bestComp = asm.components[0]; + maxComp = 0; + asm.components.forEach(c => { if (c.comprimento > maxComp) { maxComp = c.comprimento; bestComp = c; } sumComponentsWeight += c.pesoTotal; + if (c.pesoUnitario && c.pesoUnitario > 0) { + sumUnitComponentsWeight += c.pesoUnitario * c.quantidade; + } }); - perfilPrincipal = bestComp.perfil || asm.descricao; - materialPrincipal = bestComp.material || "A36"; + if (bestComp) { + perfilPrincipal = bestComp.perfil || asm.descricao; + materialPrincipal = bestComp.material || 'A36'; + maxComp = bestComp.comprimento; + } + + if (!totalPeso || totalPeso === 0) { + totalPeso = sumComponentsWeight; + } + + if (asm.quantidade === 1 && sumUnitComponentsWeight > 0 && totalPeso > 0) { + const ratio = Math.round(totalPeso / sumUnitComponentsWeight); + if (ratio > 1) { + asm.quantidade = ratio; + } + } + } else { + // Monopeça simples + if (!totalPeso || totalPeso === 0) { + totalPeso = (asm.pesoUnitario || 0) * asm.quantidade; + } + + if (asm.quantidade === 1 && asm.pesoUnitario > 0 && totalPeso > asm.pesoUnitario) { + const ratio = Math.round(totalPeso / asm.pesoUnitario); + if (ratio > 1) { + asm.quantidade = ratio; + } + } } - // Definir peso total final (prioriza o subtotal extraído do relatório) - let finalPesoTotal = asm.pesoTotal > 0 ? asm.pesoTotal : sumComponentsWeight; - - // CORREÇÃO 3: Peso Unitário = Peso Total dividido pela quantidade da peça principal - let finalPesoUnit = asm.quantidade > 0 ? (finalPesoTotal / asm.quantidade) : finalPesoTotal; + const pesoUnit = asm.quantidade > 0 ? (totalPeso / asm.quantidade) : (asm.pesoUnitario || totalPeso); return { of: asm.of, fase: asm.fase, marca: asm.marca, descricao: asm.descricao, - isComposed: hasComponents ? "SIM" : "NÃO", + isComposed: isComposed ? "SIM" : "NÃO", quantidade: asm.quantidade, material: materialPrincipal, perfilPrincipal: perfilPrincipal, comprimentoMax: maxComp > 0 ? maxComp : "-", - pesoUnit: finalPesoUnit > 0 ? finalPesoUnit.toFixed(2) : "-", - pesoTotal: finalPesoTotal > 0 ? finalPesoTotal.toFixed(2) : "-", + pesoUnit: pesoUnit > 0 ? pesoUnit.toFixed(2) : "-", + pesoTotal: totalPeso > 0 ? totalPeso.toFixed(2) : "-", tratamentoSuperficial: "pintura" }; }); diff --git a/public/conversor_relatorio_estruturas.html b/public/conversor_relatorio_estruturas.html index 8f26fa7..5e2b6fa 100644 --- a/public/conversor_relatorio_estruturas.html +++ b/public/conversor_relatorio_estruturas.html @@ -386,30 +386,57 @@ } function processPdfLines(items) { - items.sort((a, b) => { - if (a.page !== b.page) return a.page - b.page; - if (Math.abs(b.y - a.y) > 3) return b.y - a.y; - return a.x - b.x; + // 1. Agrupamento robusto de linhas por Clustering de Coordenada Y em cada página + const pagesMap = new Map(); + items.forEach((it) => { + if (!pagesMap.has(it.page)) pagesMap.set(it.page, []); + pagesMap.get(it.page).push(it); }); const lines = []; - let currentLine = []; - let lastY = null; - let lastPage = null; + const Y_TOLERANCE = 6.0; - items.forEach(item => { - if (lastPage === null || item.page !== lastPage || Math.abs(item.y - (lastY ?? item.y)) > 4.5) { - if (currentLine.length > 0) { - lines.push(currentLine); + const pageNumbers = Array.from(pagesMap.keys()).sort((a, b) => a - b); + for (const pNum of pageNumbers) { + const pageItems = pagesMap.get(pNum); + const clusters = []; + + pageItems.sort((a, b) => b.y - a.y); + + for (const item of pageItems) { + let bestCluster = null; + let minDiff = Infinity; + + for (const cluster of clusters) { + const diff = Math.abs(item.y - cluster.avgY); + if (diff <= Y_TOLERANCE && diff < minDiff) { + minDiff = diff; + bestCluster = cluster; + } + } + + if (bestCluster) { + bestCluster.items.push(item); + bestCluster.ySum += item.y; + bestCluster.count += 1; + bestCluster.avgY = bestCluster.ySum / bestCluster.count; + } else { + clusters.push({ + ySum: item.y, + count: 1, + avgY: item.y, + items: [item] + }); } - currentLine = [item]; - lastY = item.y; - lastPage = item.page; - } else { - currentLine.push(item); } - }); - if (currentLine.length > 0) lines.push(currentLine); + + clusters.sort((a, b) => b.avgY - a.avgY); + + for (const cluster of clusters) { + cluster.items.sort((a, b) => a.x - b.x); + lines.push(cluster.items); + } + } log(`Total de linhas identificadas: ${lines.length}`); @@ -449,6 +476,7 @@ if ( lineText.includes('LISTA DE PEÇAS') || lineText.includes('LISTA ESTRUTURADA') || + lineText.includes('Lista de Peças') || lineText.includes('DetailDwgExtract') || lineText.includes('Cliente:') || lineText.includes('Desenhado') || @@ -456,37 +484,37 @@ lineText.includes('Trabalho:') || lineText.includes('Kg/piece') || lineText.includes('( mm)') || + lineText.toLowerCase().includes('peso total:') || (lineText.toLowerCase().includes('marca') && lineText.toLowerCase().includes('nome')) ) { continue; } - // 1. Linha Mestre de Peça Principal (ex: "B133-1-20 2 Pl 6x160x154" ou "B133-1-20 Pl 6x160x154") + // 1. Linha Mestre de Peça Principal (ex: "B134-13-1 90 W 150x13.0" ou "B134-13-2 72 W 150x13.0") const masterMatch = lineText.match(/^([A-Za-z0-9]+(?:-\d+)+)(?:\s+(\d+))?\s+([A-Za-z].*)$/); if (masterMatch) { const fullMark = masterMatch[1]; - const inlineQuant = masterMatch[2] ? parseInt(masterMatch[2], 10) : 0; + const quantFromLine = masterMatch[2] ? parseInt(masterMatch[2], 10) : 1; const descCandidate = masterMatch[3].trim(); const parts = fullMark.split('-'); const pieceNumber = parts[parts.length - 1]; const pieceNumInt = parseInt(pieceNumber, 10); - // Peça principal tem identificador < 1000 e não possui material na linha de cabeçalho da marca if (pieceNumInt < 1000) { const hasMaterial = KNOWN_MATERIALS.some(m => descCandidate.includes(m)); if (!hasMaterial) { const ofCode = parts.length >= 2 ? parts[0] : defaultOf; - const phaseCode = parts.length >= 3 ? parts[1] : '0'; + const phaseCode = parts.length >= 3 ? parts[1] : (parts.length === 2 ? parts[0] : '1'); currentAssembly = { rawMark: fullMark, of: ofCode || defaultOf, - fase: phaseCode, + fase: phaseCode || '1', marca: pieceNumber, numeroPeca: pieceNumber, descricao: descCandidate || 'ESTRUTURA', - quantidade: inlineQuant > 0 ? inlineQuant : 1, + quantidade: quantFromLine > 0 ? quantFromLine : 1, material: '', comprimento: 0, pesoUnitario: 0, @@ -501,7 +529,7 @@ } } - // 2. Linha com Subtotal da Marca (ex: "81 3,07" ou "3,5 0,155" ou "2,6 0,25") + // 2. Linha com Subtotal da Marca const tokens = lineText.split(/\s+/); const allTokensNumeric = tokens.length > 0 && tokens.every(tok => /^[0-9.,]+$/.test(tok)); if (allTokensNumeric && tokens.length <= 3) { @@ -568,6 +596,7 @@ const nums = numTokens.map(parseNumeric); let itemComp = 0; + let itemPUn = 0; let itemPesoTot = 0; if (nums.length >= 1) { @@ -575,12 +604,16 @@ } if (nums.length === 3) { + itemPUn = nums[1]; itemPesoTot = nums[2]; } else if (nums.length === 4) { + itemPUn = nums[1]; itemPesoTot = nums[2]; } else if (nums.length === 5) { + itemPUn = nums[1]; itemPesoTot = nums[2]; } else if (nums.length >= 6) { + itemPUn = nums[2]; itemPesoTot = nums[3]; } @@ -598,15 +631,20 @@ perfil: itemDesc || currentAssembly.descricao, material: detectedMaterial || 'A36', comprimento: itemComp, + pesoUnitario: itemPUn, pesoTotal: itemPesoTot }); } else { + // Monopeça if (!currentAssembly.material && detectedMaterial) { currentAssembly.material = detectedMaterial; } if (itemComp > 0 && (!currentAssembly.comprimento || currentAssembly.comprimento === 0)) { currentAssembly.comprimento = itemComp; } + if (itemPUn > 0 && (!currentAssembly.pesoUnitario || currentAssembly.pesoUnitario === 0)) { + currentAssembly.pesoUnitario = itemPUn; + } if (itemPesoTot > 0 && (!currentAssembly.pesoTotal || currentAssembly.pesoTotal === 0)) { currentAssembly.pesoTotal = itemPesoTot; } @@ -623,10 +661,9 @@ let maxComp = asm.comprimento || 0; let totalPeso = asm.subtotalPeso > 0 ? asm.subtotalPeso : asm.pesoTotal; - let finalQuant = asm.quantidade; - if (isComposed) { let sumComponentsWeight = 0; + let sumUnitComponentsWeight = 0; let bestComp = asm.components[0]; maxComp = 0; @@ -636,6 +673,9 @@ bestComp = c; } sumComponentsWeight += c.pesoTotal; + if (c.pesoUnitario && c.pesoUnitario > 0) { + sumUnitComponentsWeight += c.pesoUnitario * c.quantidade; + } }); if (bestComp) { @@ -648,23 +688,27 @@ totalPeso = sumComponentsWeight; } - // Se a quantidade veio 1 (ou não veio no cabeçalho), calcular relação peso total / soma peso unitário dos componentes - if (finalQuant <= 1 && asm.components.length > 0) { - const sumUnitWeights = asm.components.reduce((acc, c) => { - const unitW = c.quantidade > 0 ? (c.pesoTotal / c.quantidade) : c.pesoTotal; - return acc + unitW; - }, 0); - if (sumUnitWeights > 0 && totalPeso > sumUnitWeights * 1.3) { - const ratio = Math.round(totalPeso / sumUnitWeights); - if (ratio > 1) { - finalQuant = ratio; - } + if (asm.quantidade === 1 && sumUnitComponentsWeight > 0 && totalPeso > 0) { + const ratio = Math.round(totalPeso / sumUnitComponentsWeight); + if (ratio > 1) { + asm.quantidade = ratio; + } + } + } else { + // Monopeça simples + if (!totalPeso || totalPeso === 0) { + totalPeso = (asm.pesoUnitario || 0) * asm.quantidade; + } + + if (asm.quantidade === 1 && asm.pesoUnitario > 0 && totalPeso > asm.pesoUnitario) { + const ratio = Math.round(totalPeso / asm.pesoUnitario); + if (ratio > 1) { + asm.quantidade = ratio; } } } - // Regra de Ouro: Peso Unitário = Peso Total dividido pela quantidade da peça principal - const pesoUnit = finalQuant > 0 ? (totalPeso / finalQuant) : totalPeso; + const pesoUnit = asm.quantidade > 0 ? (totalPeso / asm.quantidade) : (asm.pesoUnitario || totalPeso); return { of: asm.of, @@ -672,7 +716,7 @@ marca: asm.marca, descricao: asm.descricao, isComposed: isComposed ? "SIM" : "NÃO", - quantidade: finalQuant, + quantidade: asm.quantidade, material: materialPrincipal, perfilPrincipal: perfilPrincipal, comprimentoMax: maxComp > 0 ? maxComp : "-", diff --git a/src/components/conversores/AdvanceSteelConverter.tsx b/src/components/conversores/AdvanceSteelConverter.tsx index 33ce080..63bad3f 100644 --- a/src/components/conversores/AdvanceSteelConverter.tsx +++ b/src/components/conversores/AdvanceSteelConverter.tsx @@ -228,40 +228,81 @@ const AdvanceSteelConverterContent: React.FC = () => { }; const processPdfLines = (items: PdfItem[]) => { - items.sort((a, b) => { - if (a.page !== b.page) return a.page - b.page; - if (Math.abs(b.y - a.y) > 3) return b.y - a.y; - return a.x - b.x; + // 1. Agrupamento robusto de linhas por Clustering de Coordenada Y em cada página + const pagesMap = new Map(); + items.forEach((it) => { + if (!pagesMap.has(it.page)) pagesMap.set(it.page, []); + pagesMap.get(it.page)!.push(it); }); const lines: PdfItem[][] = []; - let currentLine: PdfItem[] = []; - let lastY: number | null = null; - let lastPage: number | null = null; + const Y_TOLERANCE = 6.0; // Tolerância vertical em pixels para agrupar na mesma linha visual - items.forEach((item) => { - if (lastPage === null || item.page !== lastPage || Math.abs(item.y - (lastY ?? item.y)) > 4.5) { - if (currentLine.length > 0) { - lines.push(currentLine); - } - currentLine = [item]; - lastY = item.y; - lastPage = item.page; - } else { - currentLine.push(item); + const pageNumbers = Array.from(pagesMap.keys()).sort((a, b) => a - b); + for (const pNum of pageNumbers) { + const pageItems = pagesMap.get(pNum)!; + + interface LineCluster { + ySum: number; + count: number; + avgY: number; + items: PdfItem[]; } - }); - if (currentLine.length > 0) lines.push(currentLine); + + const clusters: LineCluster[] = []; + + // Ordena por Y decrescente (do topo da página para o rodapé) + pageItems.sort((a, b) => b.y - a.y); + + for (const item of pageItems) { + let bestCluster: LineCluster | null = null; + let minDiff = Infinity; + + for (const cluster of clusters) { + const diff = Math.abs(item.y - cluster.avgY); + if (diff <= Y_TOLERANCE && diff < minDiff) { + minDiff = diff; + bestCluster = cluster; + } + } + + if (bestCluster) { + bestCluster.items.push(item); + bestCluster.ySum += item.y; + bestCluster.count += 1; + bestCluster.avgY = bestCluster.ySum / bestCluster.count; + } else { + clusters.push({ + ySum: item.y, + count: 1, + avgY: item.y, + items: [item] + }); + } + } + + // Ordena as linhas do topo para o rodapé da página + clusters.sort((a, b) => b.avgY - a.avgY); + + // Dentro de cada linha, ordena da esquerda para a direita (X crescente) + for (const cluster of clusters) { + cluster.items.sort((a, b) => a.x - b.x); + lines.push(cluster.items); + } + } addLog(`Total de linhas identificadas no PDF: ${lines.length}`); let defaultOf = ''; for (const line of lines) { const lineText = line.map((i) => i.text).join(' '); - const matchTrabalho = lineText.match(/Trabalho:\s*([A-Za-z0-9-]+)/i) || lineText.match(/Projeto:\s*([A-Za-z0-9-]+)/i); + const matchTrabalho = + lineText.match(/Trabalho:\s*([A-Za-z0-9-]+)/i) || + lineText.match(/Projeto:\s*([A-Za-z0-9-]+)/i); if (matchTrabalho) { defaultOf = matchTrabalho[1].replace(/-/g, ''); addLog(`OF / Trabalho identificado no cabeçalho: ${defaultOf}`); + setHeaderOf(defaultOf); break; } } @@ -292,6 +333,7 @@ const AdvanceSteelConverterContent: React.FC = () => { if ( lineText.includes('LISTA DE PEÇAS') || lineText.includes('LISTA ESTRUTURADA') || + lineText.includes('Lista de Peças') || lineText.includes('DetailDwgExtract') || lineText.includes('Cliente:') || lineText.includes('Desenhado') || @@ -299,17 +341,19 @@ const AdvanceSteelConverterContent: React.FC = () => { lineText.includes('Trabalho:') || lineText.includes('Kg/piece') || lineText.includes('( mm)') || + lineText.toLowerCase().includes('peso total:') || (lineText.toLowerCase().includes('marca') && lineText.toLowerCase().includes('nome')) ) { continue; } - // 1. Linha Mestre de Peça Principal (ex: "B133-1-20 Pl 6x160x154" ou "B133-1-20 2 Pl 6x160x154") + // 1. Linha Mestre de Peça Principal (ex: "B134-13-1 90 W 150x13.0" ou "B134-13-2 72 W 150x13.0") + // A primeira linha de cada registro contém a Marca, a Quantidade Total da Peça e o Nome/Perfil const masterMatch = lineText.match(/^([A-Za-z0-9]+(?:-\d+)+)(?:\s+(\d+))?\s+([A-Za-z].*)$/); if (masterMatch) { const fullMark = masterMatch[1]; - const quant = masterMatch[2] ? parseInt(masterMatch[2], 10) : 1; + const quantFromLine = masterMatch[2] ? parseInt(masterMatch[2], 10) : 1; const descCandidate = masterMatch[3].trim(); const parts = fullMark.split('-'); const pieceNumber = parts[parts.length - 1]; @@ -319,18 +363,23 @@ const AdvanceSteelConverterContent: React.FC = () => { if (pieceNumInt < 1000) { const hasMaterial = KNOWN_MATERIALS.some((m) => descCandidate.includes(m)); + // A linha mestre não possui indicação de material na descrição nem pesos if (!hasMaterial) { const ofCode = parts.length >= 2 ? parts[0] : defaultOf; - const phaseCode = parts.length >= 3 ? parts[1] : '0'; + const phaseCode = parts.length >= 3 ? parts[1] : (parts.length === 2 ? parts[0] : '1'); + + if (phaseCode && phaseCode !== '0' && isNaN(Number(phaseCode)) === false) { + setHeaderFase(phaseCode); + } currentAssembly = { rawMark: fullMark, of: ofCode || defaultOf, - fase: phaseCode, + fase: phaseCode || '1', marca: pieceNumber, numeroPeca: pieceNumber, descricao: descCandidate || 'ESTRUTURA', - quantidade: quant > 0 ? quant : 1, + quantidade: quantFromLine > 0 ? quantFromLine : 1, material: '', comprimento: 0, pesoUnitario: 0, @@ -345,7 +394,7 @@ const AdvanceSteelConverterContent: React.FC = () => { } } - // 2. Linha com Subtotal da Marca (ex: "81 3,07" ou "3,5 0,155" ou "2,6 0,25") + // 2. Linha com Subtotal da Marca (ex: "1.973,10 101,691" ou "408,70 20,434" ou "81 3,07") const tokens = lineText.split(/\s+/); const allTokensNumeric = tokens.length > 0 && tokens.every((tok) => /^[0-9.,]+$/.test(tok)); if (allTokensNumeric && tokens.length <= 3) { @@ -453,6 +502,7 @@ const AdvanceSteelConverterContent: React.FC = () => { }); } else { // Monopeça (peça simples sem componentes subordinados) + // Preenche os dados técnicos da peça SEM sobrescrever a quantidade mestre if (!currentAssembly.material && detectedMaterial) { currentAssembly.material = detectedMaterial; } @@ -490,7 +540,7 @@ const AdvanceSteelConverterContent: React.FC = () => { bestComp = c; } sumComponentsWeight += c.pesoTotal; - if (c.pesoUnitario > 0) { + if (c.pesoUnitario && c.pesoUnitario > 0) { sumUnitComponentsWeight += c.pesoUnitario * c.quantidade; } }); @@ -506,17 +556,30 @@ const AdvanceSteelConverterContent: React.FC = () => { totalPeso = sumComponentsWeight; } - // Detecção de quantidade de conjunto caso tenha vindo 1: + // Fallback de segurança para conjunto caso a quantidade na linha 1 não tenha sido lida if (asm.quantidade === 1 && sumUnitComponentsWeight > 0 && totalPeso > 0) { const ratio = Math.round(totalPeso / sumUnitComponentsWeight); if (ratio > 1) { asm.quantidade = ratio; } } + } else { + // Monopeça simples + if (!totalPeso || totalPeso === 0) { + totalPeso = (asm.pesoUnitario || 0) * asm.quantidade; + } + + // Fallback de segurança para monopeça caso a quantidade mestre não tenha sido lida e veio 1 + if (asm.quantidade === 1 && asm.pesoUnitario > 0 && totalPeso > asm.pesoUnitario) { + const ratio = Math.round(totalPeso / asm.pesoUnitario); + if (ratio > 1) { + asm.quantidade = ratio; + } + } } // Regra de Ouro: Peso Unitário = Peso Total dividido pela Quantidade da peça principal - const pesoUnit = asm.quantidade > 0 ? totalPeso / asm.quantidade : totalPeso; + const pesoUnit = asm.quantidade > 0 ? totalPeso / asm.quantidade : (asm.pesoUnitario || totalPeso); return { of: asm.of,