diff --git a/CadSearchGui/AssemblyComponentScanner.cs b/CadSearchGui/AssemblyComponentScanner.cs
index e1ad7aa..cdb2d70 100644
--- a/CadSearchGui/AssemblyComponentScanner.cs
+++ b/CadSearchGui/AssemblyComponentScanner.cs
@@ -21,10 +21,12 @@ namespace CadSearchGui;
/// "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 sono family table Creo: il nome che compare nel
-/// binario (es. "R6--_-_U6592<UNI6592>") è la singola istanza, non un file separato
-/// su disco - il file reale indicizzato è il generico "uni6592.prt.<versione>", quindi
-/// va estratto solo il contenuto tra "<" e ">". 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.
///
/// Lettera di revisione e suffisso "_..." possono comparire insieme sullo stesso codice
/// (es. "9284200366A_C01"), e il suffisso dopo "_" può essere puramente numerico (es.
@@ -34,20 +36,32 @@ namespace CadSearchGui;
/// 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 "Componente ID N (<codice>...)"/
-/// "Component id N (<codice>...)" (la stringa cambia con la localizzazione Creo del
-/// file). 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.
+/// richiede anche un vero placement, in una delle due forme osservate nel binario:
+/// "Componente ID N (<codice>...)"/"Component id N (<codice>...)" (la stringa
+/// cambia con la localizzazione Creo del file), oppure 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_..."). 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.
///
static class AssemblyComponentScanner
{
private static readonly Regex CodiceRegex = new(
- @"(?)", RegexOptions.Compiled);
+ @"(?)",
+ RegexOptions.Compiled);
private static readonly Regex CodiceNudoRegex = new(@"^\d{8,12}[A-Za-z]{0,2}$", RegexOptions.Compiled);
private const int OccorrenzeMinime = 2;
+ /// Codice radice (parte prima del primo "_") di un codice componente. Un
+ /// assieme con suffisso "_D.../_C..." referenzia sempre al suo interno anche l'assieme
+ /// generale a cui appartiene (codice radice, senza suffisso) - riferimento fantasma
+ /// verso l'alto, non un componente vero (vedi docs/formato-asm.md). Chi fa una
+ /// risoluzione ricorsiva dell'albero deve seminare l'insieme dei codici già visitati
+ /// anche con la radice del codice di partenza, non solo col codice stesso, altrimenti
+ /// risale fino all'assieme generale e riporta dentro l'intero albero del macchinario.
+ public static string CodiceRadice(string codice) => codice.Split('_')[0];
+
public static HashSet EstraiCodiciReferenziati(string percorsoAsm)
{
var testo = Encoding.Latin1.GetString(File.ReadAllBytes(percorsoAsm));
@@ -59,7 +73,8 @@ static class AssemblyComponentScanner
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);
+ Regex.IsMatch(testo, $@"Component(?:e)? id \d+ \([^()]*{Regex.Escape(codice)}[^()]*\)", RegexOptions.IgnoreCase)
+ || testo.Contains(codice + "\0" + codice + "\0");
candidati.RemoveWhere(c => CodiceNudoRegex.IsMatch(c) && !HaPlacement(c));
return candidati;
diff --git a/CadSearchGui/Form1.cs b/CadSearchGui/Form1.cs
index 12c5a35..ce8be92 100644
--- a/CadSearchGui/Form1.cs
+++ b/CadSearchGui/Form1.cs
@@ -671,8 +671,10 @@ public partial class Form1 : Form
IEnumerable asmIniziali, IProgress<(int Fatti, int Totale)>? progress = null)
{
var daScansionare = new Queue(asmIniziali);
+ var codiciIniziali = daScansionare.Select(f => Path.GetFileName(f).Split('.')[0]);
var codiciVisti = new HashSet(
- daScansionare.Select(f => Path.GetFileName(f).Split('.')[0]), StringComparer.OrdinalIgnoreCase);
+ codiciIniziali.Concat(codiciIniziali.Select(AssemblyComponentScanner.CodiceRadice)),
+ StringComparer.OrdinalIgnoreCase);
var trovati = new List<(string Codice, string Tipo, string Percorso)>();
var codiciNonTrovati = new List();
@@ -827,7 +829,12 @@ public partial class Form1 : Form
var daVisitare = new Queue();
daVisitare.Enqueue(percorsoAsmIniziale);
var fileVisitati = new HashSet { percorsoAsmIniziale };
- var codiciVisti = new HashSet { Path.GetFileName(percorsoAsmIniziale).Split('.')[0] };
+ var codiceIniziale = Path.GetFileName(percorsoAsmIniziale).Split('.')[0];
+ var codiciVisti = new HashSet
+ {
+ codiceIniziale,
+ AssemblyComponentScanner.CodiceRadice(codiceIniziale),
+ };
while (daVisitare.Count > 0)
{
diff --git a/docs/formato-asm.md b/docs/formato-asm.md
index f514f67..961d839 100644
--- a/docs/formato-asm.md
+++ b/docs/formato-asm.md
@@ -73,17 +73,43 @@ non essendo un componente. A differenza del proprio codice, non è noto a
priori: dipende da dove/come il file è stato aperto o da chi lo referenzia,
quindi non si può escludere per confronto diretto come per il caso
precedente. La regola generale che discrimina questo caso: un componente
-vero compare **sempre** anche dentro un vero placement
-`Componente ID N (...)`/`Component id N (...)` (la stringa
-cambia con la localizzazione Creo con cui il file è stato salvato - osservate
-sia italiana sia inglese); un riferimento incidentale al nome di un altro
-assieme, anche se ripetuto più volte, non compare mai in quel contesto.
-`scripts/ExtractCreoStrings.cs` applica questa regola ai codici "nudi" (senza
-suffisso `_...` e senza bracket UNI - vedi sotto perché limitata a questi):
-un codice ripetuto ma mai trovato dentro un placement viene scartato. Questo
-NON copre i casi fantasma/soppressi sotto, che hanno un placement
-sintatticamente identico a uno vero e restano quindi indistinguibili a
-livello testuale.
+vero compare **sempre** anche dentro un vero placement, in una delle due
+forme osservate nel binario:
+
+- `Componente ID N (...)`/`Component id N (...)` (la stringa
+ cambia con la localizzazione Creo con cui il file è stato salvato -
+ osservate sia italiana sia inglese);
+- il codice ripetuto due volte di seguito separato da un singolo byte NUL,
+ `\0\0` (osservato per componenti di family table non-UNI,
+ es. cuscinetti `CUSCI_...` - vedi sotto; il placement testuale
+ "Componente ID" non li copre, ma questa coppia doppia sì).
+
+Un riferimento incidentale al nome di un altro assieme, anche se ripetuto più
+volte, non compare mai in nessuna delle due forme. `scripts/ExtractCreoStrings.cs`
+e `AssemblyComponentScanner.cs` applicano questa regola ai codici "nudi" (senza
+suffisso `_...` e senza bracket family table - vedi sotto perché limitata a
+questi): un codice ripetuto ma mai trovato dentro un placement/coppia NUL
+viene scartato. Questo NON copre i casi fantasma/soppressi sotto, che hanno un
+placement sintatticamente identico a uno vero e restano quindi indistinguibili
+a livello testuale.
+
+**Il codice radice del proprio assieme (assieme generale, senza suffisso
+`_D.../_C...`).** Caso particolare e sistematico del precedente, non solo
+incidentale: un assieme suddiviso in sotto-assiemi con suffisso
+(`9284208188_D600.asm`, `_D610.asm`, ...) referenzia **sempre** al suo interno
+anche il codice radice dell'assieme generale (`9284208188`, senza suffisso) -
+il "padre" a cui tutti i `_D...` appartengono nella struttura Creo - con un
+vero placement sintatticamente identico a un componente reale. Non è un
+componente di quel sotto-assieme, è la risalita verso l'assieme che lo
+contiene. Osservato su `9284208188_D600.asm`: `Componente ID N (9284208188...)`.
+Se seguito in una risoluzione ricorsiva, riporta dentro l'intero albero del
+prodotto invece del solo sotto-assieme richiesto: 598 componenti raccolti
+invece dei ~65 reali. A differenza del caso generico sopra, qui il codice
+"padre" _è_ prevedibile a priori (è la parte del proprio codice prima del
+primo `_`), quindi va escluso esplicitamente seminando l'insieme dei codici
+già visitati anche con la radice del codice di partenza, non solo con il
+codice stesso — non basta il controllo generico sul placement, perché il
+placement qui è reale. Vedi `AssemblyComponentScanner.CodiceRadice`.
**Riferimenti fantasma a codici superati/rinominati.** Caso osservato:
`9244207061.asm` contiene un riferimento a `9044207061` (stesso numero, prima
@@ -134,30 +160,35 @@ 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.
-## Viti/dadi/rosette normalizzati (family table UNI)
+## Componenti normalizzati (family table UNI, CUSCI, ...)
-I componenti di minuteria standard (viti, dadi, rosette UNI) sono definiti in
-Creo come **family table**: un file generico (es. `uni6592.prt`) con tante
-righe/istanze quante sono le varianti dimensionali. Nel binario dell'assieme
-compaiono così:
+I componenti di minuteria standard (viti, dadi, rosette UNI) e i cuscinetti
+(`CUSCI_...`) sono definiti in Creo come **family table**: un file generico
+(es. `uni6592.prt`, `cusci_acbb7.prt`) con tante righe/istanze quante sono le
+varianti dimensionali. Nel binario dell'assieme compaiono così:
```
Componente ID 28755 (R6--_-_U6592.PRT)
+2510060303
```
-`R6--_-_U6592` è il nome della singola istanza (riga della family table), **non
-un file separato su disco** — sulla share esiste solo `uni6592.prt.`
-(verificato in `Unificati/`). Cercare l'istanza intera nell'indice fallisce
-sempre; va estratto solo il contenuto tra `<` e `>` (`UNI6592`), che coincide
-col nome del file generico realmente indicizzato:
+`R6--_-_U6592` e `2510060303` sono il nome/codice della singola istanza (riga
+della family table), **non un file separato su disco** — sulla share esiste
+solo il generico (`uni6592.prt.`, `cusci_acbb7.prt.`,
+verificato in `Unificati/`). Cercare l'istanza intera nell'indice fallisce
+sempre; va estratto solo il contenuto tra `<` e `>` (`UNI6592`, `CUSCI_ACBB7`),
+che coincide col nome del file generico realmente indicizzato:
```
-regex aggiuntiva: (?<=<)UNI\d+(?=>)
+regex aggiuntiva: (?<=<)(?:UNI\d+|CUSCI_[A-Za-z0-9]+)(?=>)
```
Implementata sia in [`scripts/ExtractCreoStrings.cs`](../scripts/ExtractCreoStrings.cs) sia in
[`AssemblyComponentScanner.cs`](../CadSearchGui/AssemblyComponentScanner.cs) (fase "Trova componenti
-referenziati" dell'app).
+referenziati" dell'app). Il prefisso family table non è garantito essere solo `UNI`/`CUSCI`:
+se ricompaiono famiglie diverse (altri componenti normalizzati), vanno aggiunte allo stesso modo,
+verificando prima con `scripts/Analyze.cs` che il contenuto tra `<` e `>` corrisponda a un codice
+realmente indicizzato in `Unificati/`.
## Risoluzione ricorsiva dell'albero
@@ -172,11 +203,11 @@ livello) serve una BFS:
3. Un codice non trovato nell'indice va scartato/segnalato: o è rumore
binario, o è un componente non ancora indicizzato.
-Stessa logica di estrazione candidati in due punti del codice, con scopi
-diversi:
+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) — solo 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").
-- [`scripts/ExtractCreoStrings.cs`](../scripts/ExtractCreoStrings.cs) — estrazione standalone da riga di comando, ma **un solo livello**: riporta solo i riferimenti diretti del file passato (come li mostra Creo per quell'assieme), senza scendere ricorsivamente nei sotto-assiemi. Usato per confrontare l'euristica contro l'albero Creo di un singolo file durante il reverse engineering; non genera una distinta base completa multi-livello.
+- [`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`.
Nota: le query contro `files_ultima_versione` filtrano solo per
`(cartella_radice, codice, tipo_file)` - se lo stesso codice esiste in più
diff --git a/scripts/ExtractCreoStrings.cs b/scripts/ExtractCreoStrings.cs
index b964f68..f1de61d 100644
--- a/scripts/ExtractCreoStrings.cs
+++ b/scripts/ExtractCreoStrings.cs
@@ -1,11 +1,11 @@
#:package Microsoft.Data.Sqlite@10.0.12
// dotnet run scripts/ExtractCreoStrings.cs -- [cad_index.db] [output.txt]
//
-// Estrae dal file .asm i codici dei componenti referenziati DIRETTAMENTE (un solo
-// livello, come li mostra Creo per quell'assieme - nessuna espansione ricorsiva nei
-// sotto-assiemi). Stessa euristica di CadSearchGui/AssemblyComponentScanner.cs: i codici
-// veri compaiono più volte nel file, il rumore binario quasi sempre una sola volta; gli
-// ID interni "ModelView#" sono esclusi via lookbehind - vedi scripts/Analyze.cs.
+// Estrae dal file .asm i codici dei componenti referenziati, espandendo ricorsivamente
+// ogni sotto-assieme (.asm) trovato nell'indice, finché non restano solo PRT (foglie) o
+// codici non presenti nell'indice. Stessa euristica di CadSearchGui/AssemblyComponentScanner.cs:
+// i codici veri compaiono più volte nel file, il rumore binario quasi sempre una sola volta;
+// gli ID interni "ModelView#" sono esclusi via lookbehind - vedi scripts/Analyze.cs.
// Ogni codice trovato viene validato contro l'indice cad_index.db.
using System.Text;
using System.Text.RegularExpressions;
@@ -21,18 +21,19 @@ string asmIniziale = args[0];
string dbPath = args.Length > 1 ? args[1] : "cad_index.db";
string outPath = args.Length > 2 ? args[2] : "BOM.txt";
-// Seconda alternativa: viti/dadi/rosette normalizzati UNI, definiti come family table in
-// Creo. Compaiono come "Componente ID N (R12--_-_U6592.PRT)": "R12--_-_U6592" è
-// il nome della singola istanza (riga della family table, non un file separato su disco),
-// il file reale indicizzato è il generico "uni6592.prt." - va quindi estratto
-// solo il contenuto tra "<" e ">".
+// Seconda alternativa: componenti normalizzati definiti come family table in Creo (viti/
+// dadi/rosette UNI, ma anche cuscinetti CUSCI_...). Compaiono come "Componente ID N
+// (R12--_-_U6592.PRT)": "R12--_-_U6592" è il nome della singola istanza (riga
+// della family table, non un file separato su disco), il file reale indicizzato è il
+// generico "uni6592.prt."/"cusci_acbb7.prt." - va quindi estratto solo
+// il contenuto tra "<" e ">".
// Terza alternativa: varianti dimensionali con suffisso puramente numerico (es. cinghie
// "2702220182_8188", dove "8188" è una misura, non una lettera di revisione/sotto-assieme
// come "_C01"/"_D100") - la lettera dopo "_" è quindi opzionale, non obbligatoria. La
// lettera di revisione e il suffisso "_" possono anche comparire insieme sullo stesso
// codice (es. "9284200366A_C01", componente di un sotto-assieme già di per sé revisionato).
var codiceRegex = new Regex(
- @"(?)",
+ @"(?)",
RegexOptions.Compiled);
const int occorrenzeMinime = 2;
@@ -48,14 +49,18 @@ HashSet EstraiCodiciReferenziati(string percorsoAsm)
// riferimenti al proprio assieme padre (nome sessione/file) o a codici superati/
// rinominati (caso "fantasma", docs/formato-asm.md) ripetono comunque il codice più
// volte pur non essendo mai un componente vero. Per questi si richiede un vero
- // placement "Componente ID N (...)". "Componente ID"/"Component id": la
- // stringa cambia con la localizzazione Creo con cui il file è stato salvato (osservate
- // sia italiana sia inglese). Non si applica alle varianti con suffisso "_..." né alle
- // istanze family table 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ì l'occorrenza ripetuta resta l'unico segnale disponibile.
+ // placement, in una delle due forme osservate nel binario: "Componente ID N
+ // (...)"/"Component id N (...)" (la stringa cambia con la
+ // localizzazione Creo del file), oppure il codice ripetuto due volte di seguito
+ // separato da un singolo byte NUL ("\0\0", osservato per componenti di
+ // family table non-UNI, es. cuscinetti "CUSCI_..."). Non si applica alle varianti con
+ // suffisso "_..." né alle istanze family table 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ì l'occorrenza ripetuta resta l'unico
+ // segnale disponibile.
bool HaPlacement(string codice) =>
- Regex.IsMatch(testo, $@"Component(?:e)? id \d+ \([^()]*{Regex.Escape(codice)}[^()]*\)", RegexOptions.IgnoreCase);
+ Regex.IsMatch(testo, $@"Component(?:e)? id \d+ \([^()]*{Regex.Escape(codice)}[^()]*\)", RegexOptions.IgnoreCase)
+ || testo.Contains(codice + "\0" + codice + "\0");
candidati.RemoveWhere(c => Regex.IsMatch(c, @"^\d{8,12}[A-Za-z]{0,2}$") && !HaPlacement(c));
return candidati;
@@ -98,22 +103,42 @@ Dictionary> CercaComponenti(IReadOn
return risultato;
}
-// Solo i riferimenti diretti del file indicato (un livello), come li mostra Creo per
-// quell'assieme - nessuna espansione ricorsiva nei sotto-assiemi.
+// Espansione ricorsiva: ogni sotto-assieme (.asm) trovato viene a sua volta letto e i suoi
+// componenti aggiunti alla coda, finché non restano solo PRT (foglie) o codici non presenti
+// nell'indice - stessa logica di Form1.RisolviAlberoComponenti.
var codiceProprio = Path.GetFileName(asmIniziale).Split('.')[0];
+// Un assieme con suffisso "_D..." referenzia sempre al suo interno anche l'assieme
+// generale (codice nudo, senza suffisso) a cui appartiene - riferimento fantasma verso
+// l'alto, non un componente vero (vedi docs/formato-asm.md). Va escluso subito: se
+// seguito, riporta dentro l'intero albero del macchinario invece del solo sotto-assieme
+// richiesto (visto su 9284208188_D600 -> 9284208188, che da solo genera 598 falsi
+// positivi invece di ~90 componenti reali).
+var codiceRadice = codiceProprio.Split('_')[0];
var componentiTrovati = new SortedSet(StringComparer.OrdinalIgnoreCase);
var nonTrovati = new List();
+var codiciVisti = new HashSet(StringComparer.OrdinalIgnoreCase) { codiceProprio, codiceRadice };
+var daScansionare = new Queue(new[] { asmIniziale });
-var candidati = EstraiCodiciReferenziati(asmIniziale)
- .Where(c => !string.Equals(c, codiceProprio, StringComparison.OrdinalIgnoreCase))
- .ToList();
-var trovatiPerCodice = CercaComponenti(candidati);
-foreach (var codice in candidati)
+while (daScansionare.Count > 0)
{
- if (trovatiPerCodice[codice].Count == 0)
- nonTrovati.Add(codice);
- else
+ var asm = daScansionare.Dequeue();
+ var candidati = EstraiCodiciReferenziati(asm)
+ .Where(codiciVisti.Add)
+ .ToList();
+ var trovatiPerCodice = CercaComponenti(candidati);
+ foreach (var codice in candidati)
+ {
+ var componenti = trovatiPerCodice[codice];
+ if (componenti.Count == 0)
+ {
+ nonTrovati.Add(codice);
+ continue;
+ }
componentiTrovati.Add(codice);
+ foreach (var (tipo, percorso) in componenti)
+ if (tipo == "asm")
+ daScansionare.Enqueue(percorso);
+ }
}
File.WriteAllLines(outPath, componentiTrovati);