#:project ../PrepStep/PrepStep.csproj #:property TargetFramework=net10.0-windows // dotnet run scripts/CreoBom.cs -- [-p cartella]... [-o out.bom.1] // // Genera la distinta base (BOM) di un assieme Creo, dato un elenco di cartelle dove // cercare i sottoassiemi (-p). La lettura del formato binario (sezione "members" + // parametro "ConfigName", vedi docs/formato-asm-record.md) vive in // PrepStep.AsmRecordReader, via "#:project" - stessa fonte usata da Form1 nella GUI (fase // 4/5, con fallback automatico all'euristica di AssemblyComponentScanner.cs per i file che // non rispettano il formato validato): qui solo la ricorsione sui sottoassiemi, il // riepilogo delle quantita' e la resa nel formato esatto della BOM Creo. using System.Text; using System.Text.RegularExpressions; using PrepStep; if (args.Length == 0) { Console.WriteLine("Uso: dotnet run scripts/CreoBom.cs -- [-p cartella]... [-o out.bom.1]"); return 1; } string fileIniziale = args[0]; var cartelle = new List(); string? outPath = null; for (int i = 1; i < args.Length; i++) { if ((args[i] == "-p" || args[i] == "--path") && i + 1 < args.Length) cartelle.Add(args[++i]); else if (args[i] == "-o" && i + 1 < args.Length) outPath = args[++i]; } if (cartelle.Count == 0) cartelle.Add(Path.GetDirectoryName(Path.GetFullPath(fileIniziale)) ?? "."); string testoBom = CreoAsm.GeneraBom(fileIniziale, cartelle); if (outPath is not null) { File.WriteAllBytes(outPath, new byte[] { 0xEF, 0xBB, 0xBF }.Concat(Encoding.UTF8.GetBytes(testoBom)).ToArray()); Console.WriteLine($"scritto {outPath} ({testoBom.Split("\r\n").Length} righe)"); } else { Console.Write(testoBom.Replace("\r\n", "\n")); } return 0; /// Ricorsione sui sottoassiemi e resa nel formato della BOM esportata da Creo. La /// lettura del formato binario vive in (PrepStep/, condivisa /// con la GUI). Vedi docs/formato-asm-record.md per il dettaglio dei record e i limiti noti /// (stato soppresso non gestito, matrice di posizionamento non decodificata, deduplica dei /// blocchi non dimostrata su assiemi che montano lo stesso sottoassieme due volte). public static class CreoAsm { private static readonly Dictionary TipoNome = new() { [1] = "Sottoassieme", [2] = "Parte" }; /// Voci distinte con quantita', nell'ordine di prima comparsa (come la BOM). public static List<((int Tipo, string Nome) Chiave, int Qta)> Rollup(IEnumerable membri) { var ordine = new List<(int, string)>(); var qty = new Dictionary<(int, string), int>(); foreach (var m in membri) { var chiave = (m.Tipo, m.Nome); if (!qty.ContainsKey(chiave)) ordine.Add(chiave); qty[chiave] = qty.GetValueOrDefault(chiave) + 1; } return ordine.Select(c => (c, qty[c])).ToList(); } // --- risoluzione dei file ------------------------------------------- /// nome modello -> membri, cercando <nome>.asm.<rev> nelle cartelle /// date. A parita' di nome vince la revisione piu' alta presente. Nome null se il file /// non c'e' in nessuna delle cartelle. private static Func?> CreaFinder(IEnumerable cartelle) { var indice = new Dictionary(StringComparer.OrdinalIgnoreCase); var revisioni = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var dir in cartelle) { if (!Directory.Exists(dir)) continue; foreach (var f in Directory.GetFiles(dir, "*.asm.*")) { if (!int.TryParse(Path.GetExtension(f).TrimStart('.'), out int rev)) continue; string chiave = Path.GetFileName(f).Split('.')[0]; if (!revisioni.TryGetValue(chiave, out int attuale) || rev > attuale) { indice[chiave] = f; revisioni[chiave] = rev; } } } var cache = new Dictionary?>(); return nome => { if (!cache.TryGetValue(nome, out var mem)) { mem = indice.TryGetValue(nome, out var percorso) ? AsmRecordReader.LeggiMembri(File.ReadAllBytes(percorso)) : null; cache[nome] = mem; } return mem; }; } // --- costruzione dell'albero ----------------------------------------- private sealed record Blocco(string Tipo, string Nome, List<((int Tipo, string Nome) Chiave, int Qta)>? Righe); /// Blocchi in ordine depth-first pre-order. Ogni modello compare una volta sola /// (deduplica per nome) anche se montato piu' volte. private static List Blocchi(string root, Func?> get) { var risultato = new List(); var visti = new HashSet(); void Visita(string nome, string tipo) { if (!visti.Add(nome)) return; // Creo stampa ogni modello una volta sola var mem = get(nome); if (mem is null) { risultato.Add(new Blocco(tipo, nome, null)); return; } var righe = Rollup(mem); risultato.Add(new Blocco(tipo, nome, righe)); foreach (var (chiave, _) in righe) if (chiave.Tipo == 1) Visita(chiave.Nome, "Sottoassieme"); } Visita(root, "Assieme"); return risultato; } /// Totale per parte, moltiplicato lungo l'albero, in ordine di prima comparsa: una /// parte che compare a piu' livelli occupa la posizione della sua prima comparsa e somma /// le quantita'. private static void Sommario(string root, Func?> get, int mult, List ordine, Dictionary acc) { var mem = get(root); if (mem is null) return; foreach (var (chiave, qta) in Rollup(mem)) { if (chiave.Tipo == 2) { if (!acc.ContainsKey(chiave.Nome)) ordine.Add(chiave.Nome); acc[chiave.Nome] = acc.GetValueOrDefault(chiave.Nome) + qta * mult; } else { Sommario(chiave.Nome, get, mult * qta, ordine, acc); } } } // --- resa nel formato di Creo ------------------------------------------ private static string RigaVoce(int qta, string tipo, string nome) => $"{qta,6} {tipo,12} {nome}"; /// Testo della BOM, identico byte per byte a quello esportato da Creo (UTF-8 con /// BOM, CRLF - vedi GeneraBom per la scrittura su file). private static string Render(string root, Func?> get) { var pezzi = new List(); foreach (var blocco in Blocchi(root, get)) { string testa = $"{blocco.Tipo} {blocco.Nome} contiene:"; if (blocco.Righe is null) { pezzi.Add(testa + "\r\n" + new string(' ', 5) + "*** NON RISOLTO: file .asm non trovato ***"); continue; } var righe = blocco.Righe.Select(r => RigaVoce(r.Qta, TipoNome[r.Chiave.Tipo], r.Chiave.Nome)); pezzi.Add(string.Join("\r\n", new[] { testa }.Concat(righe))); } var ordine = new List(); var acc = new Dictionary(); Sommario(root, get, 1, ordine, acc); var totale = ordine.Select(n => RigaVoce(acc[n], "Parte", n)); pezzi.Add(string.Join("\r\n", new[] { $"Summary of items for {root}:" }.Concat(totale))); // due righe vuote iniziali, tre fra i blocchi, due prima del riepilogo return "\r\n\r\n" + string.Join("\r\n\r\n\r\n\r\n", pezzi.Take(pezzi.Count - 1)) + "\r\n\r\n\r\n" + pezzi[^1] + "\r\n"; } public static string GeneraBom(string percorsoAsm, IEnumerable cartelle) { byte[] dati = File.ReadAllBytes(percorsoAsm); return Render(AsmRecordReader.NomeProprio(dati), CreaFinder(cartelle)); } }