Compare commits
20
Commits
379deec986
..
0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e29a1bf90a | ||
|
|
28aa16837b | ||
|
|
d9f16f2844 | ||
|
|
db4c444473 | ||
|
|
13f6de6402 | ||
|
|
54f267b508 | ||
|
|
0ae2a5e7cf | ||
|
|
b136e7f49b | ||
|
|
cf4c2e8e29 | ||
|
|
9af426ca1b | ||
|
|
28a72c80b2 | ||
|
|
cce377a091 | ||
|
|
60dfd26e8b | ||
|
|
35009b5cd3 | ||
|
|
3cfc040fb0 | ||
|
|
3ea582e645 | ||
|
|
9ecdccb6bb | ||
|
|
5c9661eb12 | ||
|
|
353a1028d9 | ||
|
|
4d5717f827 |
No files matched your search
@@ -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).
|
||||||
@@ -36,108 +36,91 @@ 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 — in
|
Il programma è tutto in `PrepStep/` (nessun altro progetto nella solution). Dettaglio
|
||||||
passato esisteva anche una CLI separata e uno script Python di indicizzazione,
|
completo file-per-file in [`docs/architettura.md`](docs/architettura.md); qui solo il flusso
|
||||||
entrambi rimossi: ora l'unico programma è questo, con GUI e indicizzazione integrate).
|
che conta per capire il prodotto.
|
||||||
|
|
||||||
- **Program.cs** — entry point WinForms standard, avvia `Form1`.
|
`Form1.cs` è un wizard a 5 fasi (una sola visibile alla volta, `_panelFasi[]`, si avanza con
|
||||||
- **Form1.cs** / **Form1.Designer.cs** — finestra principale, costruita come wizard: una
|
"Avanti" solo se `AvantiConsentito`), UI costruita interamente in codice (`BuildUi`, nessun
|
||||||
sola fase alla volta occupa tutta la finestra (`_panelFasi[]`/`NomiFasi`), si avanza
|
designer visuale):
|
||||||
con "Avanti" solo quando la fase corrente è pronta (`AvantiConsentito`). La UI è
|
|
||||||
costruita interamente in codice in `Form1.BuildUi()` (nessun designer visuale usato).
|
|
||||||
`Form1.Designer.cs` contiene solo lo scheletro minimo (`InitializeComponent`/`Dispose`)
|
|
||||||
generato dal template. Le fasi, in ordine:
|
|
||||||
1. **Database** — carica/aggiorna l'indice, mostra data ultimo aggiornamento; qui vive
|
|
||||||
anche la **cartella di lavoro** (`_cartellaLavoro`, mostrata in `_lblCartellaLavoro`,
|
|
||||||
cambiabile con "Cambia cartella..." → `ScegliCartellaLavoro`): destinazione di CSV,
|
|
||||||
`raw/` per tutte le fasi successive (gli STEP esportati a mano con Creo vanno in
|
|
||||||
`step/`, dentro la stessa cartella). All'avvio
|
|
||||||
`InizializzaCartellaLavoro` propone l'ultima usata (ricordata in
|
|
||||||
`%AppData%/PrepStep/ultima_cartella_lavoro.txt`, stesso pattern di
|
|
||||||
`ultimo_db.txt` per il DB) se esiste ancora, altrimenti `AppContext.BaseDirectory`
|
|
||||||
— non dipende da un marker di repo (niente più `CLAUDE.md`/`TrovaRadiceRepo`),
|
|
||||||
quindi funziona identica anche sull'exe pubblicato standalone (`PrepStep.exe`)
|
|
||||||
copiato su un'altra macchina. Qui vive anche "Pulisci raw e step" (manutenzione,
|
|
||||||
non fa parte del flusso a valle).
|
|
||||||
2. **Importa Distinta Base** — `DistintaBaseImporter.ConvertiInCsv` genera il CSV dei
|
|
||||||
componenti nella cartella di lavoro; se il nome del file segue la convenzione del
|
|
||||||
gestionale (`DBP_<codice>_<timestamp>`), l'assieme generale viene aggiunto come riga
|
|
||||||
in più nel CSV stesso (il foglio elenca solo i componenti sotto l'assieme, non
|
|
||||||
l'assieme in sé). Da qui in avanti l'assieme generale è un componente come tutti
|
|
||||||
gli altri, nessun caso speciale nel resto del codice.
|
|
||||||
3. **Crea cartella raw** — cerca nell'indice il percorso CAD di ogni componente del CSV
|
|
||||||
(`TrovaPercorsiCad`) e lo copia in `raw/` dentro la cartella di lavoro
|
|
||||||
(`CopiaFileTrovati`, parallela — vedi `docs/copia-raw-parallela.md`).
|
|
||||||
4. **Trova componenti referenziati** — legge euristicamente i metadati binari dei `.asm`
|
|
||||||
già in `raw/` (`AssemblyComponentScanner`) per risolvere ricorsivamente i
|
|
||||||
sotto-assiemi/parti che contengono (`RisolviAlberoComponenti`), copiandoli anch'essi
|
|
||||||
in `raw/`.
|
|
||||||
5. **Verifica completezza** — appena si entra nella fase, `AggiornaAnteprimaAsm`
|
|
||||||
mostra in una `ListView` a icone i file `.asm` della distinta importata (esclusi i
|
|
||||||
sotto-assiemi di fase 4) già presenti in `raw/` — letti dal filesystem locale, non
|
|
||||||
dall'indice/share — trascinabili (drag&drop, `ItemDrag` → `DoDragDrop` con
|
|
||||||
`DataFormats.FileDrop`) direttamente su Creo o Esplora risorse, per l'esportazione
|
|
||||||
STEP con Creo Distributed Batch. "Verifica completezza assiemi" controlla
|
|
||||||
ricorsivamente (sui percorsi originali in indice, non su `raw/`) se tutti i
|
|
||||||
sotto-componenti di ogni assieme della distinta sono nell'indice.
|
|
||||||
- **Indexer.cs** — logica di scansione del disco e scrittura/aggiornamento del DB
|
|
||||||
SQLite (`Indexer.ScanAsync`). Cammina tutte le cartelle di primo livello trovate sotto
|
|
||||||
la radice (scoperte dinamicamente con `Directory.GetDirectories`, non un elenco fisso)
|
|
||||||
con un walk manuale a stack (`Walk`, più veloce di `EnumerateFiles(..., AllDirectories)`
|
|
||||||
su share di rete con centinaia di migliaia di file), estrae da ogni nome file il
|
|
||||||
pattern Pro-E `<codice>.<tipo>.<versione>` via regex, e fa un upsert incrementale
|
|
||||||
riga per riga (confronto per `percorso_completo` + dimensione + data modifica).
|
|
||||||
Riporta il progresso tramite `IProgress<ScanProgress>`, marshalato automaticamente
|
|
||||||
sul thread UI. Contiene anche lo schema SQL del DB (tabella `files` + vista
|
|
||||||
`files_ultima_versione`, che seleziona solo l'ultima versione per
|
|
||||||
`(cartella_radice, codice, tipo_file)` usando `ROW_NUMBER() OVER (...)` — non un
|
|
||||||
self-join con `GROUP BY`, che su ~200k righe degenera in tempi quasi quadratici).
|
|
||||||
- **ProgressForm.cs** — dialog modale mostrato durante l'aggiornamento indice: barra
|
|
||||||
di progresso, conteggi nuovi/aggiornati/invariati/rimossi, tempo trascorso e stima
|
|
||||||
del tempo rimanente (calcolata dal ritmo file/secondo rispetto al totale atteso,
|
|
||||||
preso dal conteggio della scansione precedente salvato nel DB).
|
|
||||||
- **UiStile.cs** — stile bottoni condiviso da `Form1` e dai dialog secondari
|
|
||||||
(`Primario`/`Secondario`, entrambi `AutoSize` col padding invece di una `Size` fissa
|
|
||||||
in pixel, per evitare che il testo vada a capo e si tagli quando non ci sta) e
|
|
||||||
`Arrotonda` per gli angoli dei bottoni "Avanti"/"Indietro" del wizard.
|
|
||||||
- **app.ico** — icona dell'app (logo Cevolani + pezzo + STEP, sfondo trasparente),
|
|
||||||
referenziata da `<ApplicationIcon>` in `PrepStep.csproj` e riletta a runtime in
|
|
||||||
`Form1.BuildUi()` via `Icon.ExtractAssociatedIcon(Application.ExecutablePath)` per
|
|
||||||
la titlebar (stessa icona dell'exe, nessun file duplicato nell'output).
|
|
||||||
|
|
||||||
### Schema del DB (`cad_index.db`)
|
1. **Database** — carica/aggiorna `cad_index.db` (`Indexer.ScanAsync`, radice disco di
|
||||||
|
default `X:\` hardcoded in `RadiceDiscoDefault`); qui vive anche la **cartella di lavoro**
|
||||||
|
(destinazione di CSV/`raw/`/`step/`), proposta dall'ultima usata o dalla cartella del DB se
|
||||||
|
non c'è ancora nulla di salvato — vedi `docs/db-cad-index.md` per il dettaglio di schema e
|
||||||
|
risoluzione del percorso DB.
|
||||||
|
2. **Importa Distinta Base** — `DistintaBaseImporter.ConvertiInCsv` genera il CSV
|
||||||
|
componente;descrizione nella cartella di lavoro; se il file segue la convenzione del
|
||||||
|
gestionale (`DBP_<codice>_<timestamp>`), aggiunge l'assieme generale come riga in più (da
|
||||||
|
quel momento è un componente come tutti gli altri, nessun caso speciale nel resto del
|
||||||
|
codice).
|
||||||
|
3. **Crea cartella raw** — `TrovaPercorsiCad` + `CopiaFileTrovati` (parallela, vedi
|
||||||
|
`docs/copia-raw-parallela.md`) copiano in `raw/` i file CAD dei componenti del CSV trovati
|
||||||
|
nell'indice.
|
||||||
|
4. **Trova componenti referenziati** — legge i `.asm` in `raw/` per risolvere ricorsivamente
|
||||||
|
sotto-assiemi/parti (`RisolviAlberoComponenti`), copiandoli anch'essi in `raw/`. Usa
|
||||||
|
`AsmRecordReader` (parser esatto dei record binari, `docs/formato-asm-record.md`) e ripiega
|
||||||
|
su `AssemblyComponentScanner` (euristica testuale, `docs/formato-asm.md`) solo se il file
|
||||||
|
non rispetta il formato validato (`InvalidDataException`). 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
|
||||||
|
Creo per l'esportazione STEP (Distributed Batch, manuale, fuori dall'app).
|
||||||
|
|
||||||
Tabella `files`: un record per ogni file trovato, con `percorso_completo` (chiave
|
Menu in cima alla finestra: "File → Nuova estrazione" (Ctrl+N) azzera lo stato delle fasi 2-5
|
||||||
univoca), `cartella_radice`, `famiglia` (primo livello di sottocartella),
|
per ripartire con una nuova distinta, senza toccare DB/cartella di lavoro della fase 1;
|
||||||
`codice`/`tipo_file`/`versione` estratti dal nome file, dimensione, data modifica e
|
"Aiuto → Guida" (F1) apre una sintesi interattiva delle 5 fasi (`GuidaForm.cs`).
|
||||||
timestamp dell'ultima scansione (`scansionato_il`, usato per rilevare ed eliminare i
|
|
||||||
file non più presenti su disco a fine scansione).
|
|
||||||
|
|
||||||
Vista `files_ultima_versione`: stessa struttura, filtrata alla sola versione più
|
|
||||||
recente per codice+tipo.
|
|
||||||
|
|
||||||
### Percorso del DB
|
|
||||||
|
|
||||||
All'avvio la app cerca `cad_index.db` accanto all'eseguibile o nella working
|
|
||||||
directory corrente; se non lo trova, l'utente può selezionarlo con "Apri DB...".
|
|
||||||
Il bottone "Aggiorna indice" rilancia `Indexer.ScanAsync` sullo stesso path (radice
|
|
||||||
disco di default `X:\`, hardcoded in `Form1.RadiceDiscoDefault`).
|
|
||||||
|
|
||||||
## Script standalone (`scripts/`)
|
## Script standalone (`scripts/`)
|
||||||
|
|
||||||
`scripts/` contiene C# file-based apps (.NET 10, `dotnet run scripts/Nome.cs -- arg1 arg2`),
|
`scripts/` contiene C# file-based apps (.NET 10, `dotnet run scripts/Nome.cs -- arg1 arg2`,
|
||||||
senza csproj — dipendenze NuGet extra si aggiungono con `#:package Nome@versione` in cima
|
lanciati dalla root del repo — **non** da dentro `PrepStep/`, che ha un `.csproj` `WinExe`:
|
||||||
al file. `ExtractCreoStrings.cs` estrae la BOM di un `.asm` risolvendo ricorsivamente i
|
`dotnet run` lì avvia la GUI vera invece dello script). Elenco e dettaglio di ciascuno in
|
||||||
sotto-assiemi contro `cad_index.db` (stessa logica di `Form1.AssiemeCompleto`, solo codici
|
`docs/architettura.md`.
|
||||||
numerici Pro-E); `BomViaDb.cs` fa lo stesso ma con la tokenizzazione generica validata contro
|
|
||||||
il DB, usata per intercettare anche i nomi alfabetici delle parti commerciali (vedi
|
|
||||||
`docs/formato-asm.md`); `Analyze.cs` dumpa il contesto testo/hex attorno a un pattern in un
|
|
||||||
file binario, per investigare a mano nuovi falsi positivi prima di toccare la regex; `Query.cs`
|
|
||||||
esegue una query SQL ad-hoc contro `cad_index.db` da riga di comando.
|
|
||||||
|
|
||||||
Il formato binario `.asm`, l'euristica di estrazione codici e i falsi positivi noti (ID vista
|
Il pattern da preservare: gli script referenziano `PrepStep/PrepStep.csproj` via `#:project`
|
||||||
`ModelView#`, self-reference, riferimenti fantasma a codici superati/rinominati — sintatticamente
|
invece di duplicare la logica di lettura dei `.asm`, che vive **solo** nel progetto
|
||||||
identici a un componente vero, non filtrabili in modo affidabile) sono documentati in
|
principale — un'unica fonte di verità condivisa da GUI e script. Due famiglie distinte, non
|
||||||
`docs/formato-asm.md`.
|
intercambiabili:
|
||||||
|
|
||||||
|
- `ExtractCreoStrings.cs`/`BomViaDb.cs` usano `AssemblyComponentScanner` (euristica testuale,
|
||||||
|
`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`.
|
||||||
@@ -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"));
|
||||||
|
}
|
||||||
@@ -148,4 +148,54 @@ public class AssemblyComponentScannerTests
|
|||||||
}
|
}
|
||||||
finally { File.Delete(percorso); }
|
finally { File.Delete(percorso); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Riconosce_codice_con_suffisso_anche_senza_dettaglio_nel_placement()
|
||||||
|
{
|
||||||
|
// Regressione: caso reale 9284218024b.asm -> 2546220101_C01.._C04. Il placement
|
||||||
|
// riporta solo il codice padre senza suffisso ("Componente ID N (2546220101.PRT)"),
|
||||||
|
// il dettaglio "_C01" compare solo altrove come stringa ripetuta, mai dentro un
|
||||||
|
// placement esplicito. Un codice nudo nella stessa situazione andrebbe scartato
|
||||||
|
// (vedi Scarta_codice_ripetuto_senza_un_vero_placement), ma un codice con suffisso
|
||||||
|
// di variante è considerato "forma nota": bastano le occorrenze ripetute. Prima
|
||||||
|
// che questa eccezione fosse portata anche in scripts/BomViaDb.cs, lo script
|
||||||
|
// perdeva l'intero sotto-assieme (vedi docs/formato-asm.md).
|
||||||
|
string percorso = ScriviAsmFinto(
|
||||||
|
"Componente ID 1 (2546220101.PRT) x 2546220101_C01 y 2546220101_C01 z");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var codici = AssemblyComponentScanner.EstraiCodiciReferenziati(percorso);
|
||||||
|
Assert.Contains("2546220101_C01", codici);
|
||||||
|
}
|
||||||
|
finally { File.Delete(percorso); }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Il_placement_e_indipendente_per_ogni_file_scansionato()
|
||||||
|
{
|
||||||
|
// Regressione: caso reale 9244228558.asm -> 9244236104 dentro 9244228558_C01.asm.
|
||||||
|
// Lo stesso codice compare come riferimento fantasma (nudo, senza placement) nel
|
||||||
|
// file dell'assieme padre, e con un vero placement nel file del sotto-assieme che
|
||||||
|
// lo contiene realmente. La proprietà che rende sicura la BFS del chiamante
|
||||||
|
// (Form1.RisolviAlberoComponenti, scripts/BomViaDb.cs, scripts/ExtractCreoStrings.cs)
|
||||||
|
// è che EstraiCodiciReferenziati valuta il placement da zero per ogni file, senza
|
||||||
|
// stato condiviso tra le chiamate: un fallimento nel file padre non deve
|
||||||
|
// condizionare il risultato quando lo stesso codice viene ripassato per il file
|
||||||
|
// figlio (vedi docs/formato-asm.md, "Trappola nella BFS").
|
||||||
|
string filePadre = ScriviAsmFinto("assieme 11112222 padre 11112222 fine");
|
||||||
|
string fileFiglio = ScriviAsmFinto("Componente ID 1 (11112222.PRT) x 11112222 y");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Assert.DoesNotContain("11112222", AssemblyComponentScanner.EstraiCodiciReferenziati(filePadre));
|
||||||
|
Assert.Contains("11112222", AssemblyComponentScanner.EstraiCodiciReferenziati(fileFiglio));
|
||||||
|
}
|
||||||
|
finally { File.Delete(filePadre); File.Delete(fileFiglio); }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("9284208188", "9284208188")]
|
||||||
|
[InlineData("9284208188_D600", "9284208188")]
|
||||||
|
[InlineData("9244228558_C03-01", "9244228558")]
|
||||||
|
public void CodiceRadice_estrae_la_parte_prima_del_primo_underscore(string codice, string radiceAttesa) =>
|
||||||
|
Assert.Equal(radiceAttesa, AssemblyComponentScanner.CodiceRadice(codice));
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using PrepStep;
|
||||||
|
|
||||||
|
namespace PrepStep.Tests;
|
||||||
|
|
||||||
|
/// <summary>Integration test di Indexer.ScanAsync: disco temporaneo reale + DB SQLite reale,
|
||||||
|
/// nessun dato inserito a mano (a differenza di CaseInsensitiveLookupIntegrationTests, che
|
||||||
|
/// verifica solo le query a valle su righe già presenti). Copre la scansione vera - upsert
|
||||||
|
/// incrementale, rilevamento nuovi/aggiornati/invariati/rimossi, esclusione cartelle "temp_" -
|
||||||
|
/// che prima di questi test non aveva nessuna copertura (solo Indexer.ParseFilename era
|
||||||
|
/// testato).</summary>
|
||||||
|
public class IndexerScanTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly string _root = Path.Combine(Path.GetTempPath(), $"cadsearch_scan_{Guid.NewGuid():N}");
|
||||||
|
private readonly string _dbPath;
|
||||||
|
|
||||||
|
public IndexerScanTests()
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(_root);
|
||||||
|
_dbPath = Path.Combine(_root, "indice.db");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
SqliteConnection.ClearAllPools();
|
||||||
|
Directory.Delete(_root, recursive: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ScriviFile(string percorso, string contenuto = "dati")
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(percorso)!);
|
||||||
|
File.WriteAllText(percorso, contenuto);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>IProgress<T> sincrono: Progress<T> marshala il report tramite
|
||||||
|
/// SynchronizationContext.Post (o il thread pool, se assente, come nei test) - l'ultimo
|
||||||
|
/// valore non è garantito visibile subito dopo l'await. Qui Report gira sullo stesso
|
||||||
|
/// thread di background di Indexer.Scan, prima che Task.Run completi: l'ultimo valore è
|
||||||
|
/// quindi affidabile non appena l'await ritorna.</summary>
|
||||||
|
private sealed class ProgressoSincrono : IProgress<ScanProgress>
|
||||||
|
{
|
||||||
|
public ScanProgress? Ultimo { get; private set; }
|
||||||
|
public void Report(ScanProgress value) => Ultimo = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ScanAsync()
|
||||||
|
{
|
||||||
|
await Indexer.ScanAsync(_root, _dbPath, new ProgressoSincrono(), CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<(string Codice, string Tipo, string? Famiglia, string CartellaRadice)> LeggiFiles()
|
||||||
|
{
|
||||||
|
using var connection = new SqliteConnection($"Data Source={_dbPath};Mode=ReadOnly");
|
||||||
|
connection.Open();
|
||||||
|
using var cmd = connection.CreateCommand();
|
||||||
|
cmd.CommandText = "SELECT codice, tipo_file, famiglia, cartella_radice FROM files";
|
||||||
|
using var reader = cmd.ExecuteReader();
|
||||||
|
var righe = new List<(string, string, string?, string)>();
|
||||||
|
while (reader.Read())
|
||||||
|
righe.Add((reader.GetString(0), reader.GetString(1),
|
||||||
|
reader.IsDBNull(2) ? null : reader.GetString(2), reader.GetString(3)));
|
||||||
|
return righe;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Indicizza_un_file_estraendo_codice_tipo_versione_e_famiglia()
|
||||||
|
{
|
||||||
|
ScriviFile(Path.Combine(_root, "__Corpi", "442", "9244228573.asm.16"));
|
||||||
|
|
||||||
|
await ScanAsync();
|
||||||
|
|
||||||
|
var righe = LeggiFiles();
|
||||||
|
Assert.Single(righe);
|
||||||
|
Assert.Equal(("9244228573", "asm", "442", "__Corpi"), righe[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Esclude_le_cartelle_radice_che_iniziano_con_temp_()
|
||||||
|
{
|
||||||
|
ScriviFile(Path.Combine(_root, "temp_paris", "vecchio.asm.1"));
|
||||||
|
ScriviFile(Path.Combine(_root, "__Corpi", "9244228573.asm.16"));
|
||||||
|
|
||||||
|
await ScanAsync();
|
||||||
|
|
||||||
|
var righe = LeggiFiles();
|
||||||
|
Assert.Single(righe);
|
||||||
|
Assert.Equal("__Corpi", righe[0].CartellaRadice);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task File_non_modificato_resta_invariato_tra_due_scansioni()
|
||||||
|
{
|
||||||
|
string percorso = Path.Combine(_root, "__Corpi", "file.prt.1");
|
||||||
|
ScriviFile(percorso);
|
||||||
|
await ScanAsync();
|
||||||
|
|
||||||
|
var progress = new ProgressoSincrono();
|
||||||
|
await Indexer.ScanAsync(_root, _dbPath, progress, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.NotNull(progress.Ultimo);
|
||||||
|
Assert.Equal(0, progress.Ultimo!.Nuovi);
|
||||||
|
Assert.Equal(0, progress.Ultimo.Aggiornati);
|
||||||
|
Assert.Equal(1, progress.Ultimo.Invariati);
|
||||||
|
Assert.Equal(0, progress.Ultimo.Rimossi);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task File_modificato_viene_rilevato_come_aggiornato()
|
||||||
|
{
|
||||||
|
string percorso = Path.Combine(_root, "__Corpi", "file.prt.1");
|
||||||
|
ScriviFile(percorso, "versione 1");
|
||||||
|
await ScanAsync();
|
||||||
|
|
||||||
|
// Contenuto più lungo -> dimensione diversa, quindi rilevato come modifica anche se
|
||||||
|
// il file system non aggiornasse data_modifica entro la risoluzione del clock.
|
||||||
|
ScriviFile(percorso, "versione 2 con più testo dentro");
|
||||||
|
|
||||||
|
var progress = new ProgressoSincrono();
|
||||||
|
await Indexer.ScanAsync(_root, _dbPath, progress, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.NotNull(progress.Ultimo);
|
||||||
|
Assert.Equal(1, progress.Ultimo!.Aggiornati);
|
||||||
|
Assert.Equal(0, progress.Ultimo.Nuovi);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task File_cancellato_dal_disco_viene_rimosso_dal_db_alla_scansione_successiva()
|
||||||
|
{
|
||||||
|
string percorso = Path.Combine(_root, "__Corpi", "file.prt.1");
|
||||||
|
ScriviFile(percorso);
|
||||||
|
await ScanAsync();
|
||||||
|
Assert.Single(LeggiFiles());
|
||||||
|
|
||||||
|
File.Delete(percorso);
|
||||||
|
await ScanAsync();
|
||||||
|
|
||||||
|
Assert.Empty(LeggiFiles());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Files_ultima_versione_tiene_solo_la_versione_piu_recente_per_codice_e_tipo()
|
||||||
|
{
|
||||||
|
ScriviFile(Path.Combine(_root, "__Corpi", "9244228573.asm.40"));
|
||||||
|
ScriviFile(Path.Combine(_root, "__Corpi", "9244228573.asm.41"));
|
||||||
|
|
||||||
|
await ScanAsync();
|
||||||
|
|
||||||
|
using var connection = new SqliteConnection($"Data Source={_dbPath};Mode=ReadOnly");
|
||||||
|
connection.Open();
|
||||||
|
using var cmd = connection.CreateCommand();
|
||||||
|
cmd.CommandText = "SELECT versione FROM files_ultima_versione WHERE codice = '9244228573'";
|
||||||
|
using var reader = cmd.ExecuteReader();
|
||||||
|
Assert.True(reader.Read());
|
||||||
|
Assert.Equal(41L, reader.GetInt64(0));
|
||||||
|
Assert.False(reader.Read());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task File_senza_pattern_codice_tipo_versione_va_in_tipo_altro()
|
||||||
|
{
|
||||||
|
ScriviFile(Path.Combine(_root, "__Corpi", "leggimi.txt"));
|
||||||
|
|
||||||
|
await ScanAsync();
|
||||||
|
|
||||||
|
var righe = LeggiFiles();
|
||||||
|
Assert.Single(righe);
|
||||||
|
Assert.Equal("altro", righe[0].Tipo);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using PrepStep;
|
||||||
|
|
||||||
|
namespace PrepStep.Tests;
|
||||||
|
|
||||||
|
/// <summary>End-to-end: disco CAD finto -> Indexer.ScanAsync -> cad_index.db reale -> BFS di
|
||||||
|
/// risoluzione albero componenti (stesso algoritmo di Form1.RisolviAlberoComponenti e
|
||||||
|
/// scripts/BomViaDb.cs, che ora delegano entrambi ad AssemblyComponentScanner - vedi
|
||||||
|
/// docs/formato-asm.md). Nessuna UI coinvolta: verifica che le tre fasi si compongano
|
||||||
|
/// correttamente, non solo che ciascuna funzioni isolata. I contenuti .asm/.prt sono testo
|
||||||
|
/// sintetico con gli stessi marcatori documentati per un vero file Creo ("Componente ID N
|
||||||
|
/// (...)"), non file binari reali - sufficiente perché AssemblyComponentScanner legge il file
|
||||||
|
/// come testo Latin1 indipendentemente dal resto del formato.</summary>
|
||||||
|
public class PipelineEndToEndTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly string _root = Path.Combine(Path.GetTempPath(), $"cadsearch_e2e_{Guid.NewGuid():N}");
|
||||||
|
private readonly string _dbPath;
|
||||||
|
|
||||||
|
public PipelineEndToEndTests()
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(_root);
|
||||||
|
_dbPath = Path.Combine(_root, "indice.db");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
SqliteConnection.ClearAllPools();
|
||||||
|
Directory.Delete(_root, recursive: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ScriviFile(string percorso, string contenuto)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(percorso)!);
|
||||||
|
File.WriteAllText(percorso, contenuto);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Stesso algoritmo BFS di scripts/BomViaDb.cs: qui non è duplicazione a rischio
|
||||||
|
/// come lo era prima della riscrittura (vedi docs/formato-asm.md) - è la prova, in un
|
||||||
|
/// test, che comporre AssemblyComponentScanner con un semplice ciclo di accodamento dà il
|
||||||
|
/// risultato atteso sopra un indice reale, non un mock.</summary>
|
||||||
|
private static (SortedSet<string> Trovati, List<string> NonTrovati) RisolviAlbero(
|
||||||
|
string asmIniziale, Dictionary<string, List<(string Tipo, string Percorso)>> codiciNoti)
|
||||||
|
{
|
||||||
|
string codiceProprio = Path.GetFileName(asmIniziale).Split('.')[0];
|
||||||
|
string codiceRadice = AssemblyComponentScanner.CodiceRadice(codiceProprio);
|
||||||
|
var trovati = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var nonTrovati = new List<string>();
|
||||||
|
var visti = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { codiceProprio, codiceRadice };
|
||||||
|
var coda = new Queue<string>(new[] { asmIniziale });
|
||||||
|
|
||||||
|
while (coda.Count > 0)
|
||||||
|
{
|
||||||
|
var asm = coda.Dequeue();
|
||||||
|
foreach (var codice in AssemblyComponentScanner.EstraiCodiciReferenziati(asm, codiciNoti.Keys))
|
||||||
|
{
|
||||||
|
if (!visti.Add(codice))
|
||||||
|
continue;
|
||||||
|
if (!codiciNoti.TryGetValue(codice, out var componenti))
|
||||||
|
{
|
||||||
|
nonTrovati.Add(codice);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
trovati.Add(codice);
|
||||||
|
foreach (var (tipo, percorso) in componenti)
|
||||||
|
if (tipo == "asm")
|
||||||
|
coda.Enqueue(percorso);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (trovati, nonTrovati);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<string, List<(string Tipo, string Percorso)>> CaricaCodiciNoti()
|
||||||
|
{
|
||||||
|
using var connection = new SqliteConnection($"Data Source={_dbPath};Mode=ReadOnly");
|
||||||
|
connection.Open();
|
||||||
|
using var cmd = connection.CreateCommand();
|
||||||
|
cmd.CommandText = "SELECT codice, tipo_file, percorso_completo FROM files_ultima_versione "
|
||||||
|
+ "WHERE LOWER(tipo_file) IN ('prt', 'asm')";
|
||||||
|
using var reader = cmd.ExecuteReader();
|
||||||
|
var codiciNoti = new Dictionary<string, List<(string, string)>>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
string codice = reader.GetString(0);
|
||||||
|
var lista = codiciNoti.TryGetValue(codice, out var l) ? l : codiciNoti[codice] = new();
|
||||||
|
lista.Add((reader.GetString(1), reader.GetString(2)));
|
||||||
|
}
|
||||||
|
return codiciNoti;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Risolve_lalbero_completo_a_partire_dal_disco_indicizzato()
|
||||||
|
{
|
||||||
|
// TOP -> CHILD (vero placement, 2 occorrenze) -> LEAF (vero placement, 2 occorrenze).
|
||||||
|
// "99999999" compare una sola volta in TOP: rumore binario, va scartato anche se
|
||||||
|
// fosse indicizzato (vedi AssemblyComponentScannerTests.Scarta_codici_con_una_sola_occorrenza).
|
||||||
|
ScriviFile(Path.Combine(_root, "MACCHINE", "GRUPPO1", "12345678.asm.1"),
|
||||||
|
"inizio Componente ID 1 (23456789.ASM) testo Componente ID 2 (23456789.ASM) rumore 99999999 fine");
|
||||||
|
ScriviFile(Path.Combine(_root, "MACCHINE", "GRUPPO1", "23456789.asm.1"),
|
||||||
|
"Componente ID 1 (34567890.PRT) blah Componente ID 2 (34567890.PRT) fine");
|
||||||
|
ScriviFile(Path.Combine(_root, "MACCHINE", "GRUPPO1", "34567890.prt.1"), "dati foglia");
|
||||||
|
ScriviFile(Path.Combine(_root, "MACCHINE", "GRUPPO1", "99999999.prt.1"), "mai referenziato davvero");
|
||||||
|
|
||||||
|
var progress = new Progress<ScanProgress>();
|
||||||
|
await Indexer.ScanAsync(_root, _dbPath, progress, CancellationToken.None);
|
||||||
|
|
||||||
|
var codiciNoti = CaricaCodiciNoti();
|
||||||
|
var asmIniziale = Path.Combine(_root, "MACCHINE", "GRUPPO1", "12345678.asm.1");
|
||||||
|
var (trovati, nonTrovati) = RisolviAlbero(asmIniziale, codiciNoti);
|
||||||
|
|
||||||
|
Assert.Equal(new[] { "23456789", "34567890" }, trovati);
|
||||||
|
Assert.DoesNotContain("99999999", trovati);
|
||||||
|
Assert.Empty(nonTrovati);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Segnala_come_non_trovato_un_componente_con_placement_vero_ma_mai_indicizzato()
|
||||||
|
{
|
||||||
|
// "23456789" ha un vero placement in TOP ma il suo file non esiste sul disco finto
|
||||||
|
// (mai scansionato -> non in cad_index.db): deve finire tra i "non trovati", non far
|
||||||
|
// fallire la risoluzione dell'albero.
|
||||||
|
ScriviFile(Path.Combine(_root, "MACCHINE", "GRUPPO1", "12345678.asm.1"),
|
||||||
|
"Componente ID 1 (23456789.ASM) testo Componente ID 2 (23456789.ASM) fine");
|
||||||
|
|
||||||
|
var progress = new Progress<ScanProgress>();
|
||||||
|
await Indexer.ScanAsync(_root, _dbPath, progress, CancellationToken.None);
|
||||||
|
|
||||||
|
var codiciNoti = CaricaCodiciNoti();
|
||||||
|
var asmIniziale = Path.Combine(_root, "MACCHINE", "GRUPPO1", "12345678.asm.1");
|
||||||
|
var (trovati, nonTrovati) = RisolviAlbero(asmIniziale, codiciNoti);
|
||||||
|
|
||||||
|
Assert.Empty(trovati);
|
||||||
|
Assert.Contains("23456789", nonTrovati);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task File_in_cartella_temp_non_entra_nellindice_e_quindi_non_viene_risolto()
|
||||||
|
{
|
||||||
|
// Stesso codice "23456789" esiste per davvero, ma solo dentro una cartella radice
|
||||||
|
// "temp_..." - esclusa dalla scansione (vedi Indexer.cs e docs/formato-asm.md, falso
|
||||||
|
// positivo su copie scollegate dentro temp_*). Il componente deve restare "non
|
||||||
|
// trovato", non risolversi per sbaglio contro la copia in temp_.
|
||||||
|
ScriviFile(Path.Combine(_root, "MACCHINE", "GRUPPO1", "12345678.asm.1"),
|
||||||
|
"Componente ID 1 (23456789.ASM) testo Componente ID 2 (23456789.ASM) fine");
|
||||||
|
ScriviFile(Path.Combine(_root, "temp_davide", "23456789.asm.1"), "copia scollegata");
|
||||||
|
|
||||||
|
var progress = new Progress<ScanProgress>();
|
||||||
|
await Indexer.ScanAsync(_root, _dbPath, progress, CancellationToken.None);
|
||||||
|
|
||||||
|
var codiciNoti = CaricaCodiciNoti();
|
||||||
|
var asmIniziale = Path.Combine(_root, "MACCHINE", "GRUPPO1", "12345678.asm.1");
|
||||||
|
var (trovati, nonTrovati) = RisolviAlbero(asmIniziale, codiciNoti);
|
||||||
|
|
||||||
|
Assert.Empty(trovati);
|
||||||
|
Assert.Contains("23456789", nonTrovati);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -70,7 +70,11 @@ namespace PrepStep;
|
|||||||
/// silenzio come "nudo senza placement" - lì verrà comunque scartato come "non in indice",
|
/// silenzio come "nudo senza placement" - lì verrà comunque scartato come "non in indice",
|
||||||
/// ma in modo tracciabile.
|
/// ma in modo tracciabile.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
static class AssemblyComponentScanner
|
// Pubblica (non solo internal) perché è anche l'unica fonte di verità per l'euristica di
|
||||||
|
// estrazione, referenziata via "#:project" dagli script standalone (scripts/BomViaDb.cs,
|
||||||
|
// scripts/ExtractCreoStrings.cs) invece che duplicata al loro interno - vedi
|
||||||
|
// docs/formato-asm.md, sezione sulla trappola del "codice duplicato in tre punti".
|
||||||
|
public static class AssemblyComponentScanner
|
||||||
{
|
{
|
||||||
private static readonly Regex CodiceNumericoRegex = new(
|
private static readonly Regex CodiceNumericoRegex = new(
|
||||||
@"(?<!ModelView#)\b\d{8,12}[A-Za-z]{0,2}(?:_[A-Za-z]?\d{2,4})?\b",
|
@"(?<!ModelView#)\b\d{8,12}[A-Za-z]{0,2}(?:_[A-Za-z]?\d{2,4})?\b",
|
||||||
|
|||||||
+273
-145
@@ -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.
|
||||||
@@ -56,8 +57,8 @@ public partial class Form1 : Form
|
|||||||
BuildUi();
|
BuildUi();
|
||||||
Load += (_, _) =>
|
Load += (_, _) =>
|
||||||
{
|
{
|
||||||
InizializzaCartellaLavoro();
|
|
||||||
InizializzaDatabase();
|
InizializzaDatabase();
|
||||||
|
InizializzaCartellaLavoro();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,15 +92,23 @@ 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: linea sottile in cima e
|
||||||
|
// sfondo bianco, invece dei bottoni appoggiati direttamente sul contenuto/sulla barra
|
||||||
|
// di stato senza alcuna divisione visiva.
|
||||||
|
// 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.
|
||||||
|
var navContainer = new Panel { Dock = DockStyle.Bottom, Height = 64, BackColor = Color.White };
|
||||||
|
var navSeparatore = new Panel
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Top,
|
||||||
|
Height = 1,
|
||||||
|
BackColor = Color.FromArgb(220, 220, 220),
|
||||||
|
};
|
||||||
var navPanel = new FlowLayoutPanel
|
var navPanel = new FlowLayoutPanel
|
||||||
{
|
{
|
||||||
Dock = DockStyle.Bottom,
|
Dock = DockStyle.Fill,
|
||||||
// I bottoni si auto-dimensionano al testo (UiStile): l'altezza qui deve stare
|
|
||||||
// comoda sopra al loro MinimumSize (32) più il padding, altrimenti vengono
|
|
||||||
// tagliati sul fondo della barra invece di restarci dentro.
|
|
||||||
Height = 56,
|
|
||||||
FlowDirection = FlowDirection.RightToLeft,
|
FlowDirection = FlowDirection.RightToLeft,
|
||||||
Padding = new Padding(12, 10, 12, 10),
|
Padding = new Padding(16, 14, 16, 14),
|
||||||
};
|
};
|
||||||
_btnAvanti.Text = "Avanti ›";
|
_btnAvanti.Text = "Avanti ›";
|
||||||
UiStile.Primario(_btnAvanti);
|
UiStile.Primario(_btnAvanti);
|
||||||
@@ -113,6 +122,8 @@ public partial class Form1 : Form
|
|||||||
UiStile.Arrotonda(_btnIndietro);
|
UiStile.Arrotonda(_btnIndietro);
|
||||||
_btnIndietro.Click += (_, _) => VaiAllaFase(_faseAttuale - 1);
|
_btnIndietro.Click += (_, _) => VaiAllaFase(_faseAttuale - 1);
|
||||||
navPanel.Controls.Add(_btnIndietro);
|
navPanel.Controls.Add(_btnIndietro);
|
||||||
|
navContainer.Controls.Add(navPanel);
|
||||||
|
navContainer.Controls.Add(navSeparatore);
|
||||||
|
|
||||||
var statusPanel = new Panel { Dock = DockStyle.Bottom, Height = 28, BackColor = SystemColors.ControlLight };
|
var statusPanel = new Panel { Dock = DockStyle.Bottom, Height = 28, BackColor = SystemColors.ControlLight };
|
||||||
_lblStatus.Dock = DockStyle.Fill;
|
_lblStatus.Dock = DockStyle.Fill;
|
||||||
@@ -133,10 +144,28 @@ public partial class Form1 : Form
|
|||||||
_faseContainer.Controls.Add(panel);
|
_faseContainer.Controls.Add(panel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// 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(navPanel);
|
|
||||||
Controls.Add(statusPanel);
|
Controls.Add(statusPanel);
|
||||||
|
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
|
||||||
@@ -180,7 +209,7 @@ public partial class Form1 : Form
|
|||||||
private void AggiornaAnteprimaAsm()
|
private void AggiornaAnteprimaAsm()
|
||||||
{
|
{
|
||||||
_lstAnteprimaAsm.Items.Clear();
|
_lstAnteprimaAsm.Items.Clear();
|
||||||
_lstAnteprimaAsm.LargeImageList!.Images.Clear();
|
_lstAnteprimaAsm.SmallImageList!.Images.Clear();
|
||||||
if (_ultimaCartellaRaw is null || !Directory.Exists(_ultimaCartellaRaw) || _righeCorrenti is null)
|
if (_ultimaCartellaRaw is null || !Directory.Exists(_ultimaCartellaRaw) || _righeCorrenti is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -204,8 +233,8 @@ public partial class Form1 : Form
|
|||||||
// ponytail: file irraggiungibile (share offline) -> icona generica, non blocca gli altri
|
// ponytail: file irraggiungibile (share offline) -> icona generica, non blocca gli altri
|
||||||
}
|
}
|
||||||
|
|
||||||
int indice = _lstAnteprimaAsm.LargeImageList.Images.Count;
|
int indice = _lstAnteprimaAsm.SmallImageList.Images.Count;
|
||||||
_lstAnteprimaAsm.LargeImageList.Images.Add(icona?.ToBitmap() ?? SystemIcons.WinLogo.ToBitmap());
|
_lstAnteprimaAsm.SmallImageList.Images.Add(icona?.ToBitmap() ?? SystemIcons.WinLogo.ToBitmap());
|
||||||
icona?.Dispose();
|
icona?.Dispose();
|
||||||
_lstAnteprimaAsm.Items.Add(new ListViewItem(Path.GetFileName(percorso), indice) { Tag = percorso });
|
_lstAnteprimaAsm.Items.Add(new ListViewItem(Path.GetFileName(percorso), indice) { Tag = percorso });
|
||||||
}
|
}
|
||||||
@@ -223,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
|
||||||
@@ -503,24 +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);
|
|
||||||
|
|
||||||
_btnCompletezza.Text = "Verifica completezza assiemi";
|
|
||||||
_btnCompletezza.Margin = new Padding(0, 0, 0, 12);
|
|
||||||
UiStile.Secondario(_btnCompletezza);
|
|
||||||
_btnCompletezza.Click += (_, _) => VerificaCompletezzaAssiemi();
|
|
||||||
top.Controls.Add(_btnCompletezza);
|
|
||||||
|
|
||||||
var lblAnteprimaAsm = new Label
|
var lblAnteprimaAsm = new Label
|
||||||
{
|
{
|
||||||
AutoSize = true,
|
AutoSize = true,
|
||||||
@@ -530,15 +565,28 @@ public partial class Form1 : Form
|
|||||||
};
|
};
|
||||||
top.Controls.Add(lblAnteprimaAsm);
|
top.Controls.Add(lblAnteprimaAsm);
|
||||||
|
|
||||||
// Dock=Fill invece di una Size fissa: la lista occupa tutto lo spazio verticale
|
// View.Details invece di LargeIcon: con codici Pro-E lunghi e simili tra loro
|
||||||
// rimasto sotto le istruzioni, così si vedono più file .asm senza scroll anche a
|
// ("9244218...", "9244228..."), le icone in griglia troncavano tutti i nomi allo stesso
|
||||||
// finestra ingrandita, invece dei ~160px fissi di prima.
|
// modo (illeggibili, indistinguibili) e lasciavano un grande vuoto sotto le poche righe.
|
||||||
_lstAnteprimaAsm.View = View.LargeIcon;
|
// Una lista a riga singola mostra il nome per intero, in linea con lo stile delle altre
|
||||||
|
// griglie dell'app (_gridImport/_gridRaw), restando comunque trascinabile.
|
||||||
|
_lstAnteprimaAsm.View = View.Details;
|
||||||
_lstAnteprimaAsm.MultiSelect = true;
|
_lstAnteprimaAsm.MultiSelect = true;
|
||||||
_lstAnteprimaAsm.LargeImageList = new ImageList { ImageSize = new Size(32, 32) };
|
_lstAnteprimaAsm.FullRowSelect = true;
|
||||||
|
_lstAnteprimaAsm.GridLines = false;
|
||||||
|
_lstAnteprimaAsm.HeaderStyle = ColumnHeaderStyle.Nonclickable;
|
||||||
|
_lstAnteprimaAsm.SmallImageList = new ImageList { ImageSize = new Size(20, 20) };
|
||||||
|
_lstAnteprimaAsm.Columns.Add("File .asm", -2);
|
||||||
_lstAnteprimaAsm.Dock = DockStyle.Fill;
|
_lstAnteprimaAsm.Dock = DockStyle.Fill;
|
||||||
_lstAnteprimaAsm.Margin = new Padding(0, 4, 0, 12);
|
_lstAnteprimaAsm.Margin = new Padding(0, 4, 0, 12);
|
||||||
_lstAnteprimaAsm.BorderStyle = BorderStyle.FixedSingle;
|
_lstAnteprimaAsm.BorderStyle = BorderStyle.None;
|
||||||
|
// La colonna unica segue sempre la larghezza disponibile (niente scroll orizzontale
|
||||||
|
// né spazio sprecato a destra quando la finestra viene ingrandita).
|
||||||
|
_lstAnteprimaAsm.Resize += (_, _) =>
|
||||||
|
{
|
||||||
|
if (_lstAnteprimaAsm.Columns.Count > 0)
|
||||||
|
_lstAnteprimaAsm.Columns[0].Width = _lstAnteprimaAsm.ClientSize.Width;
|
||||||
|
};
|
||||||
_lstAnteprimaAsm.ItemDrag += (_, e) =>
|
_lstAnteprimaAsm.ItemDrag += (_, e) =>
|
||||||
{
|
{
|
||||||
var percorsi = _lstAnteprimaAsm.SelectedItems.Cast<ListViewItem>()
|
var percorsi = _lstAnteprimaAsm.SelectedItems.Cast<ListViewItem>()
|
||||||
@@ -714,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
|
||||||
@@ -722,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))
|
||||||
{
|
{
|
||||||
codiciNonTrovati.Add(codice);
|
if (!esterno)
|
||||||
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -805,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
|
||||||
@@ -975,14 +1100,17 @@ public partial class Form1 : Form
|
|||||||
private static readonly string PercorsoImpostazioni = Path.Combine(CartellaImpostazioni, "ultimo_db.txt");
|
private static readonly string PercorsoImpostazioni = Path.Combine(CartellaImpostazioni, "ultimo_db.txt");
|
||||||
private static readonly string PercorsoImpostazioniCartella = Path.Combine(CartellaImpostazioni, "ultima_cartella_lavoro.txt");
|
private static readonly string PercorsoImpostazioniCartella = Path.Combine(CartellaImpostazioni, "ultima_cartella_lavoro.txt");
|
||||||
|
|
||||||
/// <summary>Propone l'ultima cartella di lavoro usata (se ancora esiste su disco) o, la prima
|
/// <summary>Propone l'ultima cartella di lavoro usata (se ancora esiste su disco) o, se non
|
||||||
/// volta, la cartella dell'eseguibile: non richiede mai un CLAUDE.md/repo (a differenza del
|
/// c'è ancora una preferenza salvata (o punta a una cartella cancellata), la cartella dove si
|
||||||
/// vecchio TrovaRadiceRepo), quindi funziona identica anche sull'exe pubblicato standalone
|
/// trova il DB (più sensata dell'eseguibile: quella è dove l'utente tiene il proprio lavoro,
|
||||||
/// copiato su un'altra macchina. Sempre modificabile con "Cambia cartella..." in fase 1.</summary>
|
/// non dov'è installata la app). Richiede quindi che InizializzaDatabase sia già stata
|
||||||
|
/// chiamata. Sempre modificabile con "Cambia cartella..." in fase 1.</summary>
|
||||||
private void InizializzaCartellaLavoro()
|
private void InizializzaCartellaLavoro()
|
||||||
{
|
{
|
||||||
var ultima = LeggiUltimaCartellaLavoro();
|
var ultima = LeggiUltimaCartellaLavoro();
|
||||||
_cartellaLavoro = ultima is not null && Directory.Exists(ultima) ? ultima : AppContext.BaseDirectory;
|
_cartellaLavoro = ultima is not null && Directory.Exists(ultima)
|
||||||
|
? ultima
|
||||||
|
: (_dbPath is not null ? Path.GetDirectoryName(_dbPath) : null) ?? AppContext.BaseDirectory;
|
||||||
AggiornaLabelCartellaLavoro();
|
AggiornaLabelCartellaLavoro();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -8,12 +8,11 @@
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode>
|
<ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode>
|
||||||
<ApplicationIcon>app.ico</ApplicationIcon>
|
<ApplicationIcon>app.ico</ApplicationIcon>
|
||||||
<!-- Nome tecnico (cartella/csproj/namespace) e nome del file .exe pubblicato: PrepStep.
|
|
||||||
Il nome prodotto mostrato all'utente ("Cevolani StepPrep") resta separato, sotto. -->
|
|
||||||
<AssemblyTitle>Cevolani StepPrep</AssemblyTitle>
|
<AssemblyTitle>Cevolani StepPrep</AssemblyTitle>
|
||||||
<Product>Cevolani StepPrep</Product>
|
<Product>Cevolani StepPrep</Product>
|
||||||
<Company>Cevolani</Company>
|
<Company>Cevolani</Company>
|
||||||
<Version>0.1.0</Version>
|
<Version>0.1.0</Version>
|
||||||
|
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
+24
-32
@@ -3,79 +3,71 @@ namespace PrepStep;
|
|||||||
public sealed class ProgressForm : Form
|
public sealed class ProgressForm : Form
|
||||||
{
|
{
|
||||||
private readonly Label _lblCartella = new();
|
private readonly Label _lblCartella = new();
|
||||||
private readonly ProgressBar _progressBar = new();
|
private readonly ProgressBar _bar = 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 ToolTip _tipCartella = new();
|
private readonly ToolTip _tipCartella = new();
|
||||||
|
|
||||||
|
private const int Margine = 16;
|
||||||
|
|
||||||
public event EventHandler? Annullato;
|
public event EventHandler? Annullato;
|
||||||
|
|
||||||
public ProgressForm()
|
public ProgressForm()
|
||||||
{
|
{
|
||||||
Text = "Aggiornamento indice in corso...";
|
Text = "Aggiornamento indice";
|
||||||
FormBorderStyle = FormBorderStyle.FixedDialog;
|
FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||||
StartPosition = FormStartPosition.CenterParent;
|
StartPosition = FormStartPosition.CenterParent;
|
||||||
MinimizeBox = false;
|
MinimizeBox = false;
|
||||||
MaximizeBox = false;
|
MaximizeBox = false;
|
||||||
ControlBox = false;
|
ControlBox = false;
|
||||||
ClientSize = new Size(460, 190);
|
ClientSize = new Size(420, 190);
|
||||||
|
|
||||||
_lblCartella.Text = "Preparazione...";
|
_lblCartella.Text = "Preparazione...";
|
||||||
_lblCartella.Location = new Point(16, 16);
|
_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(428, 20);
|
_lblCartella.Size = new Size(ClientSize.Width - Margine * 2, 20);
|
||||||
_lblCartella.Font = new Font(Font, FontStyle.Bold);
|
|
||||||
|
|
||||||
_progressBar.Location = new Point(16, 44);
|
_bar.Location = new Point(Margine, 40);
|
||||||
_progressBar.Size = new Size(428, 24);
|
_bar.Size = new Size(ClientSize.Width - Margine * 2, 24);
|
||||||
_progressBar.Minimum = 0;
|
_bar.Minimum = 0;
|
||||||
_progressBar.Maximum = 1000;
|
_bar.Maximum = 100;
|
||||||
_progressBar.Style = ProgressBarStyle.Marquee;
|
|
||||||
|
|
||||||
_lblConteggio.Location = new Point(16, 80);
|
_lblConteggio.Location = new Point(Margine, 72);
|
||||||
_lblConteggio.AutoSize = true;
|
_lblConteggio.AutoSize = true;
|
||||||
|
|
||||||
_lblDettaglio.Location = new Point(16, 104);
|
_lblDettaglio.Location = new Point(Margine, 96);
|
||||||
_lblDettaglio.AutoSize = true;
|
_lblDettaglio.AutoSize = true;
|
||||||
_lblDettaglio.ForeColor = SystemColors.GrayText;
|
|
||||||
|
|
||||||
_lblEta.Location = new Point(16, 128);
|
_lblEta.Location = new Point(Margine, 120);
|
||||||
_lblEta.AutoSize = true;
|
_lblEta.AutoSize = true;
|
||||||
_lblEta.Font = new Font(Font, FontStyle.Bold);
|
|
||||||
|
|
||||||
_btnAnnulla.Text = "Annulla";
|
_btnAnnulla.Text = "Annulla";
|
||||||
_btnAnnulla.Location = new Point(354, 152);
|
|
||||||
_btnAnnulla.Size = new Size(90, 28);
|
_btnAnnulla.Size = new Size(90, 28);
|
||||||
UiStile.Secondario(_btnAnnulla);
|
|
||||||
_btnAnnulla.Click += (_, e) => Annullato?.Invoke(this, e);
|
_btnAnnulla.Click += (_, e) => Annullato?.Invoke(this, e);
|
||||||
|
|
||||||
Controls.AddRange(new Control[]
|
Controls.AddRange(new Control[]
|
||||||
{
|
{
|
||||||
_lblCartella, _progressBar, _lblConteggio, _lblDettaglio, _lblEta, _btnAnnulla
|
_lblCartella, _bar, _lblConteggio, _lblDettaglio, _lblEta, _btnAnnulla
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_btnAnnulla.Location = new Point(
|
||||||
|
ClientSize.Width - Margine - _btnAnnulla.Width, ClientSize.Height - Margine - _btnAnnulla.Height);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Aggiorna(ScanProgress p)
|
public void Aggiorna(ScanProgress p)
|
||||||
{
|
{
|
||||||
_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)
|
||||||
_progressBar.Style = ProgressBarStyle.Continuous;
|
: 0;
|
||||||
double frazione = Math.Min(1.0, (double)p.FattiTotali / p.PrevistiTotali);
|
|
||||||
_progressBar.Value = (int)(frazione * _progressBar.Maximum);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_progressBar.Style = ProgressBarStyle.Marquee;
|
|
||||||
}
|
|
||||||
|
|
||||||
_lblConteggio.Text = p.PrevistiTotali > 0
|
_lblConteggio.Text = p.PrevistiTotali > 0
|
||||||
? $"{p.FattiTotali:N0} / ~{p.PrevistiTotali:N0} file"
|
? $"{p.FattiTotali:N0} / ~{p.PrevistiTotali:N0} 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+6
-1
@@ -7,6 +7,11 @@ namespace PrepStep;
|
|||||||
/// (blu pieno) sia per le azioni secondarie, al posto del bottone grigio di default.</summary>
|
/// (blu pieno) sia per le azioni secondarie, al posto del bottone grigio di default.</summary>
|
||||||
internal static class UiStile
|
internal static class UiStile
|
||||||
{
|
{
|
||||||
|
/// <summary>Blu del bottone primario, esposto qui per essere riusato anche da elementi
|
||||||
|
/// 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);
|
||||||
|
|
||||||
// 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
|
||||||
// al proprio testo (a qualunque lunghezza/font), niente più testo che va a capo e viene
|
// al proprio testo (a qualunque lunghezza/font), niente più testo che va a capo e viene
|
||||||
// tagliato in verticale quando non ci sta nella larghezza indovinata a mano.
|
// tagliato in verticale quando non ci sta nella larghezza indovinata a mano.
|
||||||
@@ -25,7 +30,7 @@ internal static class UiStile
|
|||||||
b.FlatAppearance.BorderSize = 0;
|
b.FlatAppearance.BorderSize = 0;
|
||||||
b.FlatAppearance.MouseOverBackColor = Color.FromArgb(0, 84, 152);
|
b.FlatAppearance.MouseOverBackColor = Color.FromArgb(0, 84, 152);
|
||||||
b.FlatAppearance.MouseDownBackColor = Color.FromArgb(0, 70, 130);
|
b.FlatAppearance.MouseDownBackColor = Color.FromArgb(0, 70, 130);
|
||||||
b.BackColor = Color.FromArgb(0, 99, 177);
|
b.BackColor = ColorePrimario;
|
||||||
b.ForeColor = Color.White;
|
b.ForeColor = Color.White;
|
||||||
b.Font = new Font(b.Font, FontStyle.Bold);
|
b.Font = new Font(b.Font, FontStyle.Bold);
|
||||||
b.Cursor = Cursors.Hand;
|
b.Cursor = Cursors.Hand;
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 44 KiB |
+116
-14
@@ -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,6 +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** — usa `AsmRecordReader` (stesso lettore esatto della GUI, fase 4/5, via
|
||||||
|
`#:project`) invece dell'euristica testuale. Risolve i sottoassiemi cercandoli in cartelle
|
||||||
|
locali (`-p`), non contro `cad_index.db` come `ExtractCreoStrings.cs`/`BomViaDb.cs`.
|
||||||
- **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`.
|
||||||
@@ -166,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.
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
# Il database `cad_index.db`
|
||||||
|
|
||||||
|
Documentazione tecnica del DB SQLite che indicizza i file CAD: schema, come viene
|
||||||
|
popolato/aggiornato (`Indexer.ScanAsync`), come l'app lo trova all'avvio e i problemi più
|
||||||
|
comuni quando i risultati sembrano "mancanti".
|
||||||
|
|
||||||
|
## Schema
|
||||||
|
|
||||||
|
Tabella `files` (schema SQL in `Indexer.Schema`, `PrepStep/Indexer.cs`): un record per ogni
|
||||||
|
file trovato sotto la radice del disco (default `X:\`, `Form1.RadiceDiscoDefault`).
|
||||||
|
|
||||||
|
| Colonna | Contenuto |
|
||||||
|
|---|---|
|
||||||
|
| `id` | chiave primaria autoincrementale |
|
||||||
|
| `percorso_completo` | percorso assoluto del file — **chiave univoca**, usata per upsert |
|
||||||
|
| `percorso_relativo` | percorso relativo alla cartella di primo livello (`cartella_radice`) |
|
||||||
|
| `nome_file` | solo il nome del file |
|
||||||
|
| `cartella_radice` | prima cartella sotto la radice del disco (es. `__B.end`, `Commerciali`, ...) |
|
||||||
|
| `famiglia` | primo livello di sottocartella dentro `cartella_radice` (null se il file è direttamente dentro `cartella_radice`) |
|
||||||
|
| `codice` | estratto dal nome file col pattern Pro-E `<codice>.<tipo>.<versione>` |
|
||||||
|
| `tipo_file` | es. `prt`, `asm`, o `altro` se il nome non rispetta il pattern |
|
||||||
|
| `versione` | numero di versione Pro-E (intero), null se non rispetta il pattern |
|
||||||
|
| `dimensione_byte` | dimensione del file |
|
||||||
|
| `data_modifica` | `LastWriteTimeUtc` del file, formato ISO 8601 |
|
||||||
|
| `scansionato_il` | timestamp UTC dell'ultima scansione in cui il file è stato visto |
|
||||||
|
|
||||||
|
Indici su `codice`, `tipo_file`, `cartella_radice` (le colonne usate nelle query di ricerca
|
||||||
|
componenti e nel filtro per cartella).
|
||||||
|
|
||||||
|
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()
|
||||||
|
OVER (PARTITION BY ... ORDER BY versione DESC, data_modifica DESC, percorso_completo)`. Tutte
|
||||||
|
le ricerche di componenti (`Form1.TrovaPercorsiCad`, `Form1.CaricaCodiciNoti`) interrogano
|
||||||
|
questa vista, non `files` direttamente, così un componente con più versioni sulla share
|
||||||
|
risolve sempre all'ultima. Deliberatamente una `ROW_NUMBER()` invece di un self-join con
|
||||||
|
`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
|
||||||
|
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
|
||||||
|
`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`
|
||||||
|
|
||||||
|
Chiamato da "Aggiorna indice" (fase 1) e riutilizza lo stesso `dbPath` già caricato.
|
||||||
|
|
||||||
|
1. Crea lo schema se non esiste (`CREATE TABLE/VIEW IF NOT EXISTS`) — quindi funziona sia su
|
||||||
|
un DB vuoto (prima esecuzione) sia su uno esistente (aggiornamento incrementale).
|
||||||
|
2. Cancella subito tutte le righe con `cartella_radice LIKE 'temp\_%'`: le cartelle
|
||||||
|
`temp_paris`, `temp_davide`, `temp_studi esterni`, ecc. sono lavori personali/temporanei che
|
||||||
|
possono contenere copie datate con lo stesso codice numerico di file "ufficiali" altrove
|
||||||
|
sulla share, causando falsi positivi nella risoluzione dei sotto-componenti (vedi
|
||||||
|
`docs/formato-asm.md`). Vengono escluse a monte, non solo filtrate nelle singole query.
|
||||||
|
3. Elenca le cartelle di primo livello sotto la radice con `Directory.GetDirectories` (non un
|
||||||
|
elenco fisso in codice: una nuova cartella sulla share entra nell'indice al prossimo
|
||||||
|
aggiornamento senza bisogno di modificare `PrepStep`), escludendo quelle che iniziano per
|
||||||
|
`temp_`.
|
||||||
|
4. Per ogni cartella di primo livello, cammina l'albero con uno stack manuale (`Walk`, in
|
||||||
|
`Indexer.cs`) invece di `Directory.EnumerateFiles(..., SearchOption.AllDirectories)`: più
|
||||||
|
veloce su una share di rete con centinaia di migliaia di file, e ogni sottocartella
|
||||||
|
irraggiungibile (permessi, link rotti) viene saltata con un `try/catch` invece di far fallire
|
||||||
|
l'intera scansione.
|
||||||
|
5. Per ogni file trovato, estrae `codice`/`tipo`/`versione` dal nome (`ParseFilename`, vedi
|
||||||
|
sotto) e fa un **upsert riga per riga** dentro una transazione per cartella: confronta
|
||||||
|
`dimensione_byte` + `data_modifica` con quanto già salvato per lo stesso
|
||||||
|
`percorso_completo`—
|
||||||
|
- non esiste ancora → `INSERT` (contatore "nuovi")
|
||||||
|
- esiste ma dimensione/data sono cambiate → `UPDATE` (contatore "aggiornati")
|
||||||
|
- esiste e non è cambiato nulla → solo `scansionato_il` viene aggiornato (contatore
|
||||||
|
"invariati"), per non riscrivere l'intera riga inutilmente
|
||||||
|
6. Al termine di ogni cartella, cancella dal DB le righe di quella `cartella_radice` il cui
|
||||||
|
`scansionato_il` non è quello di questa scansione (`DELETE ... WHERE scansionato_il !=
|
||||||
|
@now`): sono file rimossi dal disco dall'ultima scansione. Il conteggio finisce nel
|
||||||
|
contatore "rimossi". Questo avviene **per cartella**, non a scansione intera completata: se
|
||||||
|
annulli a metà, le cartelle già scansionate hanno comunque gli eliminati ripuliti.
|
||||||
|
7. Il progresso (`ScanProgress`, con conteggi nuovi/aggiornati/invariati/rimossi, file
|
||||||
|
fatti/previsti e tempo trascorso) viene riportato ogni 200 file tramite `IProgress<T>`,
|
||||||
|
marshalato automaticamente sul thread UI, e mostrato in `ProgressForm` (che stima anche il
|
||||||
|
tempo rimanente dal ritmo file/secondo rispetto al totale atteso, preso dal conteggio della
|
||||||
|
scansione precedente salvato nel DB).
|
||||||
|
|
||||||
|
### Estrazione codice/tipo/versione: `ParseFilename`
|
||||||
|
|
||||||
|
Regex: `^(?<codice>.+)\.(?<tipo>[A-Za-z0-9]+)\.(?<versione>\d+)$` applicata al nome file
|
||||||
|
(non al percorso). Esempio: `9251208030c.asm.5` → `codice="9251208030c"`, `tipo="asm"`,
|
||||||
|
`versione=5`. Il gruppo `codice` è **greedy**, quindi cattura tutto quello che precede l'ultimo
|
||||||
|
`.tipo.versione` — se un componente ha una lettera di revisione attaccata nel nome del file
|
||||||
|
(es. `9248908002a.asm.3`), quella lettera fa parte del `codice`, non è un gruppo separato.
|
||||||
|
|
||||||
|
Se il nome file non rispetta il pattern (niente `.tipo.numero` finale), l'intero nome finisce
|
||||||
|
in `codice` e `tipo_file` diventa `"altro"` (usato per capire quanti file "strani" ci sono
|
||||||
|
sulla share, non tipicamente cercato dalla UI).
|
||||||
|
|
||||||
|
## Come l'app trova il DB all'avvio
|
||||||
|
|
||||||
|
`Form1.InizializzaDatabase` prova, in ordine, i seguenti candidati e usa il primo che esiste
|
||||||
|
su disco:
|
||||||
|
|
||||||
|
1. l'ultimo DB caricato con successo, salvato in
|
||||||
|
`%AppData%/PrepStep/ultimo_db.txt` (`Form1.LeggiUltimoDbUsato`/`SalvaUltimoDbUsato`)
|
||||||
|
2. `cad_index.db` accanto all'eseguibile (`AppContext.BaseDirectory`)
|
||||||
|
3. `cad_index.db` nella working directory corrente
|
||||||
|
|
||||||
|
Se nessuno dei tre esiste, la fase 1 resta bloccata su "Database non trovato" finché l'utente
|
||||||
|
non preme "Apri DB..." (`Form1.ScegliDatabase`, file picker libero) o "Aggiorna indice"
|
||||||
|
(`Form1.AggiornaIndice`, che crea `cad_index.db` da zero accanto all'eseguibile se non c'è
|
||||||
|
ancora un `_dbPath`).
|
||||||
|
|
||||||
|
**Attenzione in sviluppo (dotnet run / IDE):** `AppContext.BaseDirectory` è la cartella di
|
||||||
|
build (`PrepStep/bin/Release/net10.0-windows` o `Debug/...`), non la radice del repo. Se in
|
||||||
|
quella cartella finisce per errore una copia di `cad_index.db` (es. creata lì da una vecchia
|
||||||
|
sessione), l'app la userà invece di quella nella radice del repo, e le due copie divergeranno
|
||||||
|
a ogni "Aggiorna indice" fatto nell'uno o nell'altro ambiente. Se i risultati di una ricerca
|
||||||
|
sembrano incoerenti con quello che ti aspetti di vedere sul DB "giusto", controlla con "Apri
|
||||||
|
DB..." quale file è effettivamente caricato (mostrato in fase 1 accanto al conteggio dei file
|
||||||
|
indicizzati) prima di sospettare un bug nella ricerca.
|
||||||
|
|
||||||
|
## Query più usate
|
||||||
|
|
||||||
|
- **Verifica completezza / crea raw** (`Form1.TrovaPercorsiCad`): per ogni codice del CSV,
|
||||||
|
`SELECT tipo_file, percorso_completo FROM files_ultima_versione WHERE LOWER(codice) =
|
||||||
|
LOWER(@codice) AND LOWER(tipo_file) IN ('prt','asm') ORDER BY tipo_file, data_modifica DESC`.
|
||||||
|
Il match sul `codice` è **esatto** (case-insensitive, non un LIKE): se il codice del CSV ha
|
||||||
|
una lettera di revisione in più o in meno rispetto al nome del file su disco, non c'è match e
|
||||||
|
la cella "Percorso CAD" resta vuota — non è un errore della query, è che il DB non ha nessuna
|
||||||
|
riga con quell'esatto `codice`. In presenza di duplicati su più `cartella_radice`, tiene solo
|
||||||
|
il primo per `tipo_file` (il più recente, dato l'`ORDER BY data_modifica DESC`) e scarta gli
|
||||||
|
altri.
|
||||||
|
- **Risoluzione codici noti per lo scanner assiemi** (`Form1.CaricaCodiciNoti`, usata sia dalla
|
||||||
|
GUI in fase 4 sia da `scripts/BomViaDb.cs`): carica in un'unica query tutti i codici
|
||||||
|
prt/asm noti con i relativi percorsi, invece di una query per codice — usata per validare i
|
||||||
|
token generici trovati nei binari `.asm` (vedi `docs/formato-asm.md`).
|
||||||
|
|
||||||
|
## Debug ad-hoc
|
||||||
|
|
||||||
|
Per interrogare il DB direttamente da riga di comando senza aprire un tool SQLite esterno:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet run scripts/Query.cs -- cad_index.db "SELECT codice, tipo_file, percorso_completo FROM files_ultima_versione WHERE codice LIKE '925%' LIMIT 20"
|
||||||
|
```
|
||||||
|
|
||||||
|
Utile per capire in fretta se un codice mancante nella UI non è nell'indice affatto (nessuna
|
||||||
|
riga, nemmeno con un `LIKE` più permissivo) oppure c'è ma con un `codice` leggermente diverso
|
||||||
|
da quello atteso (es. lettera di revisione, maiuscole/minuscole, spazi).
|
||||||
@@ -0,0 +1,583 @@
|
|||||||
|
# Decodifica strutturale del .asm Creo: record, membri, family table
|
||||||
|
|
||||||
|
Questo documento descrive il formato **record per record** del `.asm` Creo, ricavato per
|
||||||
|
reverse engineering su un campione di assiemi reali e verificato confrontando l'output con
|
||||||
|
le BOM esportate da Creo per gli stessi file. Sostituisce, per l'estrazione dei componenti,
|
||||||
|
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
|
||||||
|
struttura effettiva dei record.
|
||||||
|
|
||||||
|
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.** Due livelli di prova, da non confondere:
|
||||||
|
|
||||||
|
1. **Byte-per-byte contro BOM Creo di riferimento** (la prova forte): 3 assiemi del
|
||||||
|
reverse engineering iniziale, più 5 casi reali aggiunti in seguito, ciascuno con la sua BOM
|
||||||
|
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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Intestazione
|
||||||
|
|
||||||
|
Il file si apre con un header ASCII a righe terminate da `\` + newline:
|
||||||
|
|
||||||
|
```
|
||||||
|
#UGC:2 ASSEMBLY 2038 1760 805 1 1 15 4000 2022414 000002d0 \
|
||||||
|
#- VERS 0 0 \
|
||||||
|
#- HOST \
|
||||||
|
#- LINK \
|
||||||
|
#- DBID \
|
||||||
|
#- REVS 0, \
|
||||||
|
```
|
||||||
|
|
||||||
|
`ASSEMBLY` distingue il tipo di modello. Gli altri campi numerici non sono
|
||||||
|
stati decodificati e non servono per la BOM. Dopo l'header inizia il corpo
|
||||||
|
binario. **Il corpo non è compresso**: i nomi dei modelli sono in chiaro,
|
||||||
|
1 byte per carattere (Latin1 — `Encoding.Latin1.GetString`, stesso metodo già
|
||||||
|
usato in `AssemblyComponentScanner.cs` per leggere il file come testo senza
|
||||||
|
alterarne i byte).
|
||||||
|
|
||||||
|
## 2. Il formato dei record
|
||||||
|
|
||||||
|
Il corpo è una serializzazione a marcatori. I byte `\xe0`-`\xff` sono
|
||||||
|
marcatori di struttura, tutti gli altri sono dati. I marcatori osservati:
|
||||||
|
|
||||||
|
| byte | significato dedotto |
|
||||||
|
|------|---------------------|
|
||||||
|
| `\xe0` | inizio campo: `\xe0 <tipo> <nome campo>\0 <valore>` |
|
||||||
|
| `\xe1` | valore nullo / vuoto |
|
||||||
|
| `\xe2` | inizio struttura o unione (per i parametri: il byte dopo è il discriminante) |
|
||||||
|
| `\xe3` | inizio struttura, usato per gli elementi di un array |
|
||||||
|
| `\xf1` | apre un contenitore (visto prima di un array) |
|
||||||
|
| `\xf7` | id di schema: `\xf7 <n>` identifica il layout dei record che seguono |
|
||||||
|
| `\xf8` | array: `\xf8 <conteggio> ... <elementi>` |
|
||||||
|
| `\xfb` | lo schema è definito in linea nel primo elemento dell'array |
|
||||||
|
| `\xf4` | chiude una sezione (seguito da `#\n#<NomeSezione>\n`) |
|
||||||
|
|
||||||
|
Il byte `<tipo>` dopo `\xe0` codifica il tipo del valore. Osservati:
|
||||||
|
`\x00` struttura/nullable, `\x01` intero, `\x02` array numerico,
|
||||||
|
`\x0a` stringa terminata da `\0`.
|
||||||
|
|
||||||
|
### 2.1 Interi
|
||||||
|
|
||||||
|
Gli interi sono big-endian con la **lunghezza codificata nei bit alti del primo
|
||||||
|
byte**, per non collidere con i marcatori `\xe0`+:
|
||||||
|
|
||||||
|
| primo byte | byte totali | valore |
|
||||||
|
|------------|-------------|--------|
|
||||||
|
| `0x00`-`0x7f` | 1 | il byte stesso |
|
||||||
|
| `0x80`-`0xbf` | 2 | `(b0 & 0x3f) << 8 \| b1` |
|
||||||
|
| `0xc0`-`0xdf` | 3 | `(b0 & 0x1f) << 16 \| b1 << 8 \| b2` |
|
||||||
|
|
||||||
|
Esempi verificati: `\x2e` = 46; `\x80\x81` = 129; `\xc0\x4d\x51` = 19793.
|
||||||
|
Questa regola è stata **ricavata** dalla necessità di far quadrare il
|
||||||
|
conteggio dei membri: con la lettura a 1 byte fisso i campioni grandi si
|
||||||
|
disallineavano. Non è confermata da una specifica; regge sui campioni
|
||||||
|
disponibili al momento del reverse engineering.
|
||||||
|
|
||||||
|
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
|
||||||
|
originale dà lo stesso offset).
|
||||||
|
|
||||||
|
### 2.2 Compressione per schema
|
||||||
|
|
||||||
|
Un array `\xf8 <n> \xf7 <idSchema> \xfb` scrive il **primo** elemento con tutti
|
||||||
|
i nomi di campo per esteso; gli elementi successivi sono **posizionali**, senza
|
||||||
|
nomi. È il motivo per cui nel file si vedono i nomi dei componenti "nudi", uno
|
||||||
|
dopo l'altro, senza etichetta. Per leggere l'elemento N bisogna conoscere lo
|
||||||
|
schema definito dall'elemento 1.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. L'elenco dei componenti: la chiave `members`
|
||||||
|
|
||||||
|
La chiave `members` è l'elenco dei **componenti diretti** dell'assieme
|
||||||
|
(livello 1), nell'ordine di montaggio.
|
||||||
|
|
||||||
|
```
|
||||||
|
\xe0 \x00 "members" \0
|
||||||
|
\xf1 \xf8 <N> \xf7 <idSchema> \xfb
|
||||||
|
\xe3 \xe0\x01 "id"\0 <int>
|
||||||
|
\xe0\x01 "type"\0 <int>
|
||||||
|
\xe0\x0a "name"\0 <stringa>\0
|
||||||
|
\xe0\x0a "family_name"\0 <stringa>\0
|
||||||
|
\xe0\x00 "tha_ref"\0 \xe1
|
||||||
|
\xe0\x02 "e1"\0 ... "e2" ... "e3" ... "origin" ...
|
||||||
|
\xe3 <id> <type> <name>\0 <family_name>\0 <resto posizionale>
|
||||||
|
\xe3 <id> <type> <name>\0 <family_name>\0 <resto posizionale>
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Campi utili:
|
||||||
|
|
||||||
|
- **`id`** — identificatore del componente **dentro questo assieme**. È lo
|
||||||
|
stesso numero che Creo mostra come "Componente ID N". Serve da chiave per
|
||||||
|
collegare il membro ad altre sezioni (vedi family table, §4).
|
||||||
|
- **`type`** — `1` = sottoassieme (`.asm`), `2` = parte (`.prt`). Sui campioni
|
||||||
|
osservati compaiono solo questi due valori.
|
||||||
|
- **`name`** — il **file su disco**, senza estensione. Per le family table è
|
||||||
|
il **generico** (`UNI6592`), non l'istanza.
|
||||||
|
- **`family_name`** — sui campioni disponibili è **sempre uguale a `name`**.
|
||||||
|
Non porta quindi l'informazione sull'istanza; il nome è fuorviante.
|
||||||
|
- `e1`, `e2`, `e3`, `origin` — la matrice di posizionamento (assi + origine).
|
||||||
|
Non decodificati: non servono per la BOM, ma occupano il resto del record e
|
||||||
|
hanno lunghezza variabile.
|
||||||
|
|
||||||
|
`N` (il conteggio dichiarato) è il numero totale di **occorrenze**, non di
|
||||||
|
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
|
||||||
|
trovati non sono esattamente `N`, la lettura si è disallineata e va segnalata
|
||||||
|
un'eccezione invece di restituire un risultato parziale silenzioso — vedi
|
||||||
|
`AsmRecordReader.LeggiMembri`, che lancia `InvalidDataException` in questo caso.
|
||||||
|
|
||||||
|
La sezione termina a `\xf4\x04#\n#MdlStatus\n`.
|
||||||
|
|
||||||
|
### 3.1 Come lo legge `AsmRecordReader.LeggiMembri`
|
||||||
|
|
||||||
|
Il parser non ricostruisce lo schema completo (servirebbe decodificare le
|
||||||
|
matrici). Si ancora invece al pattern `\xe3 <int> <int> <stringa>\0 <stringa>\0`
|
||||||
|
(`AsmRecordReader.MemberRegex`, una regex .NET sul testo Latin1 del file) e salta il
|
||||||
|
resto del record fino al `\xe3` successivo che soddisfa il pattern.
|
||||||
|
La doppia stringa terminata da `\0` rende i falsi positivi improbabili, e il
|
||||||
|
conteggio dichiarato `N` fa da rete di sicurezza.
|
||||||
|
|
||||||
|
> `ponytail:` scansione a pattern invece del parser di schema — il punto debole
|
||||||
|
> è che si appoggia al conteggio per accorgersi di un errore. Da sostituire con
|
||||||
|
> la lettura dello schema se dovessero comparire campioni che non quadrano.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Family table: dove sta il nome dell'istanza
|
||||||
|
|
||||||
|
Le parti normalizzate (viti, rondelle, spine, boccole) sono **istanze di family
|
||||||
|
table**. In Creo esiste un solo file, il **generico** (`UNI6592.prt`), che
|
||||||
|
contiene una tabella di varianti; ogni variante è un'**istanza**
|
||||||
|
(`R8--_-_U6592_AISI304`). La BOM elenca le istanze, il disco contiene il
|
||||||
|
generico. La sezione `members` cita **solo il generico**: senza risolvere
|
||||||
|
`ConfigName` (sotto), il livello 1 produrrebbe `UNI6592` dove la BOM dice
|
||||||
|
`R8--_-_U6592_AISI304`.
|
||||||
|
|
||||||
|
Il collegamento sta in un record di **parametro** applicato al membro:
|
||||||
|
|
||||||
|
```
|
||||||
|
\xe3 "ConfigName"\0 \xe2 \x33 [\xf7 <idSchema>] <nome istanza>\0 <id membro> \x82\x7b \xe1\xe1
|
||||||
|
```
|
||||||
|
|
||||||
|
- `\xe2\x33` — parametro di tipo stringa (`\x33` = `'3'`). Confronto: `\xe2\x32`
|
||||||
|
è un parametro numerico, es. `DENSITA`.
|
||||||
|
- `<nome istanza>` — il nome esatto che la BOM stampa.
|
||||||
|
- `<id membro>` — l'`id` del record `members` a cui il parametro si applica,
|
||||||
|
con la codifica intera di §2.1. **È questo il collegamento.**
|
||||||
|
- `\xf7 <idSchema>` compare solo sulla prima occorrenza (schema definito in
|
||||||
|
linea), poi è omesso.
|
||||||
|
- `\x82\x7b` — costante (= 635) identica su tutte le occorrenze di tutti i
|
||||||
|
campioni. Significato ignoto, presumibilmente attributi/flag del parametro.
|
||||||
|
`AsmRecordReader.ConfigNameRegex` la usa come ancora di fine record; se un campione
|
||||||
|
futuro avesse un valore diverso, il match salterebbe silenziosamente (nessun
|
||||||
|
contatore di sicurezza analogo a quello di `members` copre questo caso).
|
||||||
|
|
||||||
|
`ConfigName` compare **esattamente** per i membri che sono istanze, e per
|
||||||
|
nessun altro. L'algoritmo è quindi: leggere `members`, costruire la mappa
|
||||||
|
`id membro → nome istanza` dai record `ConfigName`
|
||||||
|
(`AsmRecordReader.LeggiMembri`, dizionario `istanze`), e sostituire il nome dove la
|
||||||
|
mappa ha una voce. Il generico resta disponibile come `Membro.Generico`.
|
||||||
|
|
||||||
|
> **Avvertenza.** `ConfigName` sta in mezzo a parametri palesemente aziendali
|
||||||
|
> (`TIPO_PARTE`, `CLASSE_RICAR`, `GR_PRODOTTO`, `TECN_RIORDINO`), quindi
|
||||||
|
> potrebbe non essere un meccanismo standard di Creo ma una convenzione
|
||||||
|
> interna. Regge sui campioni disponibili, ma su file provenienti da
|
||||||
|
> 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
|
||||||
|
|
||||||
|
Altrove nel file compaiono stringhe nella forma `istanza<generico>`:
|
||||||
|
|
||||||
|
```
|
||||||
|
\xe2\x02 "R8--_-_U6592_AISI304<UNI6592>"\0
|
||||||
|
```
|
||||||
|
|
||||||
|
Documenta la stessa relazione istanza→generico in modo autonomo ed è utile come
|
||||||
|
riscontro incrociato (è lo stesso pattern `<codice><GENERICO>` già sfruttato
|
||||||
|
dall'euristica testuale in `formato-asm.md` per le family table non-UNI).
|
||||||
|
**Non è però una lista dei componenti**: su un campione produceva 26 voci
|
||||||
|
contro 17 membri reali, includeva alcuni nipoti (figli dei sottoassiemi) ma
|
||||||
|
non tutti, e ripeteva le stesse voci più volte. È la lista dei riferimenti
|
||||||
|
geometrici dei vincoli di accoppiamento, non una distinta. Usarla per la BOM
|
||||||
|
dà risultati sbagliati sia in eccesso sia in difetto.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Il nome proprio del modello
|
||||||
|
|
||||||
|
La BOM intesta i blocchi col nome **del modello**, non del file: il file
|
||||||
|
`9244248550b.asm.11` produce `Assieme 9244248550B contiene:`. Il nome sta nel
|
||||||
|
primo record `objname` del file:
|
||||||
|
|
||||||
|
```
|
||||||
|
\x00 "objname"\0 \xe2 \xe0\x01 "object_type"\0 <int> \xe0\x0a "name"\0 <nome>\0
|
||||||
|
```
|
||||||
|
|
||||||
|
La **prima** occorrenza è il modello stesso; quelle successive sono altri
|
||||||
|
oggetti referenziati (disegni, modelli citati). `AsmRecordReader.NomeProprio` cerca
|
||||||
|
solo la prima occorrenza (`Regex.Match`, non `Matches`), verificato su tutti i
|
||||||
|
campioni disponibili durante il reverse engineering.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Generare la BOM: ricorsione e formato di output
|
||||||
|
|
||||||
|
`members` dà un solo livello. La BOM di Creo è ricorsiva: apre anche i `.asm`
|
||||||
|
dei sottoassiemi. Implementato in `CreoAsm.GeneraBom`/`Render` in
|
||||||
|
[`scripts/CreoBom.cs`](../scripts/CreoBom.cs).
|
||||||
|
|
||||||
|
### 6.1 Struttura del file BOM
|
||||||
|
|
||||||
|
- Codifica **UTF-8 con BOM** (`EF BB BF`), fine riga **CRLF**.
|
||||||
|
- Due righe vuote in testa, poi il primo blocco.
|
||||||
|
- **Tre** righe vuote fra un blocco e il successivo, **due** prima del
|
||||||
|
riepilogo. Il file termina con un solo CRLF.
|
||||||
|
- Riga voce: `{qta:6} {tipo:>12} {nome}` — cioè quantità allineata a destra su
|
||||||
|
6 caratteri, spazio, tipo allineato a destra su 12, due spazi, nome.
|
||||||
|
- Intestazioni: `Assieme <nome> contiene:` per la radice,
|
||||||
|
`Sottoassieme <nome> contiene:` per gli altri.
|
||||||
|
|
||||||
|
### 6.2 Ordine dei blocchi
|
||||||
|
|
||||||
|
Visita **depth-first pre-order**: si emette il blocco di un sottoassieme e
|
||||||
|
subito dopo, ricorsivamente, quelli dei suoi sottoassiemi, prima di passare al
|
||||||
|
fratello successivo. Ogni modello compare **una volta sola** anche se montato
|
||||||
|
più volte (deduplica per nome, `CreoAsm.Blocchi`).
|
||||||
|
|
||||||
|
> **Verificato.** `9244218080_C01.asm` monta `2605410010_C01` due volte
|
||||||
|
> (`scripts/9244218080_c01.asm.1` + `scripts/9244218080_c01.bom.2` di riferimento): la BOM
|
||||||
|
> 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
|
||||||
|
|
||||||
|
`Summary of items for <radice>:` elenca **solo le parti**, con la quantità
|
||||||
|
totale moltiplicata lungo l'albero (una parte in 4 esemplari dentro un
|
||||||
|
sottoassieme montato 2 volte conta 8), nell'ordine di **prima comparsa** nella
|
||||||
|
visita depth-first (`CreoAsm.Sommario`). Una parte che compare a più livelli
|
||||||
|
occupa la posizione della sua prima comparsa e somma le quantità.
|
||||||
|
|
||||||
|
### 6.4 Risoluzione dei file dei sottoassiemi
|
||||||
|
|
||||||
|
È il punto che il formato **non** risolve: il `.asm` registra il nome del
|
||||||
|
modello, non il percorso. Servono quindi cartelle di ricerca fornite
|
||||||
|
dall'esterno (`-p`/`--path`). `CreoAsm.CreaFinder` indicizza `<nome>.asm.<rev>`
|
||||||
|
senza distinzione di maiuscole e, a parità di nome, prende la **revisione più
|
||||||
|
alta** presente.
|
||||||
|
|
||||||
|
> **Verificato: è la scelta giusta.** Il `.asm` padre non registra la revisione attesa del
|
||||||
|
> figlio (cercata e non trovata, vedi §7), ma non serve: confermato che **Creo stesso apre
|
||||||
|
> sempre la revisione più alta disponibile** di un componente, indipendentemente da quale
|
||||||
|
> fosse presente quando l'assieme padre fu salvato l'ultima volta. Caso reale controllato:
|
||||||
|
> `9284208092_D110.asm` (salvato 2018-08-01) referenzia `9284202481.prt`, che sulla share ha
|
||||||
|
> revisioni sia del 2014 (v1-v3, presenti all'epoca) sia del 2024 (v4-v6, arrivate anni dopo) -
|
||||||
|
> 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
|
||||||
|
`*** NON RISOLTO: file .asm non trovato ***` e le sue parti restano fuori dal
|
||||||
|
riepilogo. Il risultato è dichiaratamente incompleto invece che silenziosamente
|
||||||
|
sbagliato.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Stato di ogni punto aperto
|
||||||
|
|
||||||
|
Tabella di riferimento: cosa è provato, come, e cosa resta genuinamente ignoto. Il dettaglio
|
||||||
|
di ciascun punto segue sotto, in due gruppi.
|
||||||
|
|
||||||
|
| Punto | Stato | Prova |
|
||||||
|
|---|---|---|
|
||||||
|
| Deduplica blocchi + moltiplicazione quantità (§6.2/§6.3) | Risolto | BOM reale (§7.1) |
|
||||||
|
| Revisione del figlio che il padre si aspetta (§6.4) | Risolto | BOM reale (§7.1) |
|
||||||
|
| Stato soppresso/escluso in `members` | Risolto | BOM reale (§7.1) |
|
||||||
|
| Family table di **assiemi** (rilevamento) | Risolto: serve la conferma del nome proprio (`objname`), il solo `model_name` dava falsi positivi | BOM reali + albero `raw/` completo (§7.1) |
|
||||||
|
| Family table di **assiemi** (contenuto di ogni istanza) | Non risolto, nessun impatto sulla BOM (fallback già corretto) | Scan sull'intera share (§7.2) |
|
||||||
|
| Costante `\x82\x7b` in `ConfigName` | Significato ignoto, rischio coperto | Scala 350 file (§7.2) |
|
||||||
|
| Occorrenze multiple di `members` (simplified rep) | Mai osservato, rischio coperto | Scala 350 file (§7.2) |
|
||||||
|
| Matrice di posizionamento (`e1`/`e2`/`e3`/`origin`) | Non letto, nessun impatto sulla BOM | — (§7.2) |
|
||||||
|
| Campi numerici dell'header dopo `ASSEMBLY` | Non letto, nessun impatto sulla BOM | — (§7.2) |
|
||||||
|
| Divergenza `family_name` ≠ `name` | Mai osservata, non letta | — (§7.2) |
|
||||||
|
|
||||||
|
"Rischio coperto" vuol dire: il codice non sa cosa significhi il byte, ma verifica un
|
||||||
|
invariante che lo renderebbe visibile (eccezione) se mai smettesse di valere - non è una prova
|
||||||
|
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
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet run scripts/CreoBom.cs -- raw/9244228558.asm.20 -p M:\modelli -o out\9244228558.bom.1
|
||||||
|
dotnet run scripts/CreoBom.cs -- raw/9244228558.asm.20 -p M:\modelli # stampa a video
|
||||||
|
dotnet run scripts/Analyze.cs -- raw/9244228558.asm.20 "ConfigName" # dump dei byte per investigare a mano
|
||||||
|
```
|
||||||
|
|
||||||
|
Senza `-p`/`--path`, la ricerca dei sottoassiemi usa la cartella del file di partenza.
|
||||||
|
`-p` è ripetibile per più cartelle. `-o` scrive il file col BOM UTF-8 e CRLF descritti
|
||||||
|
al §6.1; senza `-o` il testo va su stdout (con `\n` al posto di `\r\n`, per leggibilità
|
||||||
|
in console).
|
||||||
+94
-4
@@ -1,10 +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. Non esiste un parser strutturale completo in questo
|
documentato pubblicamente. Questo documento descrive l'euristica testuale di
|
||||||
progetto: l'estrazione dei componenti si basa su un'euristica testuale,
|
`AssemblyComponentScanner.cs`: conta le occorrenze di stringhe candidate nel testo binario,
|
||||||
validata a mano confrontando l'output con l'albero reale mostrato da Creo.
|
validata a mano confrontando l'output con l'albero reale mostrato da Creo. Riassume cosa si
|
||||||
Questo documento riassume cosa si è osservato e i suoi limiti.
|
è osservato e i suoi limiti, in particolare i falsi positivi che un conteggio testuale non
|
||||||
|
può escludere con certezza (riferimenti fantasma, componenti soppressi con placement
|
||||||
|
residuo — vedi sotto).
|
||||||
|
|
||||||
|
Un reverse engineering successivo ha decodificato la **struttura a record** del file (non
|
||||||
|
solo pattern testuali): legge esattamente la sezione `members` di un assieme, senza contare
|
||||||
|
occorrenze né rischiare i falsi positivi descritti in questo documento. Implementato in
|
||||||
|
`PrepStep/AsmRecordReader.cs`, documentato in [`formato-asm-record.md`](formato-asm-record.md).
|
||||||
|
È il metodo **preferito** in GUI (fase "Trova componenti referenziati"/"Verifica
|
||||||
|
completezza", `Form1.EstraiCodiciReferenziati`) e nello script `CreoBom.cs`:
|
||||||
|
`AssemblyComponentScanner` (l'euristica di questo documento) resta come **fallback**, usato
|
||||||
|
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
|
||||||
|
|
||||||
@@ -281,6 +295,82 @@ per `data_modifica DESC` e tenuta solo la riga più recente per tipo, altrimenti
|
|||||||
si rischia di risolvere un codice contro una copia diversa (e potenzialmente
|
si rischia di risolvere un codice contro una copia diversa (e potenzialmente
|
||||||
diversa nel contenuto) da quella realmente referenziata.
|
diversa nel contenuto) da quella realmente referenziata.
|
||||||
|
|
||||||
|
### Trappola nella BFS: un fallimento di placement non è definitivo
|
||||||
|
|
||||||
|
Un codice può comparire come riferimento fantasma (senza vero placement, vedi
|
||||||
|
sopra) in un file scansionato **prima** di quello in cui invece ha un vero
|
||||||
|
placement — tipicamente perché lo stesso codice è mostrato anche nel file
|
||||||
|
dell'assieme padre come promemoria/metadato, oltre che nel file del proprio
|
||||||
|
assieme dove è effettivamente posizionato. Osservato su un caso reale
|
||||||
|
(assieme `9244228558.asm`): il componente `9244236104` compare nel file
|
||||||
|
dell'assieme generale come riferimento nudo senza placement (viene scartato
|
||||||
|
lì, correttamente), ma ha un vero `Component id 45 (9244236104.PRT)` dentro
|
||||||
|
`9244228558_c01.asm`, il sotto-assieme che lo contiene realmente — scansionato
|
||||||
|
solo _dopo_ il file padre nella BFS.
|
||||||
|
|
||||||
|
Una BFS che marca un candidato come "già visto" al primo incontro, anche
|
||||||
|
quando quell'incontro è un fallimento di placement, blocca per sempre ogni
|
||||||
|
ri-verifica successiva: il componente resta perso anche quando il file giusto
|
||||||
|
viene scansionato più avanti. Nei casi più gravi il codice bloccato è esso
|
||||||
|
stesso un **sotto-assieme** (osservato con `9256510047` dentro
|
||||||
|
`9256518042.asm`): se il suo codice viene scartato prematuramente, il suo file
|
||||||
|
non viene nemmeno accodato, e si perde l'intero sotto-albero sottostante
|
||||||
|
(compresi `9056510047`/`9056510047_C01`/`9056510047_C02` in questo caso).
|
||||||
|
|
||||||
|
`scripts/BomViaDb.cs` (versione iniziale) aveva esattamente questo bug: un set
|
||||||
|
`codiciVisti` unico bloccava sia i candidati confermati sia quelli scartati
|
||||||
|
per mancato placement. Corretto separando le due cose: un fallimento di
|
||||||
|
placement non aggiunge il codice a nessun set di blocco, quindi resta
|
||||||
|
ritestabile ogni volta che lo stesso candidato ricompare in un file successivo
|
||||||
|
già in coda. L'accodamento di un nuovo file da scansionare resta invece
|
||||||
|
condizionato al placement confermato (non al solo essere un codice noto nel
|
||||||
|
DB) — senza questo secondo vincolo, un codice generico che per puro caso
|
||||||
|
coincide con un codice noto (es. `"915"`, `"932"`) farebbe esplodere la
|
||||||
|
scansione su file estranei in tutto l'indice (verificato: passa da ~100 a
|
||||||
|
quasi 19000 "componenti trovati" e la scansione si blocca).
|
||||||
|
|
||||||
|
**Il codice della GUI (`Form1.RisolviAlberoComponenti` +
|
||||||
|
`AssemblyComponentScanner.EstraiCodiciReferenziati`) non ha questo bug**,
|
||||||
|
verificato riproducendo lo stesso caso reale con un test dedicato: la
|
||||||
|
classificazione placement-sì/no è già interamente delegata a
|
||||||
|
`EstraiCodiciReferenziati`, chiamata da zero per ogni file scansionato — un
|
||||||
|
fallimento in un file non lascia traccia nel set `codiciVisti` del chiamante,
|
||||||
|
che accumula solo codici già confermati. Il file successivo che referenzia
|
||||||
|
davvero il componente lo ritrova quindi senza ostacoli, senza bisogno di
|
||||||
|
nessuna modifica. Vale comunque come nota di design da preservare: il giorno
|
||||||
|
in cui la logica di `RisolviAlberoComponenti` venisse riscritta per fare la
|
||||||
|
verifica di placement inline nella BFS (come `BomViaDb.cs`), andrebbe
|
||||||
|
applicata la stessa correzione.
|
||||||
|
|
||||||
|
### Seconda discrepanza trovata: l'eccezione dei codici con suffisso
|
||||||
|
|
||||||
|
Confrontando `scripts/BomViaDb.cs` (corretto come sopra) con la GUI sullo
|
||||||
|
stesso assieme di un caso reale più grande (`9284218024b.asm`, ~530
|
||||||
|
componenti), le due liste **non coincidevano**: lo script perdeva l'intero
|
||||||
|
sotto-assieme `2546220101` (`_C01`..`_C04`, inclusi i cuscinetti
|
||||||
|
`INAFAG_NA6901-XL_...` dentro `_C04`), presente invece nella BOM ufficiale
|
||||||
|
Creo e trovato correttamente dalla GUI.
|
||||||
|
|
||||||
|
Causa: `2546220101_C01`..`_C04` hanno un vero placement nel proprio file
|
||||||
|
(`2546220101.asm`), ma **senza il suffisso** - il testo riporta solo
|
||||||
|
`Componente ID N (2546220101.PRT)`, il dettaglio `_C01` compare solo altrove
|
||||||
|
nei metadati, non vicino al placement (stesso fenomeno già documentato sopra
|
||||||
|
in "Placement senza il dettaglio del suffisso/bracket", osservato lì solo per
|
||||||
|
famiglie UNI/dimensionali). `AssemblyComponentScanner.EstraiCodiciReferenziati`
|
||||||
|
già gestisce questo caso: un codice con suffisso di revisione/variante
|
||||||
|
(`SuffissoRegex`, es. `_C01`, `_D100`) o prefisso di family table nota (`UNI`,
|
||||||
|
`CUSCI_`) è considerato "forma nota" ed esentato dal placement esplicito -
|
||||||
|
basta che occorra almeno 2 volte nel file. `BomViaDb.cs` richiedeva invece
|
||||||
|
sempre il placement letterale per ogni codice, senza questa eccezione.
|
||||||
|
|
||||||
|
Corretto portando la stessa regola (`FormaNota` + conteggio occorrenze) in
|
||||||
|
`BomViaDb.cs`. Riverificato su entrambi i casi reali (`9244228558.asm`,
|
||||||
|
`9284218024b.asm`): script e GUI producono ora **esattamente lo stesso
|
||||||
|
insieme** di componenti, e in entrambi coincide con la BOM ufficiale Creo (a
|
||||||
|
meno delle sole istanze di family table risolte al nome del file realmente
|
||||||
|
indicizzato, es. `2502020105<CUSCI_DGBB7>` → `CUSCI_DGBB7` - comportamento
|
||||||
|
corretto, non una discrepanza).
|
||||||
|
|
||||||
## Strumento di analisi manuale
|
## Strumento di analisi manuale
|
||||||
|
|
||||||
[`scripts/Analyze.cs`](../scripts/Analyze.cs) stampa il contesto testuale ed
|
[`scripts/Analyze.cs`](../scripts/Analyze.cs) stampa il contesto testuale ed
|
||||||
|
|||||||
+24
-54
@@ -1,17 +1,16 @@
|
|||||||
|
#:project ../PrepStep/PrepStep.csproj
|
||||||
|
#:property TargetFramework=net10.0-windows
|
||||||
#:package Microsoft.Data.Sqlite@10.0.12
|
#:package Microsoft.Data.Sqlite@10.0.12
|
||||||
// dotnet run scripts/BomViaDb.cs -- <file.asm> [cad_index.db] [output.txt]
|
// dotnet run scripts/BomViaDb.cs -- <file.asm> [cad_index.db] [output.txt]
|
||||||
//
|
//
|
||||||
// PROVA: a differenza di ExtractCreoStrings.cs (che restringe i candidati a un solo
|
// Stessa risoluzione ricorsiva dell'albero componenti di Form1.RisolviAlberoComponenti
|
||||||
// pattern di codice, tarato sui codici numerici Pro-E), qui l'estrazione è generica -
|
// nella GUI, da riga di comando: usa direttamente PrepStep.AssemblyComponentScanner (via
|
||||||
// qualunque run di lettere/cifre/underscore/trattino - e il DB decide se è un codice
|
// "#:project", niente ProjectReference separato) invece di duplicare l'euristica di
|
||||||
// vero, così si intercettano anche i nomi alfabetici delle parti commerciali scaricate
|
// estrazione/placement. Un'unica fonte di verità - niente rischio che script e GUI
|
||||||
// come STEP (es. "ANELLO_ESTERNO-1-SOLID1"). L'essere nel DB da solo non basta però:
|
// divergano silenziosamente, come accaduto prima di questa riscrittura (vedi
|
||||||
// codici corti o generici (es. "907", "NONE") comparirebbero per puro rumore binario,
|
// docs/formato-asm.md).
|
||||||
// quindi si richiede comunque il placement esplicito nel file, come in
|
|
||||||
// ExtractCreoStrings.cs. Stessa espansione ricorsiva sui sotto-assiemi.
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using Microsoft.Data.Sqlite;
|
using Microsoft.Data.Sqlite;
|
||||||
|
using PrepStep;
|
||||||
|
|
||||||
if (args.Length < 1)
|
if (args.Length < 1)
|
||||||
{
|
{
|
||||||
@@ -23,20 +22,12 @@ string asmIniziale = args[0];
|
|||||||
string dbPath = args.Length > 1 ? args[1] : "cad_index.db";
|
string dbPath = args.Length > 1 ? args[1] : "cad_index.db";
|
||||||
string outPath = args.Length > 2 ? args[2] : "BOM.txt";
|
string outPath = args.Length > 2 ? args[2] : "BOM.txt";
|
||||||
|
|
||||||
// Oltre il 90% dei codici in indice sono numerici Pro-E, ma una minoranza (parti
|
|
||||||
// commerciali scaricate come STEP, es. "ANELLO_ESTERNO-1-SOLID1") ha nomi alfabetici
|
|
||||||
// con trattini/underscore. Niente forma di codice ipotizzata a mano: si tokenizza in
|
|
||||||
// modo generico (run di lettere/cifre/underscore/trattino) e si lascia al DB decidere
|
|
||||||
// cos'è un codice vero - stesso principio già usato per il filtro, esteso anche
|
|
||||||
// all'estrazione dei candidati. Cattura anche "UNI6592"/"CUSCI_ACBB7" dentro
|
|
||||||
// "<UNI6592>" gratis: '<' e '>' non fanno parte del charset e delimitano il token.
|
|
||||||
var codiceRegex = new Regex(@"[A-Za-z0-9_-]{3,}", RegexOptions.Compiled);
|
|
||||||
|
|
||||||
using var connection = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly");
|
using var connection = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly");
|
||||||
connection.Open();
|
connection.Open();
|
||||||
|
|
||||||
// Tutti i codici prt/asm noti all'indice, caricati una volta sola: il lookup per ogni
|
// Tutti i codici prt/asm noti all'indice, caricati una volta sola: il lookup per ogni
|
||||||
// candidato estratto dal binario diventa O(1) invece di una query per file.
|
// candidato estratto dal binario diventa O(1) invece di una query per file, e serve anche
|
||||||
|
// a EstraiCodiciReferenziati per validare i token generici delle parti commerciali.
|
||||||
var codiciNoti = new Dictionary<string, List<(string Tipo, string Percorso)>>(StringComparer.OrdinalIgnoreCase);
|
var codiciNoti = new Dictionary<string, List<(string Tipo, string Percorso)>>(StringComparer.OrdinalIgnoreCase);
|
||||||
using (var command = connection.CreateCommand())
|
using (var command = connection.CreateCommand())
|
||||||
{
|
{
|
||||||
@@ -54,51 +45,31 @@ using (var command = connection.CreateCommand())
|
|||||||
}
|
}
|
||||||
Console.WriteLine($"Codici prt/asm noti nell'indice: {codiciNoti.Count}");
|
Console.WriteLine($"Codici prt/asm noti nell'indice: {codiciNoti.Count}");
|
||||||
|
|
||||||
// Stesso controllo di placement di ExtractCreoStrings.cs/AssemblyComponentScanner.cs:
|
|
||||||
// la sola presenza del codice nel DB non basta, perché codici corti o generici
|
|
||||||
// (es. "907", "NONE", "start_part") possono comparire per puro rumore binario. Un
|
|
||||||
// componente vero è sempre referenziato in una di queste forme (vedi
|
|
||||||
// docs/formato-asm.md): "Componente ID N (<codice>...)"/"Component id N (<codice>...)",
|
|
||||||
// il codice ripetuto due volte separato da un byte NUL, il campo
|
|
||||||
// "family_name\0<codice>\0", o il codice seguito da un bracket "<codice><GENERICO>".
|
|
||||||
bool HaPlacement(string testo, string codice) =>
|
|
||||||
Regex.IsMatch(testo, $@"Component(?:e)? id \d+ \([^()]*{Regex.Escape(codice)}[^()]*\)", RegexOptions.IgnoreCase)
|
|
||||||
|| testo.Contains(codice + "\0" + codice + "\0")
|
|
||||||
|| testo.Contains("family_name\0" + codice + "\0")
|
|
||||||
|| testo.Contains(codice + "<");
|
|
||||||
|
|
||||||
var codiceProprio = Path.GetFileName(asmIniziale).Split('.')[0];
|
var codiceProprio = Path.GetFileName(asmIniziale).Split('.')[0];
|
||||||
// Stessa esclusione di ExtractCreoStrings.cs: un assieme "_D..." referenzia sempre al
|
var codiceRadice = AssemblyComponentScanner.CodiceRadice(codiceProprio);
|
||||||
// suo interno anche l'assieme generale a cui appartiene (riferimento fantasma verso
|
|
||||||
// l'alto), da escludere subito o si risale tutto l'albero del macchinario.
|
|
||||||
var codiceRadice = codiceProprio.Split('_')[0];
|
|
||||||
|
|
||||||
var componentiTrovati = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
var componentiTrovati = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
var scartatiNonInIndice = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
var nonTrovati = new List<string>();
|
||||||
var scartatiSenzaPlacement = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
var codiciVisti = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { codiceProprio, codiceRadice };
|
var codiciVisti = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { codiceProprio, codiceRadice };
|
||||||
var daScansionare = new Queue<string>(new[] { asmIniziale });
|
var daScansionare = new Queue<string>(new[] { asmIniziale });
|
||||||
|
|
||||||
while (daScansionare.Count > 0)
|
while (daScansionare.Count > 0)
|
||||||
{
|
{
|
||||||
var asm = daScansionare.Dequeue();
|
var asm = daScansionare.Dequeue();
|
||||||
var testo = Encoding.Latin1.GetString(File.ReadAllBytes(asm));
|
foreach (var codice in AssemblyComponentScanner.EstraiCodiciReferenziati(asm, codiciNoti.Keys))
|
||||||
var candidati = codiceRegex.Matches(testo).Select(m => m.Value).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
||||||
foreach (var candidato in candidati)
|
|
||||||
{
|
{
|
||||||
if (!codiciVisti.Add(candidato))
|
// Solo i codici già confermati da EstraiCodiciReferenziati arrivano qui: nessun
|
||||||
|
// rischio del bug "primo fallimento blocca per sempre" (vedi docs/formato-asm.md),
|
||||||
|
// perché il placement è già stato verificato per questo file specifico al suo
|
||||||
|
// interno, da zero, indipendentemente da cosa è successo negli altri file.
|
||||||
|
if (!codiciVisti.Add(codice))
|
||||||
continue;
|
continue;
|
||||||
if (!codiciNoti.TryGetValue(candidato, out var componenti))
|
if (!codiciNoti.TryGetValue(codice, out var componenti))
|
||||||
{
|
{
|
||||||
scartatiNonInIndice.Add(candidato);
|
nonTrovati.Add(codice);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!HaPlacement(testo, candidato))
|
componentiTrovati.Add(codice);
|
||||||
{
|
|
||||||
scartatiSenzaPlacement.Add(candidato);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
componentiTrovati.Add(candidato);
|
|
||||||
foreach (var (tipo, percorso) in componenti)
|
foreach (var (tipo, percorso) in componenti)
|
||||||
if (tipo == "asm")
|
if (tipo == "asm")
|
||||||
daScansionare.Enqueue(percorso);
|
daScansionare.Enqueue(percorso);
|
||||||
@@ -107,7 +78,6 @@ while (daScansionare.Count > 0)
|
|||||||
|
|
||||||
File.WriteAllLines(outPath, componentiTrovati);
|
File.WriteAllLines(outPath, componentiTrovati);
|
||||||
Console.WriteLine($"{componentiTrovati.Count} componenti scritti in {outPath}");
|
Console.WriteLine($"{componentiTrovati.Count} componenti scritti in {outPath}");
|
||||||
Console.WriteLine($"Candidati nell'indice ma senza placement (scartati, {scartatiSenzaPlacement.Count}): {string.Join(", ", scartatiSenzaPlacement.Take(30))}"
|
Console.WriteLine($"Candidati non nell'indice (scartati, {nonTrovati.Count}): {string.Join(", ", nonTrovati.Take(30))}"
|
||||||
+ (scartatiSenzaPlacement.Count > 30 ? ", ..." : ""));
|
+ (nonTrovati.Count > 30 ? ", ..." : ""));
|
||||||
Console.WriteLine($"Candidati non nell'indice (rumore binario): {scartatiNonInIndice.Count}");
|
|
||||||
return 0;
|
return 0;
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
#:project ../PrepStep/PrepStep.csproj
|
||||||
|
#:property TargetFramework=net10.0-windows
|
||||||
|
// dotnet run scripts/CreoBom.cs -- <file.asm.rev> [-p cartella]... [-o out.bom.1]
|
||||||
|
//
|
||||||
|
// Genera la distinta base (BOM) di un assieme Creo, dato un elenco di cartelle dove
|
||||||
|
// cercare i sottoassiemi (-p). La lettura del formato binario (sezione "members" +
|
||||||
|
// parametro "ConfigName", vedi docs/formato-asm-record.md) vive in
|
||||||
|
// PrepStep.AsmRecordReader, via "#:project" - stessa fonte usata da Form1 nella GUI (fase
|
||||||
|
// 4/5, con fallback automatico all'euristica di AssemblyComponentScanner.cs per i file che
|
||||||
|
// non rispettano il formato validato): qui solo la ricorsione sui sottoassiemi, il
|
||||||
|
// riepilogo delle quantita' e la resa nel formato esatto della BOM Creo.
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using PrepStep;
|
||||||
|
|
||||||
|
if (args.Length == 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Uso: dotnet run scripts/CreoBom.cs -- <file.asm.rev> [-p cartella]... [-o out.bom.1]");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
string fileIniziale = args[0];
|
||||||
|
var cartelle = new List<string>();
|
||||||
|
string? outPath = null;
|
||||||
|
for (int i = 1; i < args.Length; i++)
|
||||||
|
{
|
||||||
|
if ((args[i] == "-p" || args[i] == "--path") && i + 1 < args.Length)
|
||||||
|
cartelle.Add(args[++i]);
|
||||||
|
else if (args[i] == "-o" && i + 1 < args.Length)
|
||||||
|
outPath = args[++i];
|
||||||
|
}
|
||||||
|
if (cartelle.Count == 0)
|
||||||
|
cartelle.Add(Path.GetDirectoryName(Path.GetFullPath(fileIniziale)) ?? ".");
|
||||||
|
|
||||||
|
string testoBom = CreoAsm.GeneraBom(fileIniziale, cartelle);
|
||||||
|
if (outPath is not null)
|
||||||
|
{
|
||||||
|
File.WriteAllBytes(outPath, new byte[] { 0xEF, 0xBB, 0xBF }.Concat(Encoding.UTF8.GetBytes(testoBom)).ToArray());
|
||||||
|
Console.WriteLine($"scritto {outPath} ({testoBom.Split("\r\n").Length} righe)");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.Write(testoBom.Replace("\r\n", "\n"));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
/// <summary>Ricorsione sui sottoassiemi e resa nel formato della BOM esportata da Creo. La
|
||||||
|
/// lettura del formato binario vive in <see cref="AsmRecordReader"/> (PrepStep/, condivisa
|
||||||
|
/// con la GUI). Vedi docs/formato-asm-record.md per il dettaglio dei record e i limiti noti
|
||||||
|
/// (stato soppresso non gestito, matrice di posizionamento non decodificata, deduplica dei
|
||||||
|
/// blocchi non dimostrata su assiemi che montano lo stesso sottoassieme due volte).</summary>
|
||||||
|
public static class CreoAsm
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<int, string> TipoNome = new() { [1] = "Sottoassieme", [2] = "Parte" };
|
||||||
|
|
||||||
|
/// <summary>Voci distinte con quantita', nell'ordine di prima comparsa (come la BOM).</summary>
|
||||||
|
public static List<((int Tipo, string Nome) Chiave, int Qta)> Rollup(IEnumerable<AsmRecordReader.Membro> membri)
|
||||||
|
{
|
||||||
|
var ordine = new List<(int, string)>();
|
||||||
|
var qty = new Dictionary<(int, string), int>();
|
||||||
|
foreach (var m in membri)
|
||||||
|
{
|
||||||
|
var chiave = (m.Tipo, m.Nome);
|
||||||
|
if (!qty.ContainsKey(chiave))
|
||||||
|
ordine.Add(chiave);
|
||||||
|
qty[chiave] = qty.GetValueOrDefault(chiave) + 1;
|
||||||
|
}
|
||||||
|
return ordine.Select(c => (c, qty[c])).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- risoluzione dei file -------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>nome modello -> membri, cercando <nome>.asm.<rev> nelle cartelle
|
||||||
|
/// date. A parita' di nome vince la revisione piu' alta presente. Nome null se il file
|
||||||
|
/// non c'e' in nessuna delle cartelle.</summary>
|
||||||
|
private static Func<string, List<AsmRecordReader.Membro>?> CreaFinder(IEnumerable<string> cartelle)
|
||||||
|
{
|
||||||
|
var indice = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var revisioni = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var dir in cartelle)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(dir))
|
||||||
|
continue;
|
||||||
|
foreach (var f in Directory.GetFiles(dir, "*.asm.*"))
|
||||||
|
{
|
||||||
|
if (!int.TryParse(Path.GetExtension(f).TrimStart('.'), out int rev))
|
||||||
|
continue;
|
||||||
|
string chiave = Path.GetFileName(f).Split('.')[0];
|
||||||
|
if (!revisioni.TryGetValue(chiave, out int attuale) || rev > attuale)
|
||||||
|
{
|
||||||
|
indice[chiave] = f;
|
||||||
|
revisioni[chiave] = rev;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var cache = new Dictionary<string, List<AsmRecordReader.Membro>?>();
|
||||||
|
return nome =>
|
||||||
|
{
|
||||||
|
if (!cache.TryGetValue(nome, out var mem))
|
||||||
|
{
|
||||||
|
mem = indice.TryGetValue(nome, out var percorso) ? AsmRecordReader.LeggiMembri(File.ReadAllBytes(percorso)) : null;
|
||||||
|
cache[nome] = mem;
|
||||||
|
}
|
||||||
|
return mem;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- costruzione dell'albero -----------------------------------------
|
||||||
|
|
||||||
|
private sealed record Blocco(string Tipo, string Nome, List<((int Tipo, string Nome) Chiave, int Qta)>? Righe);
|
||||||
|
|
||||||
|
/// <summary>Blocchi in ordine depth-first pre-order. Ogni modello compare una volta sola
|
||||||
|
/// (deduplica per nome) anche se montato piu' volte.</summary>
|
||||||
|
private static List<Blocco> Blocchi(string root, Func<string, List<AsmRecordReader.Membro>?> get)
|
||||||
|
{
|
||||||
|
var risultato = new List<Blocco>();
|
||||||
|
var visti = new HashSet<string>();
|
||||||
|
|
||||||
|
void Visita(string nome, string tipo)
|
||||||
|
{
|
||||||
|
if (!visti.Add(nome))
|
||||||
|
return; // Creo stampa ogni modello una volta sola
|
||||||
|
var mem = get(nome);
|
||||||
|
if (mem is null)
|
||||||
|
{
|
||||||
|
risultato.Add(new Blocco(tipo, nome, null));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var righe = Rollup(mem);
|
||||||
|
risultato.Add(new Blocco(tipo, nome, righe));
|
||||||
|
foreach (var (chiave, _) in righe)
|
||||||
|
if (chiave.Tipo == 1)
|
||||||
|
Visita(chiave.Nome, "Sottoassieme");
|
||||||
|
}
|
||||||
|
|
||||||
|
Visita(root, "Assieme");
|
||||||
|
return risultato;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Totale per parte, moltiplicato lungo l'albero, in ordine di prima comparsa: una
|
||||||
|
/// parte che compare a piu' livelli occupa la posizione della sua prima comparsa e somma
|
||||||
|
/// le quantita'.</summary>
|
||||||
|
private static void Sommario(string root, Func<string, List<AsmRecordReader.Membro>?> get, int mult,
|
||||||
|
List<string> ordine, Dictionary<string, int> acc)
|
||||||
|
{
|
||||||
|
var mem = get(root);
|
||||||
|
if (mem is null)
|
||||||
|
return;
|
||||||
|
foreach (var (chiave, qta) in Rollup(mem))
|
||||||
|
{
|
||||||
|
if (chiave.Tipo == 2)
|
||||||
|
{
|
||||||
|
if (!acc.ContainsKey(chiave.Nome))
|
||||||
|
ordine.Add(chiave.Nome);
|
||||||
|
acc[chiave.Nome] = acc.GetValueOrDefault(chiave.Nome) + qta * mult;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Sommario(chiave.Nome, get, mult * qta, ordine, acc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- resa nel formato di Creo ------------------------------------------
|
||||||
|
|
||||||
|
private static string RigaVoce(int qta, string tipo, string nome) => $"{qta,6} {tipo,12} {nome}";
|
||||||
|
|
||||||
|
/// <summary>Testo della BOM, identico byte per byte a quello esportato da Creo (UTF-8 con
|
||||||
|
/// BOM, CRLF - vedi GeneraBom per la scrittura su file).</summary>
|
||||||
|
private static string Render(string root, Func<string, List<AsmRecordReader.Membro>?> get)
|
||||||
|
{
|
||||||
|
var pezzi = new List<string>();
|
||||||
|
foreach (var blocco in Blocchi(root, get))
|
||||||
|
{
|
||||||
|
string testa = $"{blocco.Tipo} {blocco.Nome} contiene:";
|
||||||
|
if (blocco.Righe is null)
|
||||||
|
{
|
||||||
|
pezzi.Add(testa + "\r\n" + new string(' ', 5) + "*** NON RISOLTO: file .asm non trovato ***");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var righe = blocco.Righe.Select(r => RigaVoce(r.Qta, TipoNome[r.Chiave.Tipo], r.Chiave.Nome));
|
||||||
|
pezzi.Add(string.Join("\r\n", new[] { testa }.Concat(righe)));
|
||||||
|
}
|
||||||
|
|
||||||
|
var ordine = new List<string>();
|
||||||
|
var acc = new Dictionary<string, int>();
|
||||||
|
Sommario(root, get, 1, ordine, acc);
|
||||||
|
var totale = ordine.Select(n => RigaVoce(acc[n], "Parte", n));
|
||||||
|
pezzi.Add(string.Join("\r\n", new[] { $"Summary of items for {root}:" }.Concat(totale)));
|
||||||
|
|
||||||
|
// due righe vuote iniziali, tre fra i blocchi, due prima del riepilogo
|
||||||
|
return "\r\n\r\n" + string.Join("\r\n\r\n\r\n\r\n", pezzi.Take(pezzi.Count - 1)) + "\r\n\r\n\r\n" + pezzi[^1] + "\r\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GeneraBom(string percorsoAsm, IEnumerable<string> cartelle)
|
||||||
|
{
|
||||||
|
byte[] dati = File.ReadAllBytes(percorsoAsm);
|
||||||
|
return Render(AsmRecordReader.NomeProprio(dati), CreaFinder(cartelle));
|
||||||
|
}
|
||||||
|
}
|
||||||
+22
-110
@@ -1,15 +1,15 @@
|
|||||||
|
#:project ../PrepStep/PrepStep.csproj
|
||||||
|
#:property TargetFramework=net10.0-windows
|
||||||
#:package Microsoft.Data.Sqlite@10.0.12
|
#:package Microsoft.Data.Sqlite@10.0.12
|
||||||
// dotnet run scripts/ExtractCreoStrings.cs -- <file.asm> [cad_index.db] [output.txt]
|
// dotnet run scripts/ExtractCreoStrings.cs -- <file.asm> [cad_index.db] [output.txt]
|
||||||
//
|
//
|
||||||
// Estrae dal file .asm i codici dei componenti referenziati, espandendo ricorsivamente
|
// Come scripts/BomViaDb.cs, ma senza passare "codiciNoti" a EstraiCodiciReferenziati: si
|
||||||
// ogni sotto-assieme (.asm) trovato nell'indice, finché non restano solo PRT (foglie) o
|
// riconoscono solo i codici numerici Pro-E (che si validano dalla forma, senza bisogno del
|
||||||
// codici non presenti nell'indice. Stessa euristica di PrepStep/AssemblyComponentScanner.cs:
|
// DB - vedi PrepStep/AssemblyComponentScanner.cs), non le parti commerciali con nome
|
||||||
// i codici veri compaiono più volte nel file, il rumore binario quasi sempre una sola volta;
|
// alfabetico. Prova più semplice/mirata rispetto a BomViaDb.cs, utile quando serve solo
|
||||||
// gli ID interni "ModelView#<numero>" sono esclusi via lookbehind - vedi scripts/Analyze.cs.
|
// individuare/segnalare codici numerici mancanti dall'indice.
|
||||||
// Ogni codice trovato viene validato contro l'indice cad_index.db.
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using Microsoft.Data.Sqlite;
|
using Microsoft.Data.Sqlite;
|
||||||
|
using PrepStep;
|
||||||
|
|
||||||
if (args.Length < 1)
|
if (args.Length < 1)
|
||||||
{
|
{
|
||||||
@@ -21,114 +21,29 @@ string asmIniziale = args[0];
|
|||||||
string dbPath = args.Length > 1 ? args[1] : "cad_index.db";
|
string dbPath = args.Length > 1 ? args[1] : "cad_index.db";
|
||||||
string outPath = args.Length > 2 ? args[2] : "BOM.txt";
|
string outPath = args.Length > 2 ? args[2] : "BOM.txt";
|
||||||
|
|
||||||
// Seconda alternativa: componenti normalizzati definiti come family table in Creo (viti/
|
|
||||||
// dadi/rosette UNI, ma anche cuscinetti CUSCI_...). Compaiono come "Componente ID N
|
|
||||||
// (R12--_-_U6592<UNI6592>.PRT)": "R12--_-_U6592" è il nome della singola istanza (riga
|
|
||||||
// della family table, non un file separato su disco), il file reale indicizzato è il
|
|
||||||
// generico "uni6592.prt.<versione>"/"cusci_acbb7.prt.<versione>" - va quindi estratto solo
|
|
||||||
// il contenuto tra "<" e ">".
|
|
||||||
// Terza alternativa: varianti dimensionali con suffisso puramente numerico (es. cinghie
|
|
||||||
// "2702220182_8188", dove "8188" è una misura, non una lettera di revisione/sotto-assieme
|
|
||||||
// come "_C01"/"_D100") - la lettera dopo "_" è quindi opzionale, non obbligatoria. La
|
|
||||||
// lettera di revisione e il suffisso "_" possono anche comparire insieme sullo stesso
|
|
||||||
// codice (es. "9284200366A_C01", componente di un sotto-assieme già di per sé revisionato).
|
|
||||||
var codiceRegex = new Regex(
|
|
||||||
@"(?<!ModelView#)\b\d{8,12}[A-Za-z]{0,2}(?:_[A-Za-z]?\d{2,4})?\b|(?<=<)(?:UNI\d+|CUSCI_[A-Za-z0-9]+)(?=>)",
|
|
||||||
RegexOptions.Compiled);
|
|
||||||
const int occorrenzeMinime = 2;
|
|
||||||
|
|
||||||
HashSet<string> EstraiCodiciReferenziati(string percorsoAsm)
|
|
||||||
{
|
|
||||||
var testo = Encoding.Latin1.GetString(File.ReadAllBytes(percorsoAsm));
|
|
||||||
var occorrenze = new Dictionary<string, int>();
|
|
||||||
foreach (Match m in codiceRegex.Matches(testo))
|
|
||||||
occorrenze[m.Value] = occorrenze.GetValueOrDefault(m.Value) + 1;
|
|
||||||
var candidati = occorrenze.Where(kv => kv.Value >= occorrenzeMinime).Select(kv => kv.Key).ToHashSet();
|
|
||||||
|
|
||||||
// Ripetersi >=2 volte nel file non basta per un codice "nudo" (senza suffisso "_..."):
|
|
||||||
// riferimenti al proprio assieme padre (nome sessione/file) o a codici superati/
|
|
||||||
// rinominati (caso "fantasma", docs/formato-asm.md) ripetono comunque il codice più
|
|
||||||
// volte pur non essendo mai un componente vero. Per questi si richiede un vero
|
|
||||||
// placement, in una delle quattro forme osservate nel binario: "Componente ID N
|
|
||||||
// (<codice>...)"/"Component id N (<codice>...)" (la stringa cambia con la
|
|
||||||
// localizzazione Creo del file); il codice ripetuto due volte di seguito separato da
|
|
||||||
// un singolo byte NUL ("<codice>\0<codice>\0", osservato per componenti di family
|
|
||||||
// table non-UNI, es. cuscinetti "CUSCI_..."); il campo "family_name\0<codice>\0"
|
|
||||||
// (osservato una sola volta per file, su un sotto-assieme referenziato come family
|
|
||||||
// table generica invece che con un placement testuale esplicito); oppure il codice
|
|
||||||
// seguito immediatamente da un bracket "<codice><GENERICO>" - l'istanza di una family
|
|
||||||
// table non-UNI vista nel nome (es. "2510060303<CUSCI_ACBB7>", un cuscinetto): il
|
|
||||||
// codice dell'istanza non è mai indicizzato a sé (solo il generico tra "<" e ">" lo è,
|
|
||||||
// vedi sotto), ma questo segnale lo fa comunque sopravvivere fino alla ricerca nel DB
|
|
||||||
// invece di scomparire in silenzio come "nudo senza placement" - lì verrà comunque
|
|
||||||
// scartato come "non in indice", ma in modo tracciabile. Non si applica alle varianti
|
|
||||||
// con suffisso "_..." né alle istanze family table UNI: in alcuni file il placement
|
|
||||||
// riporta solo il nome istanza/generico senza il dettaglio del suffisso o del bracket
|
|
||||||
// "<...>", che vive altrove nei metadati - lì l'occorrenza ripetuta resta l'unico
|
|
||||||
// segnale disponibile.
|
|
||||||
bool HaPlacement(string codice) =>
|
|
||||||
Regex.IsMatch(testo, $@"Component(?:e)? id \d+ \([^()]*{Regex.Escape(codice)}[^()]*\)", RegexOptions.IgnoreCase)
|
|
||||||
|| testo.Contains(codice + "\0" + codice + "\0")
|
|
||||||
|| testo.Contains("family_name\0" + codice + "\0")
|
|
||||||
|| testo.Contains(codice + "<");
|
|
||||||
|
|
||||||
candidati.RemoveWhere(c => Regex.IsMatch(c, @"^\d{8,12}[A-Za-z]{0,2}$") && !HaPlacement(c));
|
|
||||||
return candidati;
|
|
||||||
}
|
|
||||||
|
|
||||||
using var connection = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly");
|
using var connection = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly");
|
||||||
connection.Open();
|
connection.Open();
|
||||||
|
|
||||||
// "files_ultima_versione" è una VIEW con ROW_NUMBER() OVER (...), che SQLite non
|
var codiciNoti = new Dictionary<string, List<(string Tipo, string Percorso)>>(StringComparer.OrdinalIgnoreCase);
|
||||||
// materializza: ogni query la ricalcola sull'intera tabella "files" (~230ms anche per un
|
using (var command = connection.CreateCommand())
|
||||||
// solo codice, su ~300k righe). Batchare tutti i codici trovati in un file in un'unica
|
|
||||||
// query paga quel costo una volta per file invece che una volta per codice - decisivo
|
|
||||||
// sugli assiemi con centinaia di sotto-componenti referenziati.
|
|
||||||
Dictionary<string, List<(string Tipo, string Percorso)>> CercaComponenti(IReadOnlyCollection<string> codici)
|
|
||||||
{
|
{
|
||||||
var risultato = new Dictionary<string, List<(string, string)>>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
foreach (var codice in codici)
|
|
||||||
risultato[codice] = new List<(string, string)>();
|
|
||||||
if (codici.Count == 0)
|
|
||||||
return risultato;
|
|
||||||
|
|
||||||
using var command = connection.CreateCommand();
|
|
||||||
var nomiParametri = codici.Select((_, i) => $"@c{i}").ToList();
|
|
||||||
// Lo stesso codice+tipo può esistere in più cartelle radice (duplicati/copie storiche
|
|
||||||
// sulla share, es. un generico family table ripetuto in 5 progetti scollegati): ordinato
|
|
||||||
// per data_modifica DESC, si tiene solo il più recente per codice+tipo (stessa regola di
|
|
||||||
// Form1.RisolviAlberoComponenti, vedi docs/formato-asm.md).
|
|
||||||
command.CommandText = "SELECT codice, tipo_file, percorso_completo FROM files_ultima_versione "
|
command.CommandText = "SELECT codice, tipo_file, percorso_completo FROM files_ultima_versione "
|
||||||
+ $"WHERE LOWER(codice) IN ({string.Join(",", nomiParametri)}) AND LOWER(tipo_file) IN ('prt', 'asm') "
|
+ "WHERE LOWER(tipo_file) IN ('prt', 'asm') ORDER BY codice, tipo_file, data_modifica DESC";
|
||||||
+ "ORDER BY codice, tipo_file, data_modifica DESC";
|
|
||||||
int i = 0;
|
|
||||||
foreach (var codice in codici)
|
|
||||||
command.Parameters.AddWithValue(nomiParametri[i++], codice.ToLowerInvariant());
|
|
||||||
|
|
||||||
var tipiVisti = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
using var reader = command.ExecuteReader();
|
using var reader = command.ExecuteReader();
|
||||||
while (reader.Read())
|
while (reader.Read())
|
||||||
{
|
{
|
||||||
string codiceDb = reader.GetString(0);
|
string codice = reader.GetString(0);
|
||||||
string tipo = reader.GetString(1).ToLowerInvariant();
|
string tipo = reader.GetString(1).ToLowerInvariant();
|
||||||
var visti = tipiVisti.TryGetValue(codiceDb, out var v) ? v : tipiVisti[codiceDb] = new HashSet<string>();
|
var lista = codiciNoti.TryGetValue(codice, out var l) ? l : codiciNoti[codice] = new();
|
||||||
if (visti.Add(tipo))
|
if (!lista.Any(x => x.Tipo == tipo))
|
||||||
risultato[codiceDb].Add((tipo, reader.GetString(2)));
|
lista.Add((tipo, reader.GetString(2)));
|
||||||
}
|
}
|
||||||
return risultato;
|
|
||||||
}
|
}
|
||||||
|
Console.WriteLine($"Codici prt/asm noti nell'indice: {codiciNoti.Count}");
|
||||||
|
|
||||||
// Espansione ricorsiva: ogni sotto-assieme (.asm) trovato viene a sua volta letto e i suoi
|
|
||||||
// componenti aggiunti alla coda, finché non restano solo PRT (foglie) o codici non presenti
|
|
||||||
// nell'indice - stessa logica di Form1.RisolviAlberoComponenti.
|
|
||||||
var codiceProprio = Path.GetFileName(asmIniziale).Split('.')[0];
|
var codiceProprio = Path.GetFileName(asmIniziale).Split('.')[0];
|
||||||
// Un assieme con suffisso "_D..." referenzia sempre al suo interno anche l'assieme
|
var codiceRadice = AssemblyComponentScanner.CodiceRadice(codiceProprio);
|
||||||
// generale (codice nudo, senza suffisso) a cui appartiene - riferimento fantasma verso
|
|
||||||
// l'alto, non un componente vero (vedi docs/formato-asm.md). Va escluso subito: se
|
|
||||||
// seguito, riporta dentro l'intero albero del macchinario invece del solo sotto-assieme
|
|
||||||
// richiesto (visto su 9284208188_D600 -> 9284208188, che da solo genera 598 falsi
|
|
||||||
// positivi invece di ~90 componenti reali).
|
|
||||||
var codiceRadice = codiceProprio.Split('_')[0];
|
|
||||||
var componentiTrovati = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
var componentiTrovati = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
var nonTrovati = new List<string>();
|
var nonTrovati = new List<string>();
|
||||||
var codiciVisti = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { codiceProprio, codiceRadice };
|
var codiciVisti = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { codiceProprio, codiceRadice };
|
||||||
@@ -137,14 +52,11 @@ var daScansionare = new Queue<string>(new[] { asmIniziale });
|
|||||||
while (daScansionare.Count > 0)
|
while (daScansionare.Count > 0)
|
||||||
{
|
{
|
||||||
var asm = daScansionare.Dequeue();
|
var asm = daScansionare.Dequeue();
|
||||||
var candidati = EstraiCodiciReferenziati(asm)
|
foreach (var codice in AssemblyComponentScanner.EstraiCodiciReferenziati(asm))
|
||||||
.Where(codiciVisti.Add)
|
|
||||||
.ToList();
|
|
||||||
var trovatiPerCodice = CercaComponenti(candidati);
|
|
||||||
foreach (var codice in candidati)
|
|
||||||
{
|
{
|
||||||
var componenti = trovatiPerCodice[codice];
|
if (!codiciVisti.Add(codice))
|
||||||
if (componenti.Count == 0)
|
continue;
|
||||||
|
if (!codiciNoti.TryGetValue(codice, out var componenti))
|
||||||
{
|
{
|
||||||
nonTrovati.Add(codice);
|
nonTrovati.Add(codice);
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
Reference in new issue
Block a user