Fix runaway recursion and two missed-component gaps in .asm scanning

The bare root code of an assembly (e.g. 9284208188, without a _D... suffix)
is always referenced inside its own suffixed sub-assemblies as a genuine
placement pointing back to the general assembly. Following it during
recursive resolution pulled in the entire product tree instead of just the
requested sub-assembly (598 components collected instead of ~65 on
9284208188_D600). Fixed by seeding the visited-codes set with each starting
assembly's root code, in both scripts/ExtractCreoStrings.cs and the two BFS
call sites in Form1.cs (RisolviAlberoComponenti, AssiemeCompleto).

Also closes two recall gaps found by comparing against a real Creo BOM
report: some real placements only show up as the code repeated twice
separated by a NUL byte rather than the usual "Componente ID N (...)" text,
and family-table bearing codes (CUSCI_...) use the same <...> bracket
convention as UNI hardware but weren't recognized.

docs/formato-asm.md documents all three findings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 5 committed 2026-09-17 15:32:55 +02:00
1 parent 9660c4883d
commit a7e0df0247
4 files changed
+147 -69

No files matched your search

+27 -12
View File
@@ -21,10 +21,12 @@ namespace CadSearchGui;
/// "ModelView#&lt;numero&gt;", hanno la stessa forma di un codice (8-12 cifre) e /// "ModelView#&lt;numero&gt;", hanno la stessa forma di un codice (8-12 cifre) e
/// compaiono più volte come un riferimento vero. Vedi docs/formato-asm.md. /// 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 /// Viti/dadi/rosette normalizzati UNI (e cuscinetti CUSCI_...) sono family table Creo: il
/// binario (es. "R6--_-_U6592&lt;UNI6592&gt;") è la singola istanza, non un file separato /// nome che compare nel binario (es. "R6--_-_U6592&lt;UNI6592&gt;",
/// su disco - il file reale indicizzato è il generico "uni6592.prt.&lt;versione&gt;", quindi /// "2510060303&lt;CUSCI_ACBB7&gt;") è la singola istanza, non un file separato su disco - il
/// va estratto solo il contenuto tra "&lt;" e "&gt;". Vedi docs/formato-asm.md. /// file reale indicizzato è il generico "uni6592.prt.&lt;versione&gt;"/
/// "cusci_acbb7.prt.&lt;versione&gt;", quindi va estratto solo il contenuto tra "&lt;" e
/// "&gt;". Vedi docs/formato-asm.md.
/// ///
/// Lettera di revisione e suffisso "_..." possono comparire insieme sullo stesso codice /// Lettera di revisione e suffisso "_..." possono comparire insieme sullo stesso codice
/// (es. "9284200366A_C01"), e il suffisso dopo "_" può essere puramente numerico (es. /// (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 /// 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 /// 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 /// di un residuo di feature soppresse/superate (vedi docs/formato-asm.md). Per questi si
/// richiede anche un vero placement "Componente ID N (&lt;codice&gt;...)"/ /// richiede anche un vero placement, in una delle due forme osservate nel binario:
/// "Component id N (&lt;codice&gt;...)" (la stringa cambia con la localizzazione Creo del /// "Componente ID N (&lt;codice&gt;...)"/"Component id N (&lt;codice&gt;...)" (la stringa
/// file). Non si applica alle varianti con suffisso o bracket UNI: in alcuni file il /// cambia con la localizzazione Creo del file), oppure il codice ripetuto due volte di
/// placement riporta solo il nome istanza/generico senza il dettaglio del suffisso o del /// seguito separato da un singolo byte NUL ("&lt;codice&gt;\0&lt;codice&gt;\0", osservato per
/// bracket, che vive altrove nei metadati - lì resta necessaria la sola euristica delle /// componenti di family table non-UNI, es. cuscinetti "CUSCI_..."). Non si applica alle
/// occorrenze ripetute. /// 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.
/// </summary> /// </summary>
static class AssemblyComponentScanner static class AssemblyComponentScanner
{ {
private static readonly Regex CodiceRegex = new( 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+(?=>)", RegexOptions.Compiled); @"(?<!ModelView#)\b\d{8,12}[A-Za-z]{0,2}(?:_[A-Za-z]?\d{2,4})?\b|(?<=<)(?:UNI\d+|CUSCI_[A-Za-z0-9]+)(?=>)",
RegexOptions.Compiled);
private static readonly Regex CodiceNudoRegex = new(@"^\d{8,12}[A-Za-z]{0,2}$", RegexOptions.Compiled); private static readonly Regex CodiceNudoRegex = new(@"^\d{8,12}[A-Za-z]{0,2}$", RegexOptions.Compiled);
private const int OccorrenzeMinime = 2; private const int OccorrenzeMinime = 2;
/// <summary>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.</summary>
public static string CodiceRadice(string codice) => codice.Split('_')[0];
public static HashSet<string> EstraiCodiciReferenziati(string percorsoAsm) public static HashSet<string> EstraiCodiciReferenziati(string percorsoAsm)
{ {
var testo = Encoding.Latin1.GetString(File.ReadAllBytes(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(); var candidati = occorrenze.Where(kv => kv.Value >= OccorrenzeMinime).Select(kv => kv.Key).ToHashSet();
bool HaPlacement(string codice) => 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)); candidati.RemoveWhere(c => CodiceNudoRegex.IsMatch(c) && !HaPlacement(c));
return candidati; return candidati;
+9 -2
View File
@@ -671,8 +671,10 @@ public partial class Form1 : Form
IEnumerable<string> asmIniziali, IProgress<(int Fatti, int Totale)>? progress = null) IEnumerable<string> asmIniziali, IProgress<(int Fatti, int Totale)>? progress = null)
{ {
var daScansionare = new Queue<string>(asmIniziali); var daScansionare = new Queue<string>(asmIniziali);
var codiciIniziali = daScansionare.Select(f => Path.GetFileName(f).Split('.')[0]);
var codiciVisti = new HashSet<string>( var codiciVisti = new HashSet<string>(
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 trovati = new List<(string Codice, string Tipo, string Percorso)>();
var codiciNonTrovati = new List<string>(); var codiciNonTrovati = new List<string>();
@@ -827,7 +829,12 @@ public partial class Form1 : Form
var daVisitare = new Queue<string>(); var daVisitare = new Queue<string>();
daVisitare.Enqueue(percorsoAsmIniziale); daVisitare.Enqueue(percorsoAsmIniziale);
var fileVisitati = new HashSet<string> { percorsoAsmIniziale }; var fileVisitati = new HashSet<string> { percorsoAsmIniziale };
var codiciVisti = new HashSet<string> { Path.GetFileName(percorsoAsmIniziale).Split('.')[0] }; var codiceIniziale = Path.GetFileName(percorsoAsmIniziale).Split('.')[0];
var codiciVisti = new HashSet<string>
{
codiceIniziale,
AssemblyComponentScanner.CodiceRadice(codiceIniziale),
};
while (daVisitare.Count > 0) while (daVisitare.Count > 0)
{ {
+58 -27
View File
@@ -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, 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 quindi non si può escludere per confronto diretto come per il caso
precedente. La regola generale che discrimina questo caso: un componente precedente. La regola generale che discrimina questo caso: un componente
vero compare **sempre** anche dentro un vero placement vero compare **sempre** anche dentro un vero placement, in una delle due
`Componente ID N (<codice>...)`/`Component id N (<codice>...)` (la stringa forme osservate nel binario:
cambia con la localizzazione Creo con cui il file è stato salvato - osservate
sia italiana sia inglese); un riferimento incidentale al nome di un altro - `Componente ID N (<codice>...)`/`Component id N (<codice>...)` (la stringa
assieme, anche se ripetuto più volte, non compare mai in quel contesto. cambia con la localizzazione Creo con cui il file è stato salvato -
`scripts/ExtractCreoStrings.cs` applica questa regola ai codici "nudi" (senza osservate sia italiana sia inglese);
suffisso `_...` e senza bracket UNI - vedi sotto perché limitata a questi): - il codice ripetuto due volte di seguito separato da un singolo byte NUL,
un codice ripetuto ma mai trovato dentro un placement viene scartato. Questo `<codice>\0<codice>\0` (osservato per componenti di family table non-UNI,
NON copre i casi fantasma/soppressi sotto, che hanno un placement es. cuscinetti `CUSCI_...` - vedi sotto; il placement testuale
sintatticamente identico a uno vero e restano quindi indistinguibili a "Componente ID" non li copre, ma questa coppia doppia sì).
livello testuale.
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: **Riferimenti fantasma a codici superati/rinominati.** Caso osservato:
`9244207061.asm` contiene un riferimento a `9044207061` (stesso numero, prima `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 sola euristica delle occorrenze ripetute - meno precisa, ma l'unica
applicabile senza rischiare falsi negativi su casi come questi. 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 I componenti di minuteria standard (viti, dadi, rosette UNI) e i cuscinetti
Creo come **family table**: un file generico (es. `uni6592.prt`) con tante (`CUSCI_...`) sono definiti in Creo come **family table**: un file generico
righe/istanze quante sono le varianti dimensionali. Nel binario dell'assieme (es. `uni6592.prt`, `cusci_acbb7.prt`) con tante righe/istanze quante sono le
compaiono così: varianti dimensionali. Nel binario dell'assieme compaiono così:
``` ```
Componente ID 28755 (R6--_-_U6592<UNI6592>.PRT) Componente ID 28755 (R6--_-_U6592<UNI6592>.PRT)
2510060303<CUSCI_ACBB7>
``` ```
`R6--_-_U6592` è il nome della singola istanza (riga della family table), **non `R6--_-_U6592` e `2510060303` sono il nome/codice della singola istanza (riga
un file separato su disco** — sulla share esiste solo `uni6592.prt.<versione>` della family table), **non un file separato su disco** — sulla share esiste
(verificato in `Unificati/`). Cercare l'istanza intera nell'indice fallisce solo il generico (`uni6592.prt.<versione>`, `cusci_acbb7.prt.<versione>`,
sempre; va estratto solo il contenuto tra `<` e `>` (`UNI6592`), che coincide verificato in `Unificati/`). Cercare l'istanza intera nell'indice fallisce
col nome del file generico realmente indicizzato: 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 Implementata sia in [`scripts/ExtractCreoStrings.cs`](../scripts/ExtractCreoStrings.cs) sia in
[`AssemblyComponentScanner.cs`](../CadSearchGui/AssemblyComponentScanner.cs) (fase "Trova componenti [`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 ## Risoluzione ricorsiva dell'albero
@@ -172,11 +203,11 @@ livello) serve una BFS:
3. Un codice non trovato nell'indice va scartato/segnalato: o è rumore 3. Un codice non trovato nell'indice va scartato/segnalato: o è rumore
binario, o è un componente non ancora indicizzato. binario, o è un componente non ancora indicizzato.
Stessa logica di estrazione candidati in due punti del codice, con scopi Stessa logica di estrazione candidati ed euristica del codice radice (sopra),
diversi: 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"). - [`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) — 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. - [`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 Nota: le query contro `files_ultima_versione` filtrano solo per
`(cartella_radice, codice, tipo_file)` - se lo stesso codice esiste in più `(cartella_radice, codice, tipo_file)` - se lo stesso codice esiste in più
+53 -28
View File
@@ -1,11 +1,11 @@
#:package Microsoft.Data.Sqlite@10.0.12 #:package Microsoft.Data.Sqlite@10.0.12
// dotnet run scripts/ExtractCreoStrings.cs -- <file.asm> [cad_index.db] [output.txt] // dotnet run scripts/ExtractCreoStrings.cs -- <file.asm> [cad_index.db] [output.txt]
// //
// Estrae dal file .asm i codici dei componenti referenziati DIRETTAMENTE (un solo // Estrae dal file .asm i codici dei componenti referenziati, espandendo ricorsivamente
// livello, come li mostra Creo per quell'assieme - nessuna espansione ricorsiva nei // ogni sotto-assieme (.asm) trovato nell'indice, finché non restano solo PRT (foglie) o
// sotto-assiemi). Stessa euristica di CadSearchGui/AssemblyComponentScanner.cs: i codici // codici non presenti nell'indice. Stessa euristica di CadSearchGui/AssemblyComponentScanner.cs:
// veri compaiono più volte nel file, il rumore binario quasi sempre una sola volta; gli // i codici veri compaiono più volte nel file, il rumore binario quasi sempre una sola volta;
// ID interni "ModelView#<numero>" sono esclusi via lookbehind - vedi scripts/Analyze.cs. // gli ID interni "ModelView#<numero>" sono esclusi via lookbehind - vedi scripts/Analyze.cs.
// Ogni codice trovato viene validato contro l'indice cad_index.db. // Ogni codice trovato viene validato contro l'indice cad_index.db.
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@@ -21,18 +21,19 @@ string asmIniziale = args[0];
string dbPath = args.Length > 1 ? args[1] : "cad_index.db"; string dbPath = args.Length > 1 ? args[1] : "cad_index.db";
string outPath = args.Length > 2 ? args[2] : "BOM.txt"; string outPath = args.Length > 2 ? args[2] : "BOM.txt";
// Seconda alternativa: viti/dadi/rosette normalizzati UNI, definiti come family table in // Seconda alternativa: componenti normalizzati definiti come family table in Creo (viti/
// Creo. Compaiono come "Componente ID N (R12--_-_U6592<UNI6592>.PRT)": "R12--_-_U6592" è // dadi/rosette UNI, ma anche cuscinetti CUSCI_...). Compaiono come "Componente ID N
// il nome della singola istanza (riga della family table, non un file separato su disco), // (R12--_-_U6592<UNI6592>.PRT)": "R12--_-_U6592" è il nome della singola istanza (riga
// il file reale indicizzato è il generico "uni6592.prt.<versione>" - va quindi estratto // della family table, non un file separato su disco), il file reale indicizzato è il
// solo il contenuto tra "<" e ">". // generico "uni6592.prt.<versione>"/"cusci_acbb7.prt.<versione>" - va quindi estratto solo
// il contenuto tra "<" e ">".
// Terza alternativa: varianti dimensionali con suffisso puramente numerico (es. cinghie // Terza alternativa: varianti dimensionali con suffisso puramente numerico (es. cinghie
// "2702220182_8188", dove "8188" è una misura, non una lettera di revisione/sotto-assieme // "2702220182_8188", dove "8188" è una misura, non una lettera di revisione/sotto-assieme
// come "_C01"/"_D100") - la lettera dopo "_" è quindi opzionale, non obbligatoria. La // come "_C01"/"_D100") - la lettera dopo "_" è quindi opzionale, non obbligatoria. La
// lettera di revisione e il suffisso "_" possono anche comparire insieme sullo stesso // 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). // codice (es. "9284200366A_C01", componente di un sotto-assieme già di per sé revisionato).
var codiceRegex = new Regex( var codiceRegex = new Regex(
@"(?<!ModelView#)\b\d{8,12}[A-Za-z]{0,2}(?:_[A-Za-z]?\d{2,4})?\b|(?<=<)UNI\d+(?=>)", @"(?<!ModelView#)\b\d{8,12}[A-Za-z]{0,2}(?:_[A-Za-z]?\d{2,4})?\b|(?<=<)(?:UNI\d+|CUSCI_[A-Za-z0-9]+)(?=>)",
RegexOptions.Compiled); RegexOptions.Compiled);
const int occorrenzeMinime = 2; const int occorrenzeMinime = 2;
@@ -48,14 +49,18 @@ HashSet<string> EstraiCodiciReferenziati(string percorsoAsm)
// riferimenti al proprio assieme padre (nome sessione/file) o a codici superati/ // riferimenti al proprio assieme padre (nome sessione/file) o a codici superati/
// rinominati (caso "fantasma", docs/formato-asm.md) ripetono comunque il codice più // 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 // volte pur non essendo mai un componente vero. Per questi si richiede un vero
// placement "Componente ID N (<codice>...)". "Componente ID"/"Component id": la // placement, in una delle due forme osservate nel binario: "Componente ID N
// stringa cambia con la localizzazione Creo con cui il file è stato salvato (osservate // (<codice>...)"/"Component id N (<codice>...)" (la stringa cambia con la
// sia italiana sia inglese). Non si applica alle varianti con suffisso "_..." né alle // localizzazione Creo del file), oppure il codice ripetuto due volte di seguito
// istanze family table UNI: in alcuni file il placement riporta solo il nome // separato da un singolo byte NUL ("<codice>\0<codice>\0", osservato per componenti di
// istanza/generico senza il dettaglio del suffisso o del bracket "<...>", che vive // family table non-UNI, es. cuscinetti "CUSCI_..."). Non si applica alle varianti con
// altrove nei metadati - lì l'occorrenza ripetuta resta l'unico segnale disponibile. // 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) => 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)); candidati.RemoveWhere(c => Regex.IsMatch(c, @"^\d{8,12}[A-Za-z]{0,2}$") && !HaPlacement(c));
return candidati; return candidati;
@@ -98,22 +103,42 @@ Dictionary<string, List<(string Tipo, string Percorso)>> CercaComponenti(IReadOn
return risultato; return risultato;
} }
// Solo i riferimenti diretti del file indicato (un livello), come li mostra Creo per // Espansione ricorsiva: ogni sotto-assieme (.asm) trovato viene a sua volta letto e i suoi
// quell'assieme - nessuna espansione ricorsiva nei sotto-assiemi. // 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]; 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<string>(StringComparer.OrdinalIgnoreCase); var componentiTrovati = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
var nonTrovati = new List<string>(); var nonTrovati = new List<string>();
var codiciVisti = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { codiceProprio, codiceRadice };
var daScansionare = new Queue<string>(new[] { asmIniziale });
var candidati = EstraiCodiciReferenziati(asmIniziale) while (daScansionare.Count > 0)
.Where(c => !string.Equals(c, codiceProprio, StringComparison.OrdinalIgnoreCase))
.ToList();
var trovatiPerCodice = CercaComponenti(candidati);
foreach (var codice in candidati)
{ {
if (trovatiPerCodice[codice].Count == 0) var asm = daScansionare.Dequeue();
nonTrovati.Add(codice); var candidati = EstraiCodiciReferenziati(asm)
else .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); componentiTrovati.Add(codice);
foreach (var (tipo, percorso) in componenti)
if (tipo == "asm")
daScansionare.Enqueue(percorso);
}
} }
File.WriteAllLines(outPath, componentiTrovati); File.WriteAllLines(outPath, componentiTrovati);