Files
prep-step/CadSearchGui/AssemblyComponentScanner.cs
T
davideandClaude Sonnet 5 e7b5d85f6b Add family_name and bracket-adjacent placement signals for .asm scanning
Two more real placement forms found by comparing against Creo BOM reports:

- A sub-assembly can be referenced only through a "family_name\0<code>\0"
  metadata field instead of a textual "Componente ID" placement or the
  doubled-NUL pattern (case 9284202452 inside 9284208188_D400.asm, which
  contains 9284202452_C01/_C02/_C03 - previously dropped entirely, along
  with its three children).
- A bare numeric code immediately followed by a family-table bracket
  (<code><GENERIC>, e.g. bearing instances like 2510060303<CUSCI_ACBB7>)
  is a real reference to that instance, even though only the generic file
  is indexed on disk. Previously the instance code silently vanished before
  ever reaching the DB lookup; now it survives placement filtering and ends
  up in the "scartati" (discarded) list instead, so it's traceable rather
  than invisible.

docs/formato-asm.md documents both, bringing the total to four recognized
placement forms.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 16:14:52 +02:00

93 lines
5.7 KiB
C#

using System.Text;
using System.Text.RegularExpressions;
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).
///
/// Verificato su un caso reale confrontando con un report BOM generato da 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.
///
/// Falso positivo noto ed escluso: gli ID interni di viste salvate, nel formato
/// "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.
///
/// Viti/dadi/rosette normalizzati UNI (e cuscinetti CUSCI_...) sono family table Creo: il
/// nome che compare nel binario (es. "R6--_-_U6592&lt;UNI6592&gt;",
/// "2510060303&lt;CUSCI_ACBB7&gt;") è la singola istanza, non un file separato su disco - il
/// 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
/// (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 (&lt;codice&gt;...)"/"Component id N (&lt;codice&gt;...)" (la stringa
/// cambia con la localizzazione Creo del file); il codice ripetuto due volte di seguito
/// separato da un singolo byte NUL ("&lt;codice&gt;\0&lt;codice&gt;\0", osservato per
/// componenti di family table non-UNI, es. cuscinetti "CUSCI_..."); il campo
/// "family_name\0&lt;codice&gt;\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 "&lt;codice&gt;&lt;GENERICO&gt;" -
/// l'istanza di una family table non-UNI vista nel nome (es. "2510060303&lt;CUSCI_ACBB7&gt;",
/// un cuscinetto): il codice dell'istanza non è mai indicizzato a sé (solo il generico tra
/// "&lt;" e "&gt;" 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.
/// </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]+)(?=>)",
RegexOptions.Compiled);
private static readonly Regex CodiceNudoRegex = new(@"^\d{8,12}[A-Za-z]{0,2}$", RegexOptions.Compiled);
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)
{
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)
|| testo.Contains(codice + "\0" + codice + "\0")
|| testo.Contains("family_name\0" + codice + "\0")
|| testo.Contains(codice + "<");
candidati.RemoveWhere(c => CodiceNudoRegex.IsMatch(c) && !HaPlacement(c));
return candidati;
}
}