diff --git a/CLAUDE.md b/CLAUDE.md index 9a476f0..eb66c52 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -63,12 +63,15 @@ designer visuale): sotto-assiemi/parti (`RisolviAlberoComponenti`), copiandoli anch'essi in `raw/`. Usa `AsmRecordReader` (parser esatto dei record binari, `docs/formato-asm-record.md`) e ripiega su `AssemblyComponentScanner` (euristica testuale, `docs/formato-asm.md`) solo se il file - non rispetta il formato validato (`InvalidDataException`). Due punti da non confondere, - entrambi imparati facendo fallire Creo (`docs/formato-asm-record.md` §7.3 e §7.4): + non rispetta il formato validato (`InvalidDataException`). Tre punti da non confondere, + tutti imparati facendo fallire Creo (`docs/formato-asm-record.md` §7.3, §7.4 e §7.5): i **componenti soppressi** non sono nella distinta ma il loro file serve per aprire - l'assieme (`EstraiCodiciSoppressi`, copiati senza finire fra i "non trovati"); e quando lo - stesso codice esiste in più cartelle della share — succede per il 13% dei codici — va - copiata la copia nella **stessa cartella dell'assieme che la referenzia** + l'assieme (`EstraiCodiciSoppressi`, copiati senza finire fra i "non trovati"); i + **riferimenti esterni** (assembly feature, datum copiati) citano modelli che non sono né + membri né piazzamenti, e che Creo carica con tutto il loro albero + (`EstraiCodiciRiferimentiEsterni`, anche questi fuori dai "non trovati", ma con ricorsione); + e quando lo stesso codice esiste in più cartelle della share — succede per il 13% dei + codici — va copiata la copia nella **stessa cartella dell'assieme che la referenzia** (`ScegliPerCartella`), perché il `.asm` non registra quale usa e sbagliarla impedisce a Creo di aprire l'assieme. 5. **Verifica completezza** — mostra i `.asm` della distinta già in `raw/`, trascinabili su diff --git a/PrepStep.Tests/AsmRecordReaderTests.cs b/PrepStep.Tests/AsmRecordReaderTests.cs index 8875761..a6a3782 100644 --- a/PrepStep.Tests/AsmRecordReaderTests.cs +++ b/PrepStep.Tests/AsmRecordReaderTests.cs @@ -48,6 +48,17 @@ public class AsmRecordReaderTests Bytes(0xe0, 0x01, "model_t_type", 0, 2, 0xe0, 0x0a, "model_name", 0, nome, 0, 0xe0, 0x0a, "generic_name", 0, 0); + // array dei modelli referenziati: \xf2\xf7\x05 \xf3, ogni elemento + // \xe3\xe2\0 - vedi docs/formato-asm-record.md §7.5 + private static byte[] Riferimenti(params string[] nomi) + { + var parti = new List { 0xf2, 0xf7, 0x05 }; + foreach (var nome in nomi) + parti.AddRange(new object[] { 0xe3, 0xe2, 0x02, nome, 0 }); + parti.Add(0xf3); + return Bytes(parti.ToArray()); + } + private static byte[] Asm(int conteggioDichiarato, byte[] membri, byte[]? extra = null) => Bytes("header ", extra ?? Array.Empty(), "\0members\0", 0xf1, 0xf8, conteggioDichiarato, membri, "#MdlStatus"); @@ -229,6 +240,28 @@ public class AsmRecordReaderTests finally { File.Delete(percorso); } } + [Fact] + public void Riferimenti_esterni_non_sono_fra_i_membri_e_si_risolvono_sul_generico() + { + // Un modello legato da riferimento esterno (assembly feature "#AFn", datum copiati) non + // compare ne' in "members" ne' fra i piazzamenti, ma senza il suo file Creo non apre + // l'assieme: e' il caso che faceva fallire 9255728001_200 in Distributed Batch. Per le + // istanze di family table il file su disco e' il generico fra parentesi angolari. + byte[] dati = Asm(1, Membro(1, 2, "9244206098"), + extra: Bytes(Riferimenti("9255708017#AF2<9255708017>", "9255708018"), + Riferimenti("V6-12--_-_U5933_C"))); + + Assert.Equal(new[] { "9244206098" }, AsmRecordReader.LeggiMembri(dati).Select(m => m.Nome)); + + string percorso = ScriviFile(dati); + try + { + Assert.Equal(new[] { "9255708017", "9255708018", "UNI5933" }, + AsmRecordReader.EstraiCodiciRiferimentiEsterni(percorso).OrderBy(c => c, StringComparer.Ordinal)); + } + finally { File.Delete(percorso); } + } + [Fact] public void Sezione_members_assente_lancia_eccezione() { diff --git a/PrepStep/AsmRecordReader.cs b/PrepStep/AsmRecordReader.cs index 1e31588..62e3847 100644 --- a/PrepStep/AsmRecordReader.cs +++ b/PrepStep/AsmRecordReader.cs @@ -225,6 +225,45 @@ public static class AsmRecordReader return soppressi; } + /// Modelli che l'assieme cita nella tabella dei riferimenti (§7.5): oltre ai propri + /// membri comprende quelli legati da **riferimenti esterni** - assembly feature, datum + /// copiati - che non compaiono né in "members" né come piazzamento, ma che Creo carica + /// comunque per aprire l'assieme. + /// + /// Terza lista dopo ("cosa c'è in distinta") e : come quest'ultima risponde a "cosa serve su disco", non a + /// "cosa va nella BOM. L'array contiene anche nomi di feature (MIRROR3, CHAMFER1...) e non + /// solo di modelli: non si riesce a distinguerli dal record, ma non danno fastidio perché il + /// chiamante li cerca nell'indice e ignora quelli che non ci sono. + public static HashSet EstraiCodiciRiferimentiEsterni(string percorsoAsm) + { + string testo = Encoding.Latin1.GetString(File.ReadAllBytes(percorsoAsm)); + var codici = new HashSet(StringComparer.OrdinalIgnoreCase); + for (int i = testo.IndexOf(InizioRiferimenti, StringComparison.Ordinal); i >= 0; + i = testo.IndexOf(InizioRiferimenti, i + 1, StringComparison.Ordinal)) + { + int max = Math.Min(LunghezzaMassimaRiferimenti, testo.Length - i); + int fine = testo.IndexOf(FineRiferimenti, i, max); + foreach (Match m in RiferimentoRegex.Matches(testo.Substring(i, fine >= 0 ? fine - i : max))) + { + var bracket = IstanzaConGenericoRegex.Match(m.Groups[1].Value); + codici.Add(bracket.Success + ? bracket.Groups[2].Value + : SuffissoArrayFeatureRegex.Replace(m.Groups[1].Value, "")); + } + } + return codici; + } + + // Array dei modelli referenziati: "\xf2\xf7\x05" lo apre, "\xf3" lo chiude, ogni elemento e' + // "\xe3\xe2\x00" (§7.5). Il tetto di lunghezza limita il danno se il terminatore + // non si trova dove atteso: si perde qualche riferimento, non si raccoglie mezzo file. + private const string InizioRiferimenti = "ò÷\u0005ãâ"; + private const char FineRiferimenti = 'ó'; + private const int LunghezzaMassimaRiferimenti = 4096; + private static readonly Regex RiferimentoRegex = new( + @"\xe3\xe2[\x01\x02]([\x20-\x7e]{3,})\x00", RegexOptions.Compiled); + // "Componente ID 52 (9230100043.PRT)" - la stringa cambia con la localizzazione Creo del // file, come in AssemblyComponentScanner.HaPlacement. Minimo 3 caratteri per lo stesso // motivo per cui li' i token generici partono da 3: sotto quella soglia si raccoglie diff --git a/PrepStep/Form1.cs b/PrepStep/Form1.cs index e483d28..614a1a5 100644 --- a/PrepStep/Form1.cs +++ b/PrepStep/Form1.cs @@ -875,14 +875,18 @@ public partial class Form1 : Form { // Ogni assieme viaggia con la cartella della share da cui proviene: e' li' che Creo // cerca per primi i suoi componenti, quindi e' li' che li cerchiamo anche noi. - var daScansionare = new Queue<(string File, string? CartellaOrigine)>( - asmIniziali.Select(f => (f, (string?)null))); + // "Esterno" marca i rami raggiunti da un riferimento esterno invece che dalla distinta: + // servono su disco ma non sono componenti, quindi i loro codici mancanti non vanno fra i + // "non trovati" (sarebbero falsi allarmi, come per i soppressi). + var daScansionare = new Queue<(string File, string? CartellaOrigine, bool Esterno)>( + asmIniziali.Select(f => (f, (string?)null, false))); var codiciIniziali = asmIniziali.Select(f => Path.GetFileName(f).Split('.')[0]).ToList(); var codiciVisti = new HashSet( codiciIniziali.Concat(codiciIniziali.Select(AssemblyComponentScanner.CodiceRadice)), StringComparer.OrdinalIgnoreCase); var codiciSoppressiVisti = new HashSet(StringComparer.OrdinalIgnoreCase); + var codiciEsterniVisti = new HashSet(StringComparer.OrdinalIgnoreCase); var trovati = new List<(string Codice, string Tipo, string Percorso)>(); var codiciNonTrovati = new List(); if (daScansionare.Count == 0 || _dbPath is null) @@ -894,7 +898,7 @@ public partial class Form1 : Form int processati = 0; while (daScansionare.Count > 0) { - var (asm, cartellaOrigine) = daScansionare.Dequeue(); + var (asm, cartellaOrigine, esterno) = daScansionare.Dequeue(); cartellaOrigine ??= CartellaDiOrigine(asm, candidati); processati++; foreach (var codice in EstraiCodiciReferenziati(asm, codiciNoti.Keys)) @@ -904,7 +908,8 @@ public partial class Form1 : Form if (!candidati.TryGetValue(codice, out var perCodice)) { - codiciNonTrovati.Add(codice); + if (!esterno) + codiciNonTrovati.Add(codice); continue; } @@ -915,7 +920,7 @@ public partial class Form1 : Form if (tipo == "asm") { daScansionare.Enqueue((Path.Combine(_ultimaCartellaRaw!, Path.GetFileName(percorso)), - Path.GetDirectoryName(percorso))); + Path.GetDirectoryName(percorso), esterno)); } } } @@ -936,6 +941,28 @@ public partial class Form1 : Form trovati.Add((codice, tipo, percorso)); } } + + // I riferimenti esterni (assembly feature, datum copiati verso un altro modello) non + // sono componenti e non compaiono fra i piazzamenti, ma Creo carica il modello citato + // per aprire l'assieme - e con lui i *suoi* componenti, quindi qui, a differenza dei + // soppressi, si ricorre dentro. Vedi docs/formato-asm-record.md §7.5. + foreach (var codice in AsmRecordReader.EstraiCodiciRiferimentiEsterni(asm)) + { + if (!codiciEsterniVisti.Add(codice) || codiciVisti.Contains(codice)) + continue; + if (!candidati.TryGetValue(codice, out var perCodice)) + continue; + foreach (var (tipo, percorso) in ScegliPerCartella(perCodice, cartellaOrigine)) + { + CopiaFileTrovati(new[] { percorso }, _ultimaCartellaRaw!); + trovati.Add((codice, tipo, percorso)); + if (tipo == "asm") + { + daScansionare.Enqueue((Path.Combine(_ultimaCartellaRaw!, Path.GetFileName(percorso)), + Path.GetDirectoryName(percorso), true)); + } + } + } progress?.Report((processati, processati + daScansionare.Count)); } diff --git a/docs/formato-asm-record.md b/docs/formato-asm-record.md index 9dac344..4af4d95 100644 --- a/docs/formato-asm-record.md +++ b/docs/formato-asm-record.md @@ -495,12 +495,12 @@ seconda è più lunga. | Lista | Serve per | Si ottiene con | | --- | --- | --- | | Componenti della distinta | BOM, verifica completezza | `LeggiMembri` / `EstraiCodiciReferenziati` (sezione `members`) | -| File necessari al caricamento | popolare `raw/` per l'export STEP | i precedenti **più** `EstraiCodiciSoppressi` | +| File necessari al caricamento | popolare `raw/` per l'export STEP | i precedenti **più** `EstraiCodiciSoppressi` e `EstraiCodiciRiferimentiEsterni` (§7.5) | -La differenza sono i **componenti soppressi** (§7.1): fuori dalla BOM, ma senza il loro file -Creo non apre l'assieme. Chi popola `raw/` deve quindi usare l'unione; chi genera la BOM o -verifica la completezza deve usare **solo** i membri, altrimenti i soppressi diventano falsi -"componenti mancanti". +La differenza sono i **componenti soppressi** (§7.1) e i **riferimenti esterni** (§7.5): fuori +dalla BOM, ma senza il loro file Creo non apre l'assieme. Chi popola `raw/` deve quindi usare +l'unione; chi genera la BOM o verifica la completezza deve usare **solo** i membri, altrimenti +quelle voci diventano falsi "componenti mancanti". Il sintomo, se si sbaglia, non assomiglia per niente alla causa: Creo Distributed Batch fallisce con `Carica modello in Creo Pro | Non riuscito | [-4] Errore: Dati di input non @@ -539,6 +539,36 @@ Conseguenze per il codice: versione, una del 2011 in una cartella di file sciolti e una del 2026 accanto all'assieme, e in `raw/` finiva quella del 2011. +### 7.5 Riferimenti esterni: la tabella dei modelli citati + +Terzo modo in cui un `.asm` dipende da un file che non è fra i suoi membri, dopo i soppressi +(§7.1) e le copie ambigue (§7.4). Un assieme che porta **assembly feature** o **datum copiati** +da un altro modello lo cita in una tabella a parte, che non è `members` e non lascia una stringa +`Componente ID …`: i due lettori esistenti non la vedevano. + +Struttura: un array aperto da `\xf2\xf7\x05` e chiuso da `\xf3`, con elementi +`\xe3\xe2\x00`. Il nome può portare il suffisso `#AF` di §4.0.1 e/o la notazione +`istanza` di §4.0.2 - il file su disco è il generico: + +``` +f2 f7 05 e3 e2 01 "9255708017#AF2<9255708017>" 00 ... -> 9255708017 + e3 e2 02 "9255700181" 00 ... -> 9255700181 +f3 +``` + +Va letta con `EstraiCodiciRiferimentiEsterni`, e - a differenza dei soppressi - **si ricorre +dentro**: Creo carica il modello citato con tutti i suoi componenti, che quindi servono anch'essi +in `raw/`. La tabella contiene anche nomi di *feature* (`MIRROR3`, `CHAMFER1`, +`M10_TAPPED_HOLE1`), indistinguibili dai modelli guardando il record: non è un problema perché +chi la usa cerca ogni nome nell'indice e ignora quelli che non ci sono - per lo stesso motivo +questi codici non devono finire fra i "non trovati". + +Il caso reale: `9255728001` falliva in Distributed Batch con lo stesso `[-4] Dati di input non +completi` di §7.3 perché il suo sottoassieme `9255728001_200` referenzia per assembly feature +`9255708017` e `9255708018` (e con loro `9255700181/182/183`), e monta una vite +`V6-12--_-_U5933_C` il cui generico `uni5933.prt` non era in `raw/`. Nessuno dei sei +file compariva in `members` né fra i piazzamenti. + ## 8. Uso ```powershell