12 Commits
Author SHA1 Message Date
davideandClaude Sonnet 5 e29a1bf90a Aggiungi CHANGELOG.md con la voce 0.1.0
Formato Keep a Changelog; elenca le funzionalità attuali del programma come
prima voce di storico versioni.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 14:25:30 +02:00
davideandClaude Sonnet 5 28aa16837b Aggiungi menu Nuovo estrazione/Guida, rimuovi verifica completezza assiemi
"File -> Nuova estrazione" (Ctrl+N) azzera distinta/raw/verifica per ripartire
senza ricaricare DB e cartella di lavoro; "Aiuto -> Guida" (F1) apre una sintesi
interattiva delle 5 fasi. Rimossa "Verifica completezza assiemi": bottone,
ReportCompletezzaForm e VerificaCompletezzaAssiemi non più necessari.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 14:24:57 +02:00
davide d9f16f2844 Regola in CLAUDE.md: i doc si aggiornano nello stesso commit del codice
docs/ descrive il programma com'e' adesso, non com'e' stato: e' la prima
cosa che si legge per capirlo, quindi un doc che racconta un
comportamento che non esiste piu' fa prendere decisioni sbagliate - e'
peggio di un doc mancante.

Aggiunta la regola, la tabella di quale doc ospita cosa, e la richiesta
di annotare anche il sintomo quando il cambiamento nasce da un fallimento
di Creo: i suoi messaggi non nominano ne' il file ne' la causa, e senza
quel ponte la volta dopo si riparte da zero.
2026-09-22 14:13:07 +02:00
davide db4c444473 Documenta il processo delle 5 fasi in architettura.md
Il doc elencava i file sorgente e una tabella delle fasi, ma non il
processo: cosa entra e cosa esce da ogni fase, e perche' la 4 e' piu'
complicata di "leggi i componenti e copiali". Aggiunta una sezione che lo
descrive come pipeline, con in chiaro le tre liste che la fase 4 raccoglie
da ogni .asm - distinta, soppressi, riferimenti esterni - e per ognuna se
si ricorre dentro e se un codice mancante va segnalato.

Allineati alla stessa modifica i punti rimasti indietro: la riga della
fase 4 nella tabella, la sezione sulla lettura dei .asm (che citava solo i
soppressi), e l'elenco dei file di test, fermo a quattro su sette.
2026-09-22 14:12:27 +02:00
davideandClaude Opus 5 13f6de6402 Copia in raw anche i modelli citati per riferimento esterno
Terzo caso di "[-4] Dati di input non completi" in Creo Distributed
Batch, dopo i soppressi e le copie ambigue del commit precedente: falliva
9255728001, e solo lui fra i sei assiemi del batch.

Il file in raw/ era identico bit per bit a quello sulla share, quindi non
era il modello: mancava il contorno. Il suo sottoassieme _200 e' legato
ad altri due (9255708017, 9255708018) da assembly feature e datum
copiati, e monta una vite V6-12--_-_U5933_C il cui generico uni5933.prt
non era in raw/. Nessuno dei sei file compariva in "members" ne' fra i
piazzamenti: i due lettori esistenti non potevano vederli.

Quei modelli stanno in una tabella a parte del .asm - array \xf2\xf7\x05
… \xf3 con elementi \xe3\xe2<tipo><nome>\0 - che ora si legge con
EstraiCodiciRiferimentiEsterni, riusando la normalizzazione di #AF<n> e
istanza<generico> gia' fatta per i membri. A differenza dei soppressi si
ricorre dentro: Creo carica il modello citato con tutto il suo albero, e
infatti 9255700182 arriva solo per quella via.

La tabella contiene anche nomi di feature (MIRROR3, CHAMFER1) che dal
record non si distinguono dai modelli. Non e' un problema: come per i
soppressi si cerca ogni nome nell'indice e si ignora quello che non c'e'.
Per lo stesso motivo questi codici restano fuori dai "non trovati", e la
coda di scansione porta ora un flag che tiene fuori anche i rami
raggiunti per riferimento esterno - la lista dei componenti mancanti
continua a parlare solo della distinta.

Documentato in docs/formato-asm-record.md §7.5.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 14:09:48 +02:00
davideandClaude Opus 5 54f267b508 Copia in raw i file che servono a Creo, non solo quelli della distinta
Creo Distributed Batch falliva su 8 dei 22 assiemi con "[-4] Dati di
input non completi", un messaggio che non nomina il file mancante. Erano
due cause distinte, entrambe emerse aprendo gli assiemi da raw/ e
cercando nell'albero il componente con l'icona rossa.

Copia sbagliata del componente. Lo stesso codice esiste spesso in piu'
cartelle della share con pari versione e contenuto diverso (13% dei
codici): non sono revisioni, sono istantanee parallele lasciate da studi,
allestimenti fiera e cartelle temporanee. La vista dell'indice rompeva il
pareggio in modo arbitrario, e poteva vincere una copia obsoleta -
nascondendo del tutto quella buona. Creo poi caricava un modello diverso
da quello atteso, non risolveva i vincoli e non apriva l'assieme. Il
.asm non dice quale copia usa (ne' percorso, ne' UOBJ_ID del figlio, ne'
revisione attesa), quindi si replica la regola di Creo: vince la copia
nella stessa cartella dell'assieme che la referenzia. La vista ora rompe
il pareggio in modo deterministico e viene ricreata a ogni scan, cosi' un
DB esistente prende la definizione nuova senza reindicizzare.

Componenti soppressi. Spariscono da "members" ma restano citati dalla
feature che li posiziona, e il loro file serve comunque per aprire
l'assieme. Da quando la lettura usa il parser esatto - che legge solo
"members", ed e' la scelta giusta per la BOM - non venivano piu' copiati.
Ora si copiano a parte, senza entrare nel conteggio dei "non trovati":
non sono componenti della distinta, segnalarli come mancanti sarebbe un
falso allarme.

Documentate entrambe in docs/formato-asm-record.md §7.3 e §7.4, insieme
alla distinzione che le accomuna: la lista della distinta e la lista dei
file necessari a Creo non coincidono, e la seconda e' piu' lunga.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 11:52:16 +02:00
davideandClaude Opus 5 0ae2a5e7cf Elimina i falsi "componenti mancanti" dal rilevamento delle family table
Il record "model_name" veniva preso per l'identita' del file, ma sui file
reali nomina quasi sempre un altro modello - tipicamente uno dei membri del
file stesso. Il parser esatto rifiutava percio' file perfettamente
leggibili, il chiamante ripiegava sull'euristica testuale e quella
estraeva codici inesistenti, riportati poi come componenti non trovati
nell'indice.

Il segnale ora richiede la conferma del nome proprio del modello
(objname): scatta solo se il file dichiara davvero se stesso come istanza.

Verificato contro le BOM esportate da Creo per 7 assiemi reali: 6 vengono
riprodotte byte per byte (3 di queste prima non si generavano affatto), e
la settima differisce per una sola parte, presente nella revisione di un
sotto-assieme che Creo ha caricato ma assente in quella piu' recente. La
verifica di completezza passa su tutti e 7 (prima 3 risultavano
incompleti), e tutti i 351 .asm di una cartella raw/ reale si leggono col
parser esatto, senza mai ricadere sull'euristica.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 10:47:08 +02:00
davide b136e7f49b Semplifica la finestra di progresso di "Aggiorna indice"
Sostituita la barra disegnata a mano (colori custom, footer con
separatore, pulsante Annulla arrotondato) con la ProgressBar nativa di
Windows e un layout ad assoluti minimale, in stile con la barra di
progresso di "Trova componenti referenziati" (ProgressoComponentiForm).
Mantenuti conteggio file, dettaglio nuovi/aggiornati/invariati/rimossi,
stima del tempo rimanente e il pulsante Annulla, che l'altra finestra non
ha ma qui servono per uno scan lungo su tutta la share.

Aggiornati due commenti in Form1.cs/UiStile.cs che facevano riferimento
allo stile precedente di ProgressForm, ora rimosso.
2026-09-22 10:02:14 +02:00
davide cf4c2e8e29 Risolve i membri family table senza ConfigName e la scritta troncata in "Componenti referenziati"
Alcuni membri di family table non-UNI portano la notazione "<istanza><generico>"
direttamente nel campo name/family_name di members, senza un ConfigName
associato: AsmRecordReader cercava quella stringa intera (parentesi comprese)
nell'indice e non la trovava mai. Ora separa istanza e generico.

Corretta anche l'etichetta di PrtMancantiForm, che usciva dal bordo della
finestra invece di andare a capo.

Documentato il nuovo meccanismo in docs/formato-asm-record.md, e aggiunta una
nota sul segnale di rilevamento family table di assiemi: validato su un caso
reale ma non affidabile al 100% su scala (trovato un falso positivo).
2026-09-22 09:57:48 +02:00
davideandClaude Sonnet 5 9af426ca1b Rileva la family table di assiemi e chiude i punti oscuri residui del parser .asm
AsmRecordReader.LeggiMembri ora lancia InvalidDataException quando un assieme e'
esso stesso un'istanza di family table (members descriverebbe solo una
rappresentazione, non le istanze sorelle) invece di restituire un risultato
sbagliato in silenzio - il chiamante ripiega sull'euristica testuale, che risolve
gia' correttamente il caso. Aggiunti due controlli di coerenza analoghi (conteggio
grezzo vs interpretato dei record ConfigName; occorrenze multiple di "members"),
verificati senza falsi positivi su tutti i 350 .asm reali della share.

Documentazione riorganizzata per distinguere cio' che e' verificato contro una BOM
Creo di riferimento da cio' che e' verificato solo su scala (conteggi), e cio' che
resta genuinamente non risolto.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:20:56 +02:00
davideandClaude Sonnet 5 28a72c80b2 Normalizza il suffisso #AF di ConfigName; documenta il limite sulle family table di assiemi
Test end-to-end su un albero reale (9284218024b.asm, confrontato con la sua
BOM Creo di riferimento tramite scripts/CreoBom.cs) oltre gli 8 campioni usati
per il reverse engineering iniziale.

Trovato: i componenti creati da un pattern/array feature hanno un ConfigName
con suffisso "<codice>#AF<n>" (n = indice nel pattern), non un'istanza di
family table vera - la BOM di Creo lo ignora e unisce le occorrenze duplicate.
AsmRecordReader ora rimuove questo suffisso prima di usare il nome istanza.

Trovato ma non risolto: un intero ASSIEME (non solo le parti normalizzate)
puo' avere una family table, risolta da Creo con un meccanismo diverso da
ConfigName (non decodificato). AsmRecordReader non lo rileva e non lancia
eccezione in questo caso: il fallback automatico di Form1 non scatta, e il
risultato puo' essere silenziosamente incompleto. Documentato come limite
noto in formato-asm-record.md e in CLAUDE.md - nessun fix applicato finche'
il meccanismo non e' meglio compreso.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 16:27:15 +02:00
davideandClaude Sonnet 5 cce377a091 Integra il parser esatto del .asm nella GUI (fase 4 e 5)
AsmRecordReader (spostato da scripts/CreoBom.cs nel progetto principale,
unica fonte di verita' condivisa con lo script via #:project) sostituisce
AssemblyComponentScanner in Form1.RisolviAlberoComponenti (fase "Trova
componenti referenziati") e AssiemeCompleto (fase "Verifica completezza"):
legge esattamente la sezione "members" del binario invece di contare
occorrenze testuali, eliminando i falsi positivi/negativi noti dell'euristica
(riferimenti fantasma, placement senza suffisso, ecc.).

Se un file non rispetta il formato validato (InvalidDataException, coperto
solo dagli 8 campioni usati per il reverse engineering) Form1 ripiega
sull'euristica di AssemblyComponentScanner, che resta quindi come fallback
e come fonte di verita' per gli script BomViaDb.cs/ExtractCreoStrings.cs.

Aggiunti test per AsmRecordReader (fixture binarie costruite a mano: lettura
membri, risoluzione ConfigName, conteggio disallineato, sezione assente).
Aggiornata la documentazione (CLAUDE.md, docs/architettura.md,
docs/formato-asm.md, docs/formato-asm-record.md) di conseguenza.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 16:14:00 +02:00
16 changed files with 1492 additions and 485 deletions

No files matched your search

+35
View File
@@ -0,0 +1,35 @@
# Changelog
Tutte le modifiche rilevanti di questo progetto sono documentate in questo file.
Il formato segue [Keep a Changelog](https://keepachangelog.com/it/1.1.0/), e il progetto
aderisce a [Semantic Versioning](https://semver.org/lang/it/).
## [0.1.0] - 2026-09-22
### Aggiunto
- Wizard a 5 fasi (Database → Importa Distinta Base → Crea cartella raw → Trova componenti
referenziati → Verifica completezza) per preparare i file `raw/` da passare a Creo
Distributed Batch per l'esportazione STEP.
- Indicizzazione dei file CAD Pro-ENGINEER/Creo di un disco/share di rete (radice di default
`X:\`) in un DB SQLite locale (`cad_index.db`), con aggiornamento incrementale e barra di
progresso.
- Import della distinta base dall'export del gestionale (`.xls`/`.xml`) in CSV
componente;descrizione, incluso il riconoscimento della convenzione `DBP_<codice>_<timestamp>`
per aggiungere l'assieme generale come riga.
- Creazione della cartella `raw/` con copia parallela dei file CAD dei componenti trovati
nell'indice.
- Risoluzione ricorsiva dell'albero componenti dai file `.asm` (sotto-assiemi, parti,
componenti soppressi, riferimenti esterni), con parser esatto dei record binari
(`AsmRecordReader`) e fallback euristico testuale (`AssemblyComponentScanner`) per i file
fuori formato.
- Scelta automatica della copia corretta quando lo stesso codice esiste in più cartelle della
share, preferendo quella della cartella dell'assieme che la referenzia.
- Anteprima trascinabile (drag&drop) dei file `.asm` della distinta già in `raw/`, pronti per
Creo Distributed Batch.
- Menu "File → Nuova estrazione" (Ctrl+N) per ripartire con una nuova distinta senza ricaricare
database e cartella di lavoro, e "Aiuto → Guida" (F1) con una sintesi interattiva delle 5 fasi.
- Script standalone C# file-based (`scripts/`): `ExtractCreoStrings.cs`, `BomViaDb.cs`,
`CreoBom.cs`, per estrarre la distinta componenti da riga di comando.
- Pubblicazione come eseguibile standalone single-file self-contained (Windows x64).
+56 -13
View File
@@ -36,6 +36,32 @@ i test possono chiamare direttamente `Indexer.ParseFilename` e
`AssemblyComponentScanner.EstraiCodiciReferenziati` senza esporli come API `AssemblyComponentScanner.EstraiCodiciReferenziati` senza esporli come API
pubblica. pubblica.
## Documentazione: va tenuta allineata
`docs/` non è un archivio di note storiche: descrive **il programma com'è adesso**, ed è la
prima cosa che si legge per capirlo. Ogni modifica strutturale — un passo in più o in meno nel
flusso delle fasi, un nuovo modo di leggere il `.asm`, un cambio nello schema del DB o nel
criterio con cui si sceglie un file — va riflessa nei doc **nello stesso commit** del codice,
non "dopo". Un doc che descrive un comportamento che non esiste più è peggio di un doc
mancante: fa prendere decisioni sbagliate.
Dove scrivere cosa:
| Doc | Contenuto |
|---|---|
| `CLAUDE.md` (questo file) | il flusso essenziale e i punti che si sbaglierebbero senza saperli |
| [`docs/architettura.md`](docs/architettura.md) | il processo delle 5 fasi e la mappa file-per-file |
| [`docs/formato-asm-record.md`](docs/formato-asm-record.md) | parser esatto dei record `.asm`, con §7 per i casi limite |
| [`docs/formato-asm.md`](docs/formato-asm.md) | euristica testuale di fallback |
| [`docs/db-cad-index.md`](docs/db-cad-index.md) | schema e vista di `cad_index.db` |
| [`docs/copia-raw-parallela.md`](docs/copia-raw-parallela.md) | parallelizzazione della copia in `raw/` |
| [`CHANGELOG.md`](CHANGELOG.md) | storico delle versioni (formato Keep a Changelog); ogni release che cambia `<Version>` nel csproj aggiunge qui la sua voce |
Quando il cambiamento nasce da un fallimento reale di Creo, scrivere anche **il sintomo**: i
messaggi di Creo non nominano il file o la causa, e senza quel ponte la prossima volta si
riparte da zero (i §7.3/§7.4/§7.5 di `formato-asm-record.md` sono l'esempio del formato
atteso).
## Architettura ## Architettura
Il programma è tutto in `PrepStep/` (nessun altro progetto nella solution). Dettaglio Il programma è tutto in `PrepStep/` (nessun altro progetto nella solution). Dettaglio
@@ -59,13 +85,27 @@ designer visuale):
3. **Crea cartella raw** — `TrovaPercorsiCad` + `CopiaFileTrovati` (parallela, vedi 3. **Crea cartella raw** — `TrovaPercorsiCad` + `CopiaFileTrovati` (parallela, vedi
`docs/copia-raw-parallela.md`) copiano in `raw/` i file CAD dei componenti del CSV trovati `docs/copia-raw-parallela.md`) copiano in `raw/` i file CAD dei componenti del CSV trovati
nell'indice. nell'indice.
4. **Trova componenti referenziati** — `AssemblyComponentScanner` legge euristicamente i 4. **Trova componenti referenziati** — legge i `.asm` in `raw/` per risolvere ricorsivamente
`.asm` in `raw/` per risolvere ricorsivamente sotto-assiemi/parti (`RisolviAlberoComponenti`), sotto-assiemi/parti (`RisolviAlberoComponenti`), copiandoli anch'essi in `raw/`. Usa
copiandoli anch'essi in `raw/`. Euristica e falsi positivi noti in `docs/formato-asm.md`. `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`). 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"); 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 5. **Verifica completezza** — mostra i `.asm` della distinta già in `raw/`, trascinabili su
Creo per l'esportazione STEP (Distributed Batch, manuale, fuori dall'app); "Verifica Creo per l'esportazione STEP (Distributed Batch, manuale, fuori dall'app).
completezza assiemi" controlla ricorsivamente sull'indice (non su `raw/`) se tutti i
sotto-componenti di ogni assieme sono presenti. Menu in cima alla finestra: "File → Nuova estrazione" (Ctrl+N) azzera lo stato delle fasi 2-5
per ripartire con una nuova distinta, senza toccare DB/cartella di lavoro della fase 1;
"Aiuto → Guida" (F1) apre una sintesi interattiva delle 5 fasi (`GuidaForm.cs`).
## Script standalone (`scripts/`) ## Script standalone (`scripts/`)
@@ -74,10 +114,13 @@ lanciati dalla root del repo — **non** da dentro `PrepStep/`, che ha un `.cspr
`dotnet run` lì avvia la GUI vera invece dello script). Elenco e dettaglio di ciascuno in `dotnet run` lì avvia la GUI vera invece dello script). Elenco e dettaglio di ciascuno in
`docs/architettura.md`. `docs/architettura.md`.
Il pattern da preservare: `ExtractCreoStrings.cs`/`BomViaDb.cs` referenziano Il pattern da preservare: gli script referenziano `PrepStep/PrepStep.csproj` via `#:project`
`PrepStep/PrepStep.csproj` via `#:project` invece di duplicare l'euristica di invece di duplicare la logica di lettura dei `.asm`, che vive **solo** nel progetto
estrazione/placement, che vive **solo** in `PrepStep/AssemblyComponentScanner.cs` — un'unica principale — un'unica fonte di verità condivisa da GUI e script. Due famiglie distinte, non
fonte di verità condivisa da GUI e script (vedi `docs/formato-asm.md` per l'euristica e i intercambiabili:
falsi positivi noti). `CreoBom.cs` è l'eccezione deliberata: legge la struttura a record del
binario invece di contare occorrenze testuali (`docs/formato-asm-record.md`), non passa da - `ExtractCreoStrings.cs`/`BomViaDb.cs` usano `AssemblyComponentScanner` (euristica testuale,
`AssemblyComponentScanner`/`cad_index.db` e non la sostituisce nella GUI. `docs/formato-asm.md`) e risolvono i sottoassiemi contro `cad_index.db`.
- `CreoBom.cs` usa `AsmRecordReader` (parser esatto dei record binari,
`docs/formato-asm-record.md`, condiviso con la GUI in fase 4/5) e risolve i sottoassiemi
cercandoli in cartelle locali (`-p`), non contro `cad_index.db`.
+293
View File
@@ -0,0 +1,293 @@
using System.Text;
using PrepStep;
namespace PrepStep.Tests;
public class AsmRecordReaderTests
{
private static string ScriviFile(byte[] dati)
{
string percorso = Path.GetTempFileName();
File.WriteAllBytes(percorso, dati);
return percorso;
}
private static byte[] Bytes(params object[] parti)
{
using var ms = new MemoryStream();
foreach (var p in parti)
{
switch (p)
{
case byte b: ms.WriteByte(b); break;
case int i: ms.WriteByte((byte)i); break;
case string s: var buf = Encoding.Latin1.GetBytes(s); ms.Write(buf); break;
case byte[] arr: ms.Write(arr); break;
default: throw new ArgumentException($"tipo non gestito: {p.GetType()}");
}
}
return ms.ToArray();
}
// \xe3 id type name\0 family_name\0 - id/type come singolo byte (0x00-0x7f, un byte)
private static byte[] Membro(int id, int tipo, string nome, string? nomeFamiglia = null) =>
Bytes(0xe3, id, tipo, nome, 0, nomeFamiglia ?? nome, 0);
// \xe3 ConfigName\0 \xe23 <istanza>\0 <idMembro> \x82\x7b
private static byte[] ConfigName(string istanza, int idMembro) =>
Bytes(0xe3, "ConfigName", 0, 0xe2, (byte)'3', istanza, 0, idMembro, 0x82, 0x7b);
// \0objname\0 \xe2 \xe0\x01 object_type\0 <int> \xe0\x0a name\0 <nome>\0 - il nome proprio
// del modello (quello che la BOM intesta), vedi docs/formato-asm-record.md §5
private static byte[] ObjName(string nome) =>
Bytes(0, "objname", 0, 0xe2, 0xe0, 0x01, "object_type", 0, 2, 0xe0, 0x0a, "name", 0, nome, 0);
// identita' dichiarata dal file: \xe0\x01 model_t_type\0 <int> \xe0\x0a model_name\0 <nome>\0
// \xe0\x0a generic_name\0 \0 (vuoto)
private static byte[] ModelName(string nome) =>
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 <elementi> \xf3, ogni elemento
// \xe3\xe2<tipo><nome>\0 - vedi docs/formato-asm-record.md §7.5
private static byte[] Riferimenti(params string[] nomi)
{
var parti = new List<object> { 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<byte>(), "\0members\0", 0xf1, 0xf8, conteggioDichiarato, membri, "#MdlStatus");
[Fact]
public void Legge_membri_semplici_senza_family_table()
{
byte[] dati = Asm(2, Bytes(
Membro(1, 2, "9244206098"),
Membro(2, 1, "9244228558_C01")));
var membri = AsmRecordReader.LeggiMembri(dati);
Assert.Equal(2, membri.Count);
Assert.Equal((1, 2, "9244206098", "9244206098"), (membri[0].Id, membri[0].Tipo, membri[0].Nome, membri[0].Generico));
Assert.Equal((2, 1, "9244228558_C01", "9244228558_C01"), (membri[1].Id, membri[1].Tipo, membri[1].Nome, membri[1].Generico));
}
[Fact]
public void Risolve_il_nome_istanza_di_una_family_table_da_ConfigName()
{
// Il membro cita solo il generico (UNI6592); il nome che la BOM mostra
// (R8--_-_U6592) sta nel parametro ConfigName, collegato per id membro.
byte[] dati = Asm(1, Membro(1, 2, "UNI6592"), extra: ConfigName("R8--_-_U6592", idMembro: 1));
var membri = AsmRecordReader.LeggiMembri(dati);
Assert.Single(membri);
Assert.Equal("R8--_-_U6592", membri[0].Nome);
Assert.Equal("UNI6592", membri[0].Generico);
}
[Fact]
public void Rimuove_il_suffisso_array_feature_dal_nome_istanza()
{
// Regressione: caso reale 9244250136.asm -> ConfigName con suffisso "#AF<n>" per
// componenti creati da un pattern/array feature (id 45 e id 78, entrambi variante
// "_C03" con suffisso diverso "#AF0"/"#AF1"): non un'istanza di family table vera,
// la BOM di Creo mostra il codice pulito e unisce le occorrenze duplicate.
byte[] dati = Asm(2, Bytes(
Membro(45, 2, "9244250136_C03"),
Membro(78, 2, "9244250136_C03")),
extra: Bytes(
ConfigName("9244250136_C03#AF0", idMembro: 45),
ConfigName("9244250136_C03#AF1", idMembro: 78)));
var membri = AsmRecordReader.LeggiMembri(dati);
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 "<istanza><generico>" (stessa notazione di §4.1). Senza lo split, il codice
// cercato nell'indice sarebbe "V6-12--_-_U5931_GC<UNI5931>" per intero, mai trovato -
// va cercato invece il generico "UNI5931".
byte[] dati = Asm(1, Membro(1, 2, "V6-12--_-_U5931_GC<UNI5931>"));
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()
{
byte[] dati = Asm(2, Bytes(
Membro(1, 2, "UNI6592"),
Membro(2, 2, "9244206098")),
extra: ConfigName("R8--_-_U6592", idMembro: 1));
var membri = AsmRecordReader.LeggiMembri(dati);
Assert.Equal("R8--_-_U6592", membri.Single(m => m.Id == 1).Nome);
Assert.Equal("9244206098", membri.Single(m => m.Id == 2).Nome);
}
[Fact]
public void Conteggio_dichiarato_diverso_da_quello_letto_lancia_eccezione()
{
// Dichiara 2 membri ma ne fornisce solo 1: lettura disallineata, va segnalata invece
// di restituire un risultato parziale silenzioso.
byte[] dati = Asm(2, Membro(1, 2, "9244206098"));
Assert.Throws<InvalidDataException>(() => AsmRecordReader.LeggiMembri(dati));
}
[Fact]
public void Assieme_family_table_con_generic_name_lancia_eccezione()
{
// Regressione: un file che e' esso stesso un'istanza di family table (il suo nome
// proprio "2546220101" piu' il suffisso "_C01" in model_name) descrive in "members"
// solo quella rappresentazione, non le istanze sorelle. Va segnalato invece di
// restituire silenziosamente il contenuto sbagliato (docs/formato-asm-record.md §7).
byte[] dati = Asm(1, Membro(1, 2, "9244206098"),
extra: Bytes(ObjName("2546220101"), ModelName("2546220101_C01")));
Assert.Throws<InvalidDataException>(() => AsmRecordReader.LeggiMembri(dati));
}
[Fact]
public void Model_name_di_un_altro_modello_non_lancia_eccezione()
{
// Regressione: sui file reali della share il record "model_name" nomina spesso un
// modello diverso dal file (tipicamente un suo membro), non l'identita' del file. Il
// nome proprio (objname) non conferma, quindi non e' il sintomo della family table:
// "members" e' corretto e va usato. Verificato contro BOM Creo reali - senza questo
// controllo il file cadeva sull'euristica testuale, che produceva codici inesistenti
// segnalati poi come "componenti mancanti".
byte[] dati = Asm(1, Membro(1, 2, "9044207999A_C01"),
extra: Bytes(ObjName("9244207999A"), ModelName("9044207999A_C01")));
var membri = AsmRecordReader.LeggiMembri(dati);
Assert.Equal("9044207999A_C01", membri.Single().Nome);
}
[Fact]
public void Model_name_senza_nome_proprio_non_lancia_eccezione()
{
// Stesso caso del test sopra ma senza record "objname" (formato osservato su tutti i
// file reali che facevano scattare il falso positivo): senza conferma del nome proprio
// il record non e' un'identita', quindi "members" va letto normalmente.
byte[] dati = Asm(1, Membro(1, 2, "9244206098"), extra: ModelName("2546220101_C01"));
Assert.Single(AsmRecordReader.LeggiMembri(dati));
}
[Fact]
public void Generic_name_senza_suffisso_istanza_non_lancia_eccezione()
{
// Regressione: su ~7% dei file reali della share compare un record "model_name" +
// "generic_name" (vuoto) che NON e' il sintomo della family table di assiemi - il
// valore non ha il suffisso "_C0n" delle istanze (es. e' un codice qualunque non
// correlato al nome del file). In quei casi "members" e' letto correttamente e va
// usato normalmente, non bloccato per un falso positivo.
byte[] dati = Asm(1, Membro(1, 2, "9244206098"),
extra: Bytes(ObjName("9244230117"), ModelName("9244230117")));
var membri = AsmRecordReader.LeggiMembri(dati);
Assert.Single(membri);
}
[Fact]
public void Componente_soppresso_non_e_fra_i_membri_ma_e_segnalato_a_parte()
{
// Un componente soppresso sparisce da "members" (il conteggio scende) ma resta citato
// dalla feature che lo posiziona. Non va nella BOM, però senza il suo file Creo non
// apre l'assieme, quindi va copiato lo stesso.
byte[] dati = Asm(1, Membro(1, 2, "9244206098"),
extra: Bytes("Componente ID 52 (9230100043.PRT)", 0));
Assert.Equal(new[] { "9244206098" }, AsmRecordReader.LeggiMembri(dati).Select(m => m.Nome));
string percorso = ScriviFile(dati);
try
{
Assert.Equal(new[] { "9230100043" }, AsmRecordReader.EstraiCodiciSoppressi(percorso));
Assert.DoesNotContain("9230100043", AsmRecordReader.EstraiCodiciReferenziati(percorso));
}
finally { File.Delete(percorso); }
}
[Fact]
public void Componente_gia_fra_i_membri_non_e_segnalato_come_soppresso()
{
byte[] dati = Asm(1, Membro(1, 2, "9244206098"),
extra: Bytes("Componente ID 7 (9244206098.PRT)", 0));
string percorso = ScriviFile(dati);
try
{
Assert.Empty(AsmRecordReader.EstraiCodiciSoppressi(percorso));
}
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<UNI5933>")));
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()
{
byte[] dati = Encoding.Latin1.GetBytes("un file qualunque senza la sezione attesa");
Assert.Throws<InvalidDataException>(() => AsmRecordReader.LeggiMembri(dati));
}
[Fact]
public void EstraiCodiciReferenziati_restituisce_i_generici_distinti()
{
byte[] dati = Asm(3, Bytes(
Membro(1, 2, "9144210327"),
Membro(2, 2, "9144210327"), // stesso codice montato due volte: un solo codice distinto
Membro(3, 1, "9244238563_D100")));
string percorso = ScriviFile(dati);
try
{
var codici = AsmRecordReader.EstraiCodiciReferenziati(percorso);
Assert.Equal(new[] { "9144210327", "9244238563_D100" }, codici.OrderBy(c => c));
}
finally { File.Delete(percorso); }
}
[Fact]
public void File_inesistente_lancia_eccezione_IO() =>
Assert.Throws<DirectoryNotFoundException>(() =>
AsmRecordReader.EstraiCodiciReferenziati(@"C:\non\esiste\pippo.asm.1"));
}
+273
View File
@@ -0,0 +1,273 @@
using System.Text;
using System.Text.RegularExpressions;
namespace PrepStep;
/// <summary>Lettura esatta della struttura a record del binario .asm Creo (sezione
/// "members" + parametro "ConfigName" per le family table), ricavata per reverse
/// engineering e verificata confrontando l'output con le BOM esportate da Creo - vedi
/// docs/formato-asm-record.md per il dettaglio byte-per-byte.
///
/// Alternativa esatta all'euristica testuale di <see cref="AssemblyComponentScanner"/>
/// (docs/formato-asm.md): quella conta le occorrenze dei codici nel testo, questa legge
/// la struttura dei record. Copre solo gli 8 campioni usati per il reverse engineering: su
/// un file che non rispetta il formato atteso lancia <see cref="InvalidDataException"/>
/// invece di restituire un risultato parziale silenzioso - il chiamante (<see
/// cref="Form1"/>) ripiega sull'euristica in quel caso, non essendoci garanzia di
/// copertura completa su tutte le versioni Creo mai incontrate sulla share.
///
/// Unica fonte di verità condivisa da GUI e script (stesso pattern di
/// AssemblyComponentScanner): scripts/CreoBom.cs referenzia questa classe via
/// "#:project" per la parte di lettura, aggiungendo solo la resa in formato BOM.</summary>
public static class AsmRecordReader
{
public sealed record Membro(int Id, int Tipo, string Nome, string Generico);
// interi big-endian, lunghezza nei bit alti del primo byte (0xe0+ e' riservato ai
// marcatori di struttura): 0x00-0x7f 1 byte, 0x80-0xbf 2 byte, 0xc0-0xdf 3.
private const string Int = @"([\x00-\x7f]|[\x80-\xbf][\x00-\xff]|[\xc0-\xdf][\x00-\xff][\x00-\xff])";
// \xe3 id type name\0 family_name\0
private static readonly Regex MemberRegex = new(
@"\xe3" + Int + Int + @"([\x20-\x7e]+)\x00([\x20-\x7e]+)\x00", RegexOptions.Compiled);
// parametro ConfigName: nome istanza family table + id del membro a cui si applica.
private static readonly Regex ConfigNameRegex = new(
@"\xe3ConfigName\x00\xe23(?:\xf7[\x00-\xff])?([\x20-\x7e]+)\x00" + Int + @"\x82\x7b",
RegexOptions.Compiled);
// nome proprio del modello (quello che la BOM stampa in intestazione).
private static readonly Regex ObjNameRegex = new(
@"\x00objname\x00\xe2\xe0\x01object_type\x00[\x00-\x7f]\xe0\x0aname\x00([^\x00]+)\x00",
RegexOptions.Compiled);
// Un componente creato da un pattern/array feature ha un ConfigName con un tag interno
// "<codice>#AF<n>" (Array Feature, n = indice nel pattern), non un'istanza di family
// table vera: la BOM reale di Creo lo ignora e mostra il codice pulito - osservato su
// 9244250136.asm (4 dei suoi 8 membri, due dei quali stesso codice "_C03" con "#AF0" e
// "#AF1" diversi, che la BOM unisce in una sola riga con quantita' 2). Senza questa
// normalizzazione ogni istanza del pattern risulterebbe un componente diverso.
private static readonly Regex SuffissoArrayFeatureRegex = new(@"#AF\d+$", RegexOptions.Compiled);
// record di identita' del modello, seguito da "generic_name" (vuoto): compare su una
// minoranza di assiemi (~7% dei campioni sulla share) per motivi diversi e nella
// maggioranza dei casi "members" resta corretto - la sola presenza di "generic_name" NON
// e' un segnale affidabile (verificato: su 144 file che lo contengono, solo 25 hanno
// anche il sintomo sotto; per gli altri 119 "members" letto normalmente coincide con
// l'euristica testuale, quindi e' un contesto diverso, non lo stesso problema).
private static readonly Regex OwnModelIdentityRegex = new(
@"\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 "<istanza><generico>"
// (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<UNI5931>" - 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);
// Suffisso "_C0n" delle istanze di family table (stessa convenzione di
// SuffissoArrayFeatureRegex/ConfigName sopra). Usato insieme a OwnModelIdentityRegex per
// riconoscere un file che e' esso stesso un'istanza, il cui "members" descriverebbe solo
// quella rappresentazione invece delle istanze sorelle (docs/formato-asm-record.md §7).
// Da solo NON basta: su file reali quel record nomina spesso un altro modello (un membro
// del file, o un codice imparentato), non il file stesso - per quello serve la conferma
// del nome proprio (objname).
private static readonly Regex SuffissoIstanzaFamilyTableRegex = new(@"_C\d{2,}$", RegexOptions.Compiled);
// primo elemento dell'array "members": porta le chiavi per esteso.
private static readonly Regex FirstMemberRegex = new(
@"\x01id\x00" + Int + @"\xe0\x01type\x00" + Int
+ @"\xe0\x0aname\x00([^\x00]+)\x00\xe0\x0afamily_name\x00([^\x00]+)\x00",
RegexOptions.Compiled);
private static int ContaOccorrenze(string testo, string marcatore)
{
int conteggio = 0;
for (int i = testo.IndexOf(marcatore, StringComparison.Ordinal); i >= 0; i = testo.IndexOf(marcatore, i + 1, StringComparison.Ordinal))
conteggio++;
return conteggio;
}
private static int AsInt(string bytes)
{
int mask = bytes.Length switch { 1 => 0x7F, 2 => 0x3F, 3 => 0x1F, _ => throw new ArgumentException("lunghezza intero non valida") };
int valore = bytes[0] & mask;
for (int i = 1; i < bytes.Length; i++)
valore = (valore << 8) | bytes[i];
return valore;
}
/// <summary>Nome proprio del modello (quello che Creo stampa in intestazione BOM),
/// dalla prima occorrenza del record "objname".</summary>
public static string NomeProprio(byte[] dati)
{
var m = ObjNameRegex.Match(Encoding.Latin1.GetString(dati));
if (!m.Success)
throw new InvalidDataException("record 'objname' non trovato");
return m.Groups[1].Value;
}
/// <summary>Componenti diretti di un assieme, in ordine di montaggio. "Nome" e' il nome
/// che mostrerebbe la BOM (istanza di family table se presente), "Generico" e' il file
/// realmente su disco (coincide con "Nome" per i componenti che non sono istanze).
/// Lancia <see cref="InvalidDataException"/> se il file non ha una sezione "members" o
/// se il conteggio dichiarato non coincide con i record letti (lettura disallineata:
/// il formato del file non e' quello coperto dal reverse engineering).</summary>
public static List<Membro> LeggiMembri(byte[] dati)
{
string testo = Encoding.Latin1.GetString(dati);
var configNameMatches = ConfigNameRegex.Matches(testo);
int configNameGrezzi = ContaOccorrenze(testo, (char)0xe3 + "ConfigName\0");
if (configNameMatches.Count != configNameGrezzi)
throw new InvalidDataException(
$"trovati {configNameGrezzi} record 'ConfigName' ma solo {configNameMatches.Count} " +
"interpretati: formato del parametro diverso da quello atteso (vedi §4, costante " +
"\\x82\\x7b), lettura non affidabile");
var istanze = new Dictionary<int, string>();
foreach (Match m in configNameMatches)
istanze[AsInt(m.Groups[2].Value)] = SuffissoArrayFeatureRegex.Replace(m.Groups[1].Value, "");
const string chiaveMembers = "\0members\0";
int start = testo.IndexOf(chiaveMembers, StringComparison.Ordinal);
if (start < 0)
throw new InvalidDataException("sezione 'members' assente");
if (testo.IndexOf(chiaveMembers, start + chiaveMembers.Length, StringComparison.Ordinal) >= 0)
throw new InvalidDataException(
"trovate piu' occorrenze di 'members': probabile rappresentazione semplificata, " +
"mai verificata (vedi §7) - non e' chiaro quale sezione descriva la BOM");
start += chiaveMembers.Length;
int conteggio = dati[start + 2]; // \xf1 \xf8 <conteggio>
int end = testo.IndexOf("#MdlStatus", start, StringComparison.Ordinal);
string region = testo.Substring(start, (end > 0 ? end : testo.Length) - start);
var grezzi = new List<(int Id, int Tipo, string Nome)>();
// il primo record e' esteso: ricava i campi dalle chiavi, i successivi sono posizionali
var primo = FirstMemberRegex.Match(region);
if (primo.Success)
{
grezzi.Add((AsInt(primo.Groups[1].Value), AsInt(primo.Groups[2].Value), primo.Groups[3].Value));
region = region.Substring(primo.Index + primo.Length);
}
foreach (Match m in MemberRegex.Matches(region))
{
grezzi.Add((AsInt(m.Groups[1].Value), AsInt(m.Groups[2].Value), m.Groups[3].Value));
if (grezzi.Count == conteggio)
break;
}
if (grezzi.Count != conteggio)
throw new InvalidDataException($"attesi {conteggio} membri, trovati {grezzi.Count}: lettura disallineata");
// Il record "model_name" e' l'identita' del file solo se il nome proprio del modello
// (objname, §5) lo conferma: da solo nomina spesso un altro modello - un membro del
// file, o un modello imparentato - e in quei casi "members" e' gia' corretto. Vedi
// docs/formato-asm-record.md §7.
var identita = OwnModelIdentityRegex.Match(testo);
var nomeProprio = ObjNameRegex.Match(testo);
if (identita.Success && nomeProprio.Success
&& SuffissoIstanzaFamilyTableRegex.IsMatch(identita.Groups[2].Value)
&& string.Equals(SuffissoIstanzaFamilyTableRegex.Replace(identita.Groups[2].Value, ""),
nomeProprio.Groups[1].Value, StringComparison.OrdinalIgnoreCase))
throw new InvalidDataException(
$"assieme con family table: il file e' esso stesso l'istanza '{identita.Groups[2].Value}', " +
"'members' descriverebbe solo questa rappresentazione, non le istanze sorelle - vedi " +
"docs/formato-asm-record.md §7");
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();
}
/// <summary>Codici dei componenti diretti (il generico su disco, non il nome istanza),
/// distinti, nell'ordine in cui compaiono. Equivalente esatto di
/// <see cref="AssemblyComponentScanner.EstraiCodiciReferenziati"/> per i file coperti dal
/// formato: stesso contratto di ritorno (un insieme di codici da cercare nell'indice),
/// lettura strutturale invece di euristica testuale.</summary>
public static HashSet<string> EstraiCodiciReferenziati(string percorsoAsm)
{
var membri = LeggiMembri(File.ReadAllBytes(percorsoAsm));
return new HashSet<string>(membri.Select(m => m.Generico), StringComparer.OrdinalIgnoreCase);
}
/// <summary>Codici che compaiono come piazzamento ma non fra i membri: sono i componenti
/// **soppressi**, che spariscono da "members" senza lasciare traccia (§7.1) ma restano
/// citati dalla feature che li posiziona.
///
/// Non fanno parte della BOM - Creo non li elenca - ma il loro file serve comunque per
/// **aprire** l'assieme: senza, il caricamento fallisce con "dati di input non completi".
/// Sono quindi due domande diverse: cosa compare nella distinta (<see
/// cref="EstraiCodiciReferenziati"/>) e cosa serve avere su disco (questo metodo in
/// aggiunta), e la seconda lista e' piu' lunga della prima.</summary>
public static HashSet<string> EstraiCodiciSoppressi(string percorsoAsm)
{
byte[] dati = File.ReadAllBytes(percorsoAsm);
var membri = LeggiMembri(dati);
var noti = new HashSet<string>(
membri.Select(m => m.Generico).Concat(membri.Select(m => m.Nome)), StringComparer.OrdinalIgnoreCase);
var soppressi = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (Match m in PlacementRegex.Matches(Encoding.Latin1.GetString(dati)))
if (!noti.Contains(m.Groups[1].Value))
soppressi.Add(m.Groups[1].Value);
return soppressi;
}
/// <summary>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 <see cref="EstraiCodiciReferenziati"/> ("cosa c'è in distinta") e <see
/// cref="EstraiCodiciSoppressi"/>: 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.</summary>
public static HashSet<string> EstraiCodiciRiferimentiEsterni(string percorsoAsm)
{
string testo = Encoding.Latin1.GetString(File.ReadAllBytes(percorsoAsm));
var codici = new HashSet<string>(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<tipo><nome>\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
// rumore (sulla share esistono davvero modelli chiamati "1.prt", "2.prt"...).
private static readonly Regex PlacementRegex = new(
@"Component(?:e)? [Ii][Dd] \d+ \(([A-Za-z0-9_\-]{3,})\.(?:PRT|ASM)\)", RegexOptions.Compiled);
}
+224 -128
View File
@@ -10,7 +10,6 @@ public partial class Form1 : Form
private readonly Button _btnCreaRaw = new(); private readonly Button _btnCreaRaw = new();
private readonly Button _btnTrovaPrt = new(); private readonly Button _btnTrovaPrt = new();
private readonly Button _btnVerifica = new(); private readonly Button _btnVerifica = new();
private readonly Button _btnCompletezza = new();
private readonly ListView _lstAnteprimaAsm = new(); private readonly ListView _lstAnteprimaAsm = new();
private readonly Button _btnPulisci = new(); private readonly Button _btnPulisci = new();
private readonly Label _lblStatus = new(); private readonly Label _lblStatus = new();
@@ -22,6 +21,8 @@ public partial class Form1 : Form
private readonly DataGridView _gridRaw = new(); private readonly DataGridView _gridRaw = new();
private readonly Label _lblImportStatus = new(); private readonly Label _lblImportStatus = new();
private readonly ToolTip _tipImportStatus = new(); private readonly ToolTip _tipImportStatus = new();
private readonly MenuStrip _menu = new();
private readonly ToolStripMenuItem _mnuNuovaEstrazione = new("Nuova estrazione");
// Wizard: una fase alla volta occupa tutta la finestra, si avanza solo quando quella // Wizard: una fase alla volta occupa tutta la finestra, si avanza solo quando quella
// corrente è "pronta" (vedi AvantiConsentito). NomiFasi/_panelFasi sono paralleli per indice. // corrente è "pronta" (vedi AvantiConsentito). NomiFasi/_panelFasi sono paralleli per indice.
@@ -91,9 +92,9 @@ public partial class Form1 : Form
_lblFase.Font = new Font(Font, FontStyle.Bold); _lblFase.Font = new Font(Font, FontStyle.Bold);
headerPanel.Controls.Add(_lblFase); headerPanel.Controls.Add(_lblFase);
// Barra di navigazione come footer separato dal contenuto (stesso pattern di // Barra di navigazione come footer separato dal contenuto: linea sottile in cima e
// ProgressForm): linea sottile in cima + sfondo bianco, invece dei bottoni appoggiati // sfondo bianco, invece dei bottoni appoggiati direttamente sul contenuto/sulla barra
// direttamente sul contenuto/sulla barra di stato senza alcuna divisione visiva. // di stato senza alcuna divisione visiva.
// Padding destro = 16, lo stesso usato dai pannelli di ogni fase (Padding(16)): prima // Padding destro = 16, lo stesso usato dai pannelli di ogni fase (Padding(16)): prima
// era 12, quindi "Avanti" sporgeva 4px oltre il margine destro del contenuto sopra. // era 12, quindi "Avanti" sporgeva 4px oltre il margine destro del contenuto sopra.
var navContainer = new Panel { Dock = DockStyle.Bottom, Height = 64, BackColor = Color.White }; var navContainer = new Panel { Dock = DockStyle.Bottom, Height = 64, BackColor = Color.White };
@@ -146,10 +147,25 @@ public partial class Form1 : Form
// Ordine dei Dock.Bottom: l'ultimo aggiunto è il più esterno (il vero bordo inferiore // Ordine dei Dock.Bottom: l'ultimo aggiunto è il più esterno (il vero bordo inferiore
// della finestra). La barra di stato va tra contenuto e azioni; i bottoni Avanti/Indietro // della finestra). La barra di stato va tra contenuto e azioni; i bottoni Avanti/Indietro
// restano il footer più esterno, come in ogni wizard/dialog standard di Windows. // restano il footer più esterno, come in ogni wizard/dialog standard di Windows.
var mnuFile = new ToolStripMenuItem("File");
_mnuNuovaEstrazione.ShortcutKeys = Keys.Control | Keys.N;
_mnuNuovaEstrazione.Click += (_, _) => NuovaEstrazione();
mnuFile.DropDownItems.Add(_mnuNuovaEstrazione);
var mnuAiuto = new ToolStripMenuItem("Aiuto");
var mnuGuida = new ToolStripMenuItem("Guida") { ShortcutKeys = Keys.F1 };
mnuGuida.Click += (_, _) => new GuidaForm().ShowDialog(this);
mnuAiuto.DropDownItems.Add(mnuGuida);
_menu.Items.Add(mnuFile);
_menu.Items.Add(mnuAiuto);
MainMenuStrip = _menu;
Controls.Add(_faseContainer); Controls.Add(_faseContainer);
Controls.Add(statusPanel); Controls.Add(statusPanel);
Controls.Add(navContainer); Controls.Add(navContainer);
Controls.Add(headerPanel); Controls.Add(headerPanel);
Controls.Add(_menu);
DragEnter += (_, e) => DragEnter += (_, e) =>
e.Effect = e.Data?.GetDataPresent(DataFormats.FileDrop) == true e.Effect = e.Data?.GetDataPresent(DataFormats.FileDrop) == true
@@ -236,6 +252,30 @@ public partial class Form1 : Form
private void AggiornaStatoAvanti() => _btnAvanti.Enabled = AvantiConsentito(_faseAttuale); private void AggiornaStatoAvanti() => _btnAvanti.Enabled = AvantiConsentito(_faseAttuale);
/// <summary>Azzera distinta/raw/verifica (fasi 2-5) per ripartire con una nuova distinta;
/// DB indicizzato e cartella di lavoro (fase 1) restano quelli già caricati, non serve
/// rifare lo scan del disco per ogni estrazione.</summary>
private void NuovaEstrazione()
{
if (_dbPath is null || _cartellaLavoro is null)
{
MessageBox.Show(this, "Carica prima il database in fase 1.", "Nuova estrazione",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
_percorsoInputCorrente = null;
_righeCorrenti = null;
_ultimaCartellaRaw = null;
_ultimaVerifica = null;
_gridImport.Rows.Clear();
_gridRaw.Rows.Clear();
_lstAnteprimaAsm.Items.Clear();
ImpostaStatoImport("", Color.Black);
VaiAllaFase(1);
}
private Panel BuildFaseDatabase() private Panel BuildFaseDatabase()
{ {
var contenuto = new TableLayoutPanel var contenuto = new TableLayoutPanel
@@ -516,27 +556,6 @@ public partial class Form1 : Form
}; };
top.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); top.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
var lblIstruzioniCompletezza = new Label
{
Dock = DockStyle.Top,
AutoSize = true,
ForeColor = SystemColors.GrayText,
Margin = new Padding(0, 0, 0, 8),
Text = "Per ogni assieme della distinta, verifica se tutti i suoi componenti "
+ "(ricorsivamente fino ai PRT) sono nell'indice. Nota: la scansione dei .asm "
+ "è euristica, quindi qualche falso 'incompleto' è possibile.",
};
top.Controls.Add(lblIstruzioniCompletezza);
// Primario: è l'azione principale di questa fase, non una scelta secondaria — prima
// era uno stile grigio che si confondeva con lo sfondo.
_btnCompletezza.Text = "Verifica completezza assiemi";
_btnCompletezza.Margin = new Padding(0, 0, 0, 12);
UiStile.Primario(_btnCompletezza);
UiStile.Arrotonda(_btnCompletezza);
_btnCompletezza.Click += (_, _) => VerificaCompletezzaAssiemi();
top.Controls.Add(_btnCompletezza);
var lblAnteprimaAsm = new Label var lblAnteprimaAsm = new Label
{ {
AutoSize = true, AutoSize = true,
@@ -743,6 +762,128 @@ public partial class Form1 : Form
return codiciNoti; return codiciNoti;
} }
/// <summary>Tutti i file prt/asm dell'indice raggruppati per codice, tenendo per ogni
/// cartella la sola versione più alta. A differenza di <see cref="CaricaCodiciNoti"/> non
/// sceglie un vincitore fra le cartelle: lo stesso codice può esistere in più punti della
/// share (copie in cartelle di studio, di lavoro, temporanee) con contenuto diverso, e
/// quale sia quello giusto dipende da chi lo referenzia - vedi
/// <see cref="ScegliPerCartella"/>.</summary>
private static Dictionary<string, List<(string Tipo, string Percorso, int Versione, string Data)>> CaricaCandidati(string dbPath)
{
var candidati = new Dictionary<string, List<(string Tipo, string Percorso, int Versione, string Data)>>(StringComparer.OrdinalIgnoreCase);
using var connection = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly");
connection.Open();
using var command = connection.CreateCommand();
command.CommandText = "SELECT codice, tipo_file, percorso_completo, versione, data_modifica FROM files "
+ "WHERE LOWER(tipo_file) IN ('prt', 'asm')";
using var reader = command.ExecuteReader();
while (reader.Read())
{
string codice = reader.GetString(0);
string tipo = reader.GetString(1).ToLowerInvariant();
string percorso = reader.GetString(2);
int versione = reader.GetInt32(3);
var lista = candidati.TryGetValue(codice, out var l) ? l : candidati[codice] = new();
string cartella = Path.GetDirectoryName(percorso) ?? "";
int esistente = lista.FindIndex(x => x.Tipo == tipo
&& string.Equals(Path.GetDirectoryName(x.Percorso), cartella, StringComparison.OrdinalIgnoreCase));
if (esistente < 0)
lista.Add((tipo, percorso, versione, reader.GetString(4)));
else if (versione > lista[esistente].Versione)
lista[esistente] = (tipo, percorso, versione, reader.GetString(4));
}
return candidati;
}
/// <summary>Fra più copie dello stesso codice sulla share sceglie quella che userebbe Creo:
/// prima cerca nella cartella dell'assieme che la referenzia (Creo risolve i componenti
/// partendo da lì), poi ripiega sulla versione più alta e, a parità, sulla più recente.
/// Senza questo criterio si finisce per copiare in "raw" una copia omonima ma di un altro
/// modello - Creo la carica e i vincoli di accoppiamento non si risolvono, quindi l'assieme
/// non si apre affatto.</summary>
private static List<(string Tipo, string Percorso)> ScegliPerCartella(
List<(string Tipo, string Percorso, int Versione, string Data)> candidati, string? cartellaPadre)
{
return candidati
.GroupBy(c => c.Tipo)
.Select(perTipo =>
{
var scelto = perTipo
.OrderByDescending(c => cartellaPadre is not null
&& string.Equals(Path.GetDirectoryName(c.Percorso), cartellaPadre, StringComparison.OrdinalIgnoreCase))
.ThenByDescending(c => c.Versione)
.ThenByDescending(c => c.Data)
.First();
return (scelto.Tipo, scelto.Percorso);
})
.ToList();
}
/// <summary>Codici referenziati direttamente in un assieme: prova prima il parser esatto
/// dei record binari (<see cref="AsmRecordReader"/>, vedi docs/formato-asm-record.md), che
/// non ha i falsi positivi/negativi noti dell'euristica testuale (docs/formato-asm.md); se
/// il file non rispetta il formato validato (<see cref="InvalidDataException"/> - capita su
/// .asm di versioni Creo mai riscontrate nei campioni usati per il reverse engineering)
/// ripiega su <see cref="AssemblyComponentScanner"/>, che ha copertura più ampia anche se
/// meno precisa. Un'eventuale IOException (file irraggiungibile) non viene intercettata qui:
/// resta responsabilità del chiamante, come prima di questo cambiamento.</summary>
private static IEnumerable<string> EstraiCodiciReferenziati(string percorsoAsm, ICollection<string> codiciNoti)
{
try
{
return AsmRecordReader.EstraiCodiciReferenziati(percorsoAsm);
}
catch (InvalidDataException)
{
return AssemblyComponentScanner.EstraiCodiciReferenziati(percorsoAsm, codiciNoti);
}
}
/// <summary>Componenti soppressi di un assieme (<see cref="AsmRecordReader.EstraiCodiciSoppressi"/>):
/// non sono nella distinta, ma senza il loro file Creo non apre l'assieme. Vuoto per i file
/// che il parser esatto non sa leggere: lì si usa l'euristica testuale, che i piazzamenti li
/// riconosce già per conto suo (<see cref="AssemblyComponentScanner"/>).</summary>
private static IEnumerable<string> CodiciSoppressi(string percorsoAsm)
{
try
{
return AsmRecordReader.EstraiCodiciSoppressi(percorsoAsm);
}
catch (InvalidDataException)
{
return Array.Empty<string>();
}
}
/// <summary>Cartella della share da cui proviene un file già copiato in "raw": fra le copie
/// omonime indicizzate è quella di pari dimensione. Serve per gli assiemi di partenza, che
/// arrivano dalla fase precedente senza portarsi dietro l'origine. Null se non si riesce a
/// stabilirla (il file non è nell'indice, o la share non risponde): in quel caso la scelta
/// del componente ricade sul criterio generale.</summary>
private static string? CartellaDiOrigine(string fileInRaw,
Dictionary<string, List<(string Tipo, string Percorso, int Versione, string Data)>> candidati)
{
string nome = Path.GetFileName(fileInRaw);
if (!candidati.TryGetValue(nome.Split('.')[0], out var perCodice))
return null;
try
{
long dimensione = new FileInfo(fileInRaw).Length;
foreach (var c in perCodice.Where(c => string.Equals(Path.GetFileName(c.Percorso), nome, StringComparison.OrdinalIgnoreCase)))
{
var info = new FileInfo(c.Percorso);
if (info.Exists && info.Length == dimensione)
return Path.GetDirectoryName(c.Percorso);
}
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
// share non raggiungibile: si prosegue col criterio generale
}
return null;
}
/// <summary>Risolve ricorsivamente l'albero dei componenti a partire dai file .asm indicati /// <summary>Risolve ricorsivamente l'albero dei componenti a partire dai file .asm indicati
/// (già copiati in "raw"): per ogni assieme legge i codici referenziati al suo interno, li /// (già copiati in "raw"): per ogni assieme legge i codici referenziati al suo interno, li
/// cerca nell'indice, copia in "raw" quelli trovati e, se sono a loro volta assiemi, li /// cerca nell'indice, copia in "raw" quelli trovati e, se sono a loro volta assiemi, li
@@ -751,42 +892,93 @@ public partial class Form1 : Form
private (List<(string Codice, string Tipo, string Percorso)> Trovati, List<string> NonTrovati) RisolviAlberoComponenti( private (List<(string Codice, string Tipo, string Percorso)> Trovati, List<string> NonTrovati) RisolviAlberoComponenti(
IEnumerable<string> asmIniziali, IProgress<(int Fatti, int Totale)>? progress = null) IEnumerable<string> asmIniziali, IProgress<(int Fatti, int Totale)>? progress = null)
{ {
var daScansionare = new Queue<string>(asmIniziali); // Ogni assieme viaggia con la cartella della share da cui proviene: e' li' che Creo
var codiciIniziali = daScansionare.Select(f => Path.GetFileName(f).Split('.')[0]); // cerca per primi i suoi componenti, quindi e' li' che li cerchiamo anche noi.
// "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<string>( var codiciVisti = new HashSet<string>(
codiciIniziali.Concat(codiciIniziali.Select(AssemblyComponentScanner.CodiceRadice)), codiciIniziali.Concat(codiciIniziali.Select(AssemblyComponentScanner.CodiceRadice)),
StringComparer.OrdinalIgnoreCase); StringComparer.OrdinalIgnoreCase);
var codiciSoppressiVisti = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var codiciEsterniVisti = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var trovati = new List<(string Codice, string Tipo, string Percorso)>(); var trovati = new List<(string Codice, string Tipo, string Percorso)>();
var codiciNonTrovati = new List<string>(); var codiciNonTrovati = new List<string>();
if (daScansionare.Count == 0 || _dbPath is null) if (daScansionare.Count == 0 || _dbPath is null)
return (trovati, codiciNonTrovati); return (trovati, codiciNonTrovati);
var codiciNoti = CaricaCodiciNoti(_dbPath); var codiciNoti = CaricaCodiciNoti(_dbPath);
var candidati = CaricaCandidati(_dbPath);
int processati = 0; int processati = 0;
while (daScansionare.Count > 0) while (daScansionare.Count > 0)
{ {
var asm = daScansionare.Dequeue(); var (asm, cartellaOrigine, esterno) = daScansionare.Dequeue();
cartellaOrigine ??= CartellaDiOrigine(asm, candidati);
processati++; processati++;
foreach (var codice in AssemblyComponentScanner.EstraiCodiciReferenziati(asm, codiciNoti.Keys)) foreach (var codice in EstraiCodiciReferenziati(asm, codiciNoti.Keys))
{ {
if (!codiciVisti.Add(codice)) if (!codiciVisti.Add(codice))
continue; continue;
if (!codiciNoti.TryGetValue(codice, out var componenti)) if (!candidati.TryGetValue(codice, out var perCodice))
{ {
if (!esterno)
codiciNonTrovati.Add(codice); codiciNonTrovati.Add(codice);
continue; continue;
} }
foreach (var (tipo, percorso) in componenti) foreach (var (tipo, percorso) in ScegliPerCartella(perCodice, cartellaOrigine))
{ {
CopiaFileTrovati(new[] { percorso }, _ultimaCartellaRaw!); CopiaFileTrovati(new[] { percorso }, _ultimaCartellaRaw!);
trovati.Add((codice, tipo, percorso)); trovati.Add((codice, tipo, percorso));
if (tipo == "asm") if (tipo == "asm")
{ {
daScansionare.Enqueue(Path.Combine(_ultimaCartellaRaw!, Path.GetFileName(percorso))); daScansionare.Enqueue((Path.Combine(_ultimaCartellaRaw!, Path.GetFileName(percorso)),
Path.GetDirectoryName(percorso), esterno));
}
}
}
// I componenti soppressi non compaiono nella BOM, ma Creo ne vuole comunque il file
// per aprire l'assieme: si copiano senza entrare nel conteggio dei "non trovati"
// (non essendo componenti della distinta, segnalarli come mancanti sarebbe un falso
// allarme) e senza ricorrervi dentro.
foreach (var codice in CodiciSoppressi(asm))
{
if (!codiciSoppressiVisti.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));
}
}
// 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));
} }
} }
} }
@@ -834,102 +1026,6 @@ public partial class Form1 : Form
dialog.ShowDialog(this); dialog.ShowDialog(this);
} }
/// <summary>Per ogni assieme della distinta, verifica ricorsivamente (leggendo i .asm dai
/// percorsi originali sulla share) se tutti i componenti fino ai PRT sono nell'indice.
/// I risultati di ogni file/codice vengono in cache e condivisi tra gli assiemi controllati
/// nella stessa passata, per non rileggere più volte gli stessi sotto-componenti.</summary>
private void VerificaCompletezzaAssiemi()
{
if (_righeCorrenti is null)
return;
if (_dbPath is null)
{
ImpostaStatoImport("Nessun database caricato: impossibile verificare la completezza.", Color.DarkRed);
return;
}
var codiciNoti = CaricaCodiciNoti(_dbPath);
var cacheCodiciFile = new Dictionary<string, HashSet<string>>();
List<(string Tipo, string Percorso)> CercaComponenti(string codice) =>
codiciNoti.TryGetValue(codice, out var trovati) ? trovati : new List<(string, string)>();
bool AssiemeCompleto(string percorsoAsmIniziale)
{
var daVisitare = new Queue<string>();
daVisitare.Enqueue(percorsoAsmIniziale);
var fileVisitati = new HashSet<string> { percorsoAsmIniziale };
var codiceIniziale = Path.GetFileName(percorsoAsmIniziale).Split('.')[0];
var codiciVisti = new HashSet<string>
{
codiceIniziale,
AssemblyComponentScanner.CodiceRadice(codiceIniziale),
};
while (daVisitare.Count > 0)
{
var file = daVisitare.Dequeue();
if (!cacheCodiciFile.TryGetValue(file, out var codiciReferenziati))
{
try
{
codiciReferenziati = AssemblyComponentScanner.EstraiCodiciReferenziati(file, codiciNoti.Keys);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
return false; // file irraggiungibile (share offline) -> non verificabile, per prudenza "incompleto"
}
cacheCodiciFile[file] = codiciReferenziati;
}
foreach (var codice in codiciReferenziati)
{
if (!codiciVisti.Add(codice))
continue;
var componenti = CercaComponenti(codice);
if (componenti.Count == 0)
return false;
foreach (var (tipo, percorso) in componenti)
{
if (tipo == "asm" && fileVisitati.Add(percorso))
{
daVisitare.Enqueue(percorso);
}
}
}
}
return true;
}
// L'assieme generale è già una riga di _righeCorrenti (vedi DistintaBaseImporter), quindi
// la sua completezza viene verificata insieme a quella di tutti gli altri componenti.
var righeReport = new List<(string Componente, string Descrizione, bool? Completo)>();
foreach (var (componente, descrizione) in _righeCorrenti)
{
var componenti = CercaComponenti(componente);
string? percorsoAsm = componenti.Where(c => c.Tipo == "asm").Select(c => c.Percorso).FirstOrDefault();
bool? completo = componenti.Count == 0 ? null
: percorsoAsm is null ? true // solo prt trovato: nessuna sotto-struttura da verificare
: AssiemeCompleto(percorsoAsm);
righeReport.Add((componente, descrizione, completo));
}
int completi = righeReport.Count(r => r.Completo == true);
int incompleti = righeReport.Count(r => r.Completo == false);
int nonTrovati = righeReport.Count(r => r.Completo is null);
ImpostaStatoImport(
$"Completezza assiemi: {completi} completi, {incompleti} incompleti, "
+ $"{nonTrovati} non trovati nell'indice.",
Color.DarkGreen);
using var dialogCompletezza = new ReportCompletezzaForm(righeReport);
dialogCompletezza.ShowDialog(this);
}
/// <summary>Copia i file trovati nella cartella "raw" indicata, creandola se necessario. /// <summary>Copia i file trovati nella cartella "raw" indicata, creandola se necessario.
/// Ritorna il numero di file copiati con successo. Parallelizzata (grado limitato): il /// Ritorna il numero di file copiati con successo. Parallelizzata (grado limitato): il
/// collo di bottiglia è la latenza di rete verso la share, non la CPU, quindi poche copie /// collo di bottiglia è la latenza di rete verso la share, non la CPU, quindi poche copie
+91
View File
@@ -0,0 +1,91 @@
namespace PrepStep;
/// <summary>Guida interattiva richiamabile da "Aiuto ▸ Guida" (F1): sintesi delle 5 fasi del
/// wizard, per chi apre l'app senza aver letto i docs. Il dettaglio completo (formato .asm,
/// schema DB, ecc.) resta solo in docs/architettura.md e affini, per non duplicarlo qui.</summary>
public class GuidaForm : Form
{
private static readonly (string Titolo, string Testo)[] Sezioni =
{
("Panoramica", """
Cevolani StepPrep prepara i file CAD Pro-ENGINEER/Creo da passare a Creo
Distributed Batch per l'esportazione STEP: indicizza i file di una share di
rete e guida attraverso 5 fasi, una alla volta, con "Avanti"/"Indietro".
"File ▸ Nuova estrazione" (Ctrl+N) azzera distinta/raw/verifica per
ripartire con una nuova distinta base, senza dover ricaricare il database.
"""),
("1. Database", """
Carica o aggiorna l'indice "cad_index.db" a partire dalle cartelle di primo
livello del disco/share (radice di default X:\). Qui si sceglie anche la
cartella di lavoro, dove finiscono CSV, "raw" e "step".
"""),
("2. Importa Distinta Base", """
Converte il file della distinta (trascinabile nella finestra) in un CSV
componente;descrizione nella cartella di lavoro. Se il file segue la
convenzione del gestionale, aggiunge anche l'assieme generale come riga.
"""),
("3. Crea cartella raw", """
Cerca nell'indice i file CAD dei componenti del CSV e li copia nella
sottocartella "raw" della cartella di lavoro.
"""),
("4. Trova componenti referenziati", """
Legge i file .asm già in "raw" per risolvere ricorsivamente sotto-assiemi e
parti citate, copiando anche quelli in "raw": componenti soppressi (non in
distinta ma necessari per aprire l'assieme) e riferimenti esterni (assembly
feature, datum copiati) inclusi.
"""),
("5. Verifica completezza", """
Mostra i file .asm della distinta già in "raw", trascinabili su Creo per
l'esportazione STEP (passo manuale, fuori da quest'app). "Verifica
completezza assiemi" controlla che tutti i sotto-componenti risultino
presenti nell'indice.
"""),
};
public GuidaForm()
{
Text = "Guida";
ClientSize = new Size(640, 420);
MinimumSize = new Size(480, 320);
StartPosition = FormStartPosition.CenterParent;
Font = new Font("Segoe UI", 9.5f);
var lista = new ListBox
{
Dock = DockStyle.Left,
Width = 200,
IntegralHeight = false,
BorderStyle = BorderStyle.None,
};
foreach (var (titolo, _) in Sezioni)
lista.Items.Add(titolo);
var testo = new RichTextBox
{
Dock = DockStyle.Fill,
ReadOnly = true,
BorderStyle = BorderStyle.None,
BackColor = Color.White,
Font = new Font(Font.FontFamily, 10f),
};
lista.SelectedIndexChanged += (_, _) =>
{
if (lista.SelectedIndex >= 0)
testo.Text = Sezioni[lista.SelectedIndex].Testo.Trim();
};
var btnChiudi = new Button { Text = "Chiudi", Dock = DockStyle.Bottom, Height = 34 };
UiStile.Secondario(btnChiudi);
btnChiudi.Click += (_, _) => Close();
var contenuto = new Panel { Dock = DockStyle.Fill, Padding = new Padding(16) };
contenuto.Controls.Add(testo);
Controls.Add(contenuto);
Controls.Add(lista);
Controls.Add(btnChiudi);
lista.SelectedIndex = 0;
}
}
+11 -2
View File
@@ -40,13 +40,22 @@ public static class Indexer
CREATE INDEX IF NOT EXISTS idx_files_tipo ON files(tipo_file); CREATE INDEX IF NOT EXISTS idx_files_tipo ON files(tipo_file);
CREATE INDEX IF NOT EXISTS idx_files_cartella ON files(cartella_radice); CREATE INDEX IF NOT EXISTS idx_files_cartella ON files(cartella_radice);
CREATE VIEW IF NOT EXISTS files_ultima_versione AS -- DROP+CREATE invece di "IF NOT EXISTS": la vista e' ricalcolata a ogni avvio di scan,
-- cosi' un DB gia' esistente prende la definizione corrente senza reindicizzare.
DROP VIEW IF EXISTS files_ultima_versione;
CREATE VIEW files_ultima_versione AS
SELECT percorso_completo, percorso_relativo, nome_file, cartella_radice, famiglia, SELECT percorso_completo, percorso_relativo, nome_file, cartella_radice, famiglia,
codice, tipo_file, versione, dimensione_byte, data_modifica, scansionato_il codice, tipo_file, versione, dimensione_byte, data_modifica, scansionato_il
FROM ( FROM (
SELECT f.*, ROW_NUMBER() OVER ( SELECT f.*, ROW_NUMBER() OVER (
PARTITION BY cartella_radice, codice, tipo_file PARTITION BY cartella_radice, codice, tipo_file
ORDER BY versione DESC -- A parita' di versione (stesso codice in due sottocartelle della stessa
-- radice, es. una copia vecchia in una cartella di lavoro) vince il file
-- modificato piu' di recente: senza questo criterio il pareggio e' rotto in
-- modo arbitrario e puo' vincere la copia obsoleta, nascondendo del tutto
-- quella buona. Il file copiato in "raw" sarebbe allora un modello diverso da
-- quello che l'assieme si aspetta, e Creo non riesce ad aprirlo.
ORDER BY versione DESC, data_modifica DESC, percorso_completo
) AS rn ) AS rn
FROM files f FROM files f
) WHERE rn = 1; ) WHERE rn = 1;
+20 -85
View File
@@ -2,23 +2,15 @@ namespace PrepStep;
public sealed class ProgressForm : Form public sealed class ProgressForm : Form
{ {
private readonly Label _lblTitolo = new();
private readonly Label _lblCartella = new(); private readonly Label _lblCartella = new();
private readonly Panel _barraTraccia = new(); private readonly ProgressBar _bar = new();
private readonly Panel _barraRiempimento = new();
private readonly Label _lblPercentuale = new();
private readonly Label _lblConteggio = new(); private readonly Label _lblConteggio = new();
private readonly Label _lblDettaglio = new(); private readonly Label _lblDettaglio = new();
private readonly Label _lblEta = new(); private readonly Label _lblEta = new();
private readonly Button _btnAnnulla = new(); private readonly Button _btnAnnulla = new();
private readonly Panel _separatore = new();
private readonly Panel _piePagina = new();
private readonly ToolTip _tipCartella = new(); private readonly ToolTip _tipCartella = new();
private const int Margine = 24; private const int Margine = 16;
// SystemColors.GrayText (~3.9:1 su bianco) è sotto la soglia WCAG AA 4.5:1 per testo
// normale: usiamo un grigio più scuro, leggibile ma comunque secondario.
private static readonly Color GrigioTesto = Color.FromArgb(90, 90, 90);
public event EventHandler? Annullato; public event EventHandler? Annullato;
@@ -30,84 +22,42 @@ public sealed class ProgressForm : Form
MinimizeBox = false; MinimizeBox = false;
MaximizeBox = false; MaximizeBox = false;
ControlBox = false; ControlBox = false;
BackColor = Color.White; ClientSize = new Size(420, 190);
ClientSize = new Size(480, 270);
_lblTitolo.Text = "Aggiornamento indice in corso...";
_lblTitolo.Location = new Point(Margine, 22);
_lblTitolo.AutoSize = true;
_lblTitolo.Font = new Font(Font.FontFamily, 11f, FontStyle.Bold);
_lblCartella.Text = "Preparazione..."; _lblCartella.Text = "Preparazione...";
_lblCartella.Location = new Point(Margine, 52); _lblCartella.Location = new Point(Margine, Margine);
// AutoSize=false + AutoEllipsis: un percorso lungo (share di rete) va troncato con "..." // AutoSize=false + AutoEllipsis: un percorso lungo (share di rete) va troncato con "..."
// invece di sforare il bordo del dialog, che è FixedDialog e non si può allargare. // invece di sforare il bordo del dialog, che è FixedDialog e non si può allargare. Il
// Il testo completo resta disponibile nel tooltip (stesso pattern di Form1._lblImportStatus). // testo completo resta disponibile nel tooltip.
_lblCartella.AutoSize = false; _lblCartella.AutoSize = false;
_lblCartella.AutoEllipsis = true; _lblCartella.AutoEllipsis = true;
_lblCartella.Size = new Size(ClientSize.Width - Margine * 2, 20); _lblCartella.Size = new Size(ClientSize.Width - Margine * 2, 20);
_lblCartella.ForeColor = GrigioTesto;
// Barra piatta disegnata a mano (traccia grigia + riempimento) invece della ProgressBar _bar.Location = new Point(Margine, 40);
// nativa: quella di Windows è verde e in stile "a blocchi", stona con il blu del resto _bar.Size = new Size(ClientSize.Width - Margine * 2, 24);
// dell'app (UiStile.Primario). Larghezza = margine destro reale meno lo spazio per _bar.Minimum = 0;
// "100%" in grassetto, così barra+percentuale rispettano lo stesso margine destro del _bar.Maximum = 100;
// resto del contenuto (prima finivano ~25px prima, sbilanciate rispetto al margine
// sinistro di 24px).
_barraTraccia.Location = new Point(Margine, 90);
_barraTraccia.Size = new Size(ClientSize.Width - Margine * 2 - 46, 10);
_barraTraccia.BackColor = Color.FromArgb(230, 230, 230);
_barraRiempimento.Location = Point.Empty;
_barraRiempimento.Size = new Size(0, _barraTraccia.Height);
_barraRiempimento.BackColor = UiStile.ColorePrimario;
_barraTraccia.Controls.Add(_barraRiempimento);
_lblPercentuale.AutoSize = true; _lblConteggio.Location = new Point(Margine, 72);
_lblPercentuale.Font = new Font(Font, FontStyle.Bold);
_lblPercentuale.TextAlign = ContentAlignment.MiddleRight;
_lblConteggio.Location = new Point(Margine, 122);
_lblConteggio.AutoSize = true; _lblConteggio.AutoSize = true;
_lblDettaglio.Location = new Point(Margine, 146); _lblDettaglio.Location = new Point(Margine, 96);
_lblDettaglio.AutoSize = true; _lblDettaglio.AutoSize = true;
_lblDettaglio.ForeColor = GrigioTesto;
_lblEta.Location = new Point(Margine, 174); _lblEta.Location = new Point(Margine, 120);
_lblEta.AutoSize = true; _lblEta.AutoSize = true;
// Footer separato dal contenuto (linea + sfondo leggermente diverso) invece del
// bottone appiccicato al bordo inferiore del dialog: ancorato in basso, quindi la
// spaziatura sopra/sotto resta corretta anche se la ClientSize cambia. Altezza footer
// 60px e ClientSize 270 scelti per lasciare sotto l'ultima riga (eta, Bottom≈189) lo
// stesso margine (~20px) usato sopra il titolo, invece dei ~7px sbilanciati di prima.
_separatore.BackColor = Color.FromArgb(230, 230, 230);
_separatore.Size = new Size(ClientSize.Width, 1);
_separatore.Location = new Point(0, ClientSize.Height - 60);
_separatore.Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom;
_piePagina.BackColor = Color.FromArgb(247, 247, 248);
_piePagina.Size = new Size(ClientSize.Width, 59);
_piePagina.Location = new Point(0, ClientSize.Height - 59);
_piePagina.Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom;
_btnAnnulla.Text = "Annulla"; _btnAnnulla.Text = "Annulla";
UiStile.Secondario(_btnAnnulla); _btnAnnulla.Size = new Size(90, 28);
UiStile.Arrotonda(_btnAnnulla);
_btnAnnulla.Click += (_, e) => Annullato?.Invoke(this, e); _btnAnnulla.Click += (_, e) => Annullato?.Invoke(this, e);
_piePagina.Controls.Add(_btnAnnulla);
Controls.AddRange(new Control[] Controls.AddRange(new Control[]
{ {
_lblTitolo, _lblCartella, _barraTraccia, _lblPercentuale, _lblCartella, _bar, _lblConteggio, _lblDettaglio, _lblEta, _btnAnnulla
_lblConteggio, _lblDettaglio, _lblEta, _separatore, _piePagina
}); });
// Posizionato dopo l'aggiunta ai Controls: con AutoSize il Width vero si conosce _btnAnnulla.Location = new Point(
// solo a layout avvenuto, non subito dopo la creazione del bottone. ClientSize.Width - Margine - _btnAnnulla.Width, ClientSize.Height - Margine - _btnAnnulla.Height);
_btnAnnulla.Location = new Point(_piePagina.Width - Margine - _btnAnnulla.Width,
(_piePagina.Height - _btnAnnulla.Height) / 2);
_btnAnnulla.Anchor = AnchorStyles.Right | AnchorStyles.Top;
} }
public void Aggiorna(ScanProgress p) public void Aggiorna(ScanProgress p)
@@ -115,24 +65,9 @@ public sealed class ProgressForm : Form
_lblCartella.Text = $"Scansione: {p.Cartella}"; _lblCartella.Text = $"Scansione: {p.Cartella}";
_tipCartella.SetToolTip(_lblCartella, _lblCartella.Text); _tipCartella.SetToolTip(_lblCartella, _lblCartella.Text);
if (p.PrevistiTotali > 0) _bar.Value = p.PrevistiTotali > 0
{ ? (int)Math.Clamp(100.0 * p.FattiTotali / p.PrevistiTotali, 0, 100)
double frazione = Math.Min(1.0, (double)p.FattiTotali / p.PrevistiTotali); : 0;
_barraRiempimento.Width = (int)(frazione * _barraTraccia.Width);
_lblPercentuale.Text = $"{frazione * 100:0}%";
}
else
{
// ponytail: prima del conteggio totale (poco probabile: richiede la scansione
// precedente salvata nel DB) la barra resta vuota invece di un'animazione
// indeterminata — se serve, aggiungere un Timer che pulsa _barraRiempimento.
_barraRiempimento.Width = 0;
_lblPercentuale.Text = "";
}
// Riallineato al margine destro reale ogni volta: AutoSize aggiorna Width non appena
// Text cambia, quindi la Location va ricalcolata qui (non solo una volta nel costruttore).
_lblPercentuale.Location = new Point(
ClientSize.Width - Margine - _lblPercentuale.Width, _barraTraccia.Top - 3);
_lblConteggio.Text = p.PrevistiTotali > 0 _lblConteggio.Text = p.PrevistiTotali > 0
? $"{p.FattiTotali:N0} / ~{p.PrevistiTotali:N0} file" ? $"{p.FattiTotali:N0} / ~{p.PrevistiTotali:N0} file"
+3
View File
@@ -20,6 +20,7 @@ public class PrtMancantiForm : Form
{ {
Dock = DockStyle.Top, Dock = DockStyle.Top,
AutoSize = true, AutoSize = true,
MaximumSize = new Size(ClientSize.Width - 24, 0),
Padding = new Padding(12, 8, 12, 8), Padding = new Padding(12, 8, 12, 8),
Text = $"Copiati in \"{cartellaRaw}\": {numPrt} PRT e {numAsm} sotto-assiemi trovati nell'indice." Text = $"Copiati in \"{cartellaRaw}\": {numPrt} PRT e {numAsm} sotto-assiemi trovati nell'indice."
+ (codiciNonTrovati.Count > 0 + (codiciNonTrovati.Count > 0
@@ -70,5 +71,7 @@ public class PrtMancantiForm : Form
Controls.Add(grid); Controls.Add(grid);
Controls.Add(lblInfo); Controls.Add(lblInfo);
Controls.Add(btnApriCartella); Controls.Add(btnApriCartella);
Resize += (_, _) => lblInfo.MaximumSize = new Size(ClientSize.Width - 24, 0);
} }
} }
-61
View File
@@ -1,61 +0,0 @@
namespace PrepStep;
/// <summary>Mostra, per ogni assieme della distinta base, se tutti i suoi componenti
/// (ricorsivamente fino ai PRT) sono presenti nell'indice (v) o mancano (x).</summary>
public class ReportCompletezzaForm : Form
{
public ReportCompletezzaForm(List<(string Componente, string Descrizione, bool? Completo)> righe)
{
int completi = righe.Count(r => r.Completo == true);
int incompleti = righe.Count(r => r.Completo == false);
int nonTrovati = righe.Count(r => r.Completo is null);
Text = "Completezza assiemi";
ClientSize = new Size(720, 440);
StartPosition = FormStartPosition.CenterParent;
Font = new Font("Segoe UI", 9.5f);
var lblInfo = new Label
{
Dock = DockStyle.Top,
AutoSize = true,
Padding = new Padding(12, 8, 12, 8),
Text = $"{completi} completi, {incompleti} incompleti, {nonTrovati} non trovati nell'indice "
+ $"su {righe.Count} componenti. Nota: la scansione dei .asm è euristica, qualche "
+ "falso 'incompleto' è possibile.",
};
var grid = new DataGridView
{
Dock = DockStyle.Fill,
ReadOnly = true,
AllowUserToAddRows = false,
AllowUserToDeleteRows = false,
RowHeadersVisible = false,
SelectionMode = DataGridViewSelectionMode.FullRowSelect,
AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill,
};
grid.Columns.Add("componente", "Componente");
grid.Columns.Add("descrizione", "Descrizione");
grid.Columns.Add("completo", "Completo");
grid.Columns["componente"]!.FillWeight = 20;
grid.Columns["descrizione"]!.FillWeight = 55;
grid.Columns["completo"]!.FillWeight = 25;
foreach (var (componente, descrizione, completo) in righe.OrderBy(r => r.Completo).ThenBy(r => r.Componente))
{
string testo = completo switch { true => "✓", false => "✗", null => "?" };
int i = grid.Rows.Add(componente, descrizione, testo);
grid.Rows[i].DefaultCellStyle.ForeColor = completo switch
{
true => Color.DarkGreen,
false => Color.DarkRed,
null => Color.Gray,
};
grid.Rows[i].Cells["completo"].Style.Alignment = DataGridViewContentAlignment.MiddleCenter;
}
Controls.Add(grid);
Controls.Add(lblInfo);
}
}
+2 -1
View File
@@ -8,7 +8,8 @@ namespace PrepStep;
internal static class UiStile internal static class UiStile
{ {
/// <summary>Blu del bottone primario, esposto qui per essere riusato anche da elementi /// <summary>Blu del bottone primario, esposto qui per essere riusato anche da elementi
/// non-bottone (es. la barra di avanzamento di ProgressForm) invece di duplicare l'RGB.</summary> /// non-bottone (es. il colore di selezione delle griglie in Form1) invece di duplicare
/// l'RGB.</summary>
public static readonly Color ColorePrimario = Color.FromArgb(0, 99, 177); public static readonly Color ColorePrimario = Color.FromArgb(0, 99, 177);
// AutoSize+Padding invece di una Size fissa in pixel: il bottone si dimensiona sempre // AutoSize+Padding invece di una Size fissa in pixel: il bottone si dimensiona sempre
+116 -19
View File
@@ -4,7 +4,8 @@ Panoramica di tutti i file sorgente e di come collaborano. `PrepStep` è il nome
tecnico del progetto (cartella, namespace, csproj, eseguibile `PrepStep.exe`); tecnico del progetto (cartella, namespace, csproj, eseguibile `PrepStep.exe`);
**Cevolani StepPrep** è il nome mostrato all'utente finale (titolo finestra, icona, **Cevolani StepPrep** è il nome mostrato all'utente finale (titolo finestra, icona,
versione — vedi `<Version>`/`<Product>` in `PrepStep.csproj`). Per il "come si usa" vedi il `README.md` in root; per il versione — vedi `<Version>`/`<Product>` in `PrepStep.csproj`). Per il "come si usa" vedi il `README.md` in root; per il
formato binario `.asm` vedi `docs/formato-asm.md`; per la parallelizzazione della formato binario `.asm` vedi `docs/formato-asm.md` (euristica testuale) e
`docs/formato-asm-record.md` (parser esatto dei record); per la parallelizzazione della
copia in `raw/` vedi `docs/copia-raw-parallela.md`. copia in `raw/` vedi `docs/copia-raw-parallela.md`.
## Vista d'insieme ## Vista d'insieme
@@ -24,6 +25,56 @@ Distinta (.xls/.xml) → CSV (componente;descrizione) → raw/ (CAD copiati
drag&drop .asm su Creo → Distributed Batch → step/ (esportazione manuale) drag&drop .asm su Creo → Distributed Batch → step/ (esportazione manuale)
``` ```
## Il processo, dall'inizio alla fine
Le 5 fasi sono una pipeline: ognuna consuma quello che ha prodotto la precedente, e il
bottone "Avanti" si sblocca solo quando il risultato c'è (`AvantiConsentito`). Sotto, cosa
entra e cosa esce da ciascuna — il dettaglio dei metodi è nella tabella della sezione
successiva.
**1. Indice della share → `cad_index.db`.** `Indexer.ScanAsync` cammina tutte le cartelle di
primo livello sotto `X:\`, riconosce i nomi Pro-E `<codice>.<tipo>.<versione>` e fa un upsert
incrementale (confronto su percorso+dimensione+data), così i giri successivi costano solo il
delta. Qui si sceglie anche la **cartella di lavoro**, che da qui in poi ospita tutto il resto.
**2. Distinta del gestionale → CSV.** `DistintaBaseImporter.ConvertiInCsv` riduce l'export a
due colonne `componente;descrizione`. Se il nome del file segue la convenzione
`DBP_<codice>_<timestamp>` aggiunge l'assieme generale come riga in più: da quel momento è un
componente come tutti gli altri, nessun caso speciale a valle.
**3. Componenti del CSV → `raw/`.** Per ogni codice del CSV `TrovaPercorsiCad` cerca
nell'indice l'ultima versione (vista `files_ultima_versione`) e `CopiaFileTrovati` la copia in
`raw/` in parallelo. Finisce qui il livello "distinta": in `raw/` ci sono i componenti
elencati dal gestionale, non ancora quello che serve ad aprirli.
**4. Chiusura ricorsiva dell'albero → `raw/` completa.** È il cuore del programma, ed è
l'unica fase che deve ragionare come Creo invece che come il gestionale.
`RisolviAlberoComponenti` tiene una coda di `.asm` da leggere, partendo da quelli già in
`raw/`, e per ognuno raccoglie **tre liste diverse**, che non vanno confuse (vedi
`docs/formato-asm-record.md` §7.3 e §7.5):
| Lista | Da dove | Ricorsione | Se non è nell'indice |
|---|---|---|---|
| Componenti della distinta | sezione `members` (`EstraiCodiciReferenziati`) | sì | va nei "non trovati" |
| Componenti **soppressi** | stringhe di piazzamento (`EstraiCodiciSoppressi`) | no — Creo non carica i figli di un soppresso | ignorato: non è un componente, segnalarlo sarebbe un falso allarme |
| **Riferimenti esterni** | tabella dei modelli citati (`EstraiCodiciRiferimentiEsterni`) | sì — Creo carica il modello citato con tutto il suo albero | ignorato, stesso motivo |
Ogni codice trovato viene copiato in `raw/` scegliendo **quale** copia con `ScegliPerCartella`
(§7.4: lo stesso codice esiste in più cartelle della share nel 13% dei casi, e vince quella
nella stessa cartella dell'assieme che lo referenzia); se è un `.asm` torna in coda e il giro
ricomincia, finché restano solo foglie. I rami raggiunti per riferimento esterno viaggiano con
un flag `Esterno` che li tiene fuori dall'elenco dei "non trovati": quell'elenco continua a
parlare solo della distinta, che è quello che l'utente si aspetta di leggerci.
Il motivo per cui questa fase è più complicata di come sembra: **la lista dei componenti e la
lista dei file necessari al caricamento non coincidono**, e la seconda è più lunga. Quando si
sbaglia, Creo Distributed Batch non dice quale file manca — dice solo `[-4] Dati di input non
completi`.
**5. Verifica e consegna a Creo.** `AggiornaAnteprimaAsm` mostra i `.asm` della distinta già
in `raw/` come icone trascinabili. L'esportazione STEP vera è manuale e fuori dall'app: si
trascinano i `.asm` su Creo Distributed Batch, che scrive in `step/`.
## Entry point ## Entry point
- **Program.cs** — `Main()` standard WinForms: `Application.Run(new Form1())`. Nessuna logica. - **Program.cs** — `Main()` standard WinForms: `Application.Run(new Form1())`. Nessuna logica.
@@ -44,13 +95,18 @@ Distinta (.xls/.xml) → CSV (componente;descrizione) → raw/ (CAD copiati
| 1 | Database | "Aggiorna indice" → `Indexer.ScanAsync`; "Cambia cartella..." → `ScegliCartellaLavoro` | Crea/aggiorna `cad_index.db` scansionando la share; sceglie/mostra la cartella di lavoro | | 1 | Database | "Aggiorna indice" → `Indexer.ScanAsync`; "Cambia cartella..." → `ScegliCartellaLavoro` | Crea/aggiorna `cad_index.db` scansionando la share; sceglie/mostra la cartella di lavoro |
| 2 | Importa Distinta Base | selezione file → `ImportaDistinta` → `DistintaBaseImporter.ConvertiInCsv` | Xls/Xml gestionale → CSV componente;descrizione, scritto nella cartella di lavoro | | 2 | Importa Distinta Base | selezione file → `ImportaDistinta` → `DistintaBaseImporter.ConvertiInCsv` | Xls/Xml gestionale → CSV componente;descrizione, scritto nella cartella di lavoro |
| 3 | Crea cartella raw | "Crea cartella raw" → `CreaCartellaRaw` | `TrovaPercorsiCad` + `CopiaFileTrovati` (parallela) → `raw/` dentro la cartella di lavoro | | 3 | Crea cartella raw | "Crea cartella raw" → `CreaCartellaRaw` | `TrovaPercorsiCad` + `CopiaFileTrovati` (parallela) → `raw/` dentro la cartella di lavoro |
| 4 | Trova componenti referenziati | "Trova componenti referenziati" → `TrovaPrtReferenziati` → `RisolviAlberoComponenti` | Legge i `.asm` in `raw/` con `AssemblyComponentScanner`, risolve ricorsivamente sotto-assiemi/parti nell'indice, li copia anch'essi in `raw/` | | 4 | Trova componenti referenziati | "Trova componenti referenziati" → `TrovaPrtReferenziati` → `RisolviAlberoComponenti` | Legge i `.asm` in `raw/`, risolve ricorsivamente sotto-assiemi/parti nell'indice e li copia anch'essi in `raw/`, insieme ai componenti soppressi e ai modelli citati per riferimento esterno che servono a Creo per aprire l'assieme |
| 5 | Verifica completezza | "Verifica completezza assiemi" → `VerificaCompletezzaAssiemi` | Completezza ricorsiva sui percorsi in indice. All'ingresso nella fase, `AggiornaAnteprimaAsm` mostra le icone trascinabili dei `.asm` della distinta già in `raw/`, da trascinare su Creo per l'esportazione STEP (Distributed Batch) | | 5 | Verifica completezza | (ingresso in fase) → `AggiornaAnteprimaAsm` | Mostra le icone trascinabili dei `.asm` della distinta già in `raw/`, da trascinare su Creo per l'esportazione STEP (Distributed Batch) |
Stato condiviso tra le fasi (campi privati di `Form1`): `_dbPath`, `_cartellaLavoro`, Stato condiviso tra le fasi (campi privati di `Form1`): `_dbPath`, `_cartellaLavoro`,
`_percorsoInputCorrente`, `_righeCorrenti` (righe del CSV), `_ultimaVerifica` (cache percorsi `_percorsoInputCorrente`, `_righeCorrenti` (righe del CSV), `_ultimaVerifica` (cache percorsi
CAD), `_ultimaCartellaRaw`. CAD), `_ultimaCartellaRaw`.
Menu (`MenuStrip`, in cima alla finestra): "File → Nuova estrazione" (Ctrl+N) azzera lo
stato delle fasi 2-5 (`NuovaEstrazione`) per ripartire con una nuova distinta senza
ricaricare il database/la cartella di lavoro (fase 1, che resta invariata); "Aiuto → Guida"
(F1) apre `GuidaForm`, sintesi interattiva delle 5 fasi per chi non ha letto questi docs.
Altre responsabilità in Form1.cs: Altre responsabilità in Form1.cs:
- `TrovaPercorsiCad` / `CaricaCodiciNoti` — query sul DB (`files_ultima_versione`), la - `TrovaPercorsiCad` / `CaricaCodiciNoti` — query sul DB (`files_ultima_versione`), la
seconda carica tutti i codici prt/asm noti in un colpo solo (usata sia per risolvere i seconda carica tutti i codici prt/asm noti in un colpo solo (usata sia per risolvere i
@@ -79,14 +135,52 @@ Distinta (.xls/.xml) → CSV (componente;descrizione) → raw/ (CAD copiati
`ROW_NUMBER() OVER (...)`, non self-join+`GROUP BY` per evitare tempi quasi quadratici su `ROW_NUMBER() OVER (...)`, non self-join+`GROUP BY` per evitare tempi quasi quadratici su
~200k righe). ~200k righe).
## Scansione binaria dei `.asm` ## Lettura dei componenti referenziati in un `.asm`
`Form1.EstraiCodiciReferenziati` (usata da `RisolviAlberoComponenti`, fase 4) prova prima
`AsmRecordReader.EstraiCodiciReferenziati` e ripiega su
`AssemblyComponentScanner.EstraiCodiciReferenziati` solo se il primo lancia
`InvalidDataException` (file che non rispetta il formato validato dal reverse engineering).
- **AsmRecordReader.cs** — legge esattamente la sezione `members` del binario `.asm` (id/tipo/
nome dei componenti diretti, con risoluzione del nome istanza di family table dal parametro
`ConfigName`): nessuna euristica, struttura a record. Lancia `InvalidDataException` se il
file non ha una sezione `members` o se il conteggio dichiarato non torna. Espone anche le
due liste "non di distinta": `EstraiCodiciSoppressi` (che passa da `members` per sapere
quali codici sono già noti, quindi può lanciare la stessa eccezione — `Form1.CodiciSoppressi`
la intercetta e restituisce vuoto) e `EstraiCodiciRiferimentiEsterni` (scansione a sé, non
lancia). Dettagli in `docs/formato-asm-record.md`. Condivisa con `scripts/CreoBom.cs` via
`#:project`.
- **AssemblyComponentScanner.cs** — estrae euristicamente i codici componente referenziati - **AssemblyComponentScanner.cs** — estrae euristicamente i codici componente referenziati
dentro un `.asm` (formato per lo più binario, codici come stringhe ASCII leggibili al suo contando le occorrenze di stringhe candidate nel testo binario. Due strategie: codici
interno). Due strategie: codici numerici Pro-E riconosciuti da soli via regex (come nei nomi numerici Pro-E riconosciuti da soli via regex (come nei nomi file), e nomi alfabetici di
file), e nomi alfabetici di parti commerciali tokenizzati genericamente e validati contro parti commerciali tokenizzati genericamente e validati contro l'elenco di codici noti del DB
l'elenco di codici noti del DB (altrimenti il rumore binario sarebbe ingestibile). Dettagli (altrimenti il rumore binario sarebbe ingestibile). Dettagli sull'euristica e i falsi
sul formato, l'euristica e i falsi positivi noti in `docs/formato-asm.md`. positivi noti in `docs/formato-asm.md`. Resta il fallback per i file che il parser esatto
non copre, e la fonte di verità per `ExtractCreoStrings.cs`/`BomViaDb.cs`.
Quella lista è **la distinta**, e non coincide con i file che servono a Creo per aprire
l'assieme: ne restano fuori due categorie, che il loro file serve comunque, altrimenti il
caricamento fallisce (`docs/formato-asm-record.md` §7.3). `AsmRecordReader` le restituisce a
parte, e la fase 4 le copia in `raw/` senza contarle fra i "non trovati":
- `EstraiCodiciSoppressi` — i componenti **soppressi**: spariti da `members`, ancora citati
dalla feature che li posiziona. Non si ricorre dentro (Creo non carica i figli di un
componente soppresso).
- `EstraiCodiciRiferimentiEsterni` — i modelli legati da **riferimento esterno** (assembly
feature, datum copiati), elencati in una tabella a parte del binario (§7.5). Qui si ricorre
dentro, perché Creo carica il modello citato con tutto il suo albero.
## Quale copia di un componente si copia in `raw/`
Lo stesso codice esiste spesso in più cartelle della share, con contenuto diverso e pari
versione (13% dei codici, vedi `docs/formato-asm-record.md` §7.4). Il `.asm` non registra
quale copia usa, quindi `RisolviAlberoComponenti` replica la regola di Creo:
`Form1.ScegliPerCartella` preferisce la copia nella **stessa cartella dell'assieme che la
referenzia** (`Form1.CartellaDiOrigine` ricostruisce quella cartella per gli assiemi di
partenza), e solo in mancanza ripiega su versione più alta e data più recente. Sbagliare copia
non dà un errore comprensibile: Creo non apre l'assieme e dice solo "dati di input non
completi".
## Import distinta base ## Import distinta base
@@ -125,8 +219,8 @@ Distinta (.xls/.xml) → CSV (componente;descrizione) → raw/ (CAD copiati
sotto-assiemi trovati e copiati in `raw/`, più i codici referenziati che non corrispondono a sotto-assiemi trovati e copiati in `raw/`, più i codici referenziati che non corrispondono a
nessun file indicizzato (possono essere disegni, componenti davvero mancanti, o falsi nessun file indicizzato (possono essere disegni, componenti davvero mancanti, o falsi
positivi della scansione `.asm`). positivi della scansione `.asm`).
- **ReportCompletezzaForm.cs** — per ogni assieme della distinta, se tutti i suoi componenti - **GuidaForm.cs** — guida interattiva ("Aiuto → Guida"/F1): lista delle 5 fasi a sinistra,
(ricorsivamente fino ai PRT) sono nell'indice (✓/✗/? per "non trovato"). testo a destra; sintesi, il dettaglio resta solo in questi docs.
## Persistenza ## Persistenza
@@ -151,11 +245,9 @@ dipendenze extra con `#:package Nome@versione` in testa al file):
Pro-E). Pro-E).
- **BomViaDb.cs** — come sopra ma con la tokenizzazione generica validata contro il DB, per - **BomViaDb.cs** — come sopra ma con la tokenizzazione generica validata contro il DB, per
intercettare anche i nomi alfabetici delle parti commerciali. intercettare anche i nomi alfabetici delle parti commerciali.
- **CreoBom.cs** — indipendente dagli altri: legge esattamente la struttura a record del - **CreoBom.cs** — usa `AsmRecordReader` (stesso lettore esatto della GUI, fase 4/5, via
binario `.asm` (sezione `members` + parametro `ConfigName`, vedi `#:project`) invece dell'euristica testuale. Risolve i sottoassiemi cercandoli in cartelle
`docs/formato-asm-record.md`) invece di contare occorrenze di codici nel testo. Risolve i locali (`-p`), non contro `cad_index.db` come `ExtractCreoStrings.cs`/`BomViaDb.cs`.
sottoassiemi cercandoli in cartelle locali (`-p`), non contro `cad_index.db`. Non sostituisce
l'euristica di `AssemblyComponentScanner` nella GUI né negli altri script.
- **Analyze.cs** — dump del contesto testo/hex attorno a un pattern in un file binario, per - **Analyze.cs** — dump del contesto testo/hex attorno a un pattern in un file binario, per
investigare a mano nuovi falsi positivi prima di toccare la regex di investigare a mano nuovi falsi positivi prima di toccare la regex di
`AssemblyComponentScanner`. `AssemblyComponentScanner`.
@@ -171,6 +263,11 @@ dipendenze extra con `#:package Nome@versione` in testa al file):
- **PrepStep.Tests/** (xUnit) — riferimento al progetto principale via `ProjectReference` - **PrepStep.Tests/** (xUnit) — riferimento al progetto principale via `ProjectReference`
+ `InternalsVisibleTo` (in `PrepStep.csproj`), così i test chiamano direttamente + `InternalsVisibleTo` (in `PrepStep.csproj`), così i test chiamano direttamente
`Indexer.ParseFilename` e `AssemblyComponentScanner.EstraiCodiciReferenziati` senza esporli `Indexer.ParseFilename` e `AssemblyComponentScanner.EstraiCodiciReferenziati` senza esporli
come API pubblica. File: `IndexerParseFilenameTests.cs`, come API pubblica. File: `IndexerParseFilenameTests.cs`, `IndexerScanTests.cs`,
`AssemblyComponentScannerTests.cs`, `DistintaBaseImporterTests.cs`, `AssemblyComponentScannerTests.cs`, `AsmRecordReaderTests.cs`,
`CaseInsensitiveLookupIntegrationTests.cs`. `DistintaBaseImporterTests.cs`, `CaseInsensitiveLookupIntegrationTests.cs`,
`PipelineEndToEndTests.cs`.
`AsmRecordReaderTests.cs` costruisce i `.asm` byte per byte con dei piccoli helper
(`Membro`, `ConfigName`, `ObjName`, `Riferimenti`): ogni caso strano trovato sulla share
diventa un test minimale invece di un file binario da versionare.
+21 -5
View File
@@ -29,17 +29,33 @@ componenti e nel filtro per cartella).
Vista `files_ultima_versione`: stessa struttura di `files`, filtrata alla sola riga con Vista `files_ultima_versione`: stessa struttura di `files`, filtrata alla sola riga con
`versione` più alta per ogni `(cartella_radice, codice, tipo_file)`, tramite `ROW_NUMBER() `versione` più alta per ogni `(cartella_radice, codice, tipo_file)`, tramite `ROW_NUMBER()
OVER (PARTITION BY ... ORDER BY versione DESC)`. Tutte le ricerche di componenti (`Form1. OVER (PARTITION BY ... ORDER BY versione DESC, data_modifica DESC, percorso_completo)`. Tutte
TrovaPercorsiCad`, `Form1.CaricaCodiciNoti`) interrogano questa vista, non `files` le ricerche di componenti (`Form1.TrovaPercorsiCad`, `Form1.CaricaCodiciNoti`) interrogano
direttamente, così un componente con più versioni sulla share risolve sempre all'ultima. questa vista, non `files` direttamente, così un componente con più versioni sulla share
Deliberatamente una `ROW_NUMBER()` invece di un self-join con `GROUP BY`: su ~200k righe un risolve sempre all'ultima. Deliberatamente una `ROW_NUMBER()` invece di un self-join con
self-join degenera in tempi quasi quadratici, `ROW_NUMBER()` resta lineare. `GROUP BY`: su ~200k righe un self-join degenera in tempi quasi quadratici, `ROW_NUMBER()`
resta lineare.
La vista è ricreata (`DROP` + `CREATE`, non `CREATE IF NOT EXISTS`) a ogni "Aggiorna indice",
così un DB già esistente prende la definizione corrente senza dover reindicizzare la share.
Nota: `cartella_radice` fa parte della partition, quindi se lo stesso codice+tipo esiste Nota: `cartella_radice` fa parte della partition, quindi se lo stesso codice+tipo esiste
in **due cartelle radice diverse** (duplicati sulla share), la vista restituisce una riga per in **due cartelle radice diverse** (duplicati sulla share), la vista restituisce una riga per
ciascuna — non una sola in assoluto. `TrovaPercorsiCad` gestisce questo caso deduplicando per ciascuna — non una sola in assoluto. `TrovaPercorsiCad` gestisce questo caso deduplicando per
`tipo_file` lato C# (vedi sotto). `tipo_file` lato C# (vedi sotto).
> **I criteri di pareggio non sono decorativi.** Lo stesso codice può esistere **nella stessa
> cartella radice, con la stessa `versione`**, in due sottocartelle diverse (una copia in una
> cartella di studio, di lavoro o temporanea). Senza `data_modifica` nell'`ORDER BY` il
> pareggio veniva rotto in modo arbitrario, e poteva vincere la copia obsoleta — nascondendo
> del tutto quella buona, che non compariva più nella vista. Il file che finiva in `raw/` era
> allora un modello diverso da quello che l'assieme si aspetta: Creo lo carica, i vincoli di
> accoppiamento non si risolvono e **l'assieme non si apre affatto**.
>
> Il criterio deterministico però non basta a scegliere *bene*: è il minimo per non sbagliare
> a caso. Quale copia sia quella giusta dipende da chi la referenzia, non dalla data — vedi
> [`formato-asm-record.md`](formato-asm-record.md) §7.4 e `Form1.ScegliPerCartella`.
## Come viene popolato: `Indexer.ScanAsync` ## Come viene popolato: `Indexer.ScanAsync`
Chiamato da "Aggiorna indice" (fase 1) e riutilizza lo stesso `dbPath` già caricato. Chiamato da "Aggiorna indice" (fase 1) e riutilizza lo stesso `dbPath` già caricato.
+307 -42
View File
@@ -7,13 +7,29 @@ l'euristica testuale ("codice che compare almeno due volte") descritta in
[`formato-asm.md`](formato-asm.md): quella era una statistica sui nomi, questa legge la [`formato-asm.md`](formato-asm.md): quella era una statistica sui nomi, questa legge la
struttura effettiva dei record. struttura effettiva dei record.
Implementazione: [`scripts/CreoBom.cs`](../scripts/CreoBom.cs). Implementazione: lettura in [`PrepStep/AsmRecordReader.cs`](../PrepStep/AsmRecordReader.cs)
(condivisa con la GUI, `Form1.EstraiCodiciReferenziati` in fase "Trova componenti
referenziati"/"Verifica completezza" - con fallback su `AssemblyComponentScanner` per i file
che non rispettano il formato validato), resa in formato BOM e ricorsione sui sottoassiemi
in [`scripts/CreoBom.cs`](../scripts/CreoBom.cs) (via `#:project`).
**Stato della validazione.** Durante il reverse engineering, il livello 1 estratto da 3 **Stato della validazione.** Due livelli di prova, da non confondere:
assiemi con BOM Creo di riferimento coincideva **esattamente**: stesse voci, stesso nome
istanza, stesse quantità, stesso ordine. Altri 5 `.asm`, mai usati per ricavare le regole, 1. **Byte-per-byte contro BOM Creo di riferimento** (la prova forte): 3 assiemi del
parsavano con il conteggio dichiarato uguale a quello trovato. Tutto ciò che segue oltre reverse engineering iniziale, più 5 casi reali aggiunti in seguito, ciascuno con la sua BOM
questo perimetro (§7) è segnalato esplicitamente come ipotesi non dimostrata. esportata da Creo messa a confronto riga per riga con l'output di `CreoBom.cs` —
`9284218024b.asm` (albero profondo, multi-livello), `2546220101.asm`/`2546220101_c04.asm.1`
(istanza di family table isolata), `2546220101.asm` risalvato con un componente soppresso,
`9244218080_c01.asm` (sottoassieme montato due volte), `9284208092_D110.asm` (revisione del
figlio). Ognuno di questi è citato per nome nella sezione del documento a cui si riferisce,
così resta verificabile quale affermazione poggia su quale file.
2. **Su tutti i 350 `.asm` reali di `raw/`**, solo conteggi interni e coerenza con l'euristica
testuale (`AssemblyComponentScanner`), **non** contro BOM di riferimento: usato per
calibrare le soglie dei controlli di sicurezza (§7) su scala, non per dimostrare la
correttezza dei valori estratti file per file.
Un'affermazione verificata solo al livello 2 (conteggi su scala) è più debole di una
verificata al livello 1 (BOM reale) - il testo lo distingue sempre esplicitamente.
--- ---
@@ -75,7 +91,7 @@ conteggio dei membri: con la lettura a 1 byte fisso i campioni grandi si
disallineavano. Non è confermata da una specifica; regge sui campioni disallineavano. Non è confermata da una specifica; regge sui campioni
disponibili al momento del reverse engineering. disponibili al momento del reverse engineering.
In `CreoBom.cs` è `CreoAsm.AsInt`, applicata al testo decodificato Latin1 (ogni `char` In `CreoBom.cs` è `AsmRecordReader.AsInt`, applicata al testo decodificato Latin1 (ogni `char`
della stringa corrisponde 1:1 a un byte, quindi indicizzare la stringa o l'array di byte della stringa corrisponde 1:1 a un byte, quindi indicizzare la stringa o l'array di byte
originale dà lo stesso offset). originale dà lo stesso offset).
@@ -128,15 +144,15 @@ codici distinti: una parte montata 4 volte conta 4 e compare in 4 record con id
diversi. Il conteggio va usato come controllo di correttezza: se i record diversi. Il conteggio va usato come controllo di correttezza: se i record
trovati non sono esattamente `N`, la lettura si è disallineata e va segnalata trovati non sono esattamente `N`, la lettura si è disallineata e va segnalata
un'eccezione invece di restituire un risultato parziale silenzioso — vedi un'eccezione invece di restituire un risultato parziale silenzioso — vedi
`CreoAsm.LeggiMembri`, che lancia `InvalidDataException` in questo caso. `AsmRecordReader.LeggiMembri`, che lancia `InvalidDataException` in questo caso.
La sezione termina a `\xf4\x04#\n#MdlStatus\n`. La sezione termina a `\xf4\x04#\n#MdlStatus\n`.
### 3.1 Come lo legge `CreoAsm.LeggiMembri` ### 3.1 Come lo legge `AsmRecordReader.LeggiMembri`
Il parser non ricostruisce lo schema completo (servirebbe decodificare le Il parser non ricostruisce lo schema completo (servirebbe decodificare le
matrici). Si ancora invece al pattern `\xe3 <int> <int> <stringa>\0 <stringa>\0` matrici). Si ancora invece al pattern `\xe3 <int> <int> <stringa>\0 <stringa>\0`
(`CreoAsm.MemberRegex`, una regex .NET sul testo Latin1 del file) e salta il (`AsmRecordReader.MemberRegex`, una regex .NET sul testo Latin1 del file) e salta il
resto del record fino al `\xe3` successivo che soddisfa il pattern. resto del record fino al `\xe3` successivo che soddisfa il pattern.
La doppia stringa terminata da `\0` rende i falsi positivi improbabili, e il La doppia stringa terminata da `\0` rende i falsi positivi improbabili, e il
conteggio dichiarato `N` fa da rete di sicurezza. conteggio dichiarato `N` fa da rete di sicurezza.
@@ -172,15 +188,15 @@ Il collegamento sta in un record di **parametro** applicato al membro:
linea), poi è omesso. linea), poi è omesso.
- `\x82\x7b` — costante (= 635) identica su tutte le occorrenze di tutti i - `\x82\x7b` — costante (= 635) identica su tutte le occorrenze di tutti i
campioni. Significato ignoto, presumibilmente attributi/flag del parametro. campioni. Significato ignoto, presumibilmente attributi/flag del parametro.
`CreoAsm.ConfigNameRegex` la usa come ancora di fine record; se un campione `AsmRecordReader.ConfigNameRegex` la usa come ancora di fine record; se un campione
futuro avesse un valore diverso, il match salterebbe silenziosamente (nessun futuro avesse un valore diverso, il match salterebbe silenziosamente (nessun
contatore di sicurezza analogo a quello di `members` copre questo caso). contatore di sicurezza analogo a quello di `members` copre questo caso).
`ConfigName` compare **esattamente** per i membri che sono istanze, e per `ConfigName` compare **esattamente** per i membri che sono istanze, e per
nessun altro. L'algoritmo è quindi: leggere `members`, costruire la mappa nessun altro. L'algoritmo è quindi: leggere `members`, costruire la mappa
`id membro → nome istanza` dai record `ConfigName` `id membro → nome istanza` dai record `ConfigName`
(`CreoAsm.LeggiMembri`, dizionario `istanze`), e sostituire il nome dove la (`AsmRecordReader.LeggiMembri`, dizionario `istanze`), e sostituire il nome dove la
mappa ha una voce. Il generico resta disponibile come `Membro.File`. mappa ha una voce. Il generico resta disponibile come `Membro.Generico`.
> **Avvertenza.** `ConfigName` sta in mezzo a parametri palesemente aziendali > **Avvertenza.** `ConfigName` sta in mezzo a parametri palesemente aziendali
> (`TIPO_PARTE`, `CLASSE_RICAR`, `GR_PRODOTTO`, `TECN_RIORDINO`), quindi > (`TIPO_PARTE`, `CLASSE_RICAR`, `GR_PRODOTTO`, `TECN_RIORDINO`), quindi
@@ -188,6 +204,49 @@ mappa ha una voce. Il generico resta disponibile come `Membro.File`.
> interna. Regge sui campioni disponibili, ma su file provenienti da > interna. Regge sui campioni disponibili, ma su file provenienti da
> un'altra installazione va riverificato prima di fidarsene. > un'altra installazione va riverificato prima di fidarsene.
### 4.0.1 Eccezione: componenti da pattern/array feature (`#AF<n>`)
Un componente creato da un **pattern/array feature** (stessa geometria ripetuta N volte da
una feature di pattern, non N placement manuali distinti) ha un `ConfigName` con un nome
nella forma `<codice>#AF<n>` (`n` = indice dell'istanza nel pattern), es.
`9244250136_C01#AF0`. **Non è un'istanza di family table**: è un tag interno di
disambiguazione fra le ripetizioni del pattern. La BOM reale di Creo lo **ignora** e mostra
il codice pulito, unendo le occorrenze duplicate in una sola riga con la quantità totale.
Osservato su `9244250136.asm`: 4 dei suoi 8 membri hanno `ConfigName` con suffisso `#AF`,
due dei quali (`_C03#AF0` e `_C03#AF1`) sono lo **stesso codice** con `n` diverso — la BOM li
mostra come un'unica riga `9244250136_C03` con quantità 2. Senza normalizzare il suffisso,
ogni istanza del pattern risulterebbe un componente distinto (e con nome sbagliato).
`AsmRecordReader` rimuove `#AF\d+$` da ogni valore di `ConfigName` prima di usarlo
(`SuffissoArrayFeatureRegex`). Scoperto durante un test end-to-end su un albero reale più
ampio degli 8 campioni originali (`9284218024b.asm`, confrontato con la sua BOM Creo di
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
`<nome istanza><generico>` invece del solo nome generico:
```
\xe3 <id> <type> "<istanza><generico>"\0 "<istanza><generico>"\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 `<testo><testo>`: 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 ### 4.1 Una seconda pista, che NON va usata per la BOM
Altrove nel file compaiono stringhe nella forma `istanza<generico>`: Altrove nel file compaiono stringhe nella forma `istanza<generico>`:
@@ -218,7 +277,7 @@ primo record `objname` del file:
``` ```
La **prima** occorrenza è il modello stesso; quelle successive sono altri La **prima** occorrenza è il modello stesso; quelle successive sono altri
oggetti referenziati (disegni, modelli citati). `CreoAsm.NomeProprio` cerca oggetti referenziati (disegni, modelli citati). `AsmRecordReader.NomeProprio` cerca
solo la prima occorrenza (`Regex.Match`, non `Matches`), verificato su tutti i solo la prima occorrenza (`Regex.Match`, non `Matches`), verificato su tutti i
campioni disponibili durante il reverse engineering. campioni disponibili durante il reverse engineering.
@@ -248,9 +307,12 @@ subito dopo, ricorsivamente, quelli dei suoi sottoassiemi, prima di passare al
fratello successivo. Ogni modello compare **una volta sola** anche se montato fratello successivo. Ogni modello compare **una volta sola** anche se montato
più volte (deduplica per nome, `CreoAsm.Blocchi`). più volte (deduplica per nome, `CreoAsm.Blocchi`).
> La deduplica è un'**assunzione**: nessuno dei campioni disponibili durante il > **Verificato.** `9244218080_C01.asm` monta `2605410010_C01` due volte
> reverse engineering montava lo stesso sottoassieme due volte, quindi non è > (`scripts/9244218080_c01.asm.1` + `scripts/9244218080_c01.bom.2` di riferimento): la BOM
> dimostrata su un caso reale. > Creo mostra un solo blocco `Sottoassieme 2605410010_C01 contiene:` con quantità 2 in testa,
> non due blocchi separati, e `CreoBom.cs` riproduce l'output esatto senza modifiche. Confermata
> anche la moltiplicazione delle quantità nel riepilogo (§6.3): le parti di `2605410010_C01`
> compaiono raddoppiate (1→2, 1→2, 16→32).
### 6.3 Riepilogo finale ### 6.3 Riepilogo finale
@@ -268,13 +330,17 @@ dall'esterno (`-p`/`--path`). `CreoAsm.CreaFinder` indicizza `<nome>.asm.<rev>`
senza distinzione di maiuscole e, a parità di nome, prende la **revisione più senza distinzione di maiuscole e, a parità di nome, prende la **revisione più
alta** presente. alta** presente.
> Scelta discutibile: la revisione più alta in cartella non è > **Verificato: è la scelta giusta.** Il `.asm` padre non registra la revisione attesa del
> necessariamente quella con cui l'assieme padre è stato salvato. Il `.asm` > figlio (cercata e non trovata, vedi §7), ma non serve: confermato che **Creo stesso apre
> padre non sembra registrare la revisione attesa del figlio, o non è stata > sempre la revisione più alta disponibile** di un componente, indipendentemente da quale
> trovata. Vedi `formato-asm.md` §"Risoluzione ricorsiva dell'albero" per le > fosse presente quando l'assieme padre fu salvato l'ultima volta. Caso reale controllato:
> trappole già incontrate storicamente su questo punto (con l'euristica > `9284208092_D110.asm` (salvato 2018-08-01) referenzia `9284202481.prt`, che sulla share ha
> testuale, ma il problema della revisione attesa è indipendente dal metodo > revisioni sia del 2014 (v1-v3, presenti all'epoca) sia del 2024 (v4-v6, arrivate anni dopo) -
> di estrazione). > Creo carica v6, non v3. `CreaFinder` che prende sempre la revisione più alta in cartella
> replica esattamente questo comportamento, non è un'assunzione azzardata. Vedi `formato-
> asm.md` §"Risoluzione ricorsiva dell'albero" per le trappole storiche su questo punto (con
> l'euristica testuale - restano valide, sono su un problema diverso: trovare la cartella
> giusta, non quale revisione preferire).
Se un sottoassieme non si trova, il blocco viene emesso con Se un sottoassieme non si trova, il blocco viene emesso con
`*** NON RISOLTO: file .asm non trovato ***` e le sue parti restano fuori dal `*** NON RISOLTO: file .asm non trovato ***` e le sue parti restano fuori dal
@@ -283,26 +349,225 @@ sbagliato.
--- ---
## 7. Cosa resta sconosciuto ## 7. Stato di ogni punto aperto
Elencato esplicitamente per non far passare per dimostrato ciò che non lo è: Tabella di riferimento: cosa è provato, come, e cosa resta genuinamente ignoto. Il dettaglio
di ciascun punto segue sotto, in due gruppi.
- **Stato soppresso / escluso.** Nessuna evidenza cercata o trovata. Non si sa | Punto | Stato | Prova |
se un componente soppresso compaia in `members`, né con quale marcatore. Sui |---|---|---|
campioni disponibili il totale coincideva con la BOM, il che *suggerisce* che | Deduplica blocchi + moltiplicazione quantità (§6.2/§6.3) | Risolto | BOM reale (§7.1) |
non ce ne fossero, non che siano gestiti. | Revisione del figlio che il padre si aspetta (§6.4) | Risolto | BOM reale (§7.1) |
- **La matrice di posizionamento** (`e1`, `e2`, `e3`, `origin`): formato | Stato soppresso/escluso in `members` | Risolto | BOM reale (§7.1) |
numerico non decodificato. | Family table di **assiemi** (rilevamento) | Risolto: serve la conferma del nome proprio (`objname`), il solo `model_name` dava falsi positivi | BOM reali + albero `raw/` completo (§7.1) |
- **`family_name`**: sempre uguale a `name` sui campioni; non si sa in quale | Family table di **assiemi** (contenuto di ogni istanza) | Non risolto, nessun impatto sulla BOM (fallback già corretto) | Scan sull'intera share (§7.2) |
situazione divergerebbe. | Costante `\x82\x7b` in `ConfigName` | Significato ignoto, rischio coperto | Scala 350 file (§7.2) |
- **La costante `\x82\x7b`** nei record `ConfigName`. | Occorrenze multiple di `members` (simplified rep) | Mai osservato, rischio coperto | Scala 350 file (§7.2) |
- **I campi numerici dell'header** dopo `ASSEMBLY`. | Matrice di posizionamento (`e1`/`e2`/`e3`/`origin`) | Non letto, nessun impatto sulla BOM | — (§7.2) |
- **Occorrenze multiple di `members`.** `CreoAsm.LeggiMembri` prende la prima. | Campi numerici dell'header dopo `ASSEMBLY` | Non letto, nessun impatto sulla BOM | — (§7.2) |
Sui campioni disponibili funziona, ma non è stato verificato se le | Divergenza `family_name` ≠ `name` | Mai osservata, non letta | — (§7.2) |
rappresentazioni semplificate (simplified rep) generino sezioni `members`
aggiuntive. "Rischio coperto" vuol dire: il codice non sa cosa significhi il byte, ma verifica un
- **La deduplica dei blocchi** e **la revisione attesa dei figli** (§6.2, §6.4). invariante che lo renderebbe visibile (eccezione) se mai smettesse di valere - non è una prova
- **Quale revisione del figlio** il padre si aspetta: non trovata nel `.asm`. che il byte non conti mai, è la garanzia che se contasse il programma lo segnalerebbe invece
di sbagliare in silenzio.
### 7.1 Risolti con una BOM Creo di riferimento
- **Deduplica blocchi e moltiplicazione quantità.** `9244218080_C01.asm` monta
`2605410010_C01` due volte (`scripts/9244218080_c01.asm.1` +
`scripts/9244218080_c01.bom.2`): la BOM Creo mostra un solo blocco
`Sottoassieme 2605410010_C01 contiene:` con quantità 2 in testa (non due blocchi separati),
e le sue parti compaiono raddoppiate nel riepilogo (1→2, 1→2, 16→32). `CreoBom.cs` riproduce
l'output esatto senza modifiche: la deduplica per nome (`CreoAsm.Blocchi`) e la
moltiplicazione lungo l'albero (`CreoAsm.Sommario`) sono il comportamento reale di Creo, non
solo un'assunzione.
- **Revisione del figlio.** `9284208092_D110.asm` (salvato 2018-08-01) referenzia
`9284202481.prt`, che sulla share ha revisioni sia del 2014 (v1-v3, presenti quando il padre
fu salvato) sia del 2024 (v4-v6, arrivate anni dopo). Confermato direttamente in Creo: apre
v6, non v3 - **sempre la revisione più alta disponibile**, indipendentemente da quale fosse
presente all'ultimo salvataggio del padre. `CreoAsm.CreaFinder`, che prende sempre la
revisione più alta trovata in cartella, replica esattamente questo comportamento.
- **Stato soppresso/escluso.** Stesso assieme `2546220101.asm` risalvato da Creo con
`2546220101_C02` soppresso (`scripts/2546220101.asm.1` + `scripts/2546220101.bom.1`, con le
parti/sottoassiemi correlati). Un componente soppresso **non produce alcun record** in
`members`: sparisce e basta, il conteggio dichiarato scende di conseguenza (`\xf8 03` invece
di `\xf8 04`). Nessun marcatore di stato da gestire: `CreoBom.cs` sul file risalvato
riproduce esattamente la BOM di riferimento senza modifiche al parser.
> **Sparisce da `members`, non dal file.** La feature che lo posiziona resta, e con essa la
> stringa `Componente ID <n> (<codice>.PRT)` (localizzata come in
> `AssemblyComponentScanner.HaPlacement`). Per la BOM va ignorata - Creo non elenca i
> soppressi - ma **il file di quel componente serve comunque per aprire l'assieme**: se manca,
> Creo fallisce il caricamento con "dati di input non completi" (`[-4]`, cioè
> `PRO_TK_E_NOT_FOUND`). Verificato su un caso reale: un assieme che non si apriva dalla
> cartella `raw/`, e che si è aperto senza errori copiandoci dentro quel solo file.
> `AsmRecordReader.EstraiCodiciSoppressi` li restituisce a parte, apposta per questo - vedi
> §7.3.
- **Family table di assiemi — rilevamento.** Non solo le parti normalizzate (§4) possono
essere family table: un intero **assieme** può esserlo. Osservato su `2546220101.asm`
(dentro l'albero di `9284218024b.asm`): il file generico ha una propria sezione `members`
che elenca il contenuto di UNA rappresentazione (3 cuscinetti + 1 sotto-assieme, dati
arbitrari), ma la BOM Creo per lo stesso file mostra 4 **istanze diverse**
(`2546220101_C01`.."_C04", tre "Parte" e una "Sottoassieme" - tipi diversi fra loro),
ciascuna con un contenuto diverso. Il nome istanza qui **non** passa da `ConfigName`
(verificato: nessuna occorrenza letterale nel file), ma da una coppia di record distinta
subito dopo l'header del modello:
```
\xe0\x01model_t_type\0<int>\xe0\x0amodel_name\0<codice>_C0n\0\xe0\x0ageneric_name\0\0
...stamp\0\xe2\xf2\xf7<idSchema>
(\xe3<type>\x02<codice>_C0n\0\0\xe2)* -- un'occorrenza per ogni istanza sorella
\xf4
```
Quando questo succede il file generico è già di per sé un'istanza (`model_name` porta il
suffisso `_C0n`, non è neutro) e `stamp` elenca le sorelle.
> **Il record `model_name` da solo non è un'identità.** Su file reali quel record nomina
> quasi sempre **un altro modello** - tipicamente uno dei membri del file stesso, a volte un
> codice imparentato - e in quei casi `members` è già corretto e va usato. Serve una
> conferma indipendente: il **nome proprio del modello** (`objname`, §5). `LeggiMembri`
> segnala la family table solo se `objname` esiste **e** coincide con `model_name` privato
> del suffisso `_C0n`; negli altri casi legge `members` normalmente.
>
> Il controllo più largo (senza la conferma di `objname`) era la causa di
> **falsi "componenti mancanti"**: il parser esatto rifiutava file perfettamente leggibili,
> si ripiegava sull'euristica testuale e quella estraeva codici che non esistono, riportati
> poi come componenti non trovati nell'indice. Verificato contro BOM Creo di riferimento:
> sui file dove il segnale scattava senza conferma di `objname`, `members` elenca
> **esattamente** i componenti della BOM reale (anche nello stesso ordine); uno di questi
> file è stato inoltre aperto in Creo, e l'albero del modello mostra gli stessi componenti,
> senza alcuna family table. Con la conferma di `objname` richiesta, tutti i `.asm` di una
> cartella `raw/` reale (351 file, alberi completi di più macchine) si leggono col parser
> esatto, zero ricadute sull'euristica.
Da tenere presente: il sintomo dipende da **come il file è stato salvato**, non solo dal
modello. Rileggendo una revisione più recente dello stesso assieme generico, il record di
identità può non esserci più e `members` elencare direttamente tutte le istanze, col tipo
giusto per ciascuna - cioè esattamente quello che mostra la BOM. Il blocco quindi non è una
proprietà stabile del codice: va valutato file per file, revisione per revisione.
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
`stamp`. E che una singola istanza, se esportata da Creo come file a sé (Family Table →
istanza → "Salva una copia"), è un `.asm`/`.prt` normale senza traccia di family table al suo
interno - letto correttamente dal parser esatto (`2546220101_c04.asm.1`, verificato contro
`2546220101.bom.1`).
### 7.2 Non risolti
- **Family table di assiemi — contenuto di ogni istanza.** Come recuperare il contenuto
specifico di un'istanza **senza** che esista già un file separato per essa sul disco (il
caso comune, se nessuno l'ha mai esportata a parte con "Salva una copia"): il meccanismo
dietro `stamp` non è stato decodificato oltre a nome+tipo di ciascuna istanza sorella:
`stamp` porta **solo i nomi** delle sorelle, non il loro contenuto, quindi non è la
struttura da cui partire per risolvere questo punto.
Resta aperto solo per completezza: sui file dove il segnale scatta davvero (con conferma
del nome proprio, §7.1) il parser si ferma invece di restituire un contenuto sbagliato, e
il chiamante ripiega sull'euristica testuale, che i nomi delle istanze li trova comunque
come testo puro. Nessun comportamento dell'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:
in silenzio) e ora lancia eccezione invece di restituire il nome generico al posto
dell'istanza senza dirlo. Verificato su tutti i 350 `.asm` di `raw/`: 215 usano davvero
`ConfigName`, 0 mismatch.
- **Occorrenze multiple di `members`** (rappresentazioni semplificate, mai osservate). Rischio
coperto - `LeggiMembri` lancia eccezione se ne trova più di una invece di prendere
silenziosamente la prima senza sapere se sia quella giusta. Verificato su tutti i 350 `.asm`
di `raw/`: sempre esattamente una occorrenza.
- **La matrice di posizionamento** (`e1`, `e2`, `e3`, `origin`): formato numerico non
decodificato, nessun impatto sulla BOM (non letta da nessuna parte del codice).
- **I campi numerici dell'header** dopo `ASSEMBLY`: non letti, nessun impatto sulla BOM.
- **`family_name`**: sempre uguale a `name` su tutti i campioni disponibili; non si sa in
quale situazione divergerebbe, né se il codice dovrebbe fare qualcosa se succedesse (oggi
non la usa in nessun modo, legge solo `name`).
### 7.3 Due domande diverse: cosa va in distinta, cosa serve per aprire
È la lezione che costa di più riscoprire, quindi sta qui in chiaro: **la lista dei componenti
della BOM e la lista dei file che servono a Creo per aprire l'assieme non coincidono.** La
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` e `EstraiCodiciRiferimentiEsterni` (§7.5) |
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
completi` - un messaggio che non nomina il file mancante. Il modo più rapido per trovarlo è
aprire l'assieme in Creo **dalla cartella `raw/`** (non dalla share, dove i file ci sono tutti
e quindi si apre senza errori) e cercare nell'albero la voce con l'icona rossa.
### 7.4 Lo stesso codice esiste in più cartelle della share
Sulla share **13% dei codici** (5.193 su 39.274 fra prt e asm) ha la versione più alta in più
cartelle, con contenuto diverso. Non sono revisioni: sono copie parallele, lasciate da studi
preliminari, allestimenti fiera, cartelle di lavoro datate, raccolte temporanee.
Il `.asm` **non dice quale copia usa**: verificato che non registra né il percorso, né
l'`UOBJ_ID` del figlio (che pure distingue le copie), né la revisione attesa. Cita solo il
nome del modello. Quale copia venga caricata lo decide Creo al momento dell'apertura, e la
regola pratica è: **prima la cartella del modello che la referenzia**.
Non esiste quindi un criterio globale corretto - e i numeri lo confermano: nel 79% dei
conflitti nessuna delle copie sta in una cartella "ufficiale" (`…\M_B`, `…\A`), perché il
conflitto è fra due istantanee parallele, ognuna autoconsistente con i propri componenti. Per
data, per versione e per posizione sono equivalenti: solo il contesto di chi referenzia
discrimina.
Conseguenze per il codice:
- `Form1.ScegliPerCartella` risolve ogni componente preferendo la copia nella **stessa cartella
dell'assieme che lo referenzia**, e solo in mancanza ripiega su versione più alta e data più
recente. `Form1.CartellaDiOrigine` ricostruisce quella cartella per gli assiemi di partenza,
che arrivano dalla fase precedente senza portarsela dietro.
- La vista `files_ultima_versione` (vedi [`db-cad-index.md`](db-cad-index.md)) rompe il
pareggio fra copie di pari versione in modo deterministico invece che arbitrario.
- Resta un rischio noto: quando il componente **non** sta nella cartella del suo assieme, la
scelta torna a essere una scommessa. Se un assieme non si apre da `raw/`, è la prima cosa da
verificare - `9244206098` è il caso reale che ha portato a questa sezione: due copie di pari
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<tipo><nome>\x00`. Il nome può portare il suffisso `#AF<n>` di §4.0.1 e/o la notazione
`istanza<generico>` 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<UNI5933>` il cui generico `uni5933.prt` non era in `raw/`. Nessuno dei sei
file compariva in `members` né fra i piazzamenti.
## 8. Uso ## 8. Uso
+17 -16
View File
@@ -1,23 +1,24 @@
# Come leggere un .asm Creo per dedurne i componenti # Come leggere un .asm Creo per dedurne i componenti
Il file `.asm` di Pro/ENGINEER-Creo è un formato binario proprietario, non Il file `.asm` di Pro/ENGINEER-Creo è un formato binario proprietario, non
documentato pubblicamente. Questo documento descrive l'euristica testuale usata da documentato pubblicamente. Questo documento descrive l'euristica testuale di
`AssemblyComponentScanner.cs` (GUI, fase "Trova componenti referenziati"/"Verifica `AssemblyComponentScanner.cs`: conta le occorrenze di stringhe candidate nel testo binario,
completezza") e dagli script che risolvono l'albero componenti contro `cad_index.db` validata a mano confrontando l'output con l'albero reale mostrato da Creo. Riassume cosa si
(`ExtractCreoStrings.cs`, `BomViaDb.cs`): conta le occorrenze di stringhe candidate nel è osservato e i suoi limiti, in particolare i falsi positivi che un conteggio testuale non
testo binario, validata a mano confrontando l'output con l'albero reale mostrato da Creo. può escludere con certezza (riferimenti fantasma, componenti soppressi con placement
Riassume cosa si è osservato e i suoi limiti, in particolare i falsi positivi che residuo — vedi sotto).
un conteggio testuale non può escludere con certezza (riferimenti fantasma, componenti
soppressi con placement residuo — vedi sotto).
Un reverse engineering successivo ha invece decodificato la **struttura a record** del Un reverse engineering successivo ha decodificato la **struttura a record** del file (non
file (non solo pattern testuali): legge esattamente la sezione `members` di un assieme, solo pattern testuali): legge esattamente la sezione `members` di un assieme, senza contare
senza contare occorrenze né rischiare i falsi positivi descritti in questo documento. occorrenze né rischiare i falsi positivi descritti in questo documento. Implementato in
Implementato in [`scripts/CreoBom.cs`](../scripts/CreoBom.cs), documentato in `PrepStep/AsmRecordReader.cs`, documentato in [`formato-asm-record.md`](formato-asm-record.md).
[`formato-asm-record.md`](formato-asm-record.md). Resta però uno script di analisi È il metodo **preferito** in GUI (fase "Trova componenti referenziati"/"Verifica
indipendente: legge solo `.asm` locali dato un percorso di ricerca esplicito (non completezza", `Form1.EstraiCodiciReferenziati`) e nello script `CreoBom.cs`:
`cad_index.db`), e non è integrato in `AssemblyComponentScanner`/nella GUI, che `AssemblyComponentScanner` (l'euristica di questo documento) resta come **fallback**, usato
continuano a usare l'euristica descritta qui. solo quando `AsmRecordReader` lancia `InvalidDataException` (file che non rispetta il
formato validato dal reverse engineering — copertura limitata agli 8 campioni disponibili al
momento), e come fonte di verità per gli script `ExtractCreoStrings.cs`/`BomViaDb.cs`, che
risolvono l'albero contro `cad_index.db` invece che tramite un percorso di ricerca locale.
## Struttura osservata ## Struttura osservata
+23 -113
View File
@@ -1,18 +1,17 @@
#:project ../PrepStep/PrepStep.csproj
#:property TargetFramework=net10.0-windows
// dotnet run scripts/CreoBom.cs -- <file.asm.rev> [-p cartella]... [-o out.bom.1] // dotnet run scripts/CreoBom.cs -- <file.asm.rev> [-p cartella]... [-o out.bom.1]
// //
// Parser esatto del formato binario .asm di Creo, ricavato per reverse engineering (vedi // Genera la distinta base (BOM) di un assieme Creo, dato un elenco di cartelle dove
// docs/formato-asm-record.md per la spiegazione byte-per-byte dei record, verificata // cercare i sottoassiemi (-p). La lettura del formato binario (sezione "members" +
// confrontando l'output con le BOM esportate da Creo). Legge la sezione "members" di un // parametro "ConfigName", vedi docs/formato-asm-record.md) vive in
// assieme (id/tipo/nome dei componenti diretti, con risoluzione del nome istanza di family // PrepStep.AsmRecordReader, via "#:project" - stessa fonte usata da Form1 nella GUI (fase
// table dal parametro "ConfigName") e, dato un elenco di cartelle dove cercare i // 4/5, con fallback automatico all'euristica di AssemblyComponentScanner.cs per i file che
// sottoassiemi, genera la stessa distinta base (BOM) esportata da Creo. // non rispettano il formato validato): qui solo la ricorsione sui sottoassiemi, il
// // riepilogo delle quantita' e la resa nel formato esatto della BOM Creo.
// Alternativa esatta all'euristica testuale di PrepStep/AssemblyComponentScanner.cs (docs/
// formato-asm.md): quella conta le occorrenze dei codici nel testo, questa legge la
// struttura dei record. Per ora resta uno script di analisi indipendente: non sostituisce
// AssemblyComponentScanner nella GUI (fase 4) ne' negli altri script.
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using PrepStep;
if (args.Length == 0) if (args.Length == 0)
{ {
@@ -45,106 +44,17 @@ else
} }
return 0; return 0;
/// <summary>Lettura del formato .asm Creo (sezione "members" + parametro "ConfigName") e resa /// <summary>Ricorsione sui sottoassiemi e resa nel formato della BOM esportata da Creo. La
/// nel formato della BOM esportata da Creo. Vedi docs/formato-asm-record.md per il dettaglio /// lettura del formato binario vive in <see cref="AsmRecordReader"/> (PrepStep/, condivisa
/// dei record e i limiti noti (stato soppresso non gestito, matrice di posizionamento non /// con la GUI). Vedi docs/formato-asm-record.md per il dettaglio dei record e i limiti noti
/// decodificata, deduplica dei blocchi non dimostrata su assiemi che montano lo stesso /// (stato soppresso non gestito, matrice di posizionamento non decodificata, deduplica dei
/// sottoassieme due volte).</summary> /// blocchi non dimostrata su assiemi che montano lo stesso sottoassieme due volte).</summary>
public static class CreoAsm public static class CreoAsm
{ {
public sealed record Membro(int Id, int Tipo, string Nome, string File);
// interi big-endian, lunghezza nei bit alti del primo byte (0xe0+ e' riservato ai
// marcatori di struttura): 0x00-0x7f 1 byte, 0x80-0xbf 2 byte, 0xc0-0xdf 3.
private const string Int = @"([\x00-\x7f]|[\x80-\xbf][\x00-\xff]|[\xc0-\xdf][\x00-\xff][\x00-\xff])";
// \xe3 id type name\0 family_name\0
private static readonly Regex MemberRegex = new(
@"\xe3" + Int + Int + @"([\x20-\x7e]+)\x00([\x20-\x7e]+)\x00", RegexOptions.Compiled);
// parametro ConfigName: nome istanza family table + id del membro a cui si applica.
private static readonly Regex ConfigNameRegex = new(
@"\xe3ConfigName\x00\xe23(?:\xf7[\x00-\xff])?([\x20-\x7e]+)\x00" + Int + @"\x82\x7b",
RegexOptions.Compiled);
// nome proprio del modello (quello che la BOM stampa in intestazione).
private static readonly Regex ObjNameRegex = new(
@"\x00objname\x00\xe2\xe0\x01object_type\x00[\x00-\x7f]\xe0\x0aname\x00([^\x00]+)\x00",
RegexOptions.Compiled);
// primo elemento dell'array "members": porta le chiavi per esteso.
private static readonly Regex FirstMemberRegex = new(
@"\x01id\x00" + Int + @"\xe0\x01type\x00" + Int
+ @"\xe0\x0aname\x00([^\x00]+)\x00\xe0\x0afamily_name\x00([^\x00]+)\x00",
RegexOptions.Compiled);
private static readonly Dictionary<int, string> TipoNome = new() { [1] = "Sottoassieme", [2] = "Parte" }; private static readonly Dictionary<int, string> TipoNome = new() { [1] = "Sottoassieme", [2] = "Parte" };
private static int AsInt(string bytes)
{
int mask = bytes.Length switch { 1 => 0x7F, 2 => 0x3F, 3 => 0x1F, _ => throw new ArgumentException("lunghezza intero non valida") };
int valore = bytes[0] & mask;
for (int i = 1; i < bytes.Length; i++)
valore = (valore << 8) | bytes[i];
return valore;
}
/// <summary>Nome proprio del modello (quello che Creo stampa in intestazione BOM),
/// dalla prima occorrenza del record "objname".</summary>
public static string NomeProprio(byte[] dati)
{
var m = ObjNameRegex.Match(Encoding.Latin1.GetString(dati));
if (!m.Success)
throw new InvalidDataException("record 'objname' non trovato");
return m.Groups[1].Value;
}
/// <summary>Componenti diretti di un assieme, in ordine di montaggio. "Nome" e' il nome
/// che mostrerebbe la BOM (istanza di family table se presente), "File" e' il generico
/// realmente su disco (coincide con "Nome" per i componenti che non sono istanze).</summary>
public static List<Membro> LeggiMembri(byte[] dati)
{
string testo = Encoding.Latin1.GetString(dati);
var istanze = new Dictionary<int, string>();
foreach (Match m in ConfigNameRegex.Matches(testo))
istanze[AsInt(m.Groups[2].Value)] = m.Groups[1].Value;
const string chiaveMembers = "\0members\0";
int start = testo.IndexOf(chiaveMembers, StringComparison.Ordinal);
if (start < 0)
throw new InvalidDataException("sezione 'members' assente");
start += chiaveMembers.Length;
int conteggio = dati[start + 2]; // \xf1 \xf8 <conteggio>
int end = testo.IndexOf("#MdlStatus", start, StringComparison.Ordinal);
string region = testo.Substring(start, (end > 0 ? end : testo.Length) - start);
var grezzi = new List<(int Id, int Tipo, string Nome)>();
// il primo record e' esteso: ricava i campi dalle chiavi, i successivi sono posizionali
var primo = FirstMemberRegex.Match(region);
if (primo.Success)
{
grezzi.Add((AsInt(primo.Groups[1].Value), AsInt(primo.Groups[2].Value), primo.Groups[3].Value));
region = region.Substring(primo.Index + primo.Length);
}
foreach (Match m in MemberRegex.Matches(region))
{
grezzi.Add((AsInt(m.Groups[1].Value), AsInt(m.Groups[2].Value), m.Groups[3].Value));
if (grezzi.Count == conteggio)
break;
}
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();
}
/// <summary>Voci distinte con quantita', nell'ordine di prima comparsa (come la BOM).</summary> /// <summary>Voci distinte con quantita', nell'ordine di prima comparsa (come la BOM).</summary>
public static List<((int Tipo, string Nome) Chiave, int Qta)> Rollup(IEnumerable<Membro> membri) public static List<((int Tipo, string Nome) Chiave, int Qta)> Rollup(IEnumerable<AsmRecordReader.Membro> membri)
{ {
var ordine = new List<(int, string)>(); var ordine = new List<(int, string)>();
var qty = new Dictionary<(int, string), int>(); var qty = new Dictionary<(int, string), int>();
@@ -163,7 +73,7 @@ public static class CreoAsm
/// <summary>nome modello -> membri, cercando &lt;nome&gt;.asm.&lt;rev&gt; nelle cartelle /// <summary>nome modello -> membri, cercando &lt;nome&gt;.asm.&lt;rev&gt; nelle cartelle
/// date. A parita' di nome vince la revisione piu' alta presente. Nome null se il file /// date. A parita' di nome vince la revisione piu' alta presente. Nome null se il file
/// non c'e' in nessuna delle cartelle.</summary> /// non c'e' in nessuna delle cartelle.</summary>
private static Func<string, List<Membro>?> CreaFinder(IEnumerable<string> cartelle) private static Func<string, List<AsmRecordReader.Membro>?> CreaFinder(IEnumerable<string> cartelle)
{ {
var indice = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); var indice = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var revisioni = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); var revisioni = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
@@ -184,12 +94,12 @@ public static class CreoAsm
} }
} }
var cache = new Dictionary<string, List<Membro>?>(); var cache = new Dictionary<string, List<AsmRecordReader.Membro>?>();
return nome => return nome =>
{ {
if (!cache.TryGetValue(nome, out var mem)) if (!cache.TryGetValue(nome, out var mem))
{ {
mem = indice.TryGetValue(nome, out var percorso) ? LeggiMembri(File.ReadAllBytes(percorso)) : null; mem = indice.TryGetValue(nome, out var percorso) ? AsmRecordReader.LeggiMembri(File.ReadAllBytes(percorso)) : null;
cache[nome] = mem; cache[nome] = mem;
} }
return mem; return mem;
@@ -202,7 +112,7 @@ public static class CreoAsm
/// <summary>Blocchi in ordine depth-first pre-order. Ogni modello compare una volta sola /// <summary>Blocchi in ordine depth-first pre-order. Ogni modello compare una volta sola
/// (deduplica per nome) anche se montato piu' volte.</summary> /// (deduplica per nome) anche se montato piu' volte.</summary>
private static List<Blocco> Blocchi(string root, Func<string, List<Membro>?> get) private static List<Blocco> Blocchi(string root, Func<string, List<AsmRecordReader.Membro>?> get)
{ {
var risultato = new List<Blocco>(); var risultato = new List<Blocco>();
var visti = new HashSet<string>(); var visti = new HashSet<string>();
@@ -231,7 +141,7 @@ public static class CreoAsm
/// <summary>Totale per parte, moltiplicato lungo l'albero, in ordine di prima comparsa: una /// <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 /// parte che compare a piu' livelli occupa la posizione della sua prima comparsa e somma
/// le quantita'.</summary> /// le quantita'.</summary>
private static void Sommario(string root, Func<string, List<Membro>?> get, int mult, private static void Sommario(string root, Func<string, List<AsmRecordReader.Membro>?> get, int mult,
List<string> ordine, Dictionary<string, int> acc) List<string> ordine, Dictionary<string, int> acc)
{ {
var mem = get(root); var mem = get(root);
@@ -258,7 +168,7 @@ public static class CreoAsm
/// <summary>Testo della BOM, identico byte per byte a quello esportato da Creo (UTF-8 con /// <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> /// BOM, CRLF - vedi GeneraBom per la scrittura su file).</summary>
private static string Render(string root, Func<string, List<Membro>?> get) private static string Render(string root, Func<string, List<AsmRecordReader.Membro>?> get)
{ {
var pezzi = new List<string>(); var pezzi = new List<string>();
foreach (var blocco in Blocchi(root, get)) foreach (var blocco in Blocchi(root, get))
@@ -286,6 +196,6 @@ public static class CreoAsm
public static string GeneraBom(string percorsoAsm, IEnumerable<string> cartelle) public static string GeneraBom(string percorsoAsm, IEnumerable<string> cartelle)
{ {
byte[] dati = File.ReadAllBytes(percorsoAsm); byte[] dati = File.ReadAllBytes(percorsoAsm);
return Render(NomeProprio(dati), CreaFinder(cartelle)); return Render(AsmRecordReader.NomeProprio(dati), CreaFinder(cartelle));
} }
} }