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
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user