diff --git a/PrepStep.Tests/AsmRecordReaderTests.cs b/PrepStep.Tests/AsmRecordReaderTests.cs index 53305ab..5165be2 100644 --- a/PrepStep.Tests/AsmRecordReaderTests.cs +++ b/PrepStep.Tests/AsmRecordReaderTests.cs @@ -87,6 +87,23 @@ public class AsmRecordReaderTests Assert.All(membri, m => Assert.Equal("9244250136_C03", m.Nome)); } + [Fact] + public void Membro_con_notazione_istanza_generico_diretta_nel_nome_usa_il_generico() + { + // Regressione: 9244248577a_c01.asm - alcuni membri di family table non-UNI non + // passano da ConfigName, il campo "name"/"family_name" del record members porta gia' + // la forma "" (stessa notazione di §4.1). Senza lo split, il codice + // cercato nell'indice sarebbe "V6-12--_-_U5931_GC" per intero, mai trovato - + // va cercato invece il generico "UNI5931". + byte[] dati = Asm(1, Membro(1, 2, "V6-12--_-_U5931_GC")); + + var membri = AsmRecordReader.LeggiMembri(dati); + + Assert.Single(membri); + Assert.Equal("V6-12--_-_U5931_GC", membri[0].Nome); + Assert.Equal("UNI5931", membri[0].Generico); + } + [Fact] public void Componente_senza_ConfigName_non_e_toccato_da_istanze_di_altri_membri() { diff --git a/PrepStep/AsmRecordReader.cs b/PrepStep/AsmRecordReader.cs index b52ce6c..7bf9e9b 100644 --- a/PrepStep/AsmRecordReader.cs +++ b/PrepStep/AsmRecordReader.cs @@ -59,6 +59,14 @@ public static class AsmRecordReader @"\xe0\x01model_t_type\x00" + Int + @"\xe0\x0amodel_name\x00([\x20-\x7e]+)\x00\xe0\x0ageneric_name\x00\x00", RegexOptions.Compiled); + // Alcuni membri di family table non-UNI (es. viti/dadi commerciali) non passano da + // ConfigName: il campo "name" del record members porta gia' la forma "" + // (stessa notazione di §4.1, li' sconsigliata per la BOM perche' e' un elenco di + // riferimenti geometrici - qui invece e' il valore vero del componente, va solo separato). + // Osservato su 9244248577a_c01.asm: "V6-12--_-_U5931_GC" - senza questo split il + // codice cercato nell'indice sarebbe la stringa intera con le parentesi, mai trovata. + private static readonly Regex IstanzaConGenericoRegex = new(@"^([\x20-\x7e]+?)<([\x20-\x7e]+)>$", RegexOptions.Compiled); + // sintomo vero e proprio (docs/formato-asm-record.md §7): il file e' esso stesso // un'istanza di family table (il suo "model_name" ha il suffisso "_C0n" delle istanze, // stessa convenzione di SuffissoArrayFeatureRegex/ConfigName sopra) - "members" descrive @@ -163,7 +171,15 @@ public static class AsmRecordReader if (grezzi.Count != conteggio) throw new InvalidDataException($"attesi {conteggio} membri, trovati {grezzi.Count}: lettura disallineata"); - return grezzi.Select(g => new Membro(g.Id, g.Tipo, istanze.GetValueOrDefault(g.Id, g.Nome), g.Nome)).ToList(); + return grezzi.Select(g => + { + if (istanze.TryGetValue(g.Id, out var nomeIstanza)) + return new Membro(g.Id, g.Tipo, nomeIstanza, g.Nome); + var bracket = IstanzaConGenericoRegex.Match(g.Nome); + return bracket.Success + ? new Membro(g.Id, g.Tipo, bracket.Groups[1].Value, bracket.Groups[2].Value) + : new Membro(g.Id, g.Tipo, g.Nome, g.Nome); + }).ToList(); } /// Codici dei componenti diretti (il generico su disco, non il nome istanza), diff --git a/PrepStep/PrtMancantiForm.cs b/PrepStep/PrtMancantiForm.cs index 8002fa3..29e1c21 100644 --- a/PrepStep/PrtMancantiForm.cs +++ b/PrepStep/PrtMancantiForm.cs @@ -20,6 +20,7 @@ public class PrtMancantiForm : Form { Dock = DockStyle.Top, AutoSize = true, + MaximumSize = new Size(ClientSize.Width - 24, 0), Padding = new Padding(12, 8, 12, 8), Text = $"Copiati in \"{cartellaRaw}\": {numPrt} PRT e {numAsm} sotto-assiemi trovati nell'indice." + (codiciNonTrovati.Count > 0 @@ -70,5 +71,7 @@ public class PrtMancantiForm : Form Controls.Add(grid); Controls.Add(lblInfo); Controls.Add(btnApriCartella); + + Resize += (_, _) => lblInfo.MaximumSize = new Size(ClientSize.Width - 24, 0); } } diff --git a/docs/formato-asm-record.md b/docs/formato-asm-record.md index be53fac..c1b4543 100644 --- a/docs/formato-asm-record.md +++ b/docs/formato-asm-record.md @@ -223,6 +223,30 @@ ampio degli 8 campioni originali (`9284218024b.asm`, confrontato con la sua BOM riferimento) — non era presente in nessuno degli 8 campioni usati per il reverse engineering iniziale. +### 4.0.2 Istanza risolta direttamente in `members`, senza passare da `ConfigName` + +Non tutti i membri che sono istanze di family table hanno un record `ConfigName` associato. +Per alcune parti commerciali/normalizzate non-UNI, il campo `name` (e allo stesso modo +`family_name`) del record `members` (§3) porta già per intero la notazione +`` invece del solo nome generico: + +``` +\xe3 ""\0 ""\0 +``` + +È la stessa notazione istanza→generico della §4.1 sotto, ma qui compare **dentro** il record +`members` stesso, come valore diretto del componente - non nell'elenco di riferimenti +geometrici di cui parla la §4.1 (quello sì da ignorare per la BOM). Il generico fra `<` e `>` +è il file realmente indicizzato sul disco; il nome prima di `<` è quello che la BOM +mostrerebbe. + +`AsmRecordReader.LeggiMembri` lo riconosce per i membri **senza** un `ConfigName` +corrispondente il cui campo `name` rispetta per intero il pattern ``: il testo +dopo l'ultima `<` (fino alla `>` finale) diventa il generico da cercare nell'indice, quello +prima diventa il nome mostrato. Senza questo split, il codice cercato nell'indice sarebbe la +stringa intera con le parentesi comprese, mai presente nell'indice - il componente +risulterebbe erroneamente "non trovato". + ### 4.1 Una seconda pista, che NON va usata per la BOM Altrove nel file compaiono stringhe nella forma `istanza`: @@ -335,8 +359,8 @@ di ciascun punto segue sotto, in due gruppi. | Deduplica blocchi + moltiplicazione quantità (§6.2/§6.3) | Risolto | BOM reale (§7.1) | | Revisione del figlio che il padre si aspetta (§6.4) | Risolto | BOM reale (§7.1) | | Stato soppresso/escluso in `members` | Risolto | BOM reale (§7.1) | -| Family table di **assiemi** (rilevamento) | Risolto | BOM reale + scala 350 file (§7.1) | -| Family table di **assiemi** (contenuto di ogni istanza) | Non risolto | — (§7.2) | +| Family table di **assiemi** (rilevamento) | Risolto lato sicurezza (mai un falso negativo pericoloso), segnale non affidabile al 100% | BOM reale + scala intera share (§7.1) | +| Family table di **assiemi** (contenuto di ogni istanza) | Non risolto, nessun impatto sulla BOM (fallback già corretto) | Scan sull'intera share (§7.2) | | Costante `\x82\x7b` in `ConfigName` | Significato ignoto, rischio coperto | Scala 350 file (§7.2) | | Occorrenze multiple di `members` (simplified rep) | Mai osservato, rischio coperto | Scala 350 file (§7.2) | | Matrice di posizionamento (`e1`/`e2`/`e3`/`origin`) | Non letto, nessun impatto sulla BOM | — (§7.2) | @@ -388,7 +412,7 @@ di sbagliare in silenzio. ``` Il file generico è già di per sé un'istanza (`model_name` porta il suffisso `_C0n`, non è - neutro), e `stamp` elenca le sorelle. **Segnale affidabile**: la combinazione + neutro), e `stamp` elenca le sorelle. Il segnale usato per riconoscerlo è la combinazione `model_t_type`+`model_name` (con suffisso `_C0n`)+`generic_name` vuoto (`AsmRecordReader.OwnModelIdentityRegex` + `SuffissoIstanzaFamilyTableRegex`) - **non** la sola presenza di `generic_name`, che su altri 119 file del corpus da 350 compare in un @@ -397,6 +421,15 @@ di sbagliare in silenzio. affidabile, e su quei 25 `AsmRecordReader.LeggiMembri` lancia `InvalidDataException` invece di restituire il contenuto sbagliato della rappresentazione di default. + > **Non è un segnale affidabile al 100%.** Un controllo su scala più ampia (tutta la share, + > §7.2) ha trovato un caso in cui il segnale scatta ma **non c'è nessuna family table**: + > `9053005060.asm` è un assieme ordinario con due parti figlie `_C01`/`_C02` (geometrie + > diverse, nessuna family table nell'albero Creo - verificato dall'utente). Il perché il + > record di identità propria porti comunque il suffisso `_C0n` in quel caso resta ignoto. + > Non cambia la sicurezza del comportamento (`InvalidDataException` → fallback su + > `AssemblyComponentScanner`, che legge comunque il file correttamente), ma il segnale va + > trattato come "candidato da verificare", non come conferma di family table. + Verificato anche che il fallback su `AssemblyComponentScanner` **risolve già correttamente** il caso vero: i nomi delle istanze compaiono come testo puro nel campo `stamp`, e l'euristica ("codice che compare almeno due volte") li trova senza bisogno di decodificare @@ -413,6 +446,39 @@ di sbagliare in silenzio. dietro `stamp` non è stato decodificato oltre a nome+tipo di ciascuna istanza sorella. Aperto anche cosa sia davvero il record `generic_name` sui 119 file dove non è questo sintomo. + + **Aggiornamento (scan sull'intera share, non solo sul campione da 350 file).** Applicando + il segnale (`OwnModelIdentityRegex` + `SuffissoIstanzaFamilyTableRegex`) a tutti i `.asm` + indicizzati su `X:\` (7157 combinazioni cartella+codice uniche, ultima revisione ciascuna): + **95 candidati**, non solo i 25/350 del campione originale. + + Sul primo candidato controllato (`9053005060.asm.1`, un'unica istanza sorella secondo il + segnale), `stamp` si decodifica a mano byte per byte: + + ``` + \xe0\x00 "stamp"\0 \xe2\xf2\xf7\x07 \xe3\x02 "9053005060_C02"\0\0 \xe2 + ``` + + Confermato: **`stamp` porta solo i nomi** delle istanze sorelle, non il loro contenuto - + non è quindi la struttura giusta da cui partire per risolvere questo punto in generale. + + **Ma il candidato stesso era un falso positivo.** Verificato in Creo (albero del modello, + screenshot dell'utente): `9053005060.asm` **non** è una family table - è un assieme + ordinario con due parti figlie distinte `9053005060_C01.prt` e `9053005060_C02.prt` (pesi e + geometrie diverse, nessuna icona di family table nell'albero). Il segnale ha comunque + acceso perché, in quella revisione, il record di identità propria del file portava + `model_name = 9053005060_C01` con `generic_name` vuoto - non si sa ancora perché (forse + legato all'ultimo componente attivo/rigenerato quando il file fu salvato, non a una vera + family table). **Conseguenza pratica: il segnale, validato con BOM reale solo su + `2546220101.asm` (§7.1), non è affidabile su larga scala senza verifica caso per caso** - i + 95 candidati trovati dallo scan sono solo *papabili*, non confermati; non c'è motivo di + credere che la maggioranza sia una vera family table di assieme. + + **Resta comunque un punto aperto solo per completezza documentale, non un problema da + correggere**: qualunque sia la causa del falso positivo, il fallback su + `AssemblyComponentScanner` gestisce comunque correttamente il file (legge i due membri via + euristica testuale, non tenta di interpretare `stamp`), quindi nessun comportamento della + app dipende da questa decodifica. - **Costante `\x82\x7b`** nei record `ConfigName`: significato ignoto. Rischio coperto - `LeggiMembri` conta le occorrenze grezze di `ConfigName` e le confronta col numero di match del regex; se un campione avesse un valore diverso, il regex non farebbe più match (prima: