2026-09-21 16:14:00 +02:00
|
|
|
#:project ../PrepStep/PrepStep.csproj
|
|
|
|
|
#:property TargetFramework=net10.0-windows
|
2026-09-21 15:41:14 +02:00
|
|
|
// dotnet run scripts/CreoBom.cs -- <file.asm.rev> [-p cartella]... [-o out.bom.1]
|
|
|
|
|
//
|
2026-09-21 16:14:00 +02:00
|
|
|
// 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.
|
2026-09-21 15:41:14 +02:00
|
|
|
using System.Text;
|
|
|
|
|
using System.Text.RegularExpressions;
|
2026-09-21 16:14:00 +02:00
|
|
|
using PrepStep;
|
2026-09-21 15:41:14 +02:00
|
|
|
|
|
|
|
|
if (args.Length == 0)
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine("Uso: dotnet run scripts/CreoBom.cs -- <file.asm.rev> [-p cartella]... [-o out.bom.1]");
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string fileIniziale = args[0];
|
|
|
|
|
var cartelle = new List<string>();
|
|
|
|
|
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;
|
|
|
|
|
|
2026-09-21 16:14:00 +02:00
|
|
|
/// <summary>Ricorsione sui sottoassiemi e resa nel formato della BOM esportata da Creo. La
|
|
|
|
|
/// lettura del formato binario vive in <see cref="AsmRecordReader"/> (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).</summary>
|
2026-09-21 15:41:14 +02:00
|
|
|
public static class CreoAsm
|
|
|
|
|
{
|
|
|
|
|
private static readonly Dictionary<int, string> TipoNome = new() { [1] = "Sottoassieme", [2] = "Parte" };
|
|
|
|
|
|
|
|
|
|
/// <summary>Voci distinte con quantita', nell'ordine di prima comparsa (come la BOM).</summary>
|
2026-09-21 16:14:00 +02:00
|
|
|
public static List<((int Tipo, string Nome) Chiave, int Qta)> Rollup(IEnumerable<AsmRecordReader.Membro> membri)
|
2026-09-21 15:41:14 +02:00
|
|
|
{
|
|
|
|
|
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 -------------------------------------------
|
|
|
|
|
|
|
|
|
|
/// <summary>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.</summary>
|
2026-09-21 16:14:00 +02:00
|
|
|
private static Func<string, List<AsmRecordReader.Membro>?> CreaFinder(IEnumerable<string> cartelle)
|
2026-09-21 15:41:14 +02:00
|
|
|
{
|
|
|
|
|
var indice = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
|
var revisioni = new Dictionary<string, int>(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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-21 16:14:00 +02:00
|
|
|
var cache = new Dictionary<string, List<AsmRecordReader.Membro>?>();
|
2026-09-21 15:41:14 +02:00
|
|
|
return nome =>
|
|
|
|
|
{
|
|
|
|
|
if (!cache.TryGetValue(nome, out var mem))
|
|
|
|
|
{
|
2026-09-21 16:14:00 +02:00
|
|
|
mem = indice.TryGetValue(nome, out var percorso) ? AsmRecordReader.LeggiMembri(File.ReadAllBytes(percorso)) : null;
|
2026-09-21 15:41:14 +02:00
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
/// <summary>Blocchi in ordine depth-first pre-order. Ogni modello compare una volta sola
|
|
|
|
|
/// (deduplica per nome) anche se montato piu' volte.</summary>
|
2026-09-21 16:14:00 +02:00
|
|
|
private static List<Blocco> Blocchi(string root, Func<string, List<AsmRecordReader.Membro>?> get)
|
2026-09-21 15:41:14 +02:00
|
|
|
{
|
|
|
|
|
var risultato = new List<Blocco>();
|
|
|
|
|
var visti = new HashSet<string>();
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>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'.</summary>
|
2026-09-21 16:14:00 +02:00
|
|
|
private static void Sommario(string root, Func<string, List<AsmRecordReader.Membro>?> get, int mult,
|
2026-09-21 15:41:14 +02:00
|
|
|
List<string> ordine, Dictionary<string, int> 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}";
|
|
|
|
|
|
|
|
|
|
/// <summary>Testo della BOM, identico byte per byte a quello esportato da Creo (UTF-8 con
|
|
|
|
|
/// BOM, CRLF - vedi GeneraBom per la scrittura su file).</summary>
|
2026-09-21 16:14:00 +02:00
|
|
|
private static string Render(string root, Func<string, List<AsmRecordReader.Membro>?> get)
|
2026-09-21 15:41:14 +02:00
|
|
|
{
|
|
|
|
|
var pezzi = new List<string>();
|
|
|
|
|
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<string>();
|
|
|
|
|
var acc = new Dictionary<string, int>();
|
|
|
|
|
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<string> cartelle)
|
|
|
|
|
{
|
|
|
|
|
byte[] dati = File.ReadAllBytes(percorsoAsm);
|
2026-09-21 16:14:00 +02:00
|
|
|
return Render(AsmRecordReader.NomeProprio(dati), CreaFinder(cartelle));
|
2026-09-21 15:41:14 +02:00
|
|
|
}
|
|
|
|
|
}
|