Recognize commercial parts with alphabetic names in .asm scanning
Pro-E numeric codes cover most components, but parts downloaded as STEP from online catalogs (rings, bushings, knobs...) get alphabetic Creo names with no predictable shape. Add a second extraction path that tokenizes the binary generically and validates candidates against the known codes in cad_index.db, applying the same placement/occurrence rule used for bare numeric codes. Numeric extraction is unchanged, so missing-from-index reporting for renamed/moved Pro-E codes still works. Also load all known prt/asm codes once per scan instead of one query per candidate, in both RisolviAlberoComponenti and VerificaCompletezzaAssiemi. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c75dd21551
commit
8a8a7d0417
7 files changed
+363
-106
No files matched your search
@@ -105,9 +105,12 @@ disco di default `X:\`, hardcoded in `Form1.RadiceDiscoDefault`).
|
||||
`scripts/` contiene C# file-based apps (.NET 10, `dotnet run scripts/Nome.cs -- arg1 arg2`),
|
||||
senza csproj — dipendenze NuGet extra si aggiungono con `#:package Nome@versione` in cima
|
||||
al file. `ExtractCreoStrings.cs` estrae la BOM di un `.asm` risolvendo ricorsivamente i
|
||||
sotto-assiemi contro `cad_index.db` (stessa logica di `Form1.AssiemeCompleto`); `Analyze.cs`
|
||||
dumpa il contesto testo/hex attorno a un pattern in un file binario, per investigare a mano
|
||||
nuovi falsi positivi prima di toccare la regex.
|
||||
sotto-assiemi contro `cad_index.db` (stessa logica di `Form1.AssiemeCompleto`, solo codici
|
||||
numerici Pro-E); `BomViaDb.cs` fa lo stesso ma con la tokenizzazione generica validata contro
|
||||
il DB, usata per intercettare anche i nomi alfabetici delle parti commerciali (vedi
|
||||
`docs/formato-asm.md`); `Analyze.cs` dumpa il contesto testo/hex attorno a un pattern in un
|
||||
file binario, per investigare a mano nuovi falsi positivi prima di toccare la regex; `Query.cs`
|
||||
esegue una query SQL ad-hoc contro `cad_index.db` da riga di comando.
|
||||
|
||||
Il formato binario `.asm`, l'euristica di estrazione codici e i falsi positivi noti (ID vista
|
||||
`ModelView#`, self-reference, riferimenti fantasma a codici superati/rinominati — sintatticamente
|
||||
|
||||
@@ -92,7 +92,7 @@ public class AssemblyComponentScannerTests
|
||||
"Componente ID 1 (R12--_-_U6592<UNI6592>.PRT) x Componente ID 2 (R12--_-_U6592<UNI6592>.PRT)");
|
||||
try
|
||||
{
|
||||
var codici = AssemblyComponentScanner.EstraiCodiciReferenziati(percorso);
|
||||
var codici = AssemblyComponentScanner.EstraiCodiciReferenziati(percorso, codiciNoti: new[] { "UNI6592" });
|
||||
Assert.Contains("UNI6592", codici);
|
||||
Assert.DoesNotContain("R12--_-_U6592<UNI6592>", codici);
|
||||
}
|
||||
@@ -103,4 +103,49 @@ public class AssemblyComponentScannerTests
|
||||
public void File_inesistente_lancia_eccezione_IO() =>
|
||||
Assert.Throws<DirectoryNotFoundException>(() =>
|
||||
AssemblyComponentScanner.EstraiCodiciReferenziati(@"C:\non\esiste\pippo.asm.1"));
|
||||
|
||||
[Fact]
|
||||
public void Riconosce_parte_commerciale_con_nome_alfabetico_se_nel_db()
|
||||
{
|
||||
// Visto su un caso reale: le parti commerciali scaricate come STEP (es. anelli,
|
||||
// boccole) hanno nomi Creo alfabetici, non un codice numerico Pro-E - si
|
||||
// riconoscono solo se il nome esiste davvero come codice indicizzato.
|
||||
string percorso = ScriviAsmFinto(
|
||||
"blah Componente ID 11 (ANELLO_ESTERNO-1-SOLID1.PRT) blah RUMORE_CASUALE blah");
|
||||
try
|
||||
{
|
||||
var codiciNoti = new[] { "ANELLO_ESTERNO-1-SOLID1" };
|
||||
var codici = AssemblyComponentScanner.EstraiCodiciReferenziati(percorso, codiciNoti);
|
||||
Assert.Contains("ANELLO_ESTERNO-1-SOLID1", codici);
|
||||
}
|
||||
finally { File.Delete(percorso); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Scarta_token_alfabetico_nel_db_ma_senza_placement()
|
||||
{
|
||||
// Un token che coincide per puro caso con un codice DB corto/generico (es. una
|
||||
// keyword interna di Creo) non basta: senza un vero placement resta rumore.
|
||||
string percorso = ScriviAsmFinto("start_part blah blah start_part");
|
||||
try
|
||||
{
|
||||
var codiciNoti = new[] { "start_part" };
|
||||
var codici = AssemblyComponentScanner.EstraiCodiciReferenziati(percorso, codiciNoti);
|
||||
Assert.DoesNotContain("start_part", codici);
|
||||
}
|
||||
finally { File.Delete(percorso); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ignora_token_alfabetici_non_presenti_nel_db()
|
||||
{
|
||||
string percorso = ScriviAsmFinto(
|
||||
"blah Componente ID 1 (QUALCOSA_DI_INESISTENTE-1.PRT) blah");
|
||||
try
|
||||
{
|
||||
var codici = AssemblyComponentScanner.EstraiCodiciReferenziati(percorso, codiciNoti: new[] { "ALTRO_CODICE" });
|
||||
Assert.DoesNotContain("QUALCOSA_DI_INESISTENTE-1", codici);
|
||||
}
|
||||
finally { File.Delete(percorso); }
|
||||
}
|
||||
}
|
||||
@@ -6,59 +6,86 @@ namespace CadSearchGui;
|
||||
/// <summary>
|
||||
/// Estrae i codici componente referenziati dentro un file .asm Pro-E/Creo.
|
||||
/// Il formato è per lo più binario, ma i codici dei sotto-componenti compaiono
|
||||
/// come stringhe ASCII leggibili al suo interno: si scansiona il file come testo
|
||||
/// cercando pattern che assomigliano a un codice (stesso stile usato nei nomi file,
|
||||
/// vedi Indexer.FilenameRegex) e si lascia poi al chiamante il compito di verificarli
|
||||
/// contro l'indice DB, che scarta automaticamente i falsi positivi (numeri che non
|
||||
/// corrispondono a nessun codice reale).
|
||||
/// come stringhe ASCII leggibili al suo interno.
|
||||
///
|
||||
/// Verificato su un caso reale confrontando con un report BOM generato da Creo: i
|
||||
/// Due famiglie di codici, due strategie:
|
||||
///
|
||||
/// 1) La maggioranza (>90%) sono codici numerici Pro-E del gestionale, con una forma
|
||||
/// nota (stesso stile usato nei nomi file, vedi Indexer.FilenameRegex): si riconoscono
|
||||
/// da soli, senza bisogno del DB, e possono quindi essere segnalati come "referenziati
|
||||
/// ma non trovati nell'indice" quando un codice risulta rinominato/spostato - il
|
||||
/// chiamante fa la ricerca nel DB e riporta i mancanti.
|
||||
///
|
||||
/// 2) Una minoranza sono parti commerciali scaricate come STEP da cataloghi online
|
||||
/// (viti, anelli, boccole...), che in Creo hanno nomi alfabetici senza una forma
|
||||
/// riconoscibile a priori (es. "ANELLO_ESTERNO-1-SOLID1", non un numero). Per questi
|
||||
/// non esiste un pattern da indovinare: si tokenizza in modo generico (qualunque run
|
||||
/// di lettere/cifre/underscore/trattino) e si lascia decidere al DB, già caricato dal
|
||||
/// chiamante, se il token è un codice vero - altrimenti il rumore binario sarebbe
|
||||
/// ingestibile. Un token così "convalidato" è per costruzione già nell'indice, quindi
|
||||
/// non contribuisce mai ai "non trovati": per le parti commerciali mancanti dall'indice
|
||||
/// non c'è modo di distinguerle dal rumore, restano semplicemente invisibili come lo
|
||||
/// erano prima.
|
||||
///
|
||||
/// Verificato su un caso reale confrontando con l'albero componenti di Creo: i
|
||||
/// riferimenti veri compaiono più volte nel file (una volta per feature/placement),
|
||||
/// mentre il rumore binario casuale che assomiglia a un codice compare quasi sempre
|
||||
/// una sola volta. Si scartano quindi i candidati con una sola occorrenza.
|
||||
/// una sola volta. Si scartano quindi, per entrambe le famiglie, i candidati "nudi" (senza
|
||||
/// un suffisso di revisione/variante o una forma family-table nota) che non abbiano anche
|
||||
/// un vero placement testuale nel binario.
|
||||
///
|
||||
/// Falso positivo noto ed escluso: gli ID interni di viste salvate, nel formato
|
||||
/// "ModelView#<numero>", hanno la stessa forma di un codice (8-12 cifre) e
|
||||
/// Falso positivo noto ed escluso (famiglia 1): gli ID interni di viste salvate, nel
|
||||
/// formato "ModelView#<numero>", hanno la stessa forma di un codice (8-12 cifre) e
|
||||
/// compaiono più volte come un riferimento vero. Vedi docs/formato-asm.md.
|
||||
///
|
||||
/// Viti/dadi/rosette normalizzati UNI (e cuscinetti CUSCI_...) sono family table Creo: il
|
||||
/// nome che compare nel binario (es. "R6--_-_U6592<UNI6592>",
|
||||
/// "2510060303<CUSCI_ACBB7>") è la singola istanza, non un file separato su disco - il
|
||||
/// file reale indicizzato è il generico "uni6592.prt.<versione>"/
|
||||
/// "cusci_acbb7.prt.<versione>", quindi va estratto solo il contenuto tra "<" e
|
||||
/// ">". Vedi docs/formato-asm.md.
|
||||
/// "cusci_acbb7.prt.<versione>". Con la tokenizzazione generica "UNI6592"/
|
||||
/// "CUSCI_ACBB7" vengono catturati da soli: "<" e ">" non fanno parte del charset del
|
||||
/// token e ne delimitano naturalmente i confini. Vedi docs/formato-asm.md.
|
||||
///
|
||||
/// Lettera di revisione e suffisso "_..." possono comparire insieme sullo stesso codice
|
||||
/// (es. "9284200366A_C01"), e il suffisso dopo "_" può essere puramente numerico (es.
|
||||
/// "2702220182_8188", una variante dimensionale, non lettera+cifre come "_C01"/"_D100").
|
||||
///
|
||||
/// Un codice "nudo" (senza suffisso "_..." e senza bracket UNI) ripetuto più volte non
|
||||
/// basta: può trattarsi del riferimento al nome di un altro assieme che lo contiene (non
|
||||
/// noto a priori, quindi non escludibile per confronto diretto come il proprio codice) o
|
||||
/// di un residuo di feature soppresse/superate (vedi docs/formato-asm.md). Per questi si
|
||||
/// richiede anche un vero placement, in una delle quattro forme osservate nel binario:
|
||||
/// "Componente ID N (<codice>...)"/"Component id N (<codice>...)" (la stringa
|
||||
/// cambia con la localizzazione Creo del file); il codice ripetuto due volte di seguito
|
||||
/// separato da un singolo byte NUL ("<codice>\0<codice>\0", osservato per
|
||||
/// componenti di family table non-UNI, es. cuscinetti "CUSCI_..."); il campo
|
||||
/// "family_name\0<codice>\0" (osservato una sola volta per file, su un sotto-assieme
|
||||
/// referenziato come family table generica invece che con un placement testuale esplicito);
|
||||
/// oppure il codice seguito immediatamente da un bracket "<codice><GENERICO>" -
|
||||
/// l'istanza di una family table non-UNI vista nel nome (es. "2510060303<CUSCI_ACBB7>",
|
||||
/// un cuscinetto): il codice dell'istanza non è mai indicizzato a sé (solo il generico tra
|
||||
/// "<" e ">" lo è), ma questo segnale lo fa comunque sopravvivere fino alla ricerca nel
|
||||
/// DB invece di scomparire in silenzio come "nudo senza placement" - lì verrà comunque
|
||||
/// scartato come "non in indice", ma in modo tracciabile. Non si applica alle varianti con
|
||||
/// suffisso o bracket UNI: in alcuni file il placement riporta solo il nome
|
||||
/// istanza/generico senza il dettaglio del suffisso o del bracket, che vive altrove nei
|
||||
/// metadati - lì resta necessaria la sola euristica delle occorrenze ripetute.
|
||||
/// Un codice "nudo" (senza suffisso "_..." e senza forma family-table nota) ripetuto più
|
||||
/// volte non basta: può trattarsi del riferimento al nome di un altro assieme che lo
|
||||
/// contiene (non noto a priori, quindi non escludibile per confronto diretto come il
|
||||
/// proprio codice) o di un residuo di feature soppresse/superate, oppure - per la
|
||||
/// tokenizzazione generica - di puro rumore binario che coincide con un codice DB corto o
|
||||
/// generico (vedi docs/formato-asm.md). Per questi si richiede anche un vero placement, in
|
||||
/// una delle quattro forme osservate nel binario: "Componente ID N (<codice>...)"/
|
||||
/// "Component id N (<codice>...)" (la stringa cambia con la localizzazione Creo del
|
||||
/// file); il codice ripetuto due volte di seguito separato da un singolo byte NUL
|
||||
/// ("<codice>\0<codice>\0", osservato per componenti di family table non-UNI,
|
||||
/// es. cuscinetti "CUSCI_..."); il campo "family_name\0<codice>\0" (osservato una
|
||||
/// sola volta per file, su un sotto-assieme referenziato come family table generica invece
|
||||
/// che con un placement testuale esplicito); oppure il codice seguito immediatamente da un
|
||||
/// bracket "<codice><GENERICO>" - l'istanza di una family table non-UNI vista
|
||||
/// nel nome (es. "2510060303<CUSCI_ACBB7>", un cuscinetto): il codice dell'istanza
|
||||
/// non è mai indicizzato a sé (solo il generico tra "<" e ">" lo è), ma questo
|
||||
/// segnale lo fa comunque sopravvivere fino alla ricerca nel DB invece di scomparire in
|
||||
/// silenzio come "nudo senza placement" - lì verrà comunque scartato come "non in indice",
|
||||
/// ma in modo tracciabile.
|
||||
/// </summary>
|
||||
static class AssemblyComponentScanner
|
||||
{
|
||||
private static readonly Regex CodiceRegex = new(
|
||||
@"(?<!ModelView#)\b\d{8,12}[A-Za-z]{0,2}(?:_[A-Za-z]?\d{2,4})?\b|(?<=<)(?:UNI\d+|CUSCI_[A-Za-z0-9]+)(?=>)",
|
||||
private static readonly Regex CodiceNumericoRegex = new(
|
||||
@"(?<!ModelView#)\b\d{8,12}[A-Za-z]{0,2}(?:_[A-Za-z]?\d{2,4})?\b",
|
||||
RegexOptions.Compiled);
|
||||
private static readonly Regex CodiceNudoRegex = new(@"^\d{8,12}[A-Za-z]{0,2}$", RegexOptions.Compiled);
|
||||
|
||||
// Nessuna forma ipotizzata a mano per le parti commerciali: qualunque run di
|
||||
// lettere/cifre/underscore/trattino è un candidato, validato poi contro il DB.
|
||||
private static readonly Regex TokenGenericoRegex = new(@"[A-Za-z0-9_-]{3,}", RegexOptions.Compiled);
|
||||
|
||||
// Suffisso di revisione/variante (es. "_C01", "_D100", "_8188") o prefisso di family
|
||||
// table nota (UNI/CUSCI_): segnale forte abbastanza da non richiedere un placement
|
||||
// esplicito, come per i codici numerici nudi con lo stesso suffisso.
|
||||
private static readonly Regex SuffissoRegex = new(@"_[A-Za-z]?\d{2,4}$", RegexOptions.Compiled);
|
||||
|
||||
private const int OccorrenzeMinime = 2;
|
||||
|
||||
/// <summary>Codice radice (parte prima del primo "_") di un codice componente. Un
|
||||
@@ -70,15 +97,13 @@ static class AssemblyComponentScanner
|
||||
/// risale fino all'assieme generale e riporta dentro l'intero albero del macchinario.</summary>
|
||||
public static string CodiceRadice(string codice) => codice.Split('_')[0];
|
||||
|
||||
public static HashSet<string> EstraiCodiciReferenziati(string percorsoAsm)
|
||||
/// <param name="codiciNoti">Tutti i codici prt/asm noti all'indice (caricati una sola
|
||||
/// volta dal chiamante, non per-file): usati per validare i token generici della
|
||||
/// famiglia 2 (parti commerciali). Non serve per la famiglia 1 (codici numerici), che
|
||||
/// si riconosce dalla forma senza bisogno del DB.</param>
|
||||
public static HashSet<string> EstraiCodiciReferenziati(string percorsoAsm, ICollection<string>? codiciNoti = null)
|
||||
{
|
||||
var testo = Encoding.Latin1.GetString(File.ReadAllBytes(percorsoAsm));
|
||||
var occorrenze = new Dictionary<string, int>();
|
||||
foreach (Match m in CodiceRegex.Matches(testo))
|
||||
{
|
||||
occorrenze[m.Value] = occorrenze.GetValueOrDefault(m.Value) + 1;
|
||||
}
|
||||
var candidati = occorrenze.Where(kv => kv.Value >= OccorrenzeMinime).Select(kv => kv.Key).ToHashSet();
|
||||
|
||||
bool HaPlacement(string codice) =>
|
||||
Regex.IsMatch(testo, $@"Component(?:e)? id \d+ \([^()]*{Regex.Escape(codice)}[^()]*\)", RegexOptions.IgnoreCase)
|
||||
@@ -86,7 +111,40 @@ static class AssemblyComponentScanner
|
||||
|| testo.Contains("family_name\0" + codice + "\0")
|
||||
|| testo.Contains(codice + "<");
|
||||
|
||||
candidati.RemoveWhere(c => CodiceNudoRegex.IsMatch(c) && !HaPlacement(c));
|
||||
return candidati;
|
||||
var risultato = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var occorrenzeNumeriche = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (Match m in CodiceNumericoRegex.Matches(testo))
|
||||
occorrenzeNumeriche[m.Value] = occorrenzeNumeriche.GetValueOrDefault(m.Value) + 1;
|
||||
foreach (var (codice, occorrenze) in occorrenzeNumeriche)
|
||||
{
|
||||
if (occorrenze < OccorrenzeMinime)
|
||||
continue;
|
||||
if (CodiceNudoRegex.IsMatch(codice) && !HaPlacement(codice))
|
||||
continue;
|
||||
risultato.Add(codice);
|
||||
}
|
||||
|
||||
if (codiciNoti is { Count: > 0 })
|
||||
{
|
||||
var occorrenzeGeneriche = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (Match m in TokenGenericoRegex.Matches(testo))
|
||||
{
|
||||
if (codiciNoti.Contains(m.Value))
|
||||
occorrenzeGeneriche[m.Value] = occorrenzeGeneriche.GetValueOrDefault(m.Value) + 1;
|
||||
}
|
||||
foreach (var (codice, occorrenze) in occorrenzeGeneriche)
|
||||
{
|
||||
if (risultato.Contains(codice))
|
||||
continue;
|
||||
bool formaNota = SuffissoRegex.IsMatch(codice)
|
||||
|| codice.StartsWith("UNI", StringComparison.OrdinalIgnoreCase)
|
||||
|| codice.StartsWith("CUSCI_", StringComparison.OrdinalIgnoreCase);
|
||||
if (formaNota ? occorrenze >= OccorrenzeMinime : HaPlacement(codice))
|
||||
risultato.Add(codice);
|
||||
}
|
||||
}
|
||||
|
||||
return risultato;
|
||||
}
|
||||
}
|
||||
+32
-58
@@ -662,6 +662,31 @@ public partial class Form1 : Form
|
||||
ImpostaStatoImport($"{eliminati} file eliminati da {string.Join(" e ", cartelle)}.", Color.DarkGreen);
|
||||
}
|
||||
|
||||
/// <summary>Tutti i codici prt/asm noti all'indice, con i relativi percorsi (ultima
|
||||
/// versione, un solo file per tipo - stesso criterio di dedup per data_modifica DESC
|
||||
/// usato ovunque in questa classe). Caricato in un'unica query invece che una per
|
||||
/// codice: serve sia a risolvere i percorsi dei componenti trovati, sia a
|
||||
/// AssemblyComponentScanner per validare i token generici (parti commerciali).</summary>
|
||||
private static Dictionary<string, List<(string Tipo, string Percorso)>> CaricaCodiciNoti(string dbPath)
|
||||
{
|
||||
var codiciNoti = new Dictionary<string, List<(string Tipo, string Percorso)>>(StringComparer.OrdinalIgnoreCase);
|
||||
using var connection = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly");
|
||||
connection.Open();
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = "SELECT codice, tipo_file, percorso_completo FROM files_ultima_versione "
|
||||
+ "WHERE LOWER(tipo_file) IN ('prt', 'asm') ORDER BY codice, tipo_file, data_modifica DESC";
|
||||
using var reader = command.ExecuteReader();
|
||||
while (reader.Read())
|
||||
{
|
||||
string codice = reader.GetString(0);
|
||||
string tipo = reader.GetString(1).ToLowerInvariant();
|
||||
var lista = codiciNoti.TryGetValue(codice, out var l) ? l : codiciNoti[codice] = new();
|
||||
if (!lista.Any(x => x.Tipo == tipo))
|
||||
lista.Add((tipo, reader.GetString(2)));
|
||||
}
|
||||
return codiciNoti;
|
||||
}
|
||||
|
||||
/// <summary>Risolve ricorsivamente l'albero dei componenti a partire dai file .asm indicati
|
||||
/// (già copiati in "raw"): per ogni assieme legge i codici referenziati al suo interno, li
|
||||
/// cerca nell'indice, copia in "raw" quelli trovati e, se sono a loro volta assiemi, li
|
||||
@@ -681,42 +706,19 @@ public partial class Form1 : Form
|
||||
if (daScansionare.Count == 0 || _dbPath is null)
|
||||
return (trovati, codiciNonTrovati);
|
||||
|
||||
using var connection = new SqliteConnection($"Data Source={_dbPath};Mode=ReadOnly");
|
||||
connection.Open();
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = "SELECT tipo_file, percorso_completo FROM files_ultima_versione "
|
||||
+ "WHERE LOWER(codice) = LOWER(@codice) AND LOWER(tipo_file) IN ('prt', 'asm') "
|
||||
+ "ORDER BY tipo_file, data_modifica DESC";
|
||||
var pCodice = command.Parameters.Add("@codice", Microsoft.Data.Sqlite.SqliteType.Text);
|
||||
var codiciNoti = CaricaCodiciNoti(_dbPath);
|
||||
|
||||
int processati = 0;
|
||||
while (daScansionare.Count > 0)
|
||||
{
|
||||
var asm = daScansionare.Dequeue();
|
||||
processati++;
|
||||
foreach (var codice in AssemblyComponentScanner.EstraiCodiciReferenziati(asm))
|
||||
foreach (var codice in AssemblyComponentScanner.EstraiCodiciReferenziati(asm, codiciNoti.Keys))
|
||||
{
|
||||
if (!codiciVisti.Add(codice))
|
||||
continue;
|
||||
|
||||
pCodice.Value = codice;
|
||||
// Lo stesso codice+tipo può esistere in più cartelle radice (duplicati sulla
|
||||
// share): ordinato per data_modifica DESC, si tiene solo il più recente per tipo.
|
||||
var componenti = new List<(string Tipo, string Percorso)>();
|
||||
var tipiVisti = new HashSet<string>();
|
||||
using (var reader = command.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
string tipoLower = reader.GetString(0).ToLowerInvariant();
|
||||
if (tipiVisti.Add(tipoLower))
|
||||
{
|
||||
componenti.Add((tipoLower, reader.GetString(1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (componenti.Count == 0)
|
||||
if (!codiciNoti.TryGetValue(codice, out var componenti))
|
||||
{
|
||||
codiciNonTrovati.Add(codice);
|
||||
continue;
|
||||
@@ -790,39 +792,11 @@ public partial class Form1 : Form
|
||||
return;
|
||||
}
|
||||
|
||||
using var connection = new SqliteConnection($"Data Source={_dbPath};Mode=ReadOnly");
|
||||
connection.Open();
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = "SELECT tipo_file, percorso_completo FROM files_ultima_versione "
|
||||
+ "WHERE LOWER(codice) = LOWER(@codice) AND LOWER(tipo_file) IN ('prt', 'asm') "
|
||||
+ "ORDER BY tipo_file, data_modifica DESC";
|
||||
var pCodice = command.Parameters.Add("@codice", Microsoft.Data.Sqlite.SqliteType.Text);
|
||||
|
||||
var cacheComponenti = new Dictionary<string, List<(string Tipo, string Percorso)>>();
|
||||
var codiciNoti = CaricaCodiciNoti(_dbPath);
|
||||
var cacheCodiciFile = new Dictionary<string, HashSet<string>>();
|
||||
|
||||
List<(string Tipo, string Percorso)> CercaComponenti(string codice)
|
||||
{
|
||||
if (cacheComponenti.TryGetValue(codice, out var giaTrovati))
|
||||
return giaTrovati;
|
||||
|
||||
pCodice.Value = codice;
|
||||
var risultato = new List<(string Tipo, string Percorso)>();
|
||||
var tipiVisti = new HashSet<string>();
|
||||
using (var reader = command.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
string tipo = reader.GetString(0).ToLowerInvariant();
|
||||
if (tipiVisti.Add(tipo))
|
||||
{
|
||||
risultato.Add((tipo, reader.GetString(1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
cacheComponenti[codice] = risultato;
|
||||
return risultato;
|
||||
}
|
||||
List<(string Tipo, string Percorso)> CercaComponenti(string codice) =>
|
||||
codiciNoti.TryGetValue(codice, out var trovati) ? trovati : new List<(string, string)>();
|
||||
|
||||
bool AssiemeCompleto(string percorsoAsmIniziale)
|
||||
{
|
||||
@@ -843,7 +817,7 @@ public partial class Form1 : Form
|
||||
{
|
||||
try
|
||||
{
|
||||
codiciReferenziati = AssemblyComponentScanner.EstraiCodiciReferenziati(file);
|
||||
codiciReferenziati = AssemblyComponentScanner.EstraiCodiciReferenziati(file, codiciNoti.Keys);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
|
||||
+48
-2
@@ -179,6 +179,52 @@ table il placement può mancare del dettaglio, quindi resta necessaria la
|
||||
sola euristica delle occorrenze ripetute - meno precisa, ma l'unica
|
||||
applicabile senza rischiare falsi negativi su casi come questi.
|
||||
|
||||
## Parti commerciali con nome alfabetico (validazione via DB)
|
||||
|
||||
Oltre il 90% dei codici indicizzati sono numerici Pro-E (regex sopra), ma una minoranza
|
||||
sono parti commerciali scaricate come STEP da cataloghi online (anelli, boccole, guarnizioni,
|
||||
manopole...) che in Creo hanno un nome **alfabetico**, assegnato dal file STEP di origine, non
|
||||
un codice del gestionale — es. `NOME_PARTE-1-SOLID1`, senza nessuna cifra riconoscibile. Non
|
||||
esiste un pattern comune da ipotizzare a priori per questi nomi: variano da fornitore a
|
||||
fornitore.
|
||||
|
||||
Per questa famiglia l'estrazione si inverte: invece di restringere la regex a una forma nota
|
||||
(come per i codici numerici), si tokenizza il binario in modo generico —
|
||||
|
||||
```
|
||||
regex: [A-Za-z0-9_-]{3,}
|
||||
```
|
||||
|
||||
— e si lascia decidere al DB (`cad_index.db`, già caricato in memoria una sola volta dal
|
||||
chiamante, non per-file) se il token è un codice reale. Senza questo filtro il rumore binario
|
||||
sarebbe ingestibile: su un assieme con poche migliaia di stringhe, la tokenizzazione generica
|
||||
produce tipicamente qualche migliaio di candidati, di cui solo una manciata sopravvive al
|
||||
controllo DB.
|
||||
|
||||
L'essere nel DB da solo non basta, per lo stesso motivo dei codici numerici "nudi": codici
|
||||
corti o generici (es. una keyword interna di Creo come `start_part`, o un codice indicizzato
|
||||
per errore/caso limite) possono comparire per puro rumore. Si applica quindi la stessa regola
|
||||
di validazione della famiglia numerica:
|
||||
|
||||
- un token con un suffisso di revisione/variante (`_[A-Za-z]?\d{2,4}` in coda, es. `_C01`,
|
||||
`_D100`) o un prefisso di family table nota (`UNI`, `CUSCI_`) è considerato "forma nota":
|
||||
basta che occorra almeno 2 volte nel file;
|
||||
- un token "nudo" (nessuno dei due segnali) richiede invece un vero placement testuale, come
|
||||
per i codici numerici nudi (stesse quattro forme viste sopra).
|
||||
|
||||
Una parte commerciale referenziata ma **non ancora indicizzata** non è distinguibile dal
|
||||
rumore con questo metodo: resta invisibile, come lo era prima che questa famiglia venisse
|
||||
gestita. La famiglia numerica invece resta rilevabile anche quando manca dall'indice
|
||||
(rinominata/spostata), perché si riconosce dalla forma senza bisogno del DB — è per questo
|
||||
che le due famiglie restano due passaggi distinti invece di un unico tokenizer generico
|
||||
sempre validato contro il DB: unificarle farebbe perdere la segnalazione dei codici numerici
|
||||
mancanti (vedi "Riferimenti fantasma" sopra).
|
||||
|
||||
`AssemblyComponentScanner.EstraiCodiciReferenziati` accetta quindi un parametro opzionale
|
||||
`codiciNoti` (l'insieme di tutti i codici prt/asm noti all'indice) usato solo per questa
|
||||
seconda famiglia; se omesso, si estraggono solo i codici numerici. `scripts/BomViaDb.cs`
|
||||
è il prototipo standalone usato per validare l'approccio prima di portarlo nella GUI.
|
||||
|
||||
## Componenti normalizzati (family table UNI, CUSCI, ...)
|
||||
|
||||
I componenti di minuteria standard (viti, dadi, rosette UNI) e i cuscinetti
|
||||
@@ -225,8 +271,8 @@ livello) serve una BFS:
|
||||
Stessa logica di estrazione candidati ed euristica del codice radice (sopra),
|
||||
duplicata in due punti del codice per scopi diversi:
|
||||
|
||||
- [`AssemblyComponentScanner.EstraiCodiciReferenziati`](../CadSearchGui/AssemblyComponentScanner.cs) + `CodiceRadice` — estrazione dei candidati da un file, usata dalla BFS in [`Form1.RisolviAlberoComponenti`](../CadSearchGui/Form1.cs) (fase "Trova componenti referenziati") e [`Form1.AssiemeCompleto`](../CadSearchGui/Form1.cs) (fase "Verifica completezza"). Entrambe seminano l'insieme dei codici visitati con codice + radice di ogni file di partenza.
|
||||
- [`scripts/ExtractCreoStrings.cs`](../scripts/ExtractCreoStrings.cs) — stessa BFS ricorsiva standalone da riga di comando (copia indipendente della stessa euristica, nessun `ProjectReference` verso la GUI). Usato per confrontare l'euristica contro l'albero Creo di un singolo file durante il reverse engineering, senza dover passare dalla UI/dal DB `raw`.
|
||||
- [`AssemblyComponentScanner.EstraiCodiciReferenziati`](../CadSearchGui/AssemblyComponentScanner.cs) + `CodiceRadice` — estrazione dei candidati da un file, usata dalla BFS in [`Form1.RisolviAlberoComponenti`](../CadSearchGui/Form1.cs) (fase "Trova componenti referenziati") e [`Form1.AssiemeCompleto`](../CadSearchGui/Form1.cs) (fase "Verifica completezza"). Entrambe seminano l'insieme dei codici visitati con codice + radice di ogni file di partenza, e caricano una volta sola tutti i codici noti all'indice (`Form1.CaricaCodiciNoti`) sia per validare i token generici delle parti commerciali sia per risolvere i percorsi dei componenti trovati, senza una query SQL per candidato.
|
||||
- [`scripts/ExtractCreoStrings.cs`](../scripts/ExtractCreoStrings.cs) e [`scripts/BomViaDb.cs`](../scripts/BomViaDb.cs) — stessa BFS ricorsiva standalone da riga di comando (copia indipendente della stessa euristica, nessun `ProjectReference` verso la GUI). Usati per confrontare l'euristica contro l'albero Creo di un singolo file durante il reverse engineering, senza dover passare dalla UI/dal DB `raw`.
|
||||
|
||||
Nota: le query contro `files_ultima_versione` filtrano solo per
|
||||
`(cartella_radice, codice, tipo_file)` - se lo stesso codice esiste in più
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
#:package Microsoft.Data.Sqlite@10.0.12
|
||||
// dotnet run scripts/BomViaDb.cs -- <file.asm> [cad_index.db] [output.txt]
|
||||
//
|
||||
// PROVA: a differenza di ExtractCreoStrings.cs (che restringe i candidati a un solo
|
||||
// pattern di codice, tarato sui codici numerici Pro-E), qui l'estrazione è generica -
|
||||
// qualunque run di lettere/cifre/underscore/trattino - e il DB decide se è un codice
|
||||
// vero, così si intercettano anche i nomi alfabetici delle parti commerciali scaricate
|
||||
// come STEP (es. "ANELLO_ESTERNO-1-SOLID1"). L'essere nel DB da solo non basta però:
|
||||
// codici corti o generici (es. "907", "NONE") comparirebbero per puro rumore binario,
|
||||
// quindi si richiede comunque il placement esplicito nel file, come in
|
||||
// ExtractCreoStrings.cs. Stessa espansione ricorsiva sui sotto-assiemi.
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
if (args.Length < 1)
|
||||
{
|
||||
Console.WriteLine("Uso: dotnet run BomViaDb.cs -- <file.asm> [cad_index.db] [output.txt]");
|
||||
return 1;
|
||||
}
|
||||
|
||||
string asmIniziale = args[0];
|
||||
string dbPath = args.Length > 1 ? args[1] : "cad_index.db";
|
||||
string outPath = args.Length > 2 ? args[2] : "BOM.txt";
|
||||
|
||||
// Oltre il 90% dei codici in indice sono numerici Pro-E, ma una minoranza (parti
|
||||
// commerciali scaricate come STEP, es. "ANELLO_ESTERNO-1-SOLID1") ha nomi alfabetici
|
||||
// con trattini/underscore. Niente forma di codice ipotizzata a mano: si tokenizza in
|
||||
// modo generico (run di lettere/cifre/underscore/trattino) e si lascia al DB decidere
|
||||
// cos'è un codice vero - stesso principio già usato per il filtro, esteso anche
|
||||
// all'estrazione dei candidati. Cattura anche "UNI6592"/"CUSCI_ACBB7" dentro
|
||||
// "<UNI6592>" gratis: '<' e '>' non fanno parte del charset e delimitano il token.
|
||||
var codiceRegex = new Regex(@"[A-Za-z0-9_-]{3,}", RegexOptions.Compiled);
|
||||
|
||||
using var connection = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly");
|
||||
connection.Open();
|
||||
|
||||
// Tutti i codici prt/asm noti all'indice, caricati una volta sola: il lookup per ogni
|
||||
// candidato estratto dal binario diventa O(1) invece di una query per file.
|
||||
var codiciNoti = new Dictionary<string, List<(string Tipo, string Percorso)>>(StringComparer.OrdinalIgnoreCase);
|
||||
using (var command = connection.CreateCommand())
|
||||
{
|
||||
command.CommandText = "SELECT codice, tipo_file, percorso_completo FROM files_ultima_versione "
|
||||
+ "WHERE LOWER(tipo_file) IN ('prt', 'asm') ORDER BY codice, tipo_file, data_modifica DESC";
|
||||
using var reader = command.ExecuteReader();
|
||||
while (reader.Read())
|
||||
{
|
||||
string codice = reader.GetString(0);
|
||||
string tipo = reader.GetString(1).ToLowerInvariant();
|
||||
var lista = codiciNoti.TryGetValue(codice, out var l) ? l : codiciNoti[codice] = new();
|
||||
if (!lista.Any(x => x.Tipo == tipo))
|
||||
lista.Add((tipo, reader.GetString(2)));
|
||||
}
|
||||
}
|
||||
Console.WriteLine($"Codici prt/asm noti nell'indice: {codiciNoti.Count}");
|
||||
|
||||
// Stesso controllo di placement di ExtractCreoStrings.cs/AssemblyComponentScanner.cs:
|
||||
// la sola presenza del codice nel DB non basta, perché codici corti o generici
|
||||
// (es. "907", "NONE", "start_part") possono comparire per puro rumore binario. Un
|
||||
// componente vero è sempre referenziato in una di queste forme (vedi
|
||||
// docs/formato-asm.md): "Componente ID N (<codice>...)"/"Component id N (<codice>...)",
|
||||
// il codice ripetuto due volte separato da un byte NUL, il campo
|
||||
// "family_name\0<codice>\0", o il codice seguito da un bracket "<codice><GENERICO>".
|
||||
bool HaPlacement(string testo, string codice) =>
|
||||
Regex.IsMatch(testo, $@"Component(?:e)? id \d+ \([^()]*{Regex.Escape(codice)}[^()]*\)", RegexOptions.IgnoreCase)
|
||||
|| testo.Contains(codice + "\0" + codice + "\0")
|
||||
|| testo.Contains("family_name\0" + codice + "\0")
|
||||
|| testo.Contains(codice + "<");
|
||||
|
||||
var codiceProprio = Path.GetFileName(asmIniziale).Split('.')[0];
|
||||
// Stessa esclusione di ExtractCreoStrings.cs: un assieme "_D..." referenzia sempre al
|
||||
// suo interno anche l'assieme generale a cui appartiene (riferimento fantasma verso
|
||||
// l'alto), da escludere subito o si risale tutto l'albero del macchinario.
|
||||
var codiceRadice = codiceProprio.Split('_')[0];
|
||||
|
||||
var componentiTrovati = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var scartatiNonInIndice = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var scartatiSenzaPlacement = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var codiciVisti = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { codiceProprio, codiceRadice };
|
||||
var daScansionare = new Queue<string>(new[] { asmIniziale });
|
||||
|
||||
while (daScansionare.Count > 0)
|
||||
{
|
||||
var asm = daScansionare.Dequeue();
|
||||
var testo = Encoding.Latin1.GetString(File.ReadAllBytes(asm));
|
||||
var candidati = codiceRegex.Matches(testo).Select(m => m.Value).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var candidato in candidati)
|
||||
{
|
||||
if (!codiciVisti.Add(candidato))
|
||||
continue;
|
||||
if (!codiciNoti.TryGetValue(candidato, out var componenti))
|
||||
{
|
||||
scartatiNonInIndice.Add(candidato);
|
||||
continue;
|
||||
}
|
||||
if (!HaPlacement(testo, candidato))
|
||||
{
|
||||
scartatiSenzaPlacement.Add(candidato);
|
||||
continue;
|
||||
}
|
||||
componentiTrovati.Add(candidato);
|
||||
foreach (var (tipo, percorso) in componenti)
|
||||
if (tipo == "asm")
|
||||
daScansionare.Enqueue(percorso);
|
||||
}
|
||||
}
|
||||
|
||||
File.WriteAllLines(outPath, componentiTrovati);
|
||||
Console.WriteLine($"{componentiTrovati.Count} componenti scritti in {outPath}");
|
||||
Console.WriteLine($"Candidati nell'indice ma senza placement (scartati, {scartatiSenzaPlacement.Count}): {string.Join(", ", scartatiSenzaPlacement.Take(30))}"
|
||||
+ (scartatiSenzaPlacement.Count > 30 ? ", ..." : ""));
|
||||
Console.WriteLine($"Candidati non nell'indice (rumore binario): {scartatiNonInIndice.Count}");
|
||||
return 0;
|
||||
@@ -0,0 +1,18 @@
|
||||
#:package Microsoft.Data.Sqlite@10.0.12
|
||||
// dotnet run scripts/Query.cs -- <cad_index.db> <sql>
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
using var connection = new SqliteConnection($"Data Source={args[0]};Mode=ReadOnly");
|
||||
connection.Open();
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = args[1];
|
||||
using var reader = command.ExecuteReader();
|
||||
int righe = 0;
|
||||
while (reader.Read())
|
||||
{
|
||||
righe++;
|
||||
var valori = Enumerable.Range(0, reader.FieldCount).Select(i => reader.IsDBNull(i) ? "" : reader.GetValue(i).ToString());
|
||||
Console.WriteLine(string.Join(" | ", valori));
|
||||
}
|
||||
Console.WriteLine($"({righe} righe)");
|
||||
return 0;
|
||||
Reference in new issue
Block a user