Compare commits
17
Commits
28a72c80b2
..
0.9.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
206db85e12 | ||
|
|
d81fede4cc | ||
|
|
948937ba75 | ||
|
|
ff4e7a71e0 | ||
|
|
be4989fc8d | ||
|
|
e94331b69f | ||
|
|
3e9c78074c | ||
|
|
e29a1bf90a | ||
|
|
28aa16837b | ||
|
|
d9f16f2844 | ||
|
|
db4c444473 | ||
|
|
13f6de6402 | ||
|
|
54f267b508 | ||
|
|
0ae2a5e7cf | ||
|
|
b136e7f49b | ||
|
|
cf4c2e8e29 | ||
|
|
9af426ca1b |
No files matched your search
@@ -37,3 +37,6 @@ publish/
|
|||||||
|
|
||||||
# Output di scripts/ExtractCreoStrings.cs
|
# Output di scripts/ExtractCreoStrings.cs
|
||||||
/BOM.txt
|
/BOM.txt
|
||||||
|
|
||||||
|
# Impostazioni locali dell'editor
|
||||||
|
.vscode/
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# 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.9.0] - 2026-09-23
|
||||||
|
|
||||||
|
### Aggiunto
|
||||||
|
|
||||||
|
- Guida utente `docs/guida-utente.md`: tutti i passi, dalla distinta esportata dal gestionale
|
||||||
|
fino all'esportazione STEP con Creo Distributed Batch.
|
||||||
|
- Fase 2: bottone "Verifica presenza", visibile solo dopo un import riuscito; è lui (non il
|
||||||
|
solo import) a sbloccare "Avanti". Etichette numerate accanto ai bottoni.
|
||||||
|
- Dialogo "Componenti referenziati trovati": bottone "Chiudi" (anche con Esc) e icona dell'app.
|
||||||
|
|
||||||
|
### Modificato
|
||||||
|
|
||||||
|
- "Scegli cartella di lavoro..." e "Pulisci raw e step..." spostati dalla fase 1 al menu File,
|
||||||
|
raggiungibili in qualunque momento; "File → Nuova estrazione" rinominato in "Nuovo".
|
||||||
|
- Fase 1 ridisegnata in una card di stato "Database" con indicatore verde/rosso; rimossa la card
|
||||||
|
"Cartella di lavoro".
|
||||||
|
- Barra Avanti/Indietro trasformata in floatbar staccata dai bordi, con bottoni di dimensioni e
|
||||||
|
stile uniformi (riusata anche nel dialogo dei componenti referenziati).
|
||||||
|
- Finestra Guida ridisegnata: sezioni con titolo, passi numerati e callout "Nota"/"Attenzione"
|
||||||
|
colorati; sidebar con selezione nel blu dell'app.
|
||||||
|
- Testi delle fasi 3-4 semplificati.
|
||||||
|
|
||||||
|
### Corretto
|
||||||
|
|
||||||
|
- "Avanti" restava disabilitato in fase 1 all'avvio anche con DB e cartella di lavoro già
|
||||||
|
caricati.
|
||||||
|
- Guida in-app e README citavano ancora "Verifica completezza assiemi" e "Cambia cartella...",
|
||||||
|
rimossi da tempo.
|
||||||
|
|
||||||
|
## [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).
|
||||||
@@ -7,7 +7,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
`PrepStep` è il nome tecnico del progetto (cartella, namespace, csproj, eseguibile
|
`PrepStep` è il nome tecnico del progetto (cartella, namespace, csproj, eseguibile
|
||||||
`PrepStep.exe`); il nome mostrato all'utente finale è **Cevolani StepPrep** (`<Product>`/
|
`PrepStep.exe`); il nome mostrato all'utente finale è **Cevolani StepPrep** (`<Product>`/
|
||||||
`<AssemblyTitle>` in `PrepStep/PrepStep.csproj`, usato nel titolo della finestra insieme
|
`<AssemblyTitle>` in `PrepStep/PrepStep.csproj`, usato nel titolo della finestra insieme
|
||||||
alla versione, `<Version>`, attualmente `0.1.0`). È un'app Windows Forms (.NET 10, WinForms) che indicizza e permette di
|
alla versione, `<Version>`, attualmente `0.9.0`). È un'app Windows Forms (.NET 10, WinForms) che indicizza e permette di
|
||||||
cercare i file CAD Pro-ENGINEER/Creo contenuti in tutte le cartelle di primo livello
|
cercare i file CAD Pro-ENGINEER/Creo contenuti in tutte le cartelle di primo livello
|
||||||
di un disco/share di rete (radice di default `X:\`), salvando i metadati in un DB
|
di un disco/share di rete (radice di default `X:\`), salvando i metadati in un DB
|
||||||
SQLite locale (`cad_index.db`). Il flusso a valle (fasi 2-5, vedi sotto) prepara i file
|
SQLite locale (`cad_index.db`). Il flusso a valle (fasi 2-5, vedi sotto) prepara i file
|
||||||
@@ -36,6 +36,33 @@ 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/guida-utente.md`](docs/guida-utente.md) | cosa clicca l'utente, passo per passo, fino a Creo Distributed Batch |
|
||||||
|
| [`docs/formato-asm-record.md`](docs/formato-asm-record.md) | parser esatto dei record `.asm`, con §7 per i casi limite |
|
||||||
|
| [`docs/formato-asm.md`](docs/formato-asm.md) | euristica testuale di fallback |
|
||||||
|
| [`docs/db-cad-index.md`](docs/db-cad-index.md) | schema e vista di `cad_index.db` |
|
||||||
|
| [`docs/copia-raw-parallela.md`](docs/copia-raw-parallela.md) | parallelizzazione della copia in `raw/` |
|
||||||
|
| [`CHANGELOG.md`](CHANGELOG.md) | storico delle versioni (formato Keep a Changelog); ogni release che cambia `<Version>` nel csproj aggiunge qui la sua voce |
|
||||||
|
|
||||||
|
Quando il cambiamento nasce da un fallimento reale di Creo, scrivere anche **il sintomo**: i
|
||||||
|
messaggi di Creo non nominano il file o la causa, e senza quel ponte la prossima volta si
|
||||||
|
riparte da zero (i §7.3/§7.4/§7.5 di `formato-asm-record.md` sono l'esempio del formato
|
||||||
|
atteso).
|
||||||
|
|
||||||
## Architettura
|
## Architettura
|
||||||
|
|
||||||
Il programma è tutto in `PrepStep/` (nessun altro progetto nella solution). Dettaglio
|
Il programma è tutto in `PrepStep/` (nessun altro progetto nella solution). Dettaglio
|
||||||
@@ -47,15 +74,20 @@ che conta per capire il prodotto.
|
|||||||
designer visuale):
|
designer visuale):
|
||||||
|
|
||||||
1. **Database** — carica/aggiorna `cad_index.db` (`Indexer.ScanAsync`, radice disco di
|
1. **Database** — carica/aggiorna `cad_index.db` (`Indexer.ScanAsync`, radice disco di
|
||||||
default `X:\` hardcoded in `RadiceDiscoDefault`); qui vive anche la **cartella di lavoro**
|
default `X:\` hardcoded in `RadiceDiscoDefault`); un pallino verde/rosso accanto al titolo
|
||||||
(destinazione di CSV/`raw/`/`step/`), proposta dall'ultima usata o dalla cartella del DB se
|
indica se un DB è caricato. La **cartella di lavoro** (destinazione di CSV/`raw/`/`step/`)
|
||||||
non c'è ancora nulla di salvato — vedi `docs/db-cad-index.md` per il dettaglio di schema e
|
non è più mostrata qui — proposta dall'ultima usata o dalla cartella del DB se non c'è
|
||||||
risoluzione del percorso DB.
|
ancora nulla di salvato, cambiabile in qualunque momento da "File → Scegli cartella di
|
||||||
|
lavoro..." — 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
|
2. **Importa Distinta Base** — `DistintaBaseImporter.ConvertiInCsv` genera il CSV
|
||||||
componente;descrizione nella cartella di lavoro; se il file segue la convenzione del
|
componente;descrizione nella cartella di lavoro (da file scelto o trascinato); se il file
|
||||||
gestionale (`DBP_<codice>_<timestamp>`), aggiunge l'assieme generale come riga in più (da
|
segue la convenzione del gestionale (`DBP_<codice>_<timestamp>`), aggiunge l'assieme
|
||||||
quel momento è un componente come tutti gli altri, nessun caso speciale nel resto del
|
generale come riga in più (da quel momento è un componente come tutti gli altri, nessun
|
||||||
codice).
|
caso speciale nel resto del codice). Il bottone "Verifica presenza" compare solo dopo un
|
||||||
|
import riuscito e cerca i componenti nell'indice (`VerificaCodiciNelDb`, righe colorate in
|
||||||
|
verde/rosso in griglia): è quello — non il solo import — a sbloccare "Avanti"
|
||||||
|
(`AvantiConsentito`).
|
||||||
3. **Crea cartella raw** — `TrovaPercorsiCad` + `CopiaFileTrovati` (parallela, vedi
|
3. **Crea cartella raw** — `TrovaPercorsiCad` + `CopiaFileTrovati` (parallela, vedi
|
||||||
`docs/copia-raw-parallela.md`) copiano in `raw/` i file CAD dei componenti del CSV trovati
|
`docs/copia-raw-parallela.md`) copiano in `raw/` i file CAD dei componenti del CSV trovati
|
||||||
nell'indice.
|
nell'indice.
|
||||||
@@ -63,15 +95,26 @@ designer visuale):
|
|||||||
sotto-assiemi/parti (`RisolviAlberoComponenti`), copiandoli anch'essi in `raw/`. Usa
|
sotto-assiemi/parti (`RisolviAlberoComponenti`), copiandoli anch'essi in `raw/`. Usa
|
||||||
`AsmRecordReader` (parser esatto dei record binari, `docs/formato-asm-record.md`) e ripiega
|
`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
|
su `AssemblyComponentScanner` (euristica testuale, `docs/formato-asm.md`) solo se il file
|
||||||
non rispetta il formato validato (`InvalidDataException`). ⚠️ Limite noto non coperto dal
|
non rispetta il formato validato (`InvalidDataException`). Tre punti da non confondere,
|
||||||
fallback: un **assieme** con family table (raro, non solo le parti normalizzate possono
|
tutti imparati facendo fallire Creo (`docs/formato-asm-record.md` §7.3, §7.4 e §7.5):
|
||||||
averla) può dare un risultato silenziosamente incompleto senza lanciare eccezione — vedi
|
i **componenti soppressi** non sono nella distinta ma il loro file serve per aprire
|
||||||
`docs/formato-asm-record.md` §7.
|
l'assieme (`EstraiCodiciSoppressi`, copiati senza finire fra i "non trovati"); i
|
||||||
|
**riferimenti esterni** (assembly feature, datum copiati) citano modelli che non sono né
|
||||||
|
membri né piazzamenti, e che Creo carica con tutto il loro albero
|
||||||
|
(`EstraiCodiciRiferimentiEsterni`, anche questi fuori dai "non trovati", ma con ricorsione);
|
||||||
|
e quando lo stesso codice esiste in più cartelle della share — succede per il 13% dei
|
||||||
|
codici — va copiata la copia nella **stessa cartella dell'assieme che la referenzia**
|
||||||
|
(`ScegliPerCartella`), perché il `.asm` non registra quale usa e sbagliarla impedisce a
|
||||||
|
Creo di aprire l'assieme.
|
||||||
5. **Verifica completezza** — mostra i `.asm` della distinta già in `raw/`, trascinabili su
|
5. **Verifica completezza** — mostra i `.asm` della distinta già in `raw/`, trascinabili su
|
||||||
Creo per l'esportazione STEP (Distributed Batch, manuale, fuori dall'app); "Verifica
|
Creo per l'esportazione STEP (Distributed Batch, manuale, fuori dall'app).
|
||||||
completezza assiemi" controlla ricorsivamente sull'indice (non su `raw/`, stessa
|
|
||||||
`AsmRecordReader`/fallback della fase 4) se tutti i sotto-componenti di ogni assieme sono
|
Menu in cima alla finestra: "File → Nuovo" (Ctrl+N) azzera lo stato delle fasi 2-5
|
||||||
presenti.
|
per ripartire con una nuova distinta, senza toccare DB/cartella di lavoro della fase 1 (va
|
||||||
|
volutamente alla fase 2, non alla 1: quei dati restano quelli già caricati); "File → Scegli
|
||||||
|
cartella di lavoro..." cambia la cartella di CSV/`raw/`/`step/` in qualunque momento; "File →
|
||||||
|
Pulisci raw e step..." svuota le cartelle `raw/`/`step/` nella cartella di lavoro (non tocca
|
||||||
|
l'indice); "Aiuto → Guida" (F1) apre una sintesi interattiva delle 5 fasi (`GuidaForm.cs`).
|
||||||
|
|
||||||
## Script standalone (`scripts/`)
|
## Script standalone (`scripts/`)
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,28 @@ public class AsmRecordReaderTests
|
|||||||
private static byte[] ConfigName(string istanza, int idMembro) =>
|
private static byte[] ConfigName(string istanza, int idMembro) =>
|
||||||
Bytes(0xe3, "ConfigName", 0, 0xe2, (byte)'3', istanza, 0, idMembro, 0x82, 0x7b);
|
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) =>
|
private static byte[] Asm(int conteggioDichiarato, byte[] membri, byte[]? extra = null) =>
|
||||||
Bytes("header ", extra ?? Array.Empty<byte>(), "\0members\0", 0xf1, 0xf8, conteggioDichiarato, membri, "#MdlStatus");
|
Bytes("header ", extra ?? Array.Empty<byte>(), "\0members\0", 0xf1, 0xf8, conteggioDichiarato, membri, "#MdlStatus");
|
||||||
|
|
||||||
@@ -87,6 +109,23 @@ public class AsmRecordReaderTests
|
|||||||
Assert.All(membri, m => Assert.Equal("9244250136_C03", m.Nome));
|
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]
|
[Fact]
|
||||||
public void Componente_senza_ConfigName_non_e_toccato_da_istanze_di_altri_membri()
|
public void Componente_senza_ConfigName_non_e_toccato_da_istanze_di_altri_membri()
|
||||||
{
|
{
|
||||||
@@ -111,6 +150,118 @@ public class AsmRecordReaderTests
|
|||||||
Assert.Throws<InvalidDataException>(() => AsmRecordReader.LeggiMembri(dati));
|
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]
|
[Fact]
|
||||||
public void Sezione_members_assente_lancia_eccezione()
|
public void Sezione_members_assente_lancia_eccezione()
|
||||||
{
|
{
|
||||||
|
|||||||
+141
-2
@@ -49,12 +49,47 @@ public static class AsmRecordReader
|
|||||||
// normalizzazione ogni istanza del pattern risulterebbe un componente diverso.
|
// normalizzazione ogni istanza del pattern risulterebbe un componente diverso.
|
||||||
private static readonly Regex SuffissoArrayFeatureRegex = new(@"#AF\d+$", RegexOptions.Compiled);
|
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.
|
// primo elemento dell'array "members": porta le chiavi per esteso.
|
||||||
private static readonly Regex FirstMemberRegex = new(
|
private static readonly Regex FirstMemberRegex = new(
|
||||||
@"\x01id\x00" + Int + @"\xe0\x01type\x00" + Int
|
@"\x01id\x00" + Int + @"\xe0\x01type\x00" + Int
|
||||||
+ @"\xe0\x0aname\x00([^\x00]+)\x00\xe0\x0afamily_name\x00([^\x00]+)\x00",
|
+ @"\xe0\x0aname\x00([^\x00]+)\x00\xe0\x0afamily_name\x00([^\x00]+)\x00",
|
||||||
RegexOptions.Compiled);
|
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)
|
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 mask = bytes.Length switch { 1 => 0x7F, 2 => 0x3F, 3 => 0x1F, _ => throw new ArgumentException("lunghezza intero non valida") };
|
||||||
@@ -84,14 +119,26 @@ public static class AsmRecordReader
|
|||||||
{
|
{
|
||||||
string testo = Encoding.Latin1.GetString(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>();
|
var istanze = new Dictionary<int, string>();
|
||||||
foreach (Match m in ConfigNameRegex.Matches(testo))
|
foreach (Match m in configNameMatches)
|
||||||
istanze[AsInt(m.Groups[2].Value)] = SuffissoArrayFeatureRegex.Replace(m.Groups[1].Value, "");
|
istanze[AsInt(m.Groups[2].Value)] = SuffissoArrayFeatureRegex.Replace(m.Groups[1].Value, "");
|
||||||
|
|
||||||
const string chiaveMembers = "\0members\0";
|
const string chiaveMembers = "\0members\0";
|
||||||
int start = testo.IndexOf(chiaveMembers, StringComparison.Ordinal);
|
int start = testo.IndexOf(chiaveMembers, StringComparison.Ordinal);
|
||||||
if (start < 0)
|
if (start < 0)
|
||||||
throw new InvalidDataException("sezione 'members' assente");
|
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;
|
start += chiaveMembers.Length;
|
||||||
|
|
||||||
int conteggio = dati[start + 2]; // \xf1 \xf8 <conteggio>
|
int conteggio = dati[start + 2]; // \xf1 \xf8 <conteggio>
|
||||||
@@ -118,7 +165,30 @@ public static class AsmRecordReader
|
|||||||
if (grezzi.Count != conteggio)
|
if (grezzi.Count != conteggio)
|
||||||
throw new InvalidDataException($"attesi {conteggio} membri, trovati {grezzi.Count}: lettura disallineata");
|
throw new InvalidDataException($"attesi {conteggio} membri, trovati {grezzi.Count}: lettura disallineata");
|
||||||
|
|
||||||
return grezzi.Select(g => new Membro(g.Id, g.Tipo, istanze.GetValueOrDefault(g.Id, g.Nome), g.Nome)).ToList();
|
// 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),
|
/// <summary>Codici dei componenti diretti (il generico su disco, non il nome istanza),
|
||||||
@@ -131,4 +201,73 @@ public static class AsmRecordReader
|
|||||||
var membri = LeggiMembri(File.ReadAllBytes(percorsoAsm));
|
var membri = LeggiMembri(File.ReadAllBytes(percorsoAsm));
|
||||||
return new HashSet<string>(membri.Select(m => m.Generico), StringComparer.OrdinalIgnoreCase);
|
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);
|
||||||
}
|
}
|
||||||
+342
-232
@@ -1,4 +1,5 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Drawing.Drawing2D;
|
||||||
using Microsoft.Data.Sqlite;
|
using Microsoft.Data.Sqlite;
|
||||||
|
|
||||||
namespace PrepStep;
|
namespace PrepStep;
|
||||||
@@ -10,18 +11,21 @@ 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 FlowLayoutPanel _rigaVerifica = new();
|
||||||
private readonly ListView _lstAnteprimaAsm = new();
|
private readonly ListView _lstAnteprimaAsm = new();
|
||||||
private readonly Button _btnPulisci = new();
|
|
||||||
private readonly Label _lblStatus = new();
|
private readonly Label _lblStatus = new();
|
||||||
private readonly Label _lblDb = new();
|
private readonly Label _lblDb = new();
|
||||||
private readonly Label _lblUltimoAggiornamento = new();
|
private readonly Label _lblUltimoAggiornamento = new();
|
||||||
private readonly Label _lblCartellaLavoro = new();
|
private readonly Panel _indicatoreDb = new();
|
||||||
private readonly Button _btnCambiaCartella = new();
|
private bool _dbCaricato;
|
||||||
private readonly DataGridView _gridImport = new();
|
private readonly DataGridView _gridImport = new();
|
||||||
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("Nuovo");
|
||||||
|
private readonly ToolStripMenuItem _mnuPulisci = new("Pulisci raw e step...");
|
||||||
|
private readonly ToolStripMenuItem _mnuCambiaCartella = new("Scegli cartella di lavoro...");
|
||||||
|
|
||||||
// Wizard: una fase alla volta occupa tutta la finestra, si avanza solo quando quella
|
// Wizard: una fase alla volta occupa tutta la finestra, si avanza solo quando quella
|
||||||
// corrente è "pronta" (vedi AvantiConsentito). NomiFasi/_panelFasi sono paralleli per indice.
|
// corrente è "pronta" (vedi AvantiConsentito). NomiFasi/_panelFasi sono paralleli per indice.
|
||||||
@@ -91,38 +95,27 @@ public partial class Form1 : Form
|
|||||||
_lblFase.Font = new Font(Font, FontStyle.Bold);
|
_lblFase.Font = new Font(Font, FontStyle.Bold);
|
||||||
headerPanel.Controls.Add(_lblFase);
|
headerPanel.Controls.Add(_lblFase);
|
||||||
|
|
||||||
// Barra di navigazione come footer separato dal contenuto (stesso pattern di
|
// Floatbar di navigazione: card staccata dai bordi della finestra invece di un footer
|
||||||
// ProgressForm): linea sottile in cima + sfondo bianco, invece dei bottoni appoggiati
|
// a tutta larghezza appoggiato direttamente sul contenuto/sulla barra di stato.
|
||||||
// direttamente sul contenuto/sulla barra di stato senza alcuna divisione visiva.
|
var (navContainer, navPanel) = UiStile.FloatBar();
|
||||||
// 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
|
|
||||||
{
|
|
||||||
Dock = DockStyle.Fill,
|
|
||||||
FlowDirection = FlowDirection.RightToLeft,
|
|
||||||
Padding = new Padding(16, 14, 16, 14),
|
|
||||||
};
|
|
||||||
_btnAvanti.Text = "Avanti ›";
|
_btnAvanti.Text = "Avanti ›";
|
||||||
UiStile.Primario(_btnAvanti);
|
UiStile.Primario(_btnAvanti);
|
||||||
UiStile.Arrotonda(_btnAvanti);
|
|
||||||
_btnAvanti.Click += (_, _) => VaiAllaFase(_faseAttuale + 1);
|
|
||||||
navPanel.Controls.Add(_btnAvanti);
|
|
||||||
|
|
||||||
_btnIndietro.Text = "‹ Indietro";
|
_btnIndietro.Text = "‹ Indietro";
|
||||||
_btnIndietro.Margin = new Padding(8, 0, 0, 0);
|
_btnIndietro.Margin = new Padding(8, 0, 0, 0);
|
||||||
UiStile.Secondario(_btnIndietro);
|
UiStile.SecondarioPieno(_btnIndietro);
|
||||||
UiStile.Arrotonda(_btnIndietro);
|
|
||||||
|
// Dimensioni uguali decise PRIMA di arrotondare: se si arrotonda un bottone e solo dopo
|
||||||
|
// lo si ridimensiona, la Region calcolata sulla dimensione vecchia lascia un bordo
|
||||||
|
// "fantasma" finché non arriva un repaint completo della finestra.
|
||||||
|
UiStile.LarghezzaUguale(_btnAvanti, _btnIndietro);
|
||||||
|
UiStile.Arrotonda(_btnAvanti, 10);
|
||||||
|
UiStile.Arrotonda(_btnIndietro, 10);
|
||||||
|
|
||||||
|
_btnAvanti.Click += (_, _) => VaiAllaFase(_faseAttuale + 1);
|
||||||
|
navPanel.Controls.Add(_btnAvanti);
|
||||||
_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;
|
||||||
@@ -146,10 +139,31 @@ public partial class Form1 : Form
|
|||||||
// Ordine dei Dock.Bottom: l'ultimo aggiunto è il più esterno (il vero bordo inferiore
|
// Ordine dei Dock.Bottom: l'ultimo aggiunto è il più esterno (il vero bordo inferiore
|
||||||
// della finestra). La barra di stato va tra contenuto e azioni; i bottoni Avanti/Indietro
|
// della finestra). La barra di stato va tra contenuto e azioni; i bottoni Avanti/Indietro
|
||||||
// restano il footer più esterno, come in ogni wizard/dialog standard di Windows.
|
// restano il footer più esterno, come in ogni wizard/dialog standard di Windows.
|
||||||
|
var mnuFile = new ToolStripMenuItem("File");
|
||||||
|
_mnuNuovaEstrazione.ShortcutKeys = Keys.Control | Keys.N;
|
||||||
|
_mnuNuovaEstrazione.Click += (_, _) => NuovaEstrazione();
|
||||||
|
mnuFile.DropDownItems.Add(_mnuNuovaEstrazione);
|
||||||
|
|
||||||
|
_mnuCambiaCartella.Click += (_, _) => ScegliCartellaLavoro();
|
||||||
|
mnuFile.DropDownItems.Add(_mnuCambiaCartella);
|
||||||
|
|
||||||
|
_mnuPulisci.Click += (_, _) => PulisciRawEStep();
|
||||||
|
mnuFile.DropDownItems.Add(_mnuPulisci);
|
||||||
|
|
||||||
|
var mnuAiuto = new ToolStripMenuItem("Aiuto");
|
||||||
|
var mnuGuida = new ToolStripMenuItem("Guida") { ShortcutKeys = Keys.F1 };
|
||||||
|
mnuGuida.Click += (_, _) => new GuidaForm().ShowDialog(this);
|
||||||
|
mnuAiuto.DropDownItems.Add(mnuGuida);
|
||||||
|
|
||||||
|
_menu.Items.Add(mnuFile);
|
||||||
|
_menu.Items.Add(mnuAiuto);
|
||||||
|
MainMenuStrip = _menu;
|
||||||
|
|
||||||
Controls.Add(_faseContainer);
|
Controls.Add(_faseContainer);
|
||||||
Controls.Add(statusPanel);
|
Controls.Add(statusPanel);
|
||||||
Controls.Add(navContainer);
|
Controls.Add(navContainer);
|
||||||
Controls.Add(headerPanel);
|
Controls.Add(headerPanel);
|
||||||
|
Controls.Add(_menu);
|
||||||
|
|
||||||
DragEnter += (_, e) =>
|
DragEnter += (_, e) =>
|
||||||
e.Effect = e.Data?.GetDataPresent(DataFormats.FileDrop) == true
|
e.Effect = e.Data?.GetDataPresent(DataFormats.FileDrop) == true
|
||||||
@@ -228,7 +242,7 @@ public partial class Form1 : Form
|
|||||||
private bool AvantiConsentito(int fase) => fase switch
|
private bool AvantiConsentito(int fase) => fase switch
|
||||||
{
|
{
|
||||||
0 => _dbPath is not null && _cartellaLavoro is not null,
|
0 => _dbPath is not null && _cartellaLavoro is not null,
|
||||||
1 => _righeCorrenti is { Count: > 0 },
|
1 => _righeCorrenti is { Count: > 0 } && _ultimaVerifica is not null,
|
||||||
2 => _ultimaCartellaRaw is not null,
|
2 => _ultimaCartellaRaw is not null,
|
||||||
3 => true, // risoluzione ricorsiva opzionale: non tutti gli assiemi hanno sotto-componenti
|
3 => true, // risoluzione ricorsiva opzionale: non tutti gli assiemi hanno sotto-componenti
|
||||||
_ => true,
|
_ => true,
|
||||||
@@ -236,6 +250,31 @@ 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.", "Nuovo",
|
||||||
|
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);
|
||||||
|
_rigaVerifica.Visible = false;
|
||||||
|
|
||||||
|
VaiAllaFase(1);
|
||||||
|
}
|
||||||
|
|
||||||
private Panel BuildFaseDatabase()
|
private Panel BuildFaseDatabase()
|
||||||
{
|
{
|
||||||
var contenuto = new TableLayoutPanel
|
var contenuto = new TableLayoutPanel
|
||||||
@@ -255,78 +294,78 @@ public partial class Form1 : Form
|
|||||||
Dock = DockStyle.Top,
|
Dock = DockStyle.Top,
|
||||||
AutoSize = true,
|
AutoSize = true,
|
||||||
ForeColor = SystemColors.GrayText,
|
ForeColor = SystemColors.GrayText,
|
||||||
Margin = new Padding(0, 0, 0, 12),
|
Margin = new Padding(0, 0, 0, 16),
|
||||||
Text = "L'indice CAD viene caricato automaticamente all'avvio, se già presente. "
|
Text = "L'indice CAD viene caricato automaticamente all'avvio, se già presente.",
|
||||||
+ "Usa 'Apri DB...' per selezionarne un altro, oppure 'Aggiorna indice' per "
|
|
||||||
+ "scansionare di nuovo il disco (X:\\).",
|
|
||||||
};
|
};
|
||||||
contenuto.Controls.Add(lblIstruzioni);
|
contenuto.Controls.Add(lblIstruzioni);
|
||||||
|
|
||||||
|
var cardDb = UiStile.Card();
|
||||||
|
var contenutoCardDb = new TableLayoutPanel
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Top,
|
||||||
|
AutoSize = true,
|
||||||
|
AutoSizeMode = AutoSizeMode.GrowAndShrink,
|
||||||
|
ColumnCount = 1,
|
||||||
|
};
|
||||||
|
contenutoCardDb.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
||||||
|
|
||||||
|
var rigaTitoloDb = new FlowLayoutPanel
|
||||||
|
{
|
||||||
|
AutoSize = true,
|
||||||
|
FlowDirection = FlowDirection.LeftToRight,
|
||||||
|
Margin = new Padding(0, 0, 0, 6),
|
||||||
|
};
|
||||||
|
|
||||||
|
_indicatoreDb.Size = new Size(10, 10);
|
||||||
|
_indicatoreDb.Margin = new Padding(0, 5, 8, 0);
|
||||||
|
_indicatoreDb.Paint += (_, e) =>
|
||||||
|
{
|
||||||
|
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
|
||||||
|
var colore = _dbCaricato ? Color.FromArgb(30, 160, 90) : Color.FromArgb(200, 60, 60);
|
||||||
|
using var pennello = new SolidBrush(colore);
|
||||||
|
e.Graphics.FillEllipse(pennello, 0, 0, _indicatoreDb.Width - 1, _indicatoreDb.Height - 1);
|
||||||
|
};
|
||||||
|
rigaTitoloDb.Controls.Add(_indicatoreDb);
|
||||||
|
|
||||||
|
var lblTitoloDb = new Label
|
||||||
|
{
|
||||||
|
AutoSize = true,
|
||||||
|
Font = new Font(Font, FontStyle.Bold),
|
||||||
|
Text = "Database",
|
||||||
|
};
|
||||||
|
rigaTitoloDb.Controls.Add(lblTitoloDb);
|
||||||
|
|
||||||
|
contenutoCardDb.Controls.Add(rigaTitoloDb);
|
||||||
|
|
||||||
_lblDb.Dock = DockStyle.Top;
|
_lblDb.Dock = DockStyle.Top;
|
||||||
_lblDb.AutoSize = true;
|
_lblDb.AutoSize = true;
|
||||||
_lblDb.ForeColor = SystemColors.GrayText;
|
_lblDb.ForeColor = SystemColors.ControlText;
|
||||||
_lblDb.Margin = new Padding(0, 0, 0, 4);
|
_lblDb.Margin = new Padding(0, 0, 0, 2);
|
||||||
contenuto.Controls.Add(_lblDb);
|
contenutoCardDb.Controls.Add(_lblDb);
|
||||||
|
|
||||||
_lblUltimoAggiornamento.AutoSize = true;
|
_lblUltimoAggiornamento.AutoSize = true;
|
||||||
_lblUltimoAggiornamento.ForeColor = SystemColors.GrayText;
|
_lblUltimoAggiornamento.ForeColor = SystemColors.GrayText;
|
||||||
_lblUltimoAggiornamento.Margin = new Padding(0, 0, 0, 12);
|
_lblUltimoAggiornamento.Margin = new Padding(0, 0, 0, 14);
|
||||||
contenuto.Controls.Add(_lblUltimoAggiornamento);
|
contenutoCardDb.Controls.Add(_lblUltimoAggiornamento);
|
||||||
|
|
||||||
var bottoni = new FlowLayoutPanel { AutoSize = true, FlowDirection = FlowDirection.LeftToRight };
|
var bottoniDb = new FlowLayoutPanel { AutoSize = true, FlowDirection = FlowDirection.LeftToRight };
|
||||||
|
|
||||||
_btnApriDb.Text = "Apri DB...";
|
_btnApriDb.Text = "Apri DB...";
|
||||||
_btnApriDb.Margin = new Padding(0, 0, 8, 0);
|
_btnApriDb.Margin = new Padding(0, 0, 8, 0);
|
||||||
UiStile.Secondario(_btnApriDb);
|
UiStile.Secondario(_btnApriDb);
|
||||||
|
UiStile.Arrotonda(_btnApriDb);
|
||||||
_btnApriDb.Click += (_, _) => ScegliDatabase();
|
_btnApriDb.Click += (_, _) => ScegliDatabase();
|
||||||
bottoni.Controls.Add(_btnApriDb);
|
bottoniDb.Controls.Add(_btnApriDb);
|
||||||
|
|
||||||
_btnAggiorna.Text = "Aggiorna indice";
|
_btnAggiorna.Text = "Aggiorna indice";
|
||||||
UiStile.Secondario(_btnAggiorna);
|
UiStile.Secondario(_btnAggiorna);
|
||||||
|
UiStile.Arrotonda(_btnAggiorna);
|
||||||
_btnAggiorna.Click += (_, _) => AggiornaIndice();
|
_btnAggiorna.Click += (_, _) => AggiornaIndice();
|
||||||
bottoni.Controls.Add(_btnAggiorna);
|
bottoniDb.Controls.Add(_btnAggiorna);
|
||||||
|
|
||||||
contenuto.Controls.Add(bottoni);
|
contenutoCardDb.Controls.Add(bottoniDb);
|
||||||
|
cardDb.Controls.Add(contenutoCardDb);
|
||||||
var lblIstruzioniCartella = new Label
|
contenuto.Controls.Add(cardDb);
|
||||||
{
|
|
||||||
Dock = DockStyle.Top,
|
|
||||||
AutoSize = true,
|
|
||||||
ForeColor = SystemColors.GrayText,
|
|
||||||
Margin = new Padding(0, 20, 0, 4),
|
|
||||||
Text = "Cartella di lavoro: qui vengono scritti il CSV della distinta e la "
|
|
||||||
+ "cartella 'raw' (gli STEP esportati con Creo vanno invece in 'step', "
|
|
||||||
+ "dentro la stessa cartella). Proposta automaticamente (ricordata da un "
|
|
||||||
+ "avvio all'altro), modificabile con 'Cambia cartella...'.",
|
|
||||||
};
|
|
||||||
contenuto.Controls.Add(lblIstruzioniCartella);
|
|
||||||
|
|
||||||
_lblCartellaLavoro.Dock = DockStyle.Top;
|
|
||||||
_lblCartellaLavoro.AutoSize = true;
|
|
||||||
_lblCartellaLavoro.ForeColor = SystemColors.GrayText;
|
|
||||||
_lblCartellaLavoro.Margin = new Padding(0, 0, 0, 4);
|
|
||||||
contenuto.Controls.Add(_lblCartellaLavoro);
|
|
||||||
|
|
||||||
_btnCambiaCartella.Text = "Cambia cartella...";
|
|
||||||
UiStile.Secondario(_btnCambiaCartella);
|
|
||||||
_btnCambiaCartella.Click += (_, _) => ScegliCartellaLavoro();
|
|
||||||
contenuto.Controls.Add(_btnCambiaCartella);
|
|
||||||
|
|
||||||
var lblManutenzione = new Label
|
|
||||||
{
|
|
||||||
Dock = DockStyle.Top,
|
|
||||||
AutoSize = true,
|
|
||||||
ForeColor = SystemColors.GrayText,
|
|
||||||
Margin = new Padding(0, 20, 0, 8),
|
|
||||||
Text = "Manutenzione: svuota le cartelle 'raw' e 'step' nella cartella di lavoro "
|
|
||||||
+ "prima di un nuovo giro (non tocca l'indice, solo le copie locali).",
|
|
||||||
};
|
|
||||||
contenuto.Controls.Add(lblManutenzione);
|
|
||||||
|
|
||||||
_btnPulisci.Text = "Pulisci raw e step";
|
|
||||||
UiStile.Secondario(_btnPulisci);
|
|
||||||
_btnPulisci.Click += (_, _) => PulisciRawEStep();
|
|
||||||
contenuto.Controls.Add(_btnPulisci);
|
|
||||||
|
|
||||||
var panel = new Panel();
|
var panel = new Panel();
|
||||||
panel.Controls.Add(contenuto);
|
panel.Controls.Add(contenuto);
|
||||||
@@ -357,20 +396,47 @@ public partial class Form1 : Form
|
|||||||
};
|
};
|
||||||
contenuto.Controls.Add(lblIstruzioni);
|
contenuto.Controls.Add(lblIstruzioni);
|
||||||
|
|
||||||
|
var rigaImporta = new FlowLayoutPanel
|
||||||
|
{
|
||||||
|
AutoSize = true,
|
||||||
|
FlowDirection = FlowDirection.LeftToRight,
|
||||||
|
Margin = new Padding(0, 0, 0, 8),
|
||||||
|
};
|
||||||
var btnSfoglia = new Button
|
var btnSfoglia = new Button
|
||||||
{
|
{
|
||||||
Text = "Scegli file distinta...",
|
Text = "Scegli file distinta...",
|
||||||
Margin = new Padding(0, 0, 0, 8),
|
Margin = new Padding(0, 0, 8, 0),
|
||||||
};
|
};
|
||||||
UiStile.Secondario(btnSfoglia);
|
UiStile.Secondario(btnSfoglia);
|
||||||
btnSfoglia.Click += (_, _) => ScegliFileDistinta();
|
btnSfoglia.Click += (_, _) => ScegliFileDistinta();
|
||||||
contenuto.Controls.Add(btnSfoglia);
|
rigaImporta.Controls.Add(btnSfoglia);
|
||||||
|
rigaImporta.Controls.Add(new Label
|
||||||
|
{
|
||||||
|
AutoSize = true,
|
||||||
|
ForeColor = SystemColors.GrayText,
|
||||||
|
Margin = new Padding(0, 9, 0, 0),
|
||||||
|
Text = "1. Carica la distinta",
|
||||||
|
});
|
||||||
|
contenuto.Controls.Add(rigaImporta);
|
||||||
|
|
||||||
|
_rigaVerifica.AutoSize = true;
|
||||||
|
_rigaVerifica.FlowDirection = FlowDirection.LeftToRight;
|
||||||
|
_rigaVerifica.Margin = new Padding(0, 0, 0, 8);
|
||||||
|
_rigaVerifica.Visible = false; // compare solo dopo un import riuscito (file o drag&drop)
|
||||||
|
|
||||||
_btnVerifica.Text = "Verifica presenza";
|
_btnVerifica.Text = "Verifica presenza";
|
||||||
_btnVerifica.Margin = new Padding(0, 0, 0, 8);
|
_btnVerifica.Margin = new Padding(0, 0, 8, 0);
|
||||||
UiStile.Secondario(_btnVerifica);
|
UiStile.Secondario(_btnVerifica);
|
||||||
_btnVerifica.Click += (_, _) => VerificaCodiciNelDb();
|
_btnVerifica.Click += (_, _) => VerificaCodiciNelDb();
|
||||||
contenuto.Controls.Add(_btnVerifica);
|
_rigaVerifica.Controls.Add(_btnVerifica);
|
||||||
|
_rigaVerifica.Controls.Add(new Label
|
||||||
|
{
|
||||||
|
AutoSize = true,
|
||||||
|
ForeColor = SystemColors.GrayText,
|
||||||
|
Margin = new Padding(0, 9, 0, 0),
|
||||||
|
Text = "2. Verifica la presenza dei file sul server",
|
||||||
|
});
|
||||||
|
contenuto.Controls.Add(_rigaVerifica);
|
||||||
|
|
||||||
_lblImportStatus.Dock = DockStyle.Top;
|
_lblImportStatus.Dock = DockStyle.Top;
|
||||||
_lblImportStatus.AutoSize = false;
|
_lblImportStatus.AutoSize = false;
|
||||||
@@ -420,22 +486,25 @@ public partial class Form1 : Form
|
|||||||
};
|
};
|
||||||
contenuto.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
contenuto.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
||||||
|
|
||||||
var lblIstruzioni = new Label
|
var rigaCreaRaw = new FlowLayoutPanel
|
||||||
{
|
{
|
||||||
Dock = DockStyle.Top,
|
|
||||||
AutoSize = true,
|
AutoSize = true,
|
||||||
ForeColor = SystemColors.GrayText,
|
FlowDirection = FlowDirection.LeftToRight,
|
||||||
Margin = new Padding(0, 0, 0, 8),
|
Margin = new Padding(0, 0, 0, 12),
|
||||||
Text = "Cerca nell'indice il percorso CAD di ogni componente importato e copia i "
|
|
||||||
+ "file trovati in una cartella 'raw' dentro la cartella di lavoro (fase 1).",
|
|
||||||
};
|
};
|
||||||
contenuto.Controls.Add(lblIstruzioni);
|
|
||||||
|
|
||||||
_btnCreaRaw.Text = "Crea cartella raw";
|
_btnCreaRaw.Text = "Crea cartella raw";
|
||||||
_btnCreaRaw.Margin = new Padding(0, 0, 0, 12);
|
_btnCreaRaw.Margin = new Padding(0, 0, 8, 0);
|
||||||
UiStile.Primario(_btnCreaRaw);
|
UiStile.Primario(_btnCreaRaw);
|
||||||
_btnCreaRaw.Click += (_, _) => CreaCartellaRaw();
|
_btnCreaRaw.Click += (_, _) => CreaCartellaRaw();
|
||||||
contenuto.Controls.Add(_btnCreaRaw);
|
rigaCreaRaw.Controls.Add(_btnCreaRaw);
|
||||||
|
rigaCreaRaw.Controls.Add(new Label
|
||||||
|
{
|
||||||
|
AutoSize = true,
|
||||||
|
ForeColor = SystemColors.GrayText,
|
||||||
|
Margin = new Padding(0, 9, 0, 0),
|
||||||
|
Text = "Copia i file dal server nella cartella 'raw'.",
|
||||||
|
});
|
||||||
|
contenuto.Controls.Add(rigaCreaRaw);
|
||||||
|
|
||||||
_gridRaw.Dock = DockStyle.Fill;
|
_gridRaw.Dock = DockStyle.Fill;
|
||||||
_gridRaw.ReadOnly = true;
|
_gridRaw.ReadOnly = true;
|
||||||
@@ -486,8 +555,7 @@ public partial class Form1 : Form
|
|||||||
Margin = new Padding(0, 0, 0, 12),
|
Margin = new Padding(0, 0, 0, 12),
|
||||||
Text = "Gli assiemi referenziano altri PRT e sotto-assiemi al loro interno: premi "
|
Text = "Gli assiemi referenziano altri PRT e sotto-assiemi al loro interno: premi "
|
||||||
+ "qui per risolvere l'albero componenti (ricorsivamente) e copiare tutto ciò che "
|
+ "qui per risolvere l'albero componenti (ricorsivamente) e copiare tutto ciò che "
|
||||||
+ "viene trovato nell'indice dentro 'raw'. Passo opzionale: se la distinta contiene "
|
+ "viene trovato nell'indice dentro 'raw'.",
|
||||||
+ "solo parti singole puoi anche passare direttamente alla fase successiva.",
|
|
||||||
};
|
};
|
||||||
contenuto.Controls.Add(lblIstruzioni);
|
contenuto.Controls.Add(lblIstruzioni);
|
||||||
|
|
||||||
@@ -516,27 +584,6 @@ public partial class Form1 : Form
|
|||||||
};
|
};
|
||||||
top.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
top.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
||||||
|
|
||||||
var lblIstruzioniCompletezza = new Label
|
|
||||||
{
|
|
||||||
Dock = DockStyle.Top,
|
|
||||||
AutoSize = true,
|
|
||||||
ForeColor = SystemColors.GrayText,
|
|
||||||
Margin = new Padding(0, 0, 0, 8),
|
|
||||||
Text = "Per ogni assieme della distinta, verifica se tutti i suoi componenti "
|
|
||||||
+ "(ricorsivamente fino ai PRT) sono nell'indice. Nota: la scansione dei .asm "
|
|
||||||
+ "è euristica, quindi qualche falso 'incompleto' è possibile.",
|
|
||||||
};
|
|
||||||
top.Controls.Add(lblIstruzioniCompletezza);
|
|
||||||
|
|
||||||
// Primario: è l'azione principale di questa fase, non una scelta secondaria — prima
|
|
||||||
// era uno stile grigio che si confondeva con lo sfondo.
|
|
||||||
_btnCompletezza.Text = "Verifica completezza assiemi";
|
|
||||||
_btnCompletezza.Margin = new Padding(0, 0, 0, 12);
|
|
||||||
UiStile.Primario(_btnCompletezza);
|
|
||||||
UiStile.Arrotonda(_btnCompletezza);
|
|
||||||
_btnCompletezza.Click += (_, _) => VerificaCompletezzaAssiemi();
|
|
||||||
top.Controls.Add(_btnCompletezza);
|
|
||||||
|
|
||||||
var lblAnteprimaAsm = new Label
|
var lblAnteprimaAsm = new Label
|
||||||
{
|
{
|
||||||
AutoSize = true,
|
AutoSize = true,
|
||||||
@@ -597,7 +644,7 @@ public partial class Form1 : Form
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var (csv, righe) = DistintaBaseImporter.ConvertiInCsv(percorso, _cartellaLavoro!);
|
var (_, righe) = DistintaBaseImporter.ConvertiInCsv(percorso, _cartellaLavoro!);
|
||||||
_percorsoInputCorrente = percorso;
|
_percorsoInputCorrente = percorso;
|
||||||
_righeCorrenti = righe;
|
_righeCorrenti = righe;
|
||||||
|
|
||||||
@@ -610,7 +657,8 @@ public partial class Form1 : Form
|
|||||||
_ultimaCartellaRaw = null;
|
_ultimaCartellaRaw = null;
|
||||||
_ultimaVerifica = null;
|
_ultimaVerifica = null;
|
||||||
|
|
||||||
ImpostaStatoImport($"CSV generato ({righe.Count} righe): {csv}", Color.DarkGreen);
|
ImpostaStatoImport("", Color.Black);
|
||||||
|
_rigaVerifica.Visible = true;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -618,6 +666,7 @@ public partial class Form1 : Form
|
|||||||
_righeCorrenti = null;
|
_righeCorrenti = null;
|
||||||
_gridImport.Rows.Clear();
|
_gridImport.Rows.Clear();
|
||||||
ImpostaStatoImport($"Errore durante la conversione: {ex.Message}", Color.DarkRed);
|
ImpostaStatoImport($"Errore durante la conversione: {ex.Message}", Color.DarkRed);
|
||||||
|
_rigaVerifica.Visible = false;
|
||||||
}
|
}
|
||||||
AggiornaStatoAvanti();
|
AggiornaStatoAvanti();
|
||||||
}
|
}
|
||||||
@@ -645,6 +694,7 @@ public partial class Form1 : Form
|
|||||||
? Color.FromArgb(198, 239, 206)
|
? Color.FromArgb(198, 239, 206)
|
||||||
: Color.FromArgb(255, 199, 206);
|
: Color.FromArgb(255, 199, 206);
|
||||||
}
|
}
|
||||||
|
AggiornaStatoAvanti();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CreaCartellaRaw()
|
private void CreaCartellaRaw()
|
||||||
@@ -688,7 +738,8 @@ public partial class Form1 : Form
|
|||||||
.ToList();
|
.ToList();
|
||||||
if (cartelle.Count == 0)
|
if (cartelle.Count == 0)
|
||||||
{
|
{
|
||||||
ImpostaStatoImport($"Nessuna cartella 'raw' o 'step' trovata in {radice}.", Color.DarkRed);
|
MessageBox.Show(this, $"Nessuna cartella 'raw' o 'step' trovata in {radice}.",
|
||||||
|
"Pulisci raw e step", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -715,7 +766,8 @@ public partial class Form1 : Form
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ImpostaStatoImport($"{eliminati} file eliminati da {string.Join(" e ", cartelle)}.", Color.DarkGreen);
|
MessageBox.Show(this, $"{eliminati} file eliminati da {string.Join(" e ", cartelle)}.",
|
||||||
|
"Pulisci raw e step", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Tutti i codici prt/asm noti all'indice, con i relativi percorsi (ultima
|
/// <summary>Tutti i codici prt/asm noti all'indice, con i relativi percorsi (ultima
|
||||||
@@ -743,6 +795,64 @@ 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
|
/// <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
|
/// 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
|
/// non ha i falsi positivi/negativi noti dell'euristica testuale (docs/formato-asm.md); se
|
||||||
@@ -763,6 +873,50 @@ public partial class Form1 : Form
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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
|
||||||
@@ -771,42 +925,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 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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -854,102 +1059,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 = EstraiCodiciReferenziati(file, codiciNoti.Keys).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
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
|
||||||
@@ -1028,14 +1137,14 @@ public partial class Form1 : Form
|
|||||||
/// c'è ancora una preferenza salvata (o punta a una cartella cancellata), la cartella dove si
|
/// c'è ancora una preferenza salvata (o punta a una cartella cancellata), la cartella dove si
|
||||||
/// trova il DB (più sensata dell'eseguibile: quella è dove l'utente tiene il proprio lavoro,
|
/// trova il DB (più sensata dell'eseguibile: quella è dove l'utente tiene il proprio lavoro,
|
||||||
/// non dov'è installata la app). Richiede quindi che InizializzaDatabase sia già stata
|
/// non dov'è installata la app). Richiede quindi che InizializzaDatabase sia già stata
|
||||||
/// chiamata. Sempre modificabile con "Cambia cartella..." in fase 1.</summary>
|
/// chiamata. Sempre modificabile con "File → Scegli cartella di lavoro...".</summary>
|
||||||
private void InizializzaCartellaLavoro()
|
private void InizializzaCartellaLavoro()
|
||||||
{
|
{
|
||||||
var ultima = LeggiUltimaCartellaLavoro();
|
var ultima = LeggiUltimaCartellaLavoro();
|
||||||
_cartellaLavoro = ultima is not null && Directory.Exists(ultima)
|
_cartellaLavoro = ultima is not null && Directory.Exists(ultima)
|
||||||
? ultima
|
? ultima
|
||||||
: (_dbPath is not null ? Path.GetDirectoryName(_dbPath) : null) ?? AppContext.BaseDirectory;
|
: (_dbPath is not null ? Path.GetDirectoryName(_dbPath) : null) ?? AppContext.BaseDirectory;
|
||||||
AggiornaLabelCartellaLavoro();
|
AggiornaStatoAvanti();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ScegliCartellaLavoro()
|
private void ScegliCartellaLavoro()
|
||||||
@@ -1050,13 +1159,10 @@ public partial class Form1 : Form
|
|||||||
|
|
||||||
string percorso = dialog.SelectedPath;
|
string percorso = dialog.SelectedPath;
|
||||||
_cartellaLavoro = percorso;
|
_cartellaLavoro = percorso;
|
||||||
AggiornaLabelCartellaLavoro();
|
|
||||||
SalvaUltimaCartellaLavoro(percorso);
|
SalvaUltimaCartellaLavoro(percorso);
|
||||||
AggiornaStatoAvanti();
|
AggiornaStatoAvanti();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AggiornaLabelCartellaLavoro() => _lblCartellaLavoro.Text = $"Cartella di lavoro: {_cartellaLavoro}";
|
|
||||||
|
|
||||||
private static string? LeggiUltimaCartellaLavoro()
|
private static string? LeggiUltimaCartellaLavoro()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -1097,6 +1203,8 @@ public partial class Form1 : Form
|
|||||||
_lblStatus.Text = "Database non trovato. Usa 'Apri DB...' per selezionarlo oppure 'Aggiorna indice' per crearlo.";
|
_lblStatus.Text = "Database non trovato. Usa 'Apri DB...' per selezionarlo oppure 'Aggiorna indice' per crearlo.";
|
||||||
_lblDb.Text = "Nessun database caricato. Premi 'Aggiorna indice' per crearlo da X:\\, oppure 'Apri DB...' se ne hai gi\u00e0 uno.";
|
_lblDb.Text = "Nessun database caricato. Premi 'Aggiorna indice' per crearlo da X:\\, oppure 'Apri DB...' se ne hai gi\u00e0 uno.";
|
||||||
_lblUltimoAggiornamento.Text = "";
|
_lblUltimoAggiornamento.Text = "";
|
||||||
|
_dbCaricato = false;
|
||||||
|
_indicatoreDb.Invalidate();
|
||||||
AggiornaStatoAvanti();
|
AggiornaStatoAvanti();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1141,6 +1249,8 @@ public partial class Form1 : Form
|
|||||||
MessageBox.Show(this, $"Impossibile aprire il database:\n{ex.Message}", "Errore",
|
MessageBox.Show(this, $"Impossibile aprire il database:\n{ex.Message}", "Errore",
|
||||||
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
}
|
}
|
||||||
|
_dbCaricato = _dbPath is not null;
|
||||||
|
_indicatoreDb.Invalidate();
|
||||||
AggiornaStatoAvanti();
|
AggiornaStatoAvanti();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,281 @@
|
|||||||
|
namespace PrepStep;
|
||||||
|
|
||||||
|
/// <summary>Guida interattiva richiamabile da "Aiuto ▸ Guida" (F1): stessa guida completa di
|
||||||
|
/// docs/guida-utente.md (passo-passo fino a Creo Distributed Batch), pensata per chi apre
|
||||||
|
/// l'app senza aver letto i docs e senza esperienza pregressa. Il dettaglio tecnico (formato
|
||||||
|
/// .asm, schema DB, ecc.) resta solo in docs/architettura.md e affini, per non duplicarlo qui.
|
||||||
|
///
|
||||||
|
/// Gerarchia tipografica nel pannello di lettura (vedi ScriviSezione): titolo sezione in blu
|
||||||
|
/// primario/grassetto/14pt, corpo in grigio quasi-nero/10pt, passi numerati con il numero in
|
||||||
|
/// blu grassetto per farli scorrere a colpo d'occhio, callout con sfondo tinta piatta (nessun
|
||||||
|
/// bordo/ombra: RichTextBox non li supporta) — blu tenue per le note informative, ambra tenue
|
||||||
|
/// per gli avvisi importanti, con etichetta ("Nota"/"Attenzione") in grassetto a inizio blocco
|
||||||
|
/// invece di un colore isolato, così restano leggibili anche senza vedere i colori.</summary>
|
||||||
|
public class GuidaForm : Form
|
||||||
|
{
|
||||||
|
private sealed record Sezione(
|
||||||
|
string Titolo,
|
||||||
|
string Intro,
|
||||||
|
string[]? Passi = null,
|
||||||
|
string? NotaTesto = null,
|
||||||
|
bool NotaAttenzione = false);
|
||||||
|
|
||||||
|
// Palette: blu primario dell'app per titoli/passi, ambra tenue solo per gli avvisi che
|
||||||
|
// possono far perdere tempo se ignorati (es. l'errore Creo [-4]) — il rosso resta riservato
|
||||||
|
// ai messaggi di errore veri e propri altrove nell'app.
|
||||||
|
private static readonly Color ColoreTesto = Color.FromArgb(40, 40, 40);
|
||||||
|
private static readonly Color ColoreNotaInfoSfondo = Color.FromArgb(232, 240, 250);
|
||||||
|
private static readonly Color ColoreNotaInfoTesto = Color.FromArgb(15, 65, 120);
|
||||||
|
private static readonly Color ColoreNotaAttenzioneSfondo = Color.FromArgb(253, 240, 219);
|
||||||
|
private static readonly Color ColoreNotaAttenzioneTesto = Color.FromArgb(140, 90, 10);
|
||||||
|
|
||||||
|
private static readonly Sezione[] Sezioni =
|
||||||
|
{
|
||||||
|
new("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\".",
|
||||||
|
NotaTesto: "\"File ▸ Nuovo\" (Ctrl+N) azzera distinta/raw/verifica per ripartire " +
|
||||||
|
"con una nuova distinta base, senza dover ricaricare il database."),
|
||||||
|
|
||||||
|
new("Prima di iniziare",
|
||||||
|
"Prima di importare la prima distinta:",
|
||||||
|
Passi:
|
||||||
|
[
|
||||||
|
"Serve accesso alla share di rete con i file CAD (di default X:\\).",
|
||||||
|
"Serve l'estrazione \"Distinta Base\" da Fata (il gestionale): il file .xls " +
|
||||||
|
"così come Fata lo esporta, senza risalvarlo o convertirlo in altri " +
|
||||||
|
"formati — l'app riconosce da sola se sotto l'estensione .xls c'è XML o " +
|
||||||
|
"Excel binario vero.",
|
||||||
|
"Se il file segue la convenzione di nome DBP_<codice>_<timestamp>, l'app " +
|
||||||
|
"aggiunge da sola l'assieme generale come componente in più: non serve " +
|
||||||
|
"fare nulla di diverso.",
|
||||||
|
"Conviene impostare la cartella di lavoro giusta (fase 1, o \"File ▸ Scegli " +
|
||||||
|
"cartella di lavoro...\") prima di importare la distinta: è lì che " +
|
||||||
|
"finiscono CSV, \"raw\" e, dopo l'esportazione con Creo, \"step\".",
|
||||||
|
"Se la cartella di lavoro è già stata usata per un'altra distinta, conviene " +
|
||||||
|
"pulire \"raw\" e \"step\" prima di ripartire (\"File ▸ Pulisci raw e " +
|
||||||
|
"step...\"): \"File ▸ Nuovo\" azzera solo lo stato dell'app, non i file " +
|
||||||
|
"già copiati su disco.",
|
||||||
|
]),
|
||||||
|
|
||||||
|
new("1. Database",
|
||||||
|
"Carica o aggiorna l'indice \"cad_index.db\" a partire dalle cartelle di primo " +
|
||||||
|
"livello del disco/share (radice di default X:\\). Un pallino verde/rosso indica " +
|
||||||
|
"se un database è caricato. La cartella di lavoro, dove finiscono CSV, \"raw\" e " +
|
||||||
|
"\"step\", si cambia da \"File ▸ Scegli cartella di lavoro...\"."),
|
||||||
|
|
||||||
|
new("2. Importa Distinta Base",
|
||||||
|
"Due passi in sequenza:",
|
||||||
|
Passi:
|
||||||
|
[
|
||||||
|
"Carica la distinta: trascina il file .xls nella finestra, oppure premi " +
|
||||||
|
"\"Scegli file distinta...\". Le righe importate compaiono nella griglia " +
|
||||||
|
"(Componente/Descrizione) e viene scritto un CSV nella cartella di lavoro.",
|
||||||
|
"Verifica la presenza dei file sul server: compare solo dopo un import " +
|
||||||
|
"riuscito. Cerca ogni componente nel database — le righe della griglia si " +
|
||||||
|
"colorano di verde (trovato) o rosso (non trovato nell'indice).",
|
||||||
|
],
|
||||||
|
NotaTesto: "\"Avanti\" si sblocca solo dopo il passo 2, non basta il solo import: " +
|
||||||
|
"è il modo per accorgersi subito se il database è disallineato rispetto alla " +
|
||||||
|
"share prima di andare avanti.",
|
||||||
|
NotaAttenzione: true),
|
||||||
|
|
||||||
|
new("3. Crea cartella raw",
|
||||||
|
"Premi \"Crea cartella raw\": per ogni componente della distinta trovato " +
|
||||||
|
"nell'indice, l'app ne copia il file CAD dalla share nella cartella \"raw\" " +
|
||||||
|
"(dentro la cartella di lavoro). La griglia mostra il percorso CAD copiato per " +
|
||||||
|
"ogni componente."),
|
||||||
|
|
||||||
|
new("4. Trova componenti referenziati",
|
||||||
|
"Gli assiemi (.asm) referenziano altri file (sotto-assiemi, parti, componenti " +
|
||||||
|
"soppressi, riferimenti esterni) che Creo deve poter caricare insieme " +
|
||||||
|
"all'assieme principale, anche se non compaiono nella distinta del gestionale. " +
|
||||||
|
"Premi \"Trova componenti referenziati\": l'app legge i file .asm già in \"raw\", " +
|
||||||
|
"risolve ricorsivamente tutto l'albero e copia anche quei file in \"raw\".\n\n" +
|
||||||
|
"Si apre una finestra con l'elenco di quanto trovato e copiato, con in fondo " +
|
||||||
|
"eventuali codici non corrispondenti a nessun file indicizzato (possono essere " +
|
||||||
|
"disegni, componenti davvero mancanti, o falsi positivi — vale la pena " +
|
||||||
|
"controllarli). Da lì si può anche aprire direttamente la cartella \"raw\".",
|
||||||
|
NotaTesto: "Passo opzionale se la distinta contiene solo parti singole senza " +
|
||||||
|
"sotto-assiemi: in quel caso si può passare direttamente alla fase successiva."),
|
||||||
|
|
||||||
|
new("5. Verifica completezza",
|
||||||
|
"Mostra i file .asm della distinta già in \"raw\", trascinabili. Da qui il " +
|
||||||
|
"passaggio a Creo è manuale, fuori da quest'app:",
|
||||||
|
Passi:
|
||||||
|
[
|
||||||
|
"Apri Creo Distributed Batch (dal menu Start di Windows, o dal file " +
|
||||||
|
"ptcdbatch.bat nella cartella bin dell'installazione Creo).",
|
||||||
|
"Crea (o riusa, se già presente in azienda) un gruppo di attività per " +
|
||||||
|
"l'esportazione STEP: menu Actions ▸ New Task Group.",
|
||||||
|
"Aggiungi i file: trascina i .asm dalla finestra dell'app (o dalla cartella " +
|
||||||
|
"\"raw\" in Esplora risorse) sul gruppo di attività, oppure sfoglia e " +
|
||||||
|
"selezionali a mano.",
|
||||||
|
"Verifica che il gruppo di attività punti al config.pro/profilo di export " +
|
||||||
|
"STEP giusto (sezione Configuration Files) — di norma è già impostato " +
|
||||||
|
"una volta sola per tutti.",
|
||||||
|
"Avvia l'elaborazione (Start/Run): Distributed Batch legge ogni .asm, con " +
|
||||||
|
"tutto l'albero di sotto-componenti già raccolto nelle fasi 3-4, ed " +
|
||||||
|
"esporta i file STEP nella cartella \"step\" (dentro la cartella di " +
|
||||||
|
"lavoro, accanto a \"raw\").",
|
||||||
|
],
|
||||||
|
NotaTesto: "Se Distributed Batch fallisce con \"[-4] Dati di input non " +
|
||||||
|
"completi\", quasi sempre manca un file nell'albero dei componenti — Creo " +
|
||||||
|
"non dice quale. Torna alla fase 4 e controlla l'elenco dei \"non trovati\": " +
|
||||||
|
"è il punto di partenza per capire cosa recuperare prima di riprovare.",
|
||||||
|
NotaAttenzione: true),
|
||||||
|
|
||||||
|
new("Ripartire da capo",
|
||||||
|
"Quando devi processare un'altra distinta, usa \"File ▸ Nuovo\" (Ctrl+N): azzera " +
|
||||||
|
"CSV, raw e verifica delle fasi 2-5 e riporta al passo 2, senza dover ricaricare " +
|
||||||
|
"il database o la cartella di lavoro (restano quelli già impostati in fase 1).",
|
||||||
|
NotaTesto: "Per ripulire le cartelle \"raw\" e \"step\" della cartella di lavoro " +
|
||||||
|
"prima di un nuovo giro (senza toccare l'indice), usa \"File ▸ Pulisci raw e " +
|
||||||
|
"step...\"."),
|
||||||
|
};
|
||||||
|
|
||||||
|
public GuidaForm()
|
||||||
|
{
|
||||||
|
Text = "Guida";
|
||||||
|
ClientSize = new Size(780, 520);
|
||||||
|
MinimumSize = new Size(580, 400);
|
||||||
|
StartPosition = FormStartPosition.CenterParent;
|
||||||
|
Font = new Font("Segoe UI", 9.5f);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is IOException or ArgumentException)
|
||||||
|
{
|
||||||
|
// ponytail: nessuna icona nell'exe (es. lanciato da dotnet run) -> resta quella di default
|
||||||
|
}
|
||||||
|
|
||||||
|
var headerPanel = new Panel { Dock = DockStyle.Top, Height = 40, BackColor = SystemColors.ControlLight };
|
||||||
|
var lblHeader = new Label
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Fill,
|
||||||
|
Text = "Guida",
|
||||||
|
TextAlign = ContentAlignment.MiddleLeft,
|
||||||
|
Padding = new Padding(16, 0, 0, 0),
|
||||||
|
Font = new Font(Font, FontStyle.Bold),
|
||||||
|
};
|
||||||
|
headerPanel.Controls.Add(lblHeader);
|
||||||
|
|
||||||
|
// Sidebar disegnata a mano (OwnerDrawFixed) invece della selezione blu di sistema:
|
||||||
|
// stessa identità visiva del resto dell'app (UiStile.ColorePrimario) invece del colore
|
||||||
|
// di selezione standard di Windows, che varia con il tema dell'utente.
|
||||||
|
var lista = new ListBox
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Left,
|
||||||
|
Width = 230,
|
||||||
|
IntegralHeight = false,
|
||||||
|
BorderStyle = BorderStyle.None,
|
||||||
|
DrawMode = DrawMode.OwnerDrawFixed,
|
||||||
|
ItemHeight = 34,
|
||||||
|
BackColor = UiStile.ColoreSfondoCard,
|
||||||
|
};
|
||||||
|
foreach (var sezione in Sezioni)
|
||||||
|
lista.Items.Add(sezione.Titolo);
|
||||||
|
lista.DrawItem += (_, e) =>
|
||||||
|
{
|
||||||
|
if (e.Index < 0)
|
||||||
|
return;
|
||||||
|
bool selezionato = (e.State & DrawItemState.Selected) != 0;
|
||||||
|
using var sfondo = new SolidBrush(selezionato ? UiStile.ColorePrimario : UiStile.ColoreSfondoCard);
|
||||||
|
e.Graphics.FillRectangle(sfondo, e.Bounds);
|
||||||
|
var colore = selezionato ? Color.White : ColoreTesto;
|
||||||
|
using var testoFont = new Font(lista.Font, selezionato ? FontStyle.Bold : FontStyle.Regular);
|
||||||
|
var rettangoloTesto = new Rectangle(e.Bounds.X + 16, e.Bounds.Y, e.Bounds.Width - 20, e.Bounds.Height);
|
||||||
|
TextRenderer.DrawText(e.Graphics, lista.Items[e.Index].ToString(), testoFont, rettangoloTesto,
|
||||||
|
colore, TextFormatFlags.VerticalCenter | TextFormatFlags.Left | TextFormatFlags.EndEllipsis);
|
||||||
|
};
|
||||||
|
|
||||||
|
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)
|
||||||
|
ScriviSezione(testo, Sezioni[lista.SelectedIndex]);
|
||||||
|
};
|
||||||
|
|
||||||
|
var (barraChiudi, azioniChiudi) = UiStile.FloatBar();
|
||||||
|
var btnChiudi = new Button { Text = "Chiudi" };
|
||||||
|
UiStile.Primario(btnChiudi);
|
||||||
|
UiStile.Arrotonda(btnChiudi, 10);
|
||||||
|
btnChiudi.Click += (_, _) => Close();
|
||||||
|
azioniChiudi.Controls.Add(btnChiudi);
|
||||||
|
CancelButton = btnChiudi;
|
||||||
|
|
||||||
|
var contenuto = new Panel { Dock = DockStyle.Fill, Padding = new Padding(24, 20, 24, 20), BackColor = Color.White };
|
||||||
|
contenuto.Controls.Add(testo);
|
||||||
|
|
||||||
|
Controls.Add(contenuto);
|
||||||
|
Controls.Add(lista);
|
||||||
|
Controls.Add(barraChiudi);
|
||||||
|
Controls.Add(headerPanel);
|
||||||
|
|
||||||
|
lista.SelectedIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Costruisce il pannello di lettura con gerarchia tipografica esplicita: titolo
|
||||||
|
/// (blu primario, grassetto, 14pt), corpo (grigio quasi-nero, 10pt), passi numerati (numero
|
||||||
|
/// in blu grassetto, indentati) e callout finale con sfondo a tinta piatta ed etichetta in
|
||||||
|
/// grassetto — informativo (blu tenue) o di attenzione (ambra tenue). Applica la
|
||||||
|
/// formattazione DOPO aver scritto il testo, selezionando il range appena aggiunto: è il
|
||||||
|
/// solo modo affidabile di formattare i paragrafi in un RichTextBox (impostare
|
||||||
|
/// SelectionFont/Color su una selezione vuota prima di AppendText non è garantito che
|
||||||
|
/// "si attacchi" al testo che segue).</summary>
|
||||||
|
private static void ScriviSezione(RichTextBox box, Sezione sezione)
|
||||||
|
{
|
||||||
|
box.Clear();
|
||||||
|
var titoloFont = new Font(box.Font.FontFamily, 14f, FontStyle.Bold);
|
||||||
|
var corpoFont = new Font(box.Font.FontFamily, 10f, FontStyle.Regular);
|
||||||
|
var corpoGrassetto = new Font(box.Font.FontFamily, 10f, FontStyle.Bold);
|
||||||
|
|
||||||
|
void Scrivi(string testo, Font font, Color colore, int indent = 0, Color? sfondo = null)
|
||||||
|
{
|
||||||
|
int inizio = box.TextLength;
|
||||||
|
box.AppendText(testo);
|
||||||
|
box.Select(inizio, box.TextLength - inizio);
|
||||||
|
box.SelectionFont = font;
|
||||||
|
box.SelectionColor = colore;
|
||||||
|
box.SelectionBackColor = sfondo ?? box.BackColor;
|
||||||
|
box.SelectionIndent = indent;
|
||||||
|
}
|
||||||
|
|
||||||
|
Scrivi(sezione.Titolo + "\n\n", titoloFont, UiStile.ColorePrimario);
|
||||||
|
Scrivi(sezione.Intro + "\n", corpoFont, ColoreTesto);
|
||||||
|
|
||||||
|
if (sezione.Passi is { Length: > 0 })
|
||||||
|
{
|
||||||
|
Scrivi("\n", corpoFont, ColoreTesto);
|
||||||
|
for (int i = 0; i < sezione.Passi.Length; i++)
|
||||||
|
{
|
||||||
|
Scrivi($"{i + 1}. ", corpoGrassetto, UiStile.ColorePrimario, indent: 8);
|
||||||
|
Scrivi(sezione.Passi[i] + "\n\n", corpoFont, ColoreTesto, indent: 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sezione.NotaTesto is not null)
|
||||||
|
{
|
||||||
|
var sfondo = sezione.NotaAttenzione ? ColoreNotaAttenzioneSfondo : ColoreNotaInfoSfondo;
|
||||||
|
var coloreEtichetta = sezione.NotaAttenzione ? ColoreNotaAttenzioneTesto : ColoreNotaInfoTesto;
|
||||||
|
string etichetta = sezione.NotaAttenzione ? "Attenzione" : "Nota";
|
||||||
|
|
||||||
|
Scrivi("\n", corpoFont, ColoreTesto);
|
||||||
|
Scrivi(" " + etichetta + "\n", corpoGrassetto, coloreEtichetta, indent: 12, sfondo: sfondo);
|
||||||
|
Scrivi(" " + sezione.NotaTesto + " \n", corpoFont, ColoreTesto, indent: 12, sfondo: sfondo);
|
||||||
|
}
|
||||||
|
|
||||||
|
box.SelectionStart = 0;
|
||||||
|
box.SelectionLength = 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;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<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.9.0</Version>
|
||||||
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|||||||
+20
-85
@@ -2,23 +2,15 @@ namespace PrepStep;
|
|||||||
|
|
||||||
public sealed class ProgressForm : Form
|
public sealed class ProgressForm : Form
|
||||||
{
|
{
|
||||||
private readonly Label _lblTitolo = new();
|
|
||||||
private readonly Label _lblCartella = new();
|
private readonly Label _lblCartella = new();
|
||||||
private readonly Panel _barraTraccia = new();
|
private readonly ProgressBar _bar = new();
|
||||||
private readonly Panel _barraRiempimento = new();
|
|
||||||
private readonly Label _lblPercentuale = new();
|
|
||||||
private readonly Label _lblConteggio = new();
|
private readonly Label _lblConteggio = new();
|
||||||
private readonly Label _lblDettaglio = new();
|
private readonly Label _lblDettaglio = new();
|
||||||
private readonly Label _lblEta = new();
|
private readonly Label _lblEta = new();
|
||||||
private readonly Button _btnAnnulla = new();
|
private readonly Button _btnAnnulla = new();
|
||||||
private readonly Panel _separatore = new();
|
|
||||||
private readonly Panel _piePagina = new();
|
|
||||||
private readonly ToolTip _tipCartella = new();
|
private readonly ToolTip _tipCartella = new();
|
||||||
|
|
||||||
private const int Margine = 24;
|
private const int Margine = 16;
|
||||||
// SystemColors.GrayText (~3.9:1 su bianco) è sotto la soglia WCAG AA 4.5:1 per testo
|
|
||||||
// normale: usiamo un grigio più scuro, leggibile ma comunque secondario.
|
|
||||||
private static readonly Color GrigioTesto = Color.FromArgb(90, 90, 90);
|
|
||||||
|
|
||||||
public event EventHandler? Annullato;
|
public event EventHandler? Annullato;
|
||||||
|
|
||||||
@@ -30,84 +22,42 @@ public sealed class ProgressForm : Form
|
|||||||
MinimizeBox = false;
|
MinimizeBox = false;
|
||||||
MaximizeBox = false;
|
MaximizeBox = false;
|
||||||
ControlBox = false;
|
ControlBox = false;
|
||||||
BackColor = Color.White;
|
ClientSize = new Size(420, 190);
|
||||||
ClientSize = new Size(480, 270);
|
|
||||||
|
|
||||||
_lblTitolo.Text = "Aggiornamento indice in corso...";
|
|
||||||
_lblTitolo.Location = new Point(Margine, 22);
|
|
||||||
_lblTitolo.AutoSize = true;
|
|
||||||
_lblTitolo.Font = new Font(Font.FontFamily, 11f, FontStyle.Bold);
|
|
||||||
|
|
||||||
_lblCartella.Text = "Preparazione...";
|
_lblCartella.Text = "Preparazione...";
|
||||||
_lblCartella.Location = new Point(Margine, 52);
|
_lblCartella.Location = new Point(Margine, Margine);
|
||||||
// AutoSize=false + AutoEllipsis: un percorso lungo (share di rete) va troncato con "..."
|
// AutoSize=false + AutoEllipsis: un percorso lungo (share di rete) va troncato con "..."
|
||||||
// invece di sforare il bordo del dialog, che è FixedDialog e non si può allargare.
|
// invece di sforare il bordo del dialog, che è FixedDialog e non si può allargare. Il
|
||||||
// Il testo completo resta disponibile nel tooltip (stesso pattern di Form1._lblImportStatus).
|
// testo completo resta disponibile nel tooltip.
|
||||||
_lblCartella.AutoSize = false;
|
_lblCartella.AutoSize = false;
|
||||||
_lblCartella.AutoEllipsis = true;
|
_lblCartella.AutoEllipsis = true;
|
||||||
_lblCartella.Size = new Size(ClientSize.Width - Margine * 2, 20);
|
_lblCartella.Size = new Size(ClientSize.Width - Margine * 2, 20);
|
||||||
_lblCartella.ForeColor = GrigioTesto;
|
|
||||||
|
|
||||||
// Barra piatta disegnata a mano (traccia grigia + riempimento) invece della ProgressBar
|
_bar.Location = new Point(Margine, 40);
|
||||||
// nativa: quella di Windows è verde e in stile "a blocchi", stona con il blu del resto
|
_bar.Size = new Size(ClientSize.Width - Margine * 2, 24);
|
||||||
// dell'app (UiStile.Primario). Larghezza = margine destro reale meno lo spazio per
|
_bar.Minimum = 0;
|
||||||
// "100%" in grassetto, così barra+percentuale rispettano lo stesso margine destro del
|
_bar.Maximum = 100;
|
||||||
// resto del contenuto (prima finivano ~25px prima, sbilanciate rispetto al margine
|
|
||||||
// sinistro di 24px).
|
|
||||||
_barraTraccia.Location = new Point(Margine, 90);
|
|
||||||
_barraTraccia.Size = new Size(ClientSize.Width - Margine * 2 - 46, 10);
|
|
||||||
_barraTraccia.BackColor = Color.FromArgb(230, 230, 230);
|
|
||||||
_barraRiempimento.Location = Point.Empty;
|
|
||||||
_barraRiempimento.Size = new Size(0, _barraTraccia.Height);
|
|
||||||
_barraRiempimento.BackColor = UiStile.ColorePrimario;
|
|
||||||
_barraTraccia.Controls.Add(_barraRiempimento);
|
|
||||||
|
|
||||||
_lblPercentuale.AutoSize = true;
|
_lblConteggio.Location = new Point(Margine, 72);
|
||||||
_lblPercentuale.Font = new Font(Font, FontStyle.Bold);
|
|
||||||
_lblPercentuale.TextAlign = ContentAlignment.MiddleRight;
|
|
||||||
|
|
||||||
_lblConteggio.Location = new Point(Margine, 122);
|
|
||||||
_lblConteggio.AutoSize = true;
|
_lblConteggio.AutoSize = true;
|
||||||
|
|
||||||
_lblDettaglio.Location = new Point(Margine, 146);
|
_lblDettaglio.Location = new Point(Margine, 96);
|
||||||
_lblDettaglio.AutoSize = true;
|
_lblDettaglio.AutoSize = true;
|
||||||
_lblDettaglio.ForeColor = GrigioTesto;
|
|
||||||
|
|
||||||
_lblEta.Location = new Point(Margine, 174);
|
_lblEta.Location = new Point(Margine, 120);
|
||||||
_lblEta.AutoSize = true;
|
_lblEta.AutoSize = true;
|
||||||
|
|
||||||
// Footer separato dal contenuto (linea + sfondo leggermente diverso) invece del
|
|
||||||
// bottone appiccicato al bordo inferiore del dialog: ancorato in basso, quindi la
|
|
||||||
// spaziatura sopra/sotto resta corretta anche se la ClientSize cambia. Altezza footer
|
|
||||||
// 60px e ClientSize 270 scelti per lasciare sotto l'ultima riga (eta, Bottom≈189) lo
|
|
||||||
// stesso margine (~20px) usato sopra il titolo, invece dei ~7px sbilanciati di prima.
|
|
||||||
_separatore.BackColor = Color.FromArgb(230, 230, 230);
|
|
||||||
_separatore.Size = new Size(ClientSize.Width, 1);
|
|
||||||
_separatore.Location = new Point(0, ClientSize.Height - 60);
|
|
||||||
_separatore.Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom;
|
|
||||||
|
|
||||||
_piePagina.BackColor = Color.FromArgb(247, 247, 248);
|
|
||||||
_piePagina.Size = new Size(ClientSize.Width, 59);
|
|
||||||
_piePagina.Location = new Point(0, ClientSize.Height - 59);
|
|
||||||
_piePagina.Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom;
|
|
||||||
|
|
||||||
_btnAnnulla.Text = "Annulla";
|
_btnAnnulla.Text = "Annulla";
|
||||||
UiStile.Secondario(_btnAnnulla);
|
_btnAnnulla.Size = new Size(90, 28);
|
||||||
UiStile.Arrotonda(_btnAnnulla);
|
|
||||||
_btnAnnulla.Click += (_, e) => Annullato?.Invoke(this, e);
|
_btnAnnulla.Click += (_, e) => Annullato?.Invoke(this, e);
|
||||||
_piePagina.Controls.Add(_btnAnnulla);
|
|
||||||
|
|
||||||
Controls.AddRange(new Control[]
|
Controls.AddRange(new Control[]
|
||||||
{
|
{
|
||||||
_lblTitolo, _lblCartella, _barraTraccia, _lblPercentuale,
|
_lblCartella, _bar, _lblConteggio, _lblDettaglio, _lblEta, _btnAnnulla
|
||||||
_lblConteggio, _lblDettaglio, _lblEta, _separatore, _piePagina
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Posizionato dopo l'aggiunta ai Controls: con AutoSize il Width vero si conosce
|
_btnAnnulla.Location = new Point(
|
||||||
// solo a layout avvenuto, non subito dopo la creazione del bottone.
|
ClientSize.Width - Margine - _btnAnnulla.Width, ClientSize.Height - Margine - _btnAnnulla.Height);
|
||||||
_btnAnnulla.Location = new Point(_piePagina.Width - Margine - _btnAnnulla.Width,
|
|
||||||
(_piePagina.Height - _btnAnnulla.Height) / 2);
|
|
||||||
_btnAnnulla.Anchor = AnchorStyles.Right | AnchorStyles.Top;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Aggiorna(ScanProgress p)
|
public void Aggiorna(ScanProgress p)
|
||||||
@@ -115,24 +65,9 @@ public sealed class ProgressForm : Form
|
|||||||
_lblCartella.Text = $"Scansione: {p.Cartella}";
|
_lblCartella.Text = $"Scansione: {p.Cartella}";
|
||||||
_tipCartella.SetToolTip(_lblCartella, _lblCartella.Text);
|
_tipCartella.SetToolTip(_lblCartella, _lblCartella.Text);
|
||||||
|
|
||||||
if (p.PrevistiTotali > 0)
|
_bar.Value = p.PrevistiTotali > 0
|
||||||
{
|
? (int)Math.Clamp(100.0 * p.FattiTotali / p.PrevistiTotali, 0, 100)
|
||||||
double frazione = Math.Min(1.0, (double)p.FattiTotali / p.PrevistiTotali);
|
: 0;
|
||||||
_barraRiempimento.Width = (int)(frazione * _barraTraccia.Width);
|
|
||||||
_lblPercentuale.Text = $"{frazione * 100:0}%";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// ponytail: prima del conteggio totale (poco probabile: richiede la scansione
|
|
||||||
// precedente salvata nel DB) la barra resta vuota invece di un'animazione
|
|
||||||
// indeterminata — se serve, aggiungere un Timer che pulsa _barraRiempimento.
|
|
||||||
_barraRiempimento.Width = 0;
|
|
||||||
_lblPercentuale.Text = "";
|
|
||||||
}
|
|
||||||
// Riallineato al margine destro reale ogni volta: AutoSize aggiorna Width non appena
|
|
||||||
// Text cambia, quindi la Location va ricalcolata qui (non solo una volta nel costruttore).
|
|
||||||
_lblPercentuale.Location = new Point(
|
|
||||||
ClientSize.Width - Margine - _lblPercentuale.Width, _barraTraccia.Top - 3);
|
|
||||||
|
|
||||||
_lblConteggio.Text = p.PrevistiTotali > 0
|
_lblConteggio.Text = p.PrevistiTotali > 0
|
||||||
? $"{p.FattiTotali:N0} / ~{p.PrevistiTotali:N0} file"
|
? $"{p.FattiTotali:N0} / ~{p.PrevistiTotali:N0} file"
|
||||||
|
|||||||
@@ -15,11 +15,20 @@ public class PrtMancantiForm : Form
|
|||||||
ClientSize = new Size(720, 440);
|
ClientSize = new Size(720, 440);
|
||||||
StartPosition = FormStartPosition.CenterParent;
|
StartPosition = FormStartPosition.CenterParent;
|
||||||
Font = new Font("Segoe UI", 9.5f);
|
Font = new Font("Segoe UI", 9.5f);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is IOException or ArgumentException)
|
||||||
|
{
|
||||||
|
// ponytail: nessuna icona nell'exe (es. lanciato da dotnet run) -> resta quella di default
|
||||||
|
}
|
||||||
|
|
||||||
var lblInfo = new Label
|
var lblInfo = new Label
|
||||||
{
|
{
|
||||||
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
|
||||||
@@ -55,8 +64,17 @@ public class PrtMancantiForm : Form
|
|||||||
grid.Rows[i].DefaultCellStyle.ForeColor = Color.DarkRed;
|
grid.Rows[i].DefaultCellStyle.ForeColor = Color.DarkRed;
|
||||||
}
|
}
|
||||||
|
|
||||||
var btnApriCartella = new Button { Text = "Apri cartella raw", Dock = DockStyle.Bottom, Height = 34 };
|
var (pannelloAzioni, flowAzioni) = UiStile.FloatBar();
|
||||||
UiStile.Secondario(btnApriCartella);
|
|
||||||
|
var btnChiudi = new Button { Text = "Chiudi" };
|
||||||
|
UiStile.Primario(btnChiudi);
|
||||||
|
UiStile.Arrotonda(btnChiudi, 10);
|
||||||
|
btnChiudi.Click += (_, _) => Close();
|
||||||
|
flowAzioni.Controls.Add(btnChiudi);
|
||||||
|
|
||||||
|
var btnApriCartella = new Button { Text = "Apri cartella raw", Margin = new Padding(8, 0, 0, 0) };
|
||||||
|
UiStile.SecondarioPieno(btnApriCartella);
|
||||||
|
UiStile.Arrotonda(btnApriCartella, 10);
|
||||||
btnApriCartella.Click += (_, _) =>
|
btnApriCartella.Click += (_, _) =>
|
||||||
{
|
{
|
||||||
try { Process.Start("explorer.exe", cartellaRaw); }
|
try { Process.Start("explorer.exe", cartellaRaw); }
|
||||||
@@ -66,9 +84,13 @@ public class PrtMancantiForm : Form
|
|||||||
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
flowAzioni.Controls.Add(btnApriCartella);
|
||||||
|
|
||||||
Controls.Add(grid);
|
Controls.Add(grid);
|
||||||
Controls.Add(lblInfo);
|
Controls.Add(lblInfo);
|
||||||
Controls.Add(btnApriCartella);
|
Controls.Add(pannelloAzioni);
|
||||||
|
CancelButton = btnChiudi;
|
||||||
|
|
||||||
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+111
-12
@@ -8,9 +8,16 @@ namespace PrepStep;
|
|||||||
internal static class UiStile
|
internal static class UiStile
|
||||||
{
|
{
|
||||||
/// <summary>Blu del bottone primario, esposto qui per essere riusato anche da elementi
|
/// <summary>Blu del bottone primario, esposto qui per essere riusato anche da elementi
|
||||||
/// non-bottone (es. la barra di avanzamento di ProgressForm) invece di duplicare l'RGB.</summary>
|
/// non-bottone (es. il colore di selezione delle griglie in Form1) invece di duplicare
|
||||||
|
/// l'RGB.</summary>
|
||||||
public static readonly Color ColorePrimario = Color.FromArgb(0, 99, 177);
|
public static readonly Color ColorePrimario = Color.FromArgb(0, 99, 177);
|
||||||
|
|
||||||
|
/// <summary>Sfondo/bordo delle card di stato (es. riepilogo Database/cartella di lavoro in
|
||||||
|
/// fase 1): un grigio-azzurro appena percettibile rispetto al bianco del resto della
|
||||||
|
/// pagina, sufficiente a separare "stato attuale" da "istruzioni" senza ombre/gradienti.</summary>
|
||||||
|
public static readonly Color ColoreSfondoCard = Color.FromArgb(247, 249, 251);
|
||||||
|
public static readonly Color ColoreBordoCard = Color.FromArgb(226, 231, 236);
|
||||||
|
|
||||||
// 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.
|
||||||
@@ -47,22 +54,38 @@ internal static class UiStile
|
|||||||
b.Cursor = Cursors.Hand;
|
b.Cursor = Cursors.Hand;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Arrotonda gli angoli del bottone. Si aggancia a SizeChanged invece di calcolare
|
/// <summary>Come Secondario, ma pieno invece che a bordo: sulle card bianche delle floatbar
|
||||||
/// la Region una volta sola: con AutoSize la dimensione finale si conosce solo a layout
|
/// (vedi FloatBar) il bordo sottile di Secondario è quasi invisibile su sfondo bianco, e il
|
||||||
/// avvenuto (dopo Text/Padding), non subito dopo la creazione del controllo.</summary>
|
/// bottone risulterebbe visivamente diverso dal Primario pieno accanto — stessa costruzione
|
||||||
public static void Arrotonda(Button b, int raggio = 6)
|
/// (nessun bordo, riempimento pieno), solo grigio chiaro invece che blu.</summary>
|
||||||
|
public static void SecondarioPieno(Button b)
|
||||||
{
|
{
|
||||||
b.SizeChanged += (_, _) => AggiornaRegione(b, raggio);
|
DimensionamentoAutomatico(b);
|
||||||
AggiornaRegione(b, raggio);
|
b.FlatStyle = FlatStyle.Flat;
|
||||||
|
b.FlatAppearance.BorderSize = 0;
|
||||||
|
b.FlatAppearance.MouseOverBackColor = Color.FromArgb(230, 233, 237);
|
||||||
|
b.FlatAppearance.MouseDownBackColor = Color.FromArgb(213, 217, 222);
|
||||||
|
b.BackColor = Color.FromArgb(241, 243, 245);
|
||||||
|
b.Cursor = Cursors.Hand;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AggiornaRegione(Button b, int raggio)
|
/// <summary>Arrotonda gli angoli del controllo (bottone o card). Si aggancia a SizeChanged
|
||||||
|
/// invece di calcolare la Region una volta sola: con AutoSize la dimensione finale si
|
||||||
|
/// conosce solo a layout avvenuto (dopo Text/Padding), non subito dopo la creazione del
|
||||||
|
/// controllo.</summary>
|
||||||
|
public static void Arrotonda(Control c, int raggio = 6)
|
||||||
{
|
{
|
||||||
if (b.Width <= 0 || b.Height <= 0)
|
c.SizeChanged += (_, _) => AggiornaRegione(c, raggio);
|
||||||
|
AggiornaRegione(c, raggio);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AggiornaRegione(Control c, int raggio)
|
||||||
|
{
|
||||||
|
if (c.Width <= 0 || c.Height <= 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
int d = raggio * 2;
|
int d = raggio * 2;
|
||||||
var rect = new Rectangle(Point.Empty, b.Size);
|
var rect = new Rectangle(Point.Empty, c.Size);
|
||||||
using var percorso = new GraphicsPath();
|
using var percorso = new GraphicsPath();
|
||||||
percorso.AddArc(rect.X, rect.Y, d, d, 180, 90);
|
percorso.AddArc(rect.X, rect.Y, d, d, 180, 90);
|
||||||
percorso.AddArc(rect.Right - d, rect.Y, d, d, 270, 90);
|
percorso.AddArc(rect.Right - d, rect.Y, d, d, 270, 90);
|
||||||
@@ -70,7 +93,83 @@ internal static class UiStile
|
|||||||
percorso.AddArc(rect.X, rect.Bottom - d, d, d, 90, 90);
|
percorso.AddArc(rect.X, rect.Bottom - d, d, d, 90, 90);
|
||||||
percorso.CloseFigure();
|
percorso.CloseFigure();
|
||||||
|
|
||||||
b.Region?.Dispose();
|
c.Region?.Dispose();
|
||||||
b.Region = new Region(percorso);
|
c.Region = new Region(percorso);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Card di stato: sfondo/bordo leggermente distinti dalla pagina, angoli arrotondati,
|
||||||
|
/// padding interno. Il bordo va disegnato a mano (Paint) perché Region ritaglia il
|
||||||
|
/// controllo ma non lascia spazio a un Border WinForms nativo sugli angoli arrotondati.</summary>
|
||||||
|
public static Panel Card()
|
||||||
|
{
|
||||||
|
var card = new Panel
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Top,
|
||||||
|
AutoSize = true,
|
||||||
|
AutoSizeMode = AutoSizeMode.GrowAndShrink,
|
||||||
|
BackColor = ColoreSfondoCard,
|
||||||
|
Padding = new Padding(18, 16, 18, 16),
|
||||||
|
Margin = new Padding(0, 0, 0, 16),
|
||||||
|
};
|
||||||
|
card.Paint += (_, e) =>
|
||||||
|
{
|
||||||
|
using var penna = new Pen(ColoreBordoCard);
|
||||||
|
e.Graphics.DrawRectangle(penna, 0, 0, card.Width - 1, card.Height - 1);
|
||||||
|
};
|
||||||
|
Arrotonda(card, 8);
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Barra di bottoni "flottante" (Avanti/Indietro, Apri cartella raw/Chiudi, ecc.):
|
||||||
|
/// una card bianca staccata su tutti e 4 i lati dal bordo della finestra — non un footer a
|
||||||
|
/// tutta larghezza — con angoli arrotondati e bordo sottile, sospesa sopra lo sfondo grigio
|
||||||
|
/// della pagina invece che ancorata al bordo. Lo sfondo del contenitore esterno deve
|
||||||
|
/// combaciare con quello di default della Form (nessun BackColor impostato altrove:
|
||||||
|
/// SystemColors.Control) perché il "distacco" si veda solo nel margine attorno alla card,
|
||||||
|
/// non in una fascia di colore diverso.</summary>
|
||||||
|
public static (Panel Contenitore, FlowLayoutPanel Bottoni) FloatBar()
|
||||||
|
{
|
||||||
|
// Altezza minima per non tagliare il contenuto: bottone (MinimumSize.Height = 32) +
|
||||||
|
// padding verticale della riga bottoni (12+12) + padding verticale del contenitore
|
||||||
|
// esterno (10+16) = 82, con un po' di margine in più.
|
||||||
|
var contenitore = new Panel
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Bottom,
|
||||||
|
Height = 88,
|
||||||
|
BackColor = SystemColors.Control,
|
||||||
|
Padding = new Padding(16, 10, 16, 16),
|
||||||
|
};
|
||||||
|
|
||||||
|
var card = new Panel { Dock = DockStyle.Fill, BackColor = Color.White };
|
||||||
|
card.Paint += (_, e) =>
|
||||||
|
{
|
||||||
|
using var penna = new Pen(ColoreBordoCard);
|
||||||
|
e.Graphics.DrawRectangle(penna, 0, 0, card.Width - 1, card.Height - 1);
|
||||||
|
};
|
||||||
|
Arrotonda(card, 14);
|
||||||
|
|
||||||
|
var bottoni = new FlowLayoutPanel
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Fill,
|
||||||
|
FlowDirection = FlowDirection.RightToLeft,
|
||||||
|
Padding = new Padding(16, 12, 16, 12),
|
||||||
|
};
|
||||||
|
|
||||||
|
card.Controls.Add(bottoni);
|
||||||
|
contenitore.Controls.Add(card);
|
||||||
|
return (contenitore, bottoni);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Forza le stesse dimensioni per due bottoni della stessa barra (es. Avanti/
|
||||||
|
/// Indietro): AutoSize li dimensiona in base al proprio testo, e con testi di lunghezza
|
||||||
|
/// diversa (o solo uno dei due in grassetto, come Primario/Secondario) risulterebbero
|
||||||
|
/// disallineati sia in larghezza che in altezza — un dettaglio che un designer non
|
||||||
|
/// lascerebbe passare. Va chiamato dopo aver impostato Text/Font su entrambi.</summary>
|
||||||
|
public static void LarghezzaUguale(params Button[] bottoni)
|
||||||
|
{
|
||||||
|
int larghezza = bottoni.Max(b => b.PreferredSize.Width);
|
||||||
|
int altezza = bottoni.Max(b => b.PreferredSize.Height);
|
||||||
|
foreach (var b in bottoni)
|
||||||
|
b.MinimumSize = new Size(larghezza, altezza);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,32 +9,38 @@ passaggio, non lo sostituisce.
|
|||||||
|
|
||||||
## Funzionalità
|
## Funzionalità
|
||||||
|
|
||||||
1. **Indice CAD** — all'avvio carica automaticamente `cad_index.db` (se
|
1. **Indice CAD** — all'avvio carica automaticamente l'ultimo `cad_index.db`
|
||||||
presente accanto all'eseguibile). Se manca, si può selezionare un DB
|
usato (ricordato tra un avvio e l'altro). Se manca, si può selezionare un DB
|
||||||
esistente con **Apri DB...** o generarne uno nuovo con **Aggiorna
|
esistente con **Apri DB...** o generarne uno nuovo con **Aggiorna
|
||||||
indice**, che scansiona il disco (di default `X:\`, tutte le cartelle di primo
|
indice**, che scansiona il disco (di default `X:\`, tutte le cartelle di primo
|
||||||
livello che trova) e indicizza tutti i file. In questa fase si vede/cambia anche
|
livello che trova) e indicizza tutti i file. La **cartella di lavoro** (dove
|
||||||
la **cartella di lavoro**: dove finiscono il CSV e la cartella `raw/` (gli STEP
|
finiscono il CSV e la cartella `raw/`; gli STEP esportati con Creo vanno in
|
||||||
esportati con Creo vanno in `step/`, dentro la stessa cartella). Proposta
|
`step/`, dentro la stessa cartella) è proposta automaticamente e ricordata tra
|
||||||
automaticamente (ricordata da un avvio all'altro), modificabile con
|
un avvio e l'altro, modificabile in qualunque momento da **File → Scegli
|
||||||
**Cambia cartella...**.
|
cartella di lavoro...**.
|
||||||
2. **Importa Distinta Base** — trascina (o scegli da file) l'estrazione
|
2. **Importa Distinta Base** — trascina (o scegli da file) l'estrazione
|
||||||
`.xls`/`.xml` del gestionale (foglio "Distinta Base", colonne
|
`.xls`/`.xml` del gestionale (foglio "Distinta Base", colonne
|
||||||
Componente/Descrizione): viene generato un CSV nella cartella di lavoro, con
|
Componente/Descrizione): viene generato un CSV nella cartella di lavoro, con
|
||||||
anteprima in griglia. Se il file segue la convenzione di nome
|
anteprima in griglia. Se il file segue la convenzione di nome
|
||||||
`DBP_<codice>_<timestamp>` del gestionale, l'assieme generale dell'estrazione
|
`DBP_<codice>_<timestamp>` del gestionale, l'assieme generale dell'estrazione
|
||||||
viene aggiunto come riga in più nel CSV (il foglio elenca solo i componenti
|
viene aggiunto come riga in più nel CSV (il foglio elenca solo i componenti
|
||||||
sotto l'assieme, non l'assieme stesso).
|
sotto l'assieme, non l'assieme stesso). Il bottone **Verifica presenza**
|
||||||
|
(compare solo dopo un import riuscito) cerca i componenti nel database — è
|
||||||
|
questo passo, non il solo import, a sbloccare la fase successiva.
|
||||||
3. **Crea cartella raw** — cerca nell'indice il percorso CAD di ogni
|
3. **Crea cartella raw** — cerca nell'indice il percorso CAD di ogni
|
||||||
componente importato (assieme generale incluso) e copia i file trovati
|
componente importato (assieme generale incluso) e copia i file trovati
|
||||||
in una cartella `raw/` dentro la cartella di lavoro.
|
in una cartella `raw/` dentro la cartella di lavoro.
|
||||||
4. **Trova componenti referenziati** — legge i metadati dei file `.asm` in
|
4. **Trova componenti referenziati** — legge i metadati dei file `.asm` in
|
||||||
`raw/` per trovare ricorsivamente i sotto-assiemi/parti che li
|
`raw/` per trovare ricorsivamente i sotto-assiemi/parti che li
|
||||||
compongono, copiandoli a loro volta in `raw/`.
|
compongono, copiandoli a loro volta in `raw/` (passo opzionale se la
|
||||||
5. **Verifica completezza assiemi** — confronta la distinta con l'indice per
|
distinta contiene solo parti singole).
|
||||||
capire se manca qualche sotto-componente, e mostra i file `.asm` della
|
5. **Trascina su Creo** — mostra i file `.asm` della distinta già in `raw/`,
|
||||||
distinta trascinabili su Creo/Esplora risorse per l'esportazione STEP con
|
trascinabili su Creo Distributed Batch per l'esportazione STEP (passo
|
||||||
Creo Distributed Batch.
|
manuale, fuori da quest'app): scrive in `step/`, dentro la cartella di
|
||||||
|
lavoro.
|
||||||
|
|
||||||
|
Guida utente passo-passo completa in [`docs/guida-utente.md`](docs/guida-utente.md)
|
||||||
|
(stessa sintesi anche in-app da **Aiuto → Guida**, F1).
|
||||||
|
|
||||||
## Requisiti
|
## Requisiti
|
||||||
|
|
||||||
|
|||||||
+137
-28
@@ -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.
|
||||||
@@ -41,16 +92,25 @@ Distinta (.xls/.xml) → CSV (componente;descrizione) → raw/ (CAD copiati
|
|||||||
|
|
||||||
| # | Fase | Bottone → metodo | Cosa fa |
|
| # | Fase | Bottone → metodo | Cosa fa |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| 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` | Crea/aggiorna `cad_index.db` scansionando la share; un pallino verde/rosso accanto al titolo indica se un DB è caricato. La cartella di lavoro non è mostrata qui (cambiabile in qualunque momento da "File → Scegli 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/drag&drop → `ImportaDistinta` → `DistintaBaseImporter.ConvertiInCsv`, poi "Verifica presenza" → `VerificaCodiciNelDb` | Xls/Xml gestionale → CSV componente;descrizione, scritto nella cartella di lavoro; "Verifica presenza" (visibile solo dopo un import riuscito) cerca i componenti nell'indice e colora le righe della griglia — è quella che sblocca "Avanti", non il solo import |
|
||||||
| 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 → Nuovo" (Ctrl+N) azzera lo
|
||||||
|
stato delle fasi 2-5 (`NuovaEstrazione`) e va direttamente alla fase 2, senza passare dalla
|
||||||
|
fase 1: database e cartella di lavoro restano quelli già caricati, non serve rifare lo scan;
|
||||||
|
"File → Scegli cartella di lavoro..." → `ScegliCartellaLavoro`, cambia in qualunque momento
|
||||||
|
la destinazione di CSV/`raw/`/`step/`; "File → Pulisci raw e step..." svuota le cartelle
|
||||||
|
`raw/`/`step/` nella cartella di lavoro (`PulisciRawEStep`, non tocca l'indice); "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
|
||||||
@@ -81,8 +141,7 @@ Distinta (.xls/.xml) → CSV (componente;descrizione) → raw/ (CAD copiati
|
|||||||
|
|
||||||
## Lettura dei componenti referenziati in un `.asm`
|
## Lettura dei componenti referenziati in un `.asm`
|
||||||
|
|
||||||
`Form1.EstraiCodiciReferenziati` (usata da `RisolviAlberoComponenti`, fase 4, e da
|
`Form1.EstraiCodiciReferenziati` (usata da `RisolviAlberoComponenti`, fase 4) prova prima
|
||||||
`AssiemeCompleto` dentro `VerificaCompletezzaAssiemi`, fase 5) prova prima
|
|
||||||
`AsmRecordReader.EstraiCodiciReferenziati` e ripiega su
|
`AsmRecordReader.EstraiCodiciReferenziati` e ripiega su
|
||||||
`AssemblyComponentScanner.EstraiCodiciReferenziati` solo se il primo lancia
|
`AssemblyComponentScanner.EstraiCodiciReferenziati` solo se il primo lancia
|
||||||
`InvalidDataException` (file che non rispetta il formato validato dal reverse engineering).
|
`InvalidDataException` (file che non rispetta il formato validato dal reverse engineering).
|
||||||
@@ -90,8 +149,12 @@ Distinta (.xls/.xml) → CSV (componente;descrizione) → raw/ (CAD copiati
|
|||||||
- **AsmRecordReader.cs** — legge esattamente la sezione `members` del binario `.asm` (id/tipo/
|
- **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
|
nome dei componenti diretti, con risoluzione del nome istanza di family table dal parametro
|
||||||
`ConfigName`): nessuna euristica, struttura a record. Lancia `InvalidDataException` se il
|
`ConfigName`): nessuna euristica, struttura a record. Lancia `InvalidDataException` se il
|
||||||
file non ha una sezione `members` o se il conteggio dichiarato non torna. Dettagli in
|
file non ha una sezione `members` o se il conteggio dichiarato non torna. Espone anche le
|
||||||
`docs/formato-asm-record.md`. Condivisa con `scripts/CreoBom.cs` via `#:project`.
|
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
|
||||||
contando le occorrenze di stringhe candidate nel testo binario. Due strategie: codici
|
contando le occorrenze di stringhe candidate nel testo binario. Due strategie: codici
|
||||||
numerici Pro-E riconosciuti da soli via regex (come nei nomi file), e nomi alfabetici di
|
numerici Pro-E riconosciuti da soli via regex (come nei nomi file), e nomi alfabetici di
|
||||||
@@ -100,6 +163,29 @@ Distinta (.xls/.xml) → CSV (componente;descrizione) → raw/ (CAD copiati
|
|||||||
positivi noti in `docs/formato-asm.md`. Resta il fallback per i file che il parser esatto
|
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`.
|
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
|
||||||
|
|
||||||
- **DistintaBaseImporter.cs** — converte l'export del gestionale (SpreadsheetML XML con
|
- **DistintaBaseImporter.cs** — converte l'export del gestionale (SpreadsheetML XML con
|
||||||
@@ -107,8 +193,8 @@ Distinta (.xls/.xml) → CSV (componente;descrizione) → raw/ (CAD copiati
|
|||||||
file) nelle sole colonne Componente/Descrizione, foglio `"Distinta Base"`. Se il nome del
|
file) nelle sole colonne Componente/Descrizione, foglio `"Distinta Base"`. Se il nome del
|
||||||
file segue la convenzione `DBP_<codice>_<timestamp>`, aggiunge l'assieme generale come riga
|
file segue la convenzione `DBP_<codice>_<timestamp>`, aggiunge l'assieme generale come riga
|
||||||
in più (il foglio elenca solo i componenti sotto l'assieme). Scrive il CSV nella cartella
|
in più (il foglio elenca solo i componenti sotto l'assieme). Scrive il CSV nella cartella
|
||||||
passata da `Form1` (la cartella di lavoro scelta in fase 1), non necessariamente accanto al
|
passata da `Form1` (la cartella di lavoro, impostabile da "File → Scegli cartella di lavoro..."),
|
||||||
file di input.
|
non necessariamente accanto al file di input.
|
||||||
|
|
||||||
## Finestre di dialogo (modali, mostrate a fine step)
|
## Finestre di dialogo (modali, mostrate a fine step)
|
||||||
|
|
||||||
@@ -125,9 +211,22 @@ Distinta (.xls/.xml) → CSV (componente;descrizione) → raw/ (CAD copiati
|
|||||||
per i secondari, blu pieno per l'azione principale di ogni fase), entrambi con
|
per i secondari, blu pieno per l'azione principale di ogni fase), entrambi con
|
||||||
`AutoSize`+`Padding`+`MinimumSize` invece di una `Size` fissa in pixel indovinata a
|
`AutoSize`+`Padding`+`MinimumSize` invece di una `Size` fissa in pixel indovinata a
|
||||||
mano (altrimenti il testo va a capo e si taglia in verticale se non ci sta nella
|
mano (altrimenti il testo va a capo e si taglia in verticale se non ci sta nella
|
||||||
larghezza scelta). `Arrotonda` applica angoli arrotondati via `Region` calcolata su
|
larghezza scelta). `SecondarioPieno` è la variante senza bordo (sfondo grigio chiaro
|
||||||
`SizeChanged` (non una volta sola: con `AutoSize` la dimensione finale del bottone si
|
pieno) per i bottoni secondari sopra le card bianche di `Card`/`FloatBar`, dove il
|
||||||
conosce solo a layout avvenuto), usata sui bottoni "Avanti"/"Indietro" del wizard.
|
bordo sottile di `Secondario` è troppo poco visibile su sfondo bianco. `Arrotonda`
|
||||||
|
applica angoli arrotondati a un `Control` qualsiasi (bottone o card) via `Region`
|
||||||
|
calcolata su `SizeChanged`, non una volta sola: con `AutoSize` la dimensione finale si
|
||||||
|
conosce solo a layout avvenuto — va chiamata dopo aver fissato le dimensioni finali del
|
||||||
|
controllo (es. dopo `LarghezzaUguale`), altrimenti la `Region` vecchia resta visibile
|
||||||
|
come bordo residuo finché non arriva un repaint completo. `Card` costruisce una card di
|
||||||
|
stato (sfondo/bordo distinti, angoli arrotondati), usata in fase 1 per il riepilogo
|
||||||
|
Database. `FloatBar` costruisce la barra di navigazione "flottante" in fondo alle
|
||||||
|
finestre (card bianca staccata su tutti e 4 i lati, non un footer a tutta larghezza):
|
||||||
|
usata per Avanti/Indietro nel wizard e per Chiudi/Apri cartella raw in
|
||||||
|
`PrtMancantiForm`. `LarghezzaUguale` forza la stessa larghezza e altezza tra due
|
||||||
|
bottoni della stessa barra (es. Avanti/Indietro, che altrimenti avrebbero dimensioni
|
||||||
|
diverse: testi di lunghezza diversa, e il font in grassetto di `Primario` richiede più
|
||||||
|
altezza di quello normale di `Secondario`/`SecondarioPieno`).
|
||||||
- **app.ico** — icona dell'app, referenziata da `<ApplicationIcon>` nel csproj (icona
|
- **app.ico** — icona dell'app, referenziata da `<ApplicationIcon>` nel csproj (icona
|
||||||
dell'exe in Esplora risorse/taskbar) e riletta esplicitamente in `Form1.BuildUi()`
|
dell'exe in Esplora risorse/taskbar) e riletta esplicitamente in `Form1.BuildUi()`
|
||||||
con `Icon.ExtractAssociatedIcon(Application.ExecutablePath)` per la titlebar, senza
|
con `Icon.ExtractAssociatedIcon(Application.ExecutablePath)` per la titlebar, senza
|
||||||
@@ -137,21 +236,26 @@ 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
|
||||||
|
|
||||||
- **DB** (`cad_index.db`, SQLite) — vedi tabella `files`/vista `files_ultima_versione` sopra.
|
- **DB** (`cad_index.db`, SQLite) — vedi tabella `files`/vista `files_ultima_versione` sopra.
|
||||||
Percorso: cercato accanto all'eseguibile o nella working directory all'avvio; altrimenti
|
Percorso ricordato tra un avvio e l'altro in `%AppData%/PrepStep/ultimo_db.txt`
|
||||||
selezionabile con "Apri DB...". Radice disco di scansione di default `X:\`
|
(`SalvaUltimoDbUsato`/`LeggiUltimoDbUsato`), scritto ogni volta che si carica un DB
|
||||||
(`Form1.RadiceDiscoDefault`, hardcoded).
|
(all'avvio se già noto, oppure da "Apri DB..." o al termine di "Aggiorna indice"); se il
|
||||||
- **Cartella di lavoro** — salvata in `%AppData%/PrepStep/ultima_cartella_lavoro.txt`
|
file non c'è o punta a un percorso cancellato, `InizializzaDatabase` ripiega su
|
||||||
(stesso meccanismo del DB sopra), proposta di default `AppContext.BaseDirectory` la prima
|
`cad_index.db` accanto all'eseguibile o nella working directory. Radice disco di scansione
|
||||||
volta; selezionabile/cambiabile con "Cambia cartella..." in fase 1. Tutti i file generati a
|
di default `X:\` (`Form1.RadiceDiscoDefault`, hardcoded).
|
||||||
valle ci finiscono dentro: il CSV della distinta, `raw/` (CAD dei componenti e
|
- **Cartella di lavoro** — stesso meccanismo del DB sopra, salvata in
|
||||||
sotto-componenti); `step/` non è scritta dall'app ma è dove l'utente esporta gli STEP a
|
`%AppData%/PrepStep/ultima_cartella_lavoro.txt`; se non c'è una preferenza salvata (o punta
|
||||||
mano con Creo Distributed Batch, a partire dai file trascinati da `raw/` in fase 5.
|
a una cartella cancellata) si propone la cartella del DB caricato, o in ultima istanza
|
||||||
|
`AppContext.BaseDirectory`; cambiabile in qualunque momento da "File → Scegli cartella di
|
||||||
|
lavoro...". Tutti i file generati a valle ci finiscono dentro: il CSV della distinta, `raw/`
|
||||||
|
(CAD dei componenti e sotto-componenti); `step/` non è scritta dall'app ma è dove l'utente
|
||||||
|
esporta gli STEP a mano con Creo Distributed Batch, a partire dai file trascinati da `raw/`
|
||||||
|
in fase 5.
|
||||||
|
|
||||||
## Script standalone (`scripts/`)
|
## Script standalone (`scripts/`)
|
||||||
|
|
||||||
@@ -181,6 +285,11 @@ dipendenze extra con `#:package Nome@versione` in testa al file):
|
|||||||
- **PrepStep.Tests/** (xUnit) — riferimento al progetto principale via `ProjectReference`
|
- **PrepStep.Tests/** (xUnit) — riferimento al progetto principale via `ProjectReference`
|
||||||
+ `InternalsVisibleTo` (in `PrepStep.csproj`), così i test chiamano direttamente
|
+ `InternalsVisibleTo` (in `PrepStep.csproj`), così i test chiamano direttamente
|
||||||
`Indexer.ParseFilename` e `AssemblyComponentScanner.EstraiCodiciReferenziati` senza esporli
|
`Indexer.ParseFilename` e `AssemblyComponentScanner.EstraiCodiciReferenziati` senza esporli
|
||||||
come API pubblica. File: `IndexerParseFilenameTests.cs`,
|
come API pubblica. File: `IndexerParseFilenameTests.cs`, `IndexerScanTests.cs`,
|
||||||
`AssemblyComponentScannerTests.cs`, `DistintaBaseImporterTests.cs`,
|
`AssemblyComponentScannerTests.cs`, `AsmRecordReaderTests.cs`,
|
||||||
`CaseInsensitiveLookupIntegrationTests.cs`.
|
`DistintaBaseImporterTests.cs`, `CaseInsensitiveLookupIntegrationTests.cs`,
|
||||||
|
`PipelineEndToEndTests.cs`.
|
||||||
|
|
||||||
|
`AsmRecordReaderTests.cs` costruisce i `.asm` byte per byte con dei piccoli helper
|
||||||
|
(`Membro`, `ConfigName`, `ObjName`, `Riferimenti`): ogni caso strano trovato sulla share
|
||||||
|
diventa un test minimale invece di un file binario da versionare.
|
||||||
+21
-5
@@ -29,17 +29,33 @@ componenti e nel filtro per cartella).
|
|||||||
|
|
||||||
Vista `files_ultima_versione`: stessa struttura di `files`, filtrata alla sola riga con
|
Vista `files_ultima_versione`: stessa struttura di `files`, filtrata alla sola riga con
|
||||||
`versione` più alta per ogni `(cartella_radice, codice, tipo_file)`, tramite `ROW_NUMBER()
|
`versione` più alta per ogni `(cartella_radice, codice, tipo_file)`, tramite `ROW_NUMBER()
|
||||||
OVER (PARTITION BY ... ORDER BY versione DESC)`. Tutte le ricerche di componenti (`Form1.
|
OVER (PARTITION BY ... ORDER BY versione DESC, data_modifica DESC, percorso_completo)`. Tutte
|
||||||
TrovaPercorsiCad`, `Form1.CaricaCodiciNoti`) interrogano questa vista, non `files`
|
le ricerche di componenti (`Form1.TrovaPercorsiCad`, `Form1.CaricaCodiciNoti`) interrogano
|
||||||
direttamente, così un componente con più versioni sulla share risolve sempre all'ultima.
|
questa vista, non `files` direttamente, così un componente con più versioni sulla share
|
||||||
Deliberatamente una `ROW_NUMBER()` invece di un self-join con `GROUP BY`: su ~200k righe un
|
risolve sempre all'ultima. Deliberatamente una `ROW_NUMBER()` invece di un self-join con
|
||||||
self-join degenera in tempi quasi quadratici, `ROW_NUMBER()` resta lineare.
|
`GROUP BY`: su ~200k righe un self-join degenera in tempi quasi quadratici, `ROW_NUMBER()`
|
||||||
|
resta lineare.
|
||||||
|
|
||||||
|
La vista è ricreata (`DROP` + `CREATE`, non `CREATE IF NOT EXISTS`) a ogni "Aggiorna indice",
|
||||||
|
così un DB già esistente prende la definizione corrente senza dover reindicizzare la share.
|
||||||
|
|
||||||
Nota: `cartella_radice` fa parte della partition, quindi se lo stesso codice+tipo esiste
|
Nota: `cartella_radice` fa parte della partition, quindi se lo stesso codice+tipo esiste
|
||||||
in **due cartelle radice diverse** (duplicati sulla share), la vista restituisce una riga per
|
in **due cartelle radice diverse** (duplicati sulla share), la vista restituisce una riga per
|
||||||
ciascuna — non una sola in assoluto. `TrovaPercorsiCad` gestisce questo caso deduplicando per
|
ciascuna — non una sola in assoluto. `TrovaPercorsiCad` gestisce questo caso deduplicando per
|
||||||
`tipo_file` lato C# (vedi sotto).
|
`tipo_file` lato C# (vedi sotto).
|
||||||
|
|
||||||
|
> **I criteri di pareggio non sono decorativi.** Lo stesso codice può esistere **nella stessa
|
||||||
|
> cartella radice, con la stessa `versione`**, in due sottocartelle diverse (una copia in una
|
||||||
|
> cartella di studio, di lavoro o temporanea). Senza `data_modifica` nell'`ORDER BY` il
|
||||||
|
> pareggio veniva rotto in modo arbitrario, e poteva vincere la copia obsoleta — nascondendo
|
||||||
|
> del tutto quella buona, che non compariva più nella vista. Il file che finiva in `raw/` era
|
||||||
|
> allora un modello diverso da quello che l'assieme si aspetta: Creo lo carica, i vincoli di
|
||||||
|
> accoppiamento non si risolvono e **l'assieme non si apre affatto**.
|
||||||
|
>
|
||||||
|
> Il criterio deterministico però non basta a scegliere *bene*: è il minimo per non sbagliare
|
||||||
|
> a caso. Quale copia sia quella giusta dipende da chi la referenzia, non dalla data — vedi
|
||||||
|
> [`formato-asm-record.md`](formato-asm-record.md) §7.4 e `Form1.ScegliPerCartella`.
|
||||||
|
|
||||||
## Come viene popolato: `Indexer.ScanAsync`
|
## Come viene popolato: `Indexer.ScanAsync`
|
||||||
|
|
||||||
Chiamato da "Aggiorna indice" (fase 1) e riutilizza lo stesso `dbPath` già caricato.
|
Chiamato da "Aggiorna indice" (fase 1) e riutilizza lo stesso `dbPath` già caricato.
|
||||||
|
|||||||
+275
-51
@@ -13,11 +13,23 @@ referenziati"/"Verifica completezza" - con fallback su `AssemblyComponentScanner
|
|||||||
che non rispettano il formato validato), resa in formato BOM e ricorsione sui sottoassiemi
|
che non rispettano il formato validato), resa in formato BOM e ricorsione sui sottoassiemi
|
||||||
in [`scripts/CreoBom.cs`](../scripts/CreoBom.cs) (via `#:project`).
|
in [`scripts/CreoBom.cs`](../scripts/CreoBom.cs) (via `#:project`).
|
||||||
|
|
||||||
**Stato della validazione.** Durante il reverse engineering, il livello 1 estratto da 3
|
**Stato della validazione.** Due livelli di prova, da non confondere:
|
||||||
assiemi con BOM Creo di riferimento coincideva **esattamente**: stesse voci, stesso nome
|
|
||||||
istanza, stesse quantità, stesso ordine. Altri 5 `.asm`, mai usati per ricavare le regole,
|
1. **Byte-per-byte contro BOM Creo di riferimento** (la prova forte): 3 assiemi del
|
||||||
parsavano con il conteggio dichiarato uguale a quello trovato. Tutto ciò che segue oltre
|
reverse engineering iniziale, più 5 casi reali aggiunti in seguito, ciascuno con la sua BOM
|
||||||
questo perimetro (§7) è segnalato esplicitamente come ipotesi non dimostrata.
|
esportata da Creo messa a confronto riga per riga con l'output di `CreoBom.cs` —
|
||||||
|
`9284218024b.asm` (albero profondo, multi-livello), `2546220101.asm`/`2546220101_c04.asm.1`
|
||||||
|
(istanza di family table isolata), `2546220101.asm` risalvato con un componente soppresso,
|
||||||
|
`9244218080_c01.asm` (sottoassieme montato due volte), `9284208092_D110.asm` (revisione del
|
||||||
|
figlio). Ognuno di questi è citato per nome nella sezione del documento a cui si riferisce,
|
||||||
|
così resta verificabile quale affermazione poggia su quale file.
|
||||||
|
2. **Su tutti i 350 `.asm` reali di `raw/`**, solo conteggi interni e coerenza con l'euristica
|
||||||
|
testuale (`AssemblyComponentScanner`), **non** contro BOM di riferimento: usato per
|
||||||
|
calibrare le soglie dei controlli di sicurezza (§7) su scala, non per dimostrare la
|
||||||
|
correttezza dei valori estratti file per file.
|
||||||
|
|
||||||
|
Un'affermazione verificata solo al livello 2 (conteggi su scala) è più debole di una
|
||||||
|
verificata al livello 1 (BOM reale) - il testo lo distingue sempre esplicitamente.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -211,6 +223,30 @@ ampio degli 8 campioni originali (`9284218024b.asm`, confrontato con la sua BOM
|
|||||||
riferimento) — non era presente in nessuno degli 8 campioni usati per il reverse engineering
|
riferimento) — non era presente in nessuno degli 8 campioni usati per il reverse engineering
|
||||||
iniziale.
|
iniziale.
|
||||||
|
|
||||||
|
### 4.0.2 Istanza risolta direttamente in `members`, senza passare da `ConfigName`
|
||||||
|
|
||||||
|
Non tutti i membri che sono istanze di family table hanno un record `ConfigName` associato.
|
||||||
|
Per alcune parti commerciali/normalizzate non-UNI, il campo `name` (e allo stesso modo
|
||||||
|
`family_name`) del record `members` (§3) porta già per intero la notazione
|
||||||
|
`<nome istanza><generico>` invece del solo nome generico:
|
||||||
|
|
||||||
|
```
|
||||||
|
\xe3 <id> <type> "<istanza><generico>"\0 "<istanza><generico>"\0
|
||||||
|
```
|
||||||
|
|
||||||
|
È la stessa notazione istanza→generico della §4.1 sotto, ma qui compare **dentro** il record
|
||||||
|
`members` stesso, come valore diretto del componente - non nell'elenco di riferimenti
|
||||||
|
geometrici di cui parla la §4.1 (quello sì da ignorare per la BOM). Il generico fra `<` e `>`
|
||||||
|
è il file realmente indicizzato sul disco; il nome prima di `<` è quello che la BOM
|
||||||
|
mostrerebbe.
|
||||||
|
|
||||||
|
`AsmRecordReader.LeggiMembri` lo riconosce per i membri **senza** un `ConfigName`
|
||||||
|
corrispondente il cui campo `name` rispetta per intero il pattern `<testo><testo>`: il testo
|
||||||
|
dopo l'ultima `<` (fino alla `>` finale) diventa il generico da cercare nell'indice, quello
|
||||||
|
prima diventa il nome mostrato. Senza questo split, il codice cercato nell'indice sarebbe la
|
||||||
|
stringa intera con le parentesi comprese, mai presente nell'indice - il componente
|
||||||
|
risulterebbe erroneamente "non trovato".
|
||||||
|
|
||||||
### 4.1 Una seconda pista, che NON va usata per la BOM
|
### 4.1 Una seconda pista, che NON va usata per la BOM
|
||||||
|
|
||||||
Altrove nel file compaiono stringhe nella forma `istanza<generico>`:
|
Altrove nel file compaiono stringhe nella forma `istanza<generico>`:
|
||||||
@@ -271,9 +307,12 @@ subito dopo, ricorsivamente, quelli dei suoi sottoassiemi, prima di passare al
|
|||||||
fratello successivo. Ogni modello compare **una volta sola** anche se montato
|
fratello successivo. Ogni modello compare **una volta sola** anche se montato
|
||||||
più volte (deduplica per nome, `CreoAsm.Blocchi`).
|
più volte (deduplica per nome, `CreoAsm.Blocchi`).
|
||||||
|
|
||||||
> La deduplica è un'**assunzione**: nessuno dei campioni disponibili durante il
|
> **Verificato.** `9244218080_C01.asm` monta `2605410010_C01` due volte
|
||||||
> reverse engineering montava lo stesso sottoassieme due volte, quindi non è
|
> (`scripts/9244218080_c01.asm.1` + `scripts/9244218080_c01.bom.2` di riferimento): la BOM
|
||||||
> dimostrata su un caso reale.
|
> Creo mostra un solo blocco `Sottoassieme 2605410010_C01 contiene:` con quantità 2 in testa,
|
||||||
|
> non due blocchi separati, e `CreoBom.cs` riproduce l'output esatto senza modifiche. Confermata
|
||||||
|
> anche la moltiplicazione delle quantità nel riepilogo (§6.3): le parti di `2605410010_C01`
|
||||||
|
> compaiono raddoppiate (1→2, 1→2, 16→32).
|
||||||
|
|
||||||
### 6.3 Riepilogo finale
|
### 6.3 Riepilogo finale
|
||||||
|
|
||||||
@@ -291,13 +330,17 @@ dall'esterno (`-p`/`--path`). `CreoAsm.CreaFinder` indicizza `<nome>.asm.<rev>`
|
|||||||
senza distinzione di maiuscole e, a parità di nome, prende la **revisione più
|
senza distinzione di maiuscole e, a parità di nome, prende la **revisione più
|
||||||
alta** presente.
|
alta** presente.
|
||||||
|
|
||||||
> Scelta discutibile: la revisione più alta in cartella non è
|
> **Verificato: è la scelta giusta.** Il `.asm` padre non registra la revisione attesa del
|
||||||
> necessariamente quella con cui l'assieme padre è stato salvato. Il `.asm`
|
> figlio (cercata e non trovata, vedi §7), ma non serve: confermato che **Creo stesso apre
|
||||||
> padre non sembra registrare la revisione attesa del figlio, o non è stata
|
> sempre la revisione più alta disponibile** di un componente, indipendentemente da quale
|
||||||
> trovata. Vedi `formato-asm.md` §"Risoluzione ricorsiva dell'albero" per le
|
> fosse presente quando l'assieme padre fu salvato l'ultima volta. Caso reale controllato:
|
||||||
> trappole già incontrate storicamente su questo punto (con l'euristica
|
> `9284208092_D110.asm` (salvato 2018-08-01) referenzia `9284202481.prt`, che sulla share ha
|
||||||
> testuale, ma il problema della revisione attesa è indipendente dal metodo
|
> revisioni sia del 2014 (v1-v3, presenti all'epoca) sia del 2024 (v4-v6, arrivate anni dopo) -
|
||||||
> di estrazione).
|
> Creo carica v6, non v3. `CreaFinder` che prende sempre la revisione più alta in cartella
|
||||||
|
> replica esattamente questo comportamento, non è un'assunzione azzardata. Vedi `formato-
|
||||||
|
> asm.md` §"Risoluzione ricorsiva dell'albero" per le trappole storiche su questo punto (con
|
||||||
|
> l'euristica testuale - restano valide, sono su un problema diverso: trovare la cartella
|
||||||
|
> giusta, non quale revisione preferire).
|
||||||
|
|
||||||
Se un sottoassieme non si trova, il blocco viene emesso con
|
Se un sottoassieme non si trova, il blocco viene emesso con
|
||||||
`*** NON RISOLTO: file .asm non trovato ***` e le sue parti restano fuori dal
|
`*** NON RISOLTO: file .asm non trovato ***` e le sue parti restano fuori dal
|
||||||
@@ -306,44 +349,225 @@ sbagliato.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Cosa resta sconosciuto
|
## 7. Stato di ogni punto aperto
|
||||||
|
|
||||||
Elencato esplicitamente per non far passare per dimostrato ciò che non lo è:
|
Tabella di riferimento: cosa è provato, come, e cosa resta genuinamente ignoto. Il dettaglio
|
||||||
|
di ciascun punto segue sotto, in due gruppi.
|
||||||
|
|
||||||
- **⚠️ Family table di ASSIEMI (non solo di parti), risultato silenziosamente sbagliato.**
|
| Punto | Stato | Prova |
|
||||||
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
|
| Deduplica blocchi + moltiplicazione quantità (§6.2/§6.3) | Risolto | BOM reale (§7.1) |
|
||||||
generico ha una propria sezione `members` che elenca il contenuto di UNA rappresentazione
|
| Revisione del figlio che il padre si aspetta (§6.4) | Risolto | BOM reale (§7.1) |
|
||||||
(nell'esempio, 3 cuscinetti + 1 sotto-assieme), ma la BOM Creo per lo stesso file mostra 4
|
| Stato soppresso/escluso in `members` | Risolto | BOM reale (§7.1) |
|
||||||
**istanze diverse** (`2546220101_C01`.."_C04", tre "Parte" e una "Sottoassieme" - tipi
|
| 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) |
|
||||||
diversi fra loro!), ciascuna con un proprio contenuto potenzialmente diverso. Il nome
|
| Family table di **assiemi** (contenuto di ogni istanza) | Non risolto, nessun impatto sulla BOM (fallback già corretto) | Scan sull'intera share (§7.2) |
|
||||||
istanza qui **non** passa da un record `ConfigName` (verificato: nessuna occorrenza
|
| Costante `\x82\x7b` in `ConfigName` | Significato ignoto, rischio coperto | Scala 350 file (§7.2) |
|
||||||
letterale di `ConfigName` nel file) ma da un meccanismo diverso, imparentato con la
|
| Occorrenze multiple di `members` (simplified rep) | Mai osservato, rischio coperto | Scala 350 file (§7.2) |
|
||||||
"seconda pista" del §4.1 (`\xe3<id>\xe2\x02<stringa>\0`, senza il bracket
|
| Matrice di posizionamento (`e1`/`e2`/`e3`/`origin`) | Non letto, nessun impatto sulla BOM | — (§7.2) |
|
||||||
`<generico>` in questo caso) - non decodificato. **`AsmRecordReader.LeggiMembri` non lo
|
| Campi numerici dell'header dopo `ASSEMBLY` | Non letto, nessun impatto sulla BOM | — (§7.2) |
|
||||||
rileva e non lancia eccezione**: restituisce silenziosamente il contenuto della
|
| Divergenza `family_name` ≠ `name` | Mai osservata, non letta | — (§7.2) |
|
||||||
rappresentazione di default sotto un solo nome, invece delle 4 istanze corrette - a
|
|
||||||
differenza degli altri limiti di questa sezione, qui il fallimento non è segnalato in
|
"Rischio coperto" vuol dire: il codice non sa cosa significhi il byte, ma verifica un
|
||||||
nessun modo al chiamante (né un conteggio che non torna, né un'eccezione). Scoperto sullo
|
invariante che lo renderebbe visibile (eccezione) se mai smettesse di valere - non è una prova
|
||||||
stesso test end-to-end del punto `#AF` sopra, mai osservato negli 8 campioni originali.
|
che il byte non conti mai, è la garanzia che se contasse il programma lo segnalerebbe invece
|
||||||
**Prima di fidarsi del parser esatto su un albero con assiemi a family table, verificare a
|
di sbagliare in silenzio.
|
||||||
mano contro la BOM Creo.**
|
|
||||||
- **Stato soppresso / escluso.** Nessuna evidenza cercata o trovata. Non si sa
|
### 7.1 Risolti con una BOM Creo di riferimento
|
||||||
se un componente soppresso compaia in `members`, né con quale marcatore. Sui
|
|
||||||
campioni disponibili il totale coincideva con la BOM, il che *suggerisce* che
|
- **Deduplica blocchi e moltiplicazione quantità.** `9244218080_C01.asm` monta
|
||||||
non ce ne fossero, non che siano gestiti.
|
`2605410010_C01` due volte (`scripts/9244218080_c01.asm.1` +
|
||||||
- **La matrice di posizionamento** (`e1`, `e2`, `e3`, `origin`): formato
|
`scripts/9244218080_c01.bom.2`): la BOM Creo mostra un solo blocco
|
||||||
numerico non decodificato.
|
`Sottoassieme 2605410010_C01 contiene:` con quantità 2 in testa (non due blocchi separati),
|
||||||
- **`family_name`**: sempre uguale a `name` sui campioni; non si sa in quale
|
e le sue parti compaiono raddoppiate nel riepilogo (1→2, 1→2, 16→32). `CreoBom.cs` riproduce
|
||||||
situazione divergerebbe.
|
l'output esatto senza modifiche: la deduplica per nome (`CreoAsm.Blocchi`) e la
|
||||||
- **La costante `\x82\x7b`** nei record `ConfigName`.
|
moltiplicazione lungo l'albero (`CreoAsm.Sommario`) sono il comportamento reale di Creo, non
|
||||||
- **I campi numerici dell'header** dopo `ASSEMBLY`.
|
solo un'assunzione.
|
||||||
- **Occorrenze multiple di `members`.** `AsmRecordReader.LeggiMembri` prende la prima.
|
- **Revisione del figlio.** `9284208092_D110.asm` (salvato 2018-08-01) referenzia
|
||||||
Sui campioni disponibili funziona, ma non è stato verificato se le
|
`9284202481.prt`, che sulla share ha revisioni sia del 2014 (v1-v3, presenti quando il padre
|
||||||
rappresentazioni semplificate (simplified rep) generino sezioni `members`
|
fu salvato) sia del 2024 (v4-v6, arrivate anni dopo). Confermato direttamente in Creo: apre
|
||||||
aggiuntive.
|
v6, non v3 - **sempre la revisione più alta disponibile**, indipendentemente da quale fosse
|
||||||
- **La deduplica dei blocchi** e **la revisione attesa dei figli** (§6.2, §6.4).
|
presente all'ultimo salvataggio del padre. `CreoAsm.CreaFinder`, che prende sempre la
|
||||||
- **Quale revisione del figlio** il padre si aspetta: non trovata nel `.asm`.
|
revisione più alta trovata in cartella, replica esattamente questo comportamento.
|
||||||
|
- **Stato soppresso/escluso.** Stesso assieme `2546220101.asm` risalvato da Creo con
|
||||||
|
`2546220101_C02` soppresso (`scripts/2546220101.asm.1` + `scripts/2546220101.bom.1`, con le
|
||||||
|
parti/sottoassiemi correlati). Un componente soppresso **non produce alcun record** in
|
||||||
|
`members`: sparisce e basta, il conteggio dichiarato scende di conseguenza (`\xf8 03` invece
|
||||||
|
di `\xf8 04`). Nessun marcatore di stato da gestire: `CreoBom.cs` sul file risalvato
|
||||||
|
riproduce esattamente la BOM di riferimento senza modifiche al parser.
|
||||||
|
|
||||||
|
> **Sparisce da `members`, non dal file.** La feature che lo posiziona resta, e con essa la
|
||||||
|
> stringa `Componente ID <n> (<codice>.PRT)` (localizzata come in
|
||||||
|
> `AssemblyComponentScanner.HaPlacement`). Per la BOM va ignorata - Creo non elenca i
|
||||||
|
> soppressi - ma **il file di quel componente serve comunque per aprire l'assieme**: se manca,
|
||||||
|
> Creo fallisce il caricamento con "dati di input non completi" (`[-4]`, cioè
|
||||||
|
> `PRO_TK_E_NOT_FOUND`). Verificato su un caso reale: un assieme che non si apriva dalla
|
||||||
|
> cartella `raw/`, e che si è aperto senza errori copiandoci dentro quel solo file.
|
||||||
|
> `AsmRecordReader.EstraiCodiciSoppressi` li restituisce a parte, apposta per questo - vedi
|
||||||
|
> §7.3.
|
||||||
|
- **Family table di assiemi — rilevamento.** Non solo le parti normalizzate (§4) possono
|
||||||
|
essere family table: un intero **assieme** può esserlo. Osservato su `2546220101.asm`
|
||||||
|
(dentro l'albero di `9284218024b.asm`): il file generico ha una propria sezione `members`
|
||||||
|
che elenca il contenuto di UNA rappresentazione (3 cuscinetti + 1 sotto-assieme, dati
|
||||||
|
arbitrari), ma la BOM Creo per lo stesso file mostra 4 **istanze diverse**
|
||||||
|
(`2546220101_C01`.."_C04", tre "Parte" e una "Sottoassieme" - tipi diversi fra loro),
|
||||||
|
ciascuna con un contenuto diverso. Il nome istanza qui **non** passa da `ConfigName`
|
||||||
|
(verificato: nessuna occorrenza letterale nel file), ma da una coppia di record distinta
|
||||||
|
subito dopo l'header del modello:
|
||||||
|
|
||||||
|
```
|
||||||
|
\xe0\x01model_t_type\0<int>\xe0\x0amodel_name\0<codice>_C0n\0\xe0\x0ageneric_name\0\0
|
||||||
|
...stamp\0\xe2\xf2\xf7<idSchema>
|
||||||
|
(\xe3<type>\x02<codice>_C0n\0\0\xe2)* -- un'occorrenza per ogni istanza sorella
|
||||||
|
\xf4
|
||||||
|
```
|
||||||
|
|
||||||
|
Quando questo succede il file generico è già di per sé un'istanza (`model_name` porta il
|
||||||
|
suffisso `_C0n`, non è neutro) e `stamp` elenca le sorelle.
|
||||||
|
|
||||||
|
> **Il record `model_name` da solo non è un'identità.** Su file reali quel record nomina
|
||||||
|
> quasi sempre **un altro modello** - tipicamente uno dei membri del file stesso, a volte un
|
||||||
|
> codice imparentato - e in quei casi `members` è già corretto e va usato. Serve una
|
||||||
|
> conferma indipendente: il **nome proprio del modello** (`objname`, §5). `LeggiMembri`
|
||||||
|
> segnala la family table solo se `objname` esiste **e** coincide con `model_name` privato
|
||||||
|
> del suffisso `_C0n`; negli altri casi legge `members` normalmente.
|
||||||
|
>
|
||||||
|
> Il controllo più largo (senza la conferma di `objname`) era la causa di
|
||||||
|
> **falsi "componenti mancanti"**: il parser esatto rifiutava file perfettamente leggibili,
|
||||||
|
> si ripiegava sull'euristica testuale e quella estraeva codici che non esistono, riportati
|
||||||
|
> poi come componenti non trovati nell'indice. Verificato contro BOM Creo di riferimento:
|
||||||
|
> sui file dove il segnale scattava senza conferma di `objname`, `members` elenca
|
||||||
|
> **esattamente** i componenti della BOM reale (anche nello stesso ordine); uno di questi
|
||||||
|
> file è stato inoltre aperto in Creo, e l'albero del modello mostra gli stessi componenti,
|
||||||
|
> senza alcuna family table. Con la conferma di `objname` richiesta, tutti i `.asm` di una
|
||||||
|
> cartella `raw/` reale (351 file, alberi completi di più macchine) si leggono col parser
|
||||||
|
> esatto, zero ricadute sull'euristica.
|
||||||
|
|
||||||
|
Da tenere presente: il sintomo dipende da **come il file è stato salvato**, non solo dal
|
||||||
|
modello. Rileggendo una revisione più recente dello stesso assieme generico, il record di
|
||||||
|
identità può non esserci più e `members` elencare direttamente tutte le istanze, col tipo
|
||||||
|
giusto per ciascuna - cioè esattamente quello che mostra la BOM. Il blocco quindi non è una
|
||||||
|
proprietà stabile del codice: va valutato file per file, revisione per revisione.
|
||||||
|
|
||||||
|
Verificato anche che il fallback su `AssemblyComponentScanner` **risolve già correttamente**
|
||||||
|
il caso vero: i nomi delle istanze compaiono come testo puro nel campo `stamp`, e
|
||||||
|
l'euristica ("codice che compare almeno due volte") li trova senza bisogno di decodificare
|
||||||
|
`stamp`. E che una singola istanza, se esportata da Creo come file a sé (Family Table →
|
||||||
|
istanza → "Salva una copia"), è un `.asm`/`.prt` normale senza traccia di family table al suo
|
||||||
|
interno - letto correttamente dal parser esatto (`2546220101_c04.asm.1`, verificato contro
|
||||||
|
`2546220101.bom.1`).
|
||||||
|
|
||||||
|
### 7.2 Non risolti
|
||||||
|
|
||||||
|
- **Family table di assiemi — contenuto di ogni istanza.** Come recuperare il contenuto
|
||||||
|
specifico di un'istanza **senza** che esista già un file separato per essa sul disco (il
|
||||||
|
caso comune, se nessuno l'ha mai esportata a parte con "Salva una copia"): il meccanismo
|
||||||
|
dietro `stamp` non è stato decodificato oltre a nome+tipo di ciascuna istanza sorella:
|
||||||
|
`stamp` porta **solo i nomi** delle sorelle, non il loro contenuto, quindi non è la
|
||||||
|
struttura da cui partire per risolvere questo punto.
|
||||||
|
|
||||||
|
Resta aperto solo per completezza: sui file dove il segnale scatta davvero (con conferma
|
||||||
|
del nome proprio, §7.1) il parser si ferma invece di restituire un contenuto sbagliato, e
|
||||||
|
il chiamante ripiega sull'euristica testuale, che i nomi delle istanze li trova comunque
|
||||||
|
come testo puro. Nessun comportamento dell'app dipende da questa decodifica.
|
||||||
|
- **Costante `\x82\x7b`** nei record `ConfigName`: significato ignoto. Rischio coperto -
|
||||||
|
`LeggiMembri` conta le occorrenze grezze di `ConfigName` e le confronta col numero di match
|
||||||
|
del regex; se un campione avesse un valore diverso, il regex non farebbe più match (prima:
|
||||||
|
in silenzio) e ora lancia eccezione invece di restituire il nome generico al posto
|
||||||
|
dell'istanza senza dirlo. Verificato su tutti i 350 `.asm` di `raw/`: 215 usano davvero
|
||||||
|
`ConfigName`, 0 mismatch.
|
||||||
|
- **Occorrenze multiple di `members`** (rappresentazioni semplificate, mai osservate). Rischio
|
||||||
|
coperto - `LeggiMembri` lancia eccezione se ne trova più di una invece di prendere
|
||||||
|
silenziosamente la prima senza sapere se sia quella giusta. Verificato su tutti i 350 `.asm`
|
||||||
|
di `raw/`: sempre esattamente una occorrenza.
|
||||||
|
- **La matrice di posizionamento** (`e1`, `e2`, `e3`, `origin`): formato numerico non
|
||||||
|
decodificato, nessun impatto sulla BOM (non letta da nessuna parte del codice).
|
||||||
|
- **I campi numerici dell'header** dopo `ASSEMBLY`: non letti, nessun impatto sulla BOM.
|
||||||
|
- **`family_name`**: sempre uguale a `name` su tutti i campioni disponibili; non si sa in
|
||||||
|
quale situazione divergerebbe, né se il codice dovrebbe fare qualcosa se succedesse (oggi
|
||||||
|
non la usa in nessun modo, legge solo `name`).
|
||||||
|
|
||||||
|
### 7.3 Due domande diverse: cosa va in distinta, cosa serve per aprire
|
||||||
|
|
||||||
|
È la lezione che costa di più riscoprire, quindi sta qui in chiaro: **la lista dei componenti
|
||||||
|
della BOM e la lista dei file che servono a Creo per aprire l'assieme non coincidono.** La
|
||||||
|
seconda è più lunga.
|
||||||
|
|
||||||
|
| Lista | Serve per | Si ottiene con |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Componenti della distinta | BOM, verifica completezza | `LeggiMembri` / `EstraiCodiciReferenziati` (sezione `members`) |
|
||||||
|
| File necessari al caricamento | popolare `raw/` per l'export STEP | i precedenti **più** `EstraiCodiciSoppressi` e `EstraiCodiciRiferimentiEsterni` (§7.5) |
|
||||||
|
|
||||||
|
La differenza sono i **componenti soppressi** (§7.1) e i **riferimenti esterni** (§7.5): fuori
|
||||||
|
dalla BOM, ma senza il loro file Creo non apre l'assieme. Chi popola `raw/` deve quindi usare
|
||||||
|
l'unione; chi genera la BOM o verifica la completezza deve usare **solo** i membri, altrimenti
|
||||||
|
quelle voci diventano falsi "componenti mancanti".
|
||||||
|
|
||||||
|
Il sintomo, se si sbaglia, non assomiglia per niente alla causa: Creo Distributed Batch
|
||||||
|
fallisce con `Carica modello in Creo Pro | Non riuscito | [-4] Errore: Dati di input non
|
||||||
|
completi` - un messaggio che non nomina il file mancante. Il modo più rapido per trovarlo è
|
||||||
|
aprire l'assieme in Creo **dalla cartella `raw/`** (non dalla share, dove i file ci sono tutti
|
||||||
|
e quindi si apre senza errori) e cercare nell'albero la voce con l'icona rossa.
|
||||||
|
|
||||||
|
### 7.4 Lo stesso codice esiste in più cartelle della share
|
||||||
|
|
||||||
|
Sulla share **13% dei codici** (5.193 su 39.274 fra prt e asm) ha la versione più alta in più
|
||||||
|
cartelle, con contenuto diverso. Non sono revisioni: sono copie parallele, lasciate da studi
|
||||||
|
preliminari, allestimenti fiera, cartelle di lavoro datate, raccolte temporanee.
|
||||||
|
|
||||||
|
Il `.asm` **non dice quale copia usa**: verificato che non registra né il percorso, né
|
||||||
|
l'`UOBJ_ID` del figlio (che pure distingue le copie), né la revisione attesa. Cita solo il
|
||||||
|
nome del modello. Quale copia venga caricata lo decide Creo al momento dell'apertura, e la
|
||||||
|
regola pratica è: **prima la cartella del modello che la referenzia**.
|
||||||
|
|
||||||
|
Non esiste quindi un criterio globale corretto - e i numeri lo confermano: nel 79% dei
|
||||||
|
conflitti nessuna delle copie sta in una cartella "ufficiale" (`…\M_B`, `…\A`), perché il
|
||||||
|
conflitto è fra due istantanee parallele, ognuna autoconsistente con i propri componenti. Per
|
||||||
|
data, per versione e per posizione sono equivalenti: solo il contesto di chi referenzia
|
||||||
|
discrimina.
|
||||||
|
|
||||||
|
Conseguenze per il codice:
|
||||||
|
|
||||||
|
- `Form1.ScegliPerCartella` risolve ogni componente preferendo la copia nella **stessa cartella
|
||||||
|
dell'assieme che lo referenzia**, e solo in mancanza ripiega su versione più alta e data più
|
||||||
|
recente. `Form1.CartellaDiOrigine` ricostruisce quella cartella per gli assiemi di partenza,
|
||||||
|
che arrivano dalla fase precedente senza portarsela dietro.
|
||||||
|
- La vista `files_ultima_versione` (vedi [`db-cad-index.md`](db-cad-index.md)) rompe il
|
||||||
|
pareggio fra copie di pari versione in modo deterministico invece che arbitrario.
|
||||||
|
- Resta un rischio noto: quando il componente **non** sta nella cartella del suo assieme, la
|
||||||
|
scelta torna a essere una scommessa. Se un assieme non si apre da `raw/`, è la prima cosa da
|
||||||
|
verificare - `9244206098` è il caso reale che ha portato a questa sezione: due copie di pari
|
||||||
|
versione, una del 2011 in una cartella di file sciolti e una del 2026 accanto all'assieme,
|
||||||
|
e in `raw/` finiva quella del 2011.
|
||||||
|
|
||||||
|
### 7.5 Riferimenti esterni: la tabella dei modelli citati
|
||||||
|
|
||||||
|
Terzo modo in cui un `.asm` dipende da un file che non è fra i suoi membri, dopo i soppressi
|
||||||
|
(§7.1) e le copie ambigue (§7.4). Un assieme che porta **assembly feature** o **datum copiati**
|
||||||
|
da un altro modello lo cita in una tabella a parte, che non è `members` e non lascia una stringa
|
||||||
|
`Componente ID …`: i due lettori esistenti non la vedevano.
|
||||||
|
|
||||||
|
Struttura: un array aperto da `\xf2\xf7\x05` e chiuso da `\xf3`, con elementi
|
||||||
|
`\xe3\xe2<tipo><nome>\x00`. Il nome può portare il suffisso `#AF<n>` di §4.0.1 e/o la notazione
|
||||||
|
`istanza<generico>` di §4.0.2 - il file su disco è il generico:
|
||||||
|
|
||||||
|
```
|
||||||
|
f2 f7 05 e3 e2 01 "9255708017#AF2<9255708017>" 00 ... -> 9255708017
|
||||||
|
e3 e2 02 "9255700181" 00 ... -> 9255700181
|
||||||
|
f3
|
||||||
|
```
|
||||||
|
|
||||||
|
Va letta con `EstraiCodiciRiferimentiEsterni`, e - a differenza dei soppressi - **si ricorre
|
||||||
|
dentro**: Creo carica il modello citato con tutti i suoi componenti, che quindi servono anch'essi
|
||||||
|
in `raw/`. La tabella contiene anche nomi di *feature* (`MIRROR3`, `CHAMFER1`,
|
||||||
|
`M10_TAPPED_HOLE1`), indistinguibili dai modelli guardando il record: non è un problema perché
|
||||||
|
chi la usa cerca ogni nome nell'indice e ignora quelli che non ci sono - per lo stesso motivo
|
||||||
|
questi codici non devono finire fra i "non trovati".
|
||||||
|
|
||||||
|
Il caso reale: `9255728001` falliva in Distributed Batch con lo stesso `[-4] Dati di input non
|
||||||
|
completi` di §7.3 perché il suo sottoassieme `9255728001_200` referenzia per assembly feature
|
||||||
|
`9255708017` e `9255708018` (e con loro `9255700181/182/183`), e monta una vite
|
||||||
|
`V6-12--_-_U5933_C<UNI5933>` il cui generico `uni5933.prt` non era in `raw/`. Nessuno dei sei
|
||||||
|
file compariva in `members` né fra i piazzamenti.
|
||||||
|
|
||||||
## 8. Uso
|
## 8. Uso
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
# Guida utente
|
||||||
|
|
||||||
|
Questa guida descrive, in ordine, tutti i passi da fare in **Cevolani StepPrep** per
|
||||||
|
arrivare dalla distinta base esportata dal gestionale ai file STEP pronti, passando per
|
||||||
|
**Creo Distributed Batch**. Per il dettaglio tecnico di come funziona internamente ogni
|
||||||
|
fase vedi [`architettura.md`](architettura.md); questa guida si ferma a cosa clicca e
|
||||||
|
cosa vede l'utente. La stessa sintesi è disponibile anche dentro l'app da **Aiuto → Guida**
|
||||||
|
(F1).
|
||||||
|
|
||||||
|
## Prima di iniziare
|
||||||
|
|
||||||
|
- Serve accesso alla share di rete con i file CAD (di default `X:\`).
|
||||||
|
- Serve l'estrazione "Distinta Base" dal gestionale (Fata): il file `.xls` così come
|
||||||
|
Fata lo salva, con le colonne Componente/Descrizione (foglio `"Distinta Base"`) — va
|
||||||
|
lasciato nel formato/estensione in cui viene esportato, senza risalvarlo o convertirlo
|
||||||
|
in altri formati prima di usarlo nell'app. Sotto l'estensione `.xls` il contenuto può
|
||||||
|
essere XML (SpreadsheetML) o Excel binario vero: l'app riconosce da sola quale dei due
|
||||||
|
è, non serve fare nulla di diverso a seconda del caso.
|
||||||
|
- Se il file segue la convenzione di nome del gestionale (`DBP_<codice>_<timestamp>`),
|
||||||
|
l'app aggiunge da sola l'assieme generale come componente in più — non serve fare
|
||||||
|
nulla di diverso.
|
||||||
|
- Conviene definire la **cartella di lavoro** giusta (fase 1, o "File → Scegli cartella
|
||||||
|
di lavoro...") prima di importare la distinta: è lì che finiscono CSV, `raw/` e, dopo
|
||||||
|
l'esportazione con Creo, `step/`.
|
||||||
|
- Se la cartella di lavoro è già stata usata per un'altra distinta, conviene **pulire
|
||||||
|
`raw/` e `step/`** prima di ripartire ("File → Pulisci raw e step..."): evita di
|
||||||
|
confondere i file CAD/STEP della copia precedente con quelli della nuova distinta (il
|
||||||
|
"File → Nuovo" descritto più sotto azzera solo lo stato di CSV/griglie dell'app, non
|
||||||
|
tocca i file già copiati su disco).
|
||||||
|
|
||||||
|
## 1. Database
|
||||||
|
|
||||||
|
Alla prima apertura, se un database (`cad_index.db`) è già stato creato in precedenza,
|
||||||
|
l'app lo ricarica da sola: si vede il pallino verde accanto a "Database", il percorso del
|
||||||
|
file e la data dell'ultimo aggiornamento.
|
||||||
|
|
||||||
|
- **Se il pallino è rosso** (nessun database trovato): premi **Aggiorna indice** per
|
||||||
|
scansionare la share (`X:\`) e crearne uno nuovo — l'operazione può richiedere qualche
|
||||||
|
minuto la prima volta, i giri successivi sono più veloci perché aggiornano solo quello
|
||||||
|
che è cambiato.
|
||||||
|
- **Se hai già un database da un'altra postazione o cartella**: premi **Apri DB...** e
|
||||||
|
selezionalo a mano.
|
||||||
|
- La **cartella di lavoro** (dove l'app scrive il CSV della distinta e la cartella
|
||||||
|
`raw/`) è scelta automaticamente e ricordata tra un avvio e l'altro; per cambiarla usa
|
||||||
|
**File → Scegli cartella di lavoro...**.
|
||||||
|
|
||||||
|
Premi **Avanti** quando il database è caricato.
|
||||||
|
|
||||||
|
## 2. Importa Distinta Base
|
||||||
|
|
||||||
|
- **1. Carica la distinta**: trascina il file `.xls` esportato da Fata nella finestra,
|
||||||
|
oppure premi **Scegli file distinta...** e selezionalo da Esplora risorse. Le righe importate
|
||||||
|
compaiono nella griglia (Componente/Descrizione) e viene scritto un CSV nella cartella
|
||||||
|
di lavoro.
|
||||||
|
- **2. Verifica la presenza dei file sul server**: compare solo dopo un import riuscito.
|
||||||
|
Premila per cercare ogni componente nel database — le righe della griglia si colorano
|
||||||
|
di verde (trovato) o rosso (non trovato nell'indice).
|
||||||
|
|
||||||
|
**Avanti** si sblocca solo dopo aver premuto "Verifica la presenza dei file sul server",
|
||||||
|
non basta il solo import: è il modo per accorgersi subito se il database è disallineato
|
||||||
|
rispetto alla share (es. indice non aggiornato) prima di andare avanti.
|
||||||
|
|
||||||
|
## 3. Crea cartella raw
|
||||||
|
|
||||||
|
Premi **Crea cartella raw**: per ogni componente della distinta trovato nell'indice,
|
||||||
|
l'app ne copia il file CAD dalla share nella cartella `raw/` (dentro la cartella di
|
||||||
|
lavoro). La griglia mostra il percorso CAD copiato per ogni componente.
|
||||||
|
|
||||||
|
## 4. Trova componenti referenziati
|
||||||
|
|
||||||
|
Gli assiemi (`.asm`) referenziano altri file (sotto-assiemi, parti, componenti soppressi,
|
||||||
|
riferimenti esterni) che Creo deve poter caricare insieme all'assieme principale, anche
|
||||||
|
se non compaiono nella distinta del gestionale. Premi **Trova componenti referenziati**:
|
||||||
|
l'app legge i file `.asm` già in `raw/`, risolve ricorsivamente tutto l'albero e copia
|
||||||
|
anche quei file in `raw/`.
|
||||||
|
|
||||||
|
Si apre una finestra con l'elenco di quanto trovato e copiato (PRT e sotto-assiemi), con
|
||||||
|
in fondo eventuali codici referenziati che non corrispondono a nessun file indicizzato
|
||||||
|
(possono essere disegni, componenti davvero mancanti, o falsi positivi della scansione —
|
||||||
|
non bloccano il lavoro, ma vale la pena controllarli). Da qui puoi anche aprire
|
||||||
|
direttamente la cartella `raw/` con **Apri cartella raw**.
|
||||||
|
|
||||||
|
Questo passo è opzionale se la distinta contiene solo parti singole senza sotto-assiemi:
|
||||||
|
in quel caso si può passare direttamente alla fase successiva.
|
||||||
|
|
||||||
|
## 5. Trascina su Creo Distributed Batch
|
||||||
|
|
||||||
|
L'ultima fase mostra l'elenco dei file `.asm` della distinta già presenti in `raw/`,
|
||||||
|
trascinabili. Da qui il passaggio a Creo è manuale, fuori dall'app:
|
||||||
|
|
||||||
|
1. Apri **Creo Distributed Batch** (dal menu Start di Windows, o dal file
|
||||||
|
`ptcdbatch.bat` nella cartella `bin` dell'installazione Creo).
|
||||||
|
2. Crea un nuovo gruppo di attività per l'esportazione STEP (menu **Actions → New Task
|
||||||
|
Group**), scegliendo il tipo di attività per l'export STEP — se in azienda è già
|
||||||
|
presente un profilo/gruppo predefinito per l'export STEP, riusa quello invece di
|
||||||
|
crearne uno nuovo.
|
||||||
|
3. Aggiungi i file: trascina i `.asm` dalla finestra dell'app (o direttamente da Esplora
|
||||||
|
risorse, dalla cartella `raw/`) sul gruppo di attività, oppure sfoglia e selezionali a
|
||||||
|
mano da Distributed Batch.
|
||||||
|
4. Verifica che il gruppo di attività punti al `config.pro`/profilo di export STEP giusto
|
||||||
|
(sezione **Configuration Files** del gruppo) — di norma è già impostato una volta sola
|
||||||
|
per tutti, non richiede modifiche a ogni giro.
|
||||||
|
5. Avvia l'elaborazione (**Start**/**Run**): Distributed Batch legge ogni `.asm`, con
|
||||||
|
tutto l'albero di sotto-componenti già raccolto in `raw/` nelle fasi 3-4, ed esporta i
|
||||||
|
file STEP nella cartella `step/` (dentro la cartella di lavoro, accanto a `raw/`).
|
||||||
|
|
||||||
|
I nomi esatti dei menu possono variare leggermente a seconda della versione di Creo e di
|
||||||
|
come Distributed Batch è già configurato in azienda — questi passi descrivono il flusso
|
||||||
|
generale (fonte: [documentazione e community PTC](https://www.ptc.com/en/support/article/CS28241));
|
||||||
|
in caso di dubbio su profili/config già pronti, chiedi a chi ha impostato Distributed
|
||||||
|
Batch la prima volta.
|
||||||
|
|
||||||
|
**Se Distributed Batch fallisce con `[-4] Dati di input non completi`**, quasi sempre
|
||||||
|
manca un file nell'albero dei componenti — Creo non dice quale. Torna alla fase 4 e
|
||||||
|
controlla l'elenco dei "non trovati": è il punto di partenza per capire cosa recuperare
|
||||||
|
prima di riprovare.
|
||||||
|
|
||||||
|
## Ripartire con una nuova distinta
|
||||||
|
|
||||||
|
Quando devi processare un'altra distinta, usa **File → Nuovo** (Ctrl+N): azzera CSV, raw
|
||||||
|
e verifica delle fasi 2-5 e riporta al passo 2, senza dover ricaricare il database o
|
||||||
|
la cartella di lavoro (restano quelli già impostati in fase 1).
|
||||||
|
|
||||||
|
Per ripulire le cartelle `raw/` e `step/` della cartella di lavoro prima di un nuovo giro
|
||||||
|
(senza toccare l'indice), usa **File → Pulisci raw e step...**.
|
||||||
Reference in new issue
Block a user