21 Commits
Author SHA1 Message Date
davideandClaude Opus 5.5 5fac2edf52 Rilascia la 1.0.0: licenza, CHANGELOG e documentazione riallineata al codice
Versione portata a 1.0.0 (csproj, CLAUDE.md) e voce [1.0.0] nel
CHANGELOG con tutto il lavoro dalla 0.9.0: setup per-utente, nuovo
nome "PrepStep - Cevolani", DB fisso nella cartella di installazione,
"Elimina CSV delle distinte", guide aggiornate.

Nuova LICENSE proprietaria (Davide Grilli, tutti i diritti riservati),
riportata anche in <Copyright> dell'exe e AppCopyright del setup.
README: sequenza completa di comandi per generare il setup.

Documentazione riallineata al codice dopo un controllo file per file:
nomi delle fasi come nella UI, "File → Nuovo" che non cancella file,
mappa dei file e test nel README, riferimenti ad AssiemeCompleto (non
esiste più) e ruolo di CaricaCodiciNoti/CaricaCandidati in fase 4.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 08:53:35 +02:00
davideandClaude Opus 5.5 4ae3d0e087 Aggiungi "Elimina CSV delle distinte" e allinea le guide alla cartella di installazione
Nuova voce "File → Elimina CSV delle distinte..." (EliminaCsv): elimina
i *.csv al primo livello della cartella di lavoro, che altrimenti si
accumulano uno per distinta importata. Chiede conferma mostrando
l'elenco dei file; gli .xls del gestionale e raw/step non sono toccati.

Guida utente e guida in-app: nuova sezione "Dove finiscono i file"
(DB sempre nella cartella di installazione, CSV/raw/step nella cartella
di lavoro, comandi di pulizia, come aprire la cartella nascosta in
AppData), fase 1 allineata al DB fisso e ad "Apri DB..." solo per la
sessione. Tolta dalla guida utente la sezione di installazione: chi la
legge ha già il programma installato (resta nel README).

Doc aggiornati nello stesso commit: CLAUDE.md, docs/architettura.md,
docs/guida-utente.md, CHANGELOG.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 08:39:58 +02:00
davideandClaude Opus 5.5 64ff1d8770 Rinomina l'app in "PrepStep - Cevolani"
Nome mostrato all'utente cambiato da "Cevolani StepPrep" a "PrepStep -
Cevolani": titolo finestra, <Product>/<AssemblyTitle>, guida in-app,
setup (nome, collegamenti nel menu Start, voce in App installate).
Aggiornando, il setup rimuove i collegamenti col vecchio nome
([InstallDelete]) per non lasciarli doppi nel menu Start.

Doc aggiornati nello stesso commit: README.md, CLAUDE.md,
docs/architettura.md, docs/guida-utente.md, CHANGELOG.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 08:33:34 +02:00
davideandClaude Opus 5.5 57020f9964 Aggiungi il setup per-utente e fissa il DB nella cartella di installazione
Nuovo installer/PrepStep.iss (Inno Setup 6): installa in
%LOCALAPPDATA%\Programs\PrepStep senza diritti di amministratore, con
collegamenti nel menu Start al programma e al disinstallatore. Se l'app
è già installata chiede se aggiornarla o disinstallarla (Inno da solo
aggiornerebbe sempre); con /VERYSILENT aggiorna senza chiedere.

Il DB è ora sempre cad_index.db accanto all'eseguibile, cioè nella
cartella di installazione: non si ricorda più l'ultimo DB aperto
(ultimo_db.txt) e "Apri DB..." vale solo per la sessione. Senza una
cartella di lavoro salvata si propone la cartella di installazione
invece di quella del DB.

Doc aggiornati nello stesso commit: README.md, CLAUDE.md,
docs/architettura.md, docs/db-cad-index.md, docs/guida-utente.md,
CHANGELOG.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 08:29:03 +02:00
davideandClaude Opus 5.5 206db85e12 Porta la versione a 0.9.0 e aggiorna il CHANGELOG
Voce 0.9.0 in CHANGELOG.md con le modifiche dal tag 0.1.0 (menu File,
card di stato in fase 1, verifica presenza in fase 2, floatbar, guida
utente e finestra Guida ridisegnata). Versione aggiornata anche in
CLAUDE.md; .vscode/ aggiunta al .gitignore.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 08:05:21 +02:00
davideandClaude Sonnet 5 d81fede4cc Ridisegna la finestra Guida con gerarchia visiva e callout colorati
La guida in-app era testo semplice senza gerarchia (stesso peso per
titoli, corpo, passi, avvisi) e una sidebar con la selezione blu di
sistema, fuori stile rispetto al resto dell'app.

Sezioni ristrutturate come dati (Titolo/Intro/Passi/Nota) invece di un
unico blob di testo libero, e renderizzate con gerarchia esplicita nel
RichTextBox: titolo blu primario grassetto, passi numerati con il
numero in blu, callout "Nota"/"Attenzione" con sfondo a tinta piatta
(blu tenue per le info, ambra tenue per gli avvisi che fanno perdere
tempo se ignorati, come l'errore Creo "[-4]"). Sidebar con selezione
disegnata a mano nel blu dell'app invece del blu di sistema. Bottone
Chiudi spostato sulla stessa floatbar già usata nelle altre finestre.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 16:29:42 +02:00
davideandClaude Sonnet 5 948937ba75 Aggiungi la guida utente e allinea i testi rimasti indietro sulla fase 5
Nuova docs/guida-utente.md: tutti i passi in ordine che deve fare
l'utente, dalla distinta esportata da Fata (gestionale) fino
all'esportazione STEP con Creo Distributed Batch (passi generali presi
da documentazione/community PTC, con avviso che i nomi dei menu
variano per versione/configurazione aziendale). Include anche i
consigli pratici di impostare la cartella di lavoro e pulire raw/step
prima di ripartire con una distinta diversa.

GuidaForm.cs e README.md citavano ancora una "Verifica completezza
assiemi" e un bottone "Cambia cartella..." rimossi da tempo dal
codice, mai allineati nei testi: corretti nello stesso commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 16:22:39 +02:00
davideandClaude Sonnet 5 ff4e7a71e0 Trasforma la barra di navigazione in una floatbar e uniforma i bottoni Avanti/Indietro
La barra Avanti/Indietro era un footer bianco a tutta larghezza con una
linea di separazione da 1px. Ora è una card bianca staccata su tutti e
4 i lati dalla finestra (UiStile.FloatBar, angoli arrotondati, bordo
sottile), riusata anche in PrtMancantiForm per Chiudi/Apri cartella raw.

Avanti e Indietro avevano dimensioni diverse (testi di lunghezza
diversa, più il grassetto di Primario che richiede più altezza del
testo normale) ed erano visivamente disomogenei (bordo sottile quasi
invisibile su sfondo bianco per Indietro contro il blu pieno di
Avanti). Aggiunti UiStile.SecondarioPieno (variante piena, senza
bordo, stessa costruzione di Primario ma grigio chiaro) e
UiStile.LarghezzaUguale (stessa larghezza e altezza tra due bottoni,
chiamata PRIMA di arrotondare gli angoli: arrotondare e poi
ridimensionare lasciava un bordo residuo della Region vecchia).

Doc aggiornati nello stesso commit: docs/architettura.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 16:13:34 +02:00
davideandClaude Sonnet 5 be4989fc8d Sistema il testo delle fasi 3-4 e completa il dialogo "Componenti referenziati trovati"
Fase 3: spostata la spiegazione da paragrafo sopra il bottone a etichetta
affiancata ("Copia i file dal server nella cartella 'raw'."), stesso
pattern già usato in fase 2. Fase 4: tolta la frase finale sul passo
opzionale, ridondante con la frase precedente.

PrtMancantiForm (l'esito di "Trova componenti referenziati"): mancava un
modo esplicito per chiudere la finestra (solo la X) e usava l'icona
default di WinForms invece di quella dell'app. Aggiunto "Chiudi" accanto
ad "Apri cartella raw" (Esc la chiude) e impostata l'icona dall'eseguibile,
stesso pattern già usato in Form1.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 16:00:25 +02:00
davideandClaude Sonnet 5 e94331b69f Ridisegna la fase 1 in card di stato e aggiunge un passo esplicito di verifica in fase 2
Fase 1: le info su DB e cartella di lavoro erano testo grigio uniforme
disallineato con bottoni squadrati senza gerarchia. Ora vivono in una
card di stato ("Database") con un indicatore verde/rosso per lo stato
di caricamento; la card "Cartella di lavoro" è stata rimossa (resta
gestibile da File → Scegli cartella di lavoro..., già presente nel
menu). Le icone sui bottoni (prima Segoe MDL2 Assets, poi disegnate a
mano via GDI+) sono state provate e infine tolte su richiesta: restano
solo testo, più semplici da mantenere per due bottoni.

Fase 2: "Verifica presenza" ora compare solo dopo un import riuscito
(dialogo o drag&drop) invece di essere sempre visibile, ed è lei —non
il solo import— a sbloccare "Avanti" (mancava la constatazione che
l'utente può ancora avanzare senza aver verificato che i file esistano
sul server). Aggiunte etichette numerate "1. Carica la distinta" /
"2. Verifica la presenza dei file sul server" accanto ai bottoni, e
rimosso il messaggio verde ridondante dopo l'import.

Doc aggiornati nello stesso commit: CLAUDE.md, docs/architettura.md,
GuidaForm.cs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 15:50:29 +02:00
davideandClaude Sonnet 5 3e9c78074c Sposta 'Pulisci raw e step' e 'Cambia cartella' nel menu File, rinomina 'Nuovo'
Fase 1 (Database) aveva 4 bottoni tutti sullo stesso piano visivo: due
configurano il DB, gli altri due (cambio cartella di lavoro, pulizia
raw/step) sono azioni occasionali di manutenzione, non passi del wizard.
Spostate in File → "Scegli cartella di lavoro..." e "Pulisci raw e
step...", raggiungibili in qualunque momento invece che solo dalla fase 1.

"File → Nuova estrazione" diventa "Nuovo"; corretto anche un bug per cui
InizializzaCartellaLavoro non richiamava AggiornaStatoAvanti, lasciando
"Avanti" disabilitato in fase 1 finché non si toccava un altro controllo
anche a DB e cartella già caricati all'avvio.

Doc aggiornati nello stesso commit: CLAUDE.md, docs/architettura.md,
GuidaForm.cs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 14:44:27 +02:00
davideandClaude Sonnet 5 e29a1bf90a Aggiungi CHANGELOG.md con la voce 0.1.0
Formato Keep a Changelog; elenca le funzionalità attuali del programma come
prima voce di storico versioni.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:20:56 +02:00
21 changed files with 2230 additions and 611 deletions

No files matched your search

+6
View File
@@ -37,3 +37,9 @@ publish/
# Output di scripts/ExtractCreoStrings.cs
/BOM.txt
# Impostazioni locali dell'editor
.vscode/
# Setup generato da Inno Setup (installer/PrepStep.iss)
installer/Output/
+110
View File
@@ -0,0 +1,110 @@
# 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/).
## [1.0.0] - 2026-09-23
Prima versione distribuita agli utenti tramite setup di installazione.
### Aggiunto
- Setup di installazione per-utente (`installer/PrepStep.iss`, Inno Setup 6):
`PrepStep-Setup-<versione>.exe` installa in `%LOCALAPPDATA%\Programs\PrepStep` senza diritti
di amministratore, crea nel menu Start i collegamenti al programma e al disinstallatore
(quello sul desktop è facoltativo) e registra l'app in "Impostazioni → App installate".
- Il setup, se l'app è già installata, chiede se aggiornarla/reinstallarla, disinstallarla o
uscire; con `/VERYSILENT` aggiorna senza chiedere. Aggiornando, database e impostazioni
restano.
- "File → Elimina CSV delle distinte...": elimina i CSV generati dall'import, che altrimenti si
accumulano nella cartella di lavoro (uno per distinta). Chiede conferma mostrando l'elenco
dei file; non tocca gli `.xls` del gestionale né `raw/`/`step/`.
- Guida utente e guida in-app: nuova sezione "Dove finiscono i file" (database, CSV, `raw/`,
`step/`, comandi di pulizia, come aprire la cartella nascosta in `AppData`, file che restano
dopo la disinstallazione).
- Licenza proprietaria (`LICENSE`, © 2026 Davide Grilli, tutti i diritti riservati), anche nei
metadati dell'exe e del setup.
- README: sequenza completa di comandi per generare il setup a ogni release.
### Modificato
- Nome dell'app mostrato all'utente: da "Cevolani StepPrep" a "PrepStep - Cevolani" (titolo
finestra, guida, setup, menu Start, "App installate").
- Il database è sempre `cad_index.db` nella cartella di installazione: all'avvio si carica
solo quello e "Aggiorna indice" lo crea lì se manca. "Apri DB..." carica un altro database
solo per la sessione corrente.
- La cartella di lavoro predefinita (se non ne è stata scelta una con "File → Scegli cartella
di lavoro...") è la cartella di installazione, invece della cartella del database.
- Guida utente: fase 1 riscritta per il database fisso; tolta la sezione di installazione (chi
legge la guida ha già il programma; le istruzioni restano nel `README.md`).
- Finestra Guida più grande (1000×680) e testo non più "da editor": niente cursore
lampeggiante, niente cursore a "I" e niente selezione col mouse.
- Documentazione riallineata al codice: nomi delle fasi come nella UI ("5. Verifica
completezza"), "File → Nuovo" che non cancella file su disco, mappa dei file e test nel
README, riferimenti a metodi non più esistenti (`AssiemeCompleto`) e ruolo di
`CaricaCodiciNoti`/`CaricaCandidati` in fase 4.
### Rimosso
- Memoria dell'ultimo database aperto (`%AppData%\PrepStep\ultimo_db.txt`) e ricerca di
`cad_index.db` nella cartella corrente.
## [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).
+73 -20
View File
@@ -5,9 +5,9 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Cos'è questo progetto
`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 è **PrepStep - Cevolani** (`<Product>`/
`<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 `1.0.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
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
@@ -29,6 +29,13 @@ Pubblicazione come eseguibile standalone (single-file, self-contained, Windows x
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -o publish
```
Setup di installazione per-utente (`%LOCALAPPDATA%\Programs\PrepStep`, niente admin), dalla
root del repo dopo il publish — dettagli in `README.md` e `docs/architettura.md`:
```powershell
& "$env:LOCALAPPDATA\Programs\Inno Setup 6\ISCC.exe" installer\PrepStep.iss
```
`PrepStep.Tests/` (xUnit) contiene unit e integration test — vedi
`dotnet test` da dentro quella cartella. Riferimento al progetto principale
via `ProjectReference` + `InternalsVisibleTo` (in `PrepStep.csproj`), così
@@ -36,9 +43,37 @@ i test possono chiamare direttamente `Indexer.ParseFilename` e
`AssemblyComponentScanner.EstraiCodiciReferenziati` senza esporli come API
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
Il programma è tutto in `PrepStep/` (nessun altro progetto nella solution). Dettaglio
Il programma è tutto in `PrepStep/` (accanto ci sono solo i test `PrepStep.Tests/`, gli
script `scripts/` e il setup `installer/`). Dettaglio
completo file-per-file in [`docs/architettura.md`](docs/architettura.md); qui solo il flusso
che conta per capire il prodotto.
@@ -47,15 +82,21 @@ che conta per capire il prodotto.
designer visuale):
1. **Database** — carica/aggiorna `cad_index.db` (`Indexer.ScanAsync`, radice disco di
default `X:\` hardcoded in `RadiceDiscoDefault`); qui vive anche la **cartella di lavoro**
(destinazione di CSV/`raw/`/`step/`), proposta dall'ultima usata o dalla cartella del DB se
non c'è ancora nulla di salvato — vedi `docs/db-cad-index.md` per il dettaglio di schema e
risoluzione del percorso DB.
default `X:\` hardcoded in `RadiceDiscoDefault`); un pallino verde/rosso accanto al titolo
indica se un DB è caricato. La **cartella di lavoro** (destinazione di CSV/`raw/`/`step/`)
non è più mostrata qui — proposta dall'ultima usata o dalla cartella di installazione se
non c'è 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 percorso del DB
(sempre `cad_index.db` nella cartella di installazione; "Apri DB..." vale solo per la
sessione).
2. **Importa Distinta Base** — `DistintaBaseImporter.ConvertiInCsv` genera il CSV
componente;descrizione nella cartella di lavoro; se il file segue la convenzione del
gestionale (`DBP_<codice>_<timestamp>`), aggiunge l'assieme generale come riga in più (da
quel momento è un componente come tutti gli altri, nessun caso speciale nel resto del
codice).
componente;descrizione nella cartella di lavoro (da file scelto o trascinato); se il file
segue la convenzione del gestionale (`DBP_<codice>_<timestamp>`), aggiunge l'assieme
generale come riga in più (da quel momento è un componente come tutti gli altri, nessun
caso speciale nel resto del codice). 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
`docs/copia-raw-parallela.md`) copiano in `raw/` i file CAD dei componenti del CSV trovati
nell'indice.
@@ -63,15 +104,27 @@ designer visuale):
sotto-assiemi/parti (`RisolviAlberoComponenti`), copiandoli anch'essi in `raw/`. Usa
`AsmRecordReader` (parser esatto dei record binari, `docs/formato-asm-record.md`) e ripiega
su `AssemblyComponentScanner` (euristica testuale, `docs/formato-asm.md`) solo se il file
non rispetta il formato validato (`InvalidDataException`). ⚠️ Limite noto non coperto dal
fallback: un **assieme** con family table (raro, non solo le parti normalizzate possono
averla) può dare un risultato silenziosamente incompleto senza lanciare eccezione — vedi
`docs/formato-asm-record.md` §7.
non rispetta il formato validato (`InvalidDataException`). Tre punti da non confondere,
tutti imparati facendo fallire Creo (`docs/formato-asm-record.md` §7.3, §7.4 e §7.5):
i **componenti soppressi** non sono nella distinta ma il loro file serve per aprire
l'assieme (`EstraiCodiciSoppressi`, copiati senza finire fra i "non trovati"); i
**riferimenti esterni** (assembly feature, datum copiati) citano modelli che non sono né
membri né piazzamenti, e che Creo carica con tutto il loro albero
(`EstraiCodiciRiferimentiEsterni`, anche questi fuori dai "non trovati", ma con ricorsione);
e quando lo stesso codice esiste in più cartelle della share — succede per il 13% dei
codici — va copiata la copia nella **stessa cartella dell'assieme che la referenzia**
(`ScegliPerCartella`), perché il `.asm` non registra quale usa e sbagliarla impedisce a
Creo di aprire l'assieme.
5. **Verifica completezza** — mostra i `.asm` della distinta già in `raw/`, trascinabili su
Creo per l'esportazione STEP (Distributed Batch, manuale, fuori dall'app); "Verifica
completezza assiemi" controlla ricorsivamente sull'indice (non su `raw/`, stessa
`AsmRecordReader`/fallback della fase 4) se tutti i sotto-componenti di ogni assieme sono
presenti.
Creo per l'esportazione STEP (Distributed Batch, manuale, fuori dall'app).
Menu in cima alla finestra: "File → Nuovo" (Ctrl+N) azzera lo stato delle fasi 2-5
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); "File → Elimina CSV delle distinte..." elimina i `*.csv` generati in fase 2, che
altrimenti si accumulano uno per distinta nella cartella di lavoro; "Aiuto → Guida" (F1) apre una sintesi interattiva delle 5 fasi (`GuidaForm.cs`).
## Script standalone (`scripts/`)
@@ -88,5 +141,5 @@ intercambiabili:
- `ExtractCreoStrings.cs`/`BomViaDb.cs` usano `AssemblyComponentScanner` (euristica testuale,
`docs/formato-asm.md`) e risolvono i sottoassiemi contro `cad_index.db`.
- `CreoBom.cs` usa `AsmRecordReader` (parser esatto dei record binari,
`docs/formato-asm-record.md`, condiviso con la GUI in fase 4/5) e risolve i sottoassiemi
`docs/formato-asm-record.md`, condiviso con la GUI in fase 4) e risolve i sottoassiemi
cercandoli in cartelle locali (`-p`), non contro `cad_index.db`.
+17
View File
@@ -0,0 +1,17 @@
Copyright (c) 2026 Davide Grilli. Tutti i diritti riservati.
Questo software, il codice sorgente, la documentazione e i file correlati ("il Software")
sono di proprietà esclusiva di Davide Grilli.
Senza previa autorizzazione scritta del titolare è vietato copiare, modificare, unire,
pubblicare, distribuire, concedere in sublicenza o vendere copie del Software, in tutto o
in parte, e ricavarne opere derivate.
L'uso del Software è consentito solo alle persone e alle organizzazioni espressamente
autorizzate dal titolare, e nei limiti di tale autorizzazione.
IL SOFTWARE È FORNITO "COSÌ COM'È", SENZA GARANZIE DI ALCUN TIPO, ESPRESSE O IMPLICITE,
INCLUSE, SENZA LIMITAZIONE, LE GARANZIE DI COMMERCIABILITÀ, IDONEITÀ A UNO SCOPO
PARTICOLARE E NON VIOLAZIONE. IN NESSUN CASO IL TITOLARE SARÀ RESPONSABILE PER QUALSIASI
RECLAMO, DANNO O ALTRA RESPONSABILITÀ DERIVANTE DALL'USO DEL SOFTWARE O IN RELAZIONE AD
ESSO.
+151
View File
@@ -37,6 +37,28 @@ public class AsmRecordReaderTests
private static byte[] ConfigName(string istanza, int idMembro) =>
Bytes(0xe3, "ConfigName", 0, 0xe2, (byte)'3', istanza, 0, idMembro, 0x82, 0x7b);
// \0objname\0 \xe2 \xe0\x01 object_type\0 <int> \xe0\x0a name\0 <nome>\0 - il nome proprio
// del modello (quello che la BOM intesta), vedi docs/formato-asm-record.md §5
private static byte[] ObjName(string nome) =>
Bytes(0, "objname", 0, 0xe2, 0xe0, 0x01, "object_type", 0, 2, 0xe0, 0x0a, "name", 0, nome, 0);
// identita' dichiarata dal file: \xe0\x01 model_t_type\0 <int> \xe0\x0a model_name\0 <nome>\0
// \xe0\x0a generic_name\0 \0 (vuoto)
private static byte[] ModelName(string nome) =>
Bytes(0xe0, 0x01, "model_t_type", 0, 2, 0xe0, 0x0a, "model_name", 0, nome, 0,
0xe0, 0x0a, "generic_name", 0, 0);
// array dei modelli referenziati: \xf2\xf7\x05 <elementi> \xf3, ogni elemento
// \xe3\xe2<tipo><nome>\0 - vedi docs/formato-asm-record.md §7.5
private static byte[] Riferimenti(params string[] nomi)
{
var parti = new List<object> { 0xf2, 0xf7, 0x05 };
foreach (var nome in nomi)
parti.AddRange(new object[] { 0xe3, 0xe2, 0x02, nome, 0 });
parti.Add(0xf3);
return Bytes(parti.ToArray());
}
private static byte[] Asm(int conteggioDichiarato, byte[] membri, byte[]? extra = null) =>
Bytes("header ", extra ?? Array.Empty<byte>(), "\0members\0", 0xf1, 0xf8, conteggioDichiarato, membri, "#MdlStatus");
@@ -87,6 +109,23 @@ public class AsmRecordReaderTests
Assert.All(membri, m => Assert.Equal("9244250136_C03", m.Nome));
}
[Fact]
public void Membro_con_notazione_istanza_generico_diretta_nel_nome_usa_il_generico()
{
// Regressione: 9244248577a_c01.asm - alcuni membri di family table non-UNI non
// passano da ConfigName, il campo "name"/"family_name" del record members porta gia'
// la forma "<istanza><generico>" (stessa notazione di §4.1). Senza lo split, il codice
// cercato nell'indice sarebbe "V6-12--_-_U5931_GC<UNI5931>" per intero, mai trovato -
// va cercato invece il generico "UNI5931".
byte[] dati = Asm(1, Membro(1, 2, "V6-12--_-_U5931_GC<UNI5931>"));
var membri = AsmRecordReader.LeggiMembri(dati);
Assert.Single(membri);
Assert.Equal("V6-12--_-_U5931_GC", membri[0].Nome);
Assert.Equal("UNI5931", membri[0].Generico);
}
[Fact]
public void Componente_senza_ConfigName_non_e_toccato_da_istanze_di_altri_membri()
{
@@ -111,6 +150,118 @@ public class AsmRecordReaderTests
Assert.Throws<InvalidDataException>(() => AsmRecordReader.LeggiMembri(dati));
}
[Fact]
public void Assieme_family_table_con_generic_name_lancia_eccezione()
{
// Regressione: un file che e' esso stesso un'istanza di family table (il suo nome
// proprio "2546220101" piu' il suffisso "_C01" in model_name) descrive in "members"
// solo quella rappresentazione, non le istanze sorelle. Va segnalato invece di
// restituire silenziosamente il contenuto sbagliato (docs/formato-asm-record.md §7).
byte[] dati = Asm(1, Membro(1, 2, "9244206098"),
extra: Bytes(ObjName("2546220101"), ModelName("2546220101_C01")));
Assert.Throws<InvalidDataException>(() => AsmRecordReader.LeggiMembri(dati));
}
[Fact]
public void Model_name_di_un_altro_modello_non_lancia_eccezione()
{
// Regressione: sui file reali della share il record "model_name" nomina spesso un
// modello diverso dal file (tipicamente un suo membro), non l'identita' del file. Il
// nome proprio (objname) non conferma, quindi non e' il sintomo della family table:
// "members" e' corretto e va usato. Verificato contro BOM Creo reali - senza questo
// controllo il file cadeva sull'euristica testuale, che produceva codici inesistenti
// segnalati poi come "componenti mancanti".
byte[] dati = Asm(1, Membro(1, 2, "9044207999A_C01"),
extra: Bytes(ObjName("9244207999A"), ModelName("9044207999A_C01")));
var membri = AsmRecordReader.LeggiMembri(dati);
Assert.Equal("9044207999A_C01", membri.Single().Nome);
}
[Fact]
public void Model_name_senza_nome_proprio_non_lancia_eccezione()
{
// Stesso caso del test sopra ma senza record "objname" (formato osservato su tutti i
// file reali che facevano scattare il falso positivo): senza conferma del nome proprio
// il record non e' un'identita', quindi "members" va letto normalmente.
byte[] dati = Asm(1, Membro(1, 2, "9244206098"), extra: ModelName("2546220101_C01"));
Assert.Single(AsmRecordReader.LeggiMembri(dati));
}
[Fact]
public void Generic_name_senza_suffisso_istanza_non_lancia_eccezione()
{
// Regressione: su ~7% dei file reali della share compare un record "model_name" +
// "generic_name" (vuoto) che NON e' il sintomo della family table di assiemi - il
// valore non ha il suffisso "_C0n" delle istanze (es. e' un codice qualunque non
// correlato al nome del file). In quei casi "members" e' letto correttamente e va
// usato normalmente, non bloccato per un falso positivo.
byte[] dati = Asm(1, Membro(1, 2, "9244206098"),
extra: Bytes(ObjName("9244230117"), ModelName("9244230117")));
var membri = AsmRecordReader.LeggiMembri(dati);
Assert.Single(membri);
}
[Fact]
public void Componente_soppresso_non_e_fra_i_membri_ma_e_segnalato_a_parte()
{
// Un componente soppresso sparisce da "members" (il conteggio scende) ma resta citato
// dalla feature che lo posiziona. Non va nella BOM, però senza il suo file Creo non
// apre l'assieme, quindi va copiato lo stesso.
byte[] dati = Asm(1, Membro(1, 2, "9244206098"),
extra: Bytes("Componente ID 52 (9230100043.PRT)", 0));
Assert.Equal(new[] { "9244206098" }, AsmRecordReader.LeggiMembri(dati).Select(m => m.Nome));
string percorso = ScriviFile(dati);
try
{
Assert.Equal(new[] { "9230100043" }, AsmRecordReader.EstraiCodiciSoppressi(percorso));
Assert.DoesNotContain("9230100043", AsmRecordReader.EstraiCodiciReferenziati(percorso));
}
finally { File.Delete(percorso); }
}
[Fact]
public void Componente_gia_fra_i_membri_non_e_segnalato_come_soppresso()
{
byte[] dati = Asm(1, Membro(1, 2, "9244206098"),
extra: Bytes("Componente ID 7 (9244206098.PRT)", 0));
string percorso = ScriviFile(dati);
try
{
Assert.Empty(AsmRecordReader.EstraiCodiciSoppressi(percorso));
}
finally { File.Delete(percorso); }
}
[Fact]
public void Riferimenti_esterni_non_sono_fra_i_membri_e_si_risolvono_sul_generico()
{
// Un modello legato da riferimento esterno (assembly feature "#AFn", datum copiati) non
// compare ne' in "members" ne' fra i piazzamenti, ma senza il suo file Creo non apre
// l'assieme: e' il caso che faceva fallire 9255728001_200 in Distributed Batch. Per le
// istanze di family table il file su disco e' il generico fra parentesi angolari.
byte[] dati = Asm(1, Membro(1, 2, "9244206098"),
extra: Bytes(Riferimenti("9255708017#AF2<9255708017>", "9255708018"),
Riferimenti("V6-12--_-_U5933_C<UNI5933>")));
Assert.Equal(new[] { "9244206098" }, AsmRecordReader.LeggiMembri(dati).Select(m => m.Nome));
string percorso = ScriviFile(dati);
try
{
Assert.Equal(new[] { "9255708017", "9255708018", "UNI5933" },
AsmRecordReader.EstraiCodiciRiferimentiEsterni(percorso).OrderBy(c => c, StringComparer.Ordinal));
}
finally { File.Delete(percorso); }
}
[Fact]
public void Sezione_members_assente_lancia_eccezione()
{
+141 -2
View File
@@ -49,12 +49,47 @@ public static class AsmRecordReader
// normalizzazione ogni istanza del pattern risulterebbe un componente diverso.
private static readonly Regex SuffissoArrayFeatureRegex = new(@"#AF\d+$", RegexOptions.Compiled);
// record di identita' del modello, seguito da "generic_name" (vuoto): compare su una
// minoranza di assiemi (~7% dei campioni sulla share) per motivi diversi e nella
// maggioranza dei casi "members" resta corretto - la sola presenza di "generic_name" NON
// e' un segnale affidabile (verificato: su 144 file che lo contengono, solo 25 hanno
// anche il sintomo sotto; per gli altri 119 "members" letto normalmente coincide con
// l'euristica testuale, quindi e' un contesto diverso, non lo stesso problema).
private static readonly Regex OwnModelIdentityRegex = new(
@"\xe0\x01model_t_type\x00" + Int + @"\xe0\x0amodel_name\x00([\x20-\x7e]+)\x00\xe0\x0ageneric_name\x00\x00",
RegexOptions.Compiled);
// Alcuni membri di family table non-UNI (es. viti/dadi commerciali) non passano da
// ConfigName: il campo "name" del record members porta gia' la forma "<istanza><generico>"
// (stessa notazione di §4.1, li' sconsigliata per la BOM perche' e' un elenco di
// riferimenti geometrici - qui invece e' il valore vero del componente, va solo separato).
// Osservato su 9244248577a_c01.asm: "V6-12--_-_U5931_GC<UNI5931>" - senza questo split il
// codice cercato nell'indice sarebbe la stringa intera con le parentesi, mai trovata.
private static readonly Regex IstanzaConGenericoRegex = new(@"^([\x20-\x7e]+?)<([\x20-\x7e]+)>$", RegexOptions.Compiled);
// Suffisso "_C0n" delle istanze di family table (stessa convenzione di
// SuffissoArrayFeatureRegex/ConfigName sopra). Usato insieme a OwnModelIdentityRegex per
// riconoscere un file che e' esso stesso un'istanza, il cui "members" descriverebbe solo
// quella rappresentazione invece delle istanze sorelle (docs/formato-asm-record.md §7).
// Da solo NON basta: su file reali quel record nomina spesso un altro modello (un membro
// del file, o un codice imparentato), non il file stesso - per quello serve la conferma
// del nome proprio (objname).
private static readonly Regex SuffissoIstanzaFamilyTableRegex = new(@"_C\d{2,}$", RegexOptions.Compiled);
// primo elemento dell'array "members": porta le chiavi per esteso.
private static readonly Regex FirstMemberRegex = new(
@"\x01id\x00" + Int + @"\xe0\x01type\x00" + Int
+ @"\xe0\x0aname\x00([^\x00]+)\x00\xe0\x0afamily_name\x00([^\x00]+)\x00",
RegexOptions.Compiled);
private static int ContaOccorrenze(string testo, string marcatore)
{
int conteggio = 0;
for (int i = testo.IndexOf(marcatore, StringComparison.Ordinal); i >= 0; i = testo.IndexOf(marcatore, i + 1, StringComparison.Ordinal))
conteggio++;
return conteggio;
}
private static int AsInt(string bytes)
{
int mask = bytes.Length switch { 1 => 0x7F, 2 => 0x3F, 3 => 0x1F, _ => throw new ArgumentException("lunghezza intero non valida") };
@@ -84,14 +119,26 @@ public static class AsmRecordReader
{
string testo = Encoding.Latin1.GetString(dati);
var configNameMatches = ConfigNameRegex.Matches(testo);
int configNameGrezzi = ContaOccorrenze(testo, (char)0xe3 + "ConfigName\0");
if (configNameMatches.Count != configNameGrezzi)
throw new InvalidDataException(
$"trovati {configNameGrezzi} record 'ConfigName' ma solo {configNameMatches.Count} " +
"interpretati: formato del parametro diverso da quello atteso (vedi §4, costante " +
"\\x82\\x7b), lettura non affidabile");
var istanze = new Dictionary<int, string>();
foreach (Match m in ConfigNameRegex.Matches(testo))
foreach (Match m in configNameMatches)
istanze[AsInt(m.Groups[2].Value)] = SuffissoArrayFeatureRegex.Replace(m.Groups[1].Value, "");
const string chiaveMembers = "\0members\0";
int start = testo.IndexOf(chiaveMembers, StringComparison.Ordinal);
if (start < 0)
throw new InvalidDataException("sezione 'members' assente");
if (testo.IndexOf(chiaveMembers, start + chiaveMembers.Length, StringComparison.Ordinal) >= 0)
throw new InvalidDataException(
"trovate piu' occorrenze di 'members': probabile rappresentazione semplificata, " +
"mai verificata (vedi §7) - non e' chiaro quale sezione descriva la BOM");
start += chiaveMembers.Length;
int conteggio = dati[start + 2]; // \xf1 \xf8 <conteggio>
@@ -118,7 +165,30 @@ public static class AsmRecordReader
if (grezzi.Count != conteggio)
throw new InvalidDataException($"attesi {conteggio} membri, trovati {grezzi.Count}: lettura disallineata");
return grezzi.Select(g => new Membro(g.Id, g.Tipo, istanze.GetValueOrDefault(g.Id, g.Nome), g.Nome)).ToList();
// 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),
@@ -131,4 +201,73 @@ public static class AsmRecordReader
var membri = LeggiMembri(File.ReadAllBytes(percorsoAsm));
return new HashSet<string>(membri.Select(m => m.Generico), StringComparer.OrdinalIgnoreCase);
}
/// <summary>Codici che compaiono come piazzamento ma non fra i membri: sono i componenti
/// **soppressi**, che spariscono da "members" senza lasciare traccia (§7.1) ma restano
/// citati dalla feature che li posiziona.
///
/// Non fanno parte della BOM - Creo non li elenca - ma il loro file serve comunque per
/// **aprire** l'assieme: senza, il caricamento fallisce con "dati di input non completi".
/// Sono quindi due domande diverse: cosa compare nella distinta (<see
/// cref="EstraiCodiciReferenziati"/>) e cosa serve avere su disco (questo metodo in
/// aggiunta), e la seconda lista e' piu' lunga della prima.</summary>
public static HashSet<string> EstraiCodiciSoppressi(string percorsoAsm)
{
byte[] dati = File.ReadAllBytes(percorsoAsm);
var membri = LeggiMembri(dati);
var noti = new HashSet<string>(
membri.Select(m => m.Generico).Concat(membri.Select(m => m.Nome)), StringComparer.OrdinalIgnoreCase);
var soppressi = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (Match m in PlacementRegex.Matches(Encoding.Latin1.GetString(dati)))
if (!noti.Contains(m.Groups[1].Value))
soppressi.Add(m.Groups[1].Value);
return soppressi;
}
/// <summary>Modelli che l'assieme cita nella tabella dei riferimenti (§7.5): oltre ai propri
/// membri comprende quelli legati da **riferimenti esterni** - assembly feature, datum
/// copiati - che non compaiono né in "members" né come piazzamento, ma che Creo carica
/// comunque per aprire l'assieme.
///
/// Terza lista dopo <see cref="EstraiCodiciReferenziati"/> ("cosa c'è in distinta") e <see
/// cref="EstraiCodiciSoppressi"/>: come quest'ultima risponde a "cosa serve su disco", non a
/// "cosa va nella BOM. L'array contiene anche nomi di feature (MIRROR3, CHAMFER1...) e non
/// solo di modelli: non si riesce a distinguerli dal record, ma non danno fastidio perché il
/// chiamante li cerca nell'indice e ignora quelli che non ci sono.</summary>
public static HashSet<string> EstraiCodiciRiferimentiEsterni(string percorsoAsm)
{
string testo = Encoding.Latin1.GetString(File.ReadAllBytes(percorsoAsm));
var codici = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
for (int i = testo.IndexOf(InizioRiferimenti, StringComparison.Ordinal); i >= 0;
i = testo.IndexOf(InizioRiferimenti, i + 1, StringComparison.Ordinal))
{
int max = Math.Min(LunghezzaMassimaRiferimenti, testo.Length - i);
int fine = testo.IndexOf(FineRiferimenti, i, max);
foreach (Match m in RiferimentoRegex.Matches(testo.Substring(i, fine >= 0 ? fine - i : max)))
{
var bracket = IstanzaConGenericoRegex.Match(m.Groups[1].Value);
codici.Add(bracket.Success
? bracket.Groups[2].Value
: SuffissoArrayFeatureRegex.Replace(m.Groups[1].Value, ""));
}
}
return codici;
}
// Array dei modelli referenziati: "\xf2\xf7\x05" lo apre, "\xf3" lo chiude, ogni elemento e'
// "\xe3\xe2<tipo><nome>\x00" (§7.5). Il tetto di lunghezza limita il danno se il terminatore
// non si trova dove atteso: si perde qualche riferimento, non si raccoglie mezzo file.
private const string InizioRiferimenti = "ò÷\u0005ãâ";
private const char FineRiferimenti = 'ó';
private const int LunghezzaMassimaRiferimenti = 4096;
private static readonly Regex RiferimentoRegex = new(
@"\xe3\xe2[\x01\x02]([\x20-\x7e]{3,})\x00", RegexOptions.Compiled);
// "Componente ID 52 (9230100043.PRT)" - la stringa cambia con la localizzazione Creo del
// file, come in AssemblyComponentScanner.HaPlacement. Minimo 3 caratteri per lo stesso
// motivo per cui li' i token generici partono da 3: sotto quella soglia si raccoglie
// rumore (sulla share esistono davvero modelli chiamati "1.prt", "2.prt"...).
private static readonly Regex PlacementRegex = new(
@"Component(?:e)? [Ii][Dd] \d+ \(([A-Za-z0-9_\-]{3,})\.(?:PRT|ASM)\)", RegexOptions.Compiled);
}
+396 -276
View File
@@ -1,4 +1,5 @@
using System.Diagnostics;
using System.Drawing.Drawing2D;
using Microsoft.Data.Sqlite;
namespace PrepStep;
@@ -10,18 +11,22 @@ public partial class Form1 : Form
private readonly Button _btnCreaRaw = new();
private readonly Button _btnTrovaPrt = new();
private readonly Button _btnVerifica = new();
private readonly Button _btnCompletezza = new();
private readonly FlowLayoutPanel _rigaVerifica = new();
private readonly ListView _lstAnteprimaAsm = new();
private readonly Button _btnPulisci = new();
private readonly Label _lblStatus = new();
private readonly Label _lblDb = new();
private readonly Label _lblUltimoAggiornamento = new();
private readonly Label _lblCartellaLavoro = new();
private readonly Button _btnCambiaCartella = new();
private readonly Panel _indicatoreDb = new();
private bool _dbCaricato;
private readonly DataGridView _gridImport = new();
private readonly DataGridView _gridRaw = new();
private readonly Label _lblImportStatus = 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 _mnuEliminaCsv = new("Elimina CSV delle distinte...");
private readonly ToolStripMenuItem _mnuCambiaCartella = new("Scegli cartella di lavoro...");
// Wizard: una fase alla volta occupa tutta la finestra, si avanza solo quando quella
// corrente è "pronta" (vedi AvantiConsentito). NomiFasi/_panelFasi sono paralleli per indice.
@@ -70,7 +75,7 @@ public partial class Form1 : Form
AllowDrop = true;
// Versione letta dall'assembly (<Version> nel csproj) invece che duplicata qui a mano.
var versione = typeof(Form1).Assembly.GetName().Version;
Text = $"Cevolani StepPrep {versione?.ToString(3)}";
Text = $"PrepStep - Cevolani {versione?.ToString(3)}";
MinimumSize = new Size(760, 480);
Font = new Font("Segoe UI", 9.5f);
// Stessa icona di ApplicationIcon nel csproj: la si rilegge dall'exe compilato invece
@@ -91,38 +96,27 @@ public partial class Form1 : Form
_lblFase.Font = new Font(Font, FontStyle.Bold);
headerPanel.Controls.Add(_lblFase);
// Barra di navigazione come footer separato dal contenuto (stesso pattern di
// ProgressForm): linea sottile in cima + sfondo bianco, invece dei bottoni appoggiati
// direttamente sul contenuto/sulla barra di stato senza alcuna divisione visiva.
// Padding destro = 16, lo stesso usato dai pannelli di ogni fase (Padding(16)): prima
// era 12, quindi "Avanti" sporgeva 4px oltre il margine destro del contenuto sopra.
var navContainer = new Panel { Dock = DockStyle.Bottom, Height = 64, BackColor = Color.White };
var navSeparatore = new Panel
{
Dock = DockStyle.Top,
Height = 1,
BackColor = Color.FromArgb(220, 220, 220),
};
var navPanel = new FlowLayoutPanel
{
Dock = DockStyle.Fill,
FlowDirection = FlowDirection.RightToLeft,
Padding = new Padding(16, 14, 16, 14),
};
// Floatbar di navigazione: card staccata dai bordi della finestra invece di un footer
// a tutta larghezza appoggiato direttamente sul contenuto/sulla barra di stato.
var (navContainer, navPanel) = UiStile.FloatBar();
_btnAvanti.Text = "Avanti ›";
UiStile.Primario(_btnAvanti);
UiStile.Arrotonda(_btnAvanti);
_btnAvanti.Click += (_, _) => VaiAllaFase(_faseAttuale + 1);
navPanel.Controls.Add(_btnAvanti);
_btnIndietro.Text = "‹ Indietro";
_btnIndietro.Margin = new Padding(8, 0, 0, 0);
UiStile.Secondario(_btnIndietro);
UiStile.Arrotonda(_btnIndietro);
UiStile.SecondarioPieno(_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);
navPanel.Controls.Add(_btnIndietro);
navContainer.Controls.Add(navPanel);
navContainer.Controls.Add(navSeparatore);
var statusPanel = new Panel { Dock = DockStyle.Bottom, Height = 28, BackColor = SystemColors.ControlLight };
_lblStatus.Dock = DockStyle.Fill;
@@ -146,10 +140,34 @@ public partial class Form1 : Form
// Ordine dei Dock.Bottom: l'ultimo aggiunto è il più esterno (il vero bordo inferiore
// della finestra). La barra di stato va tra contenuto e azioni; i bottoni Avanti/Indietro
// restano il footer più esterno, come in ogni wizard/dialog standard di Windows.
var mnuFile = new ToolStripMenuItem("File");
_mnuNuovaEstrazione.ShortcutKeys = Keys.Control | Keys.N;
_mnuNuovaEstrazione.Click += (_, _) => NuovaEstrazione();
mnuFile.DropDownItems.Add(_mnuNuovaEstrazione);
_mnuCambiaCartella.Click += (_, _) => ScegliCartellaLavoro();
mnuFile.DropDownItems.Add(_mnuCambiaCartella);
_mnuPulisci.Click += (_, _) => PulisciRawEStep();
mnuFile.DropDownItems.Add(_mnuPulisci);
_mnuEliminaCsv.Click += (_, _) => EliminaCsv();
mnuFile.DropDownItems.Add(_mnuEliminaCsv);
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(statusPanel);
Controls.Add(navContainer);
Controls.Add(headerPanel);
Controls.Add(_menu);
DragEnter += (_, e) =>
e.Effect = e.Data?.GetDataPresent(DataFormats.FileDrop) == true
@@ -228,7 +246,7 @@ public partial class Form1 : Form
private bool AvantiConsentito(int fase) => fase switch
{
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,
3 => true, // risoluzione ricorsiva opzionale: non tutti gli assiemi hanno sotto-componenti
_ => true,
@@ -236,6 +254,31 @@ public partial class Form1 : Form
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()
{
var contenuto = new TableLayoutPanel
@@ -255,78 +298,78 @@ public partial class Form1 : Form
Dock = DockStyle.Top,
AutoSize = true,
ForeColor = SystemColors.GrayText,
Margin = new Padding(0, 0, 0, 12),
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:\\).",
Margin = new Padding(0, 0, 0, 16),
Text = "L'indice CAD viene caricato automaticamente all'avvio, se già presente.",
};
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.AutoSize = true;
_lblDb.ForeColor = SystemColors.GrayText;
_lblDb.Margin = new Padding(0, 0, 0, 4);
contenuto.Controls.Add(_lblDb);
_lblDb.ForeColor = SystemColors.ControlText;
_lblDb.Margin = new Padding(0, 0, 0, 2);
contenutoCardDb.Controls.Add(_lblDb);
_lblUltimoAggiornamento.AutoSize = true;
_lblUltimoAggiornamento.ForeColor = SystemColors.GrayText;
_lblUltimoAggiornamento.Margin = new Padding(0, 0, 0, 12);
contenuto.Controls.Add(_lblUltimoAggiornamento);
_lblUltimoAggiornamento.Margin = new Padding(0, 0, 0, 14);
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.Margin = new Padding(0, 0, 8, 0);
UiStile.Secondario(_btnApriDb);
UiStile.Arrotonda(_btnApriDb);
_btnApriDb.Click += (_, _) => ScegliDatabase();
bottoni.Controls.Add(_btnApriDb);
bottoniDb.Controls.Add(_btnApriDb);
_btnAggiorna.Text = "Aggiorna indice";
UiStile.Secondario(_btnAggiorna);
UiStile.Arrotonda(_btnAggiorna);
_btnAggiorna.Click += (_, _) => AggiornaIndice();
bottoni.Controls.Add(_btnAggiorna);
bottoniDb.Controls.Add(_btnAggiorna);
contenuto.Controls.Add(bottoni);
var lblIstruzioniCartella = new Label
{
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);
contenutoCardDb.Controls.Add(bottoniDb);
cardDb.Controls.Add(contenutoCardDb);
contenuto.Controls.Add(cardDb);
var panel = new Panel();
panel.Controls.Add(contenuto);
@@ -357,20 +400,47 @@ public partial class Form1 : Form
};
contenuto.Controls.Add(lblIstruzioni);
var rigaImporta = new FlowLayoutPanel
{
AutoSize = true,
FlowDirection = FlowDirection.LeftToRight,
Margin = new Padding(0, 0, 0, 8),
};
var btnSfoglia = new Button
{
Text = "Scegli file distinta...",
Margin = new Padding(0, 0, 0, 8),
Margin = new Padding(0, 0, 8, 0),
};
UiStile.Secondario(btnSfoglia);
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.Margin = new Padding(0, 0, 0, 8);
_btnVerifica.Margin = new Padding(0, 0, 8, 0);
UiStile.Secondario(_btnVerifica);
_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.AutoSize = false;
@@ -420,22 +490,25 @@ public partial class Form1 : Form
};
contenuto.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
var lblIstruzioni = new Label
var rigaCreaRaw = new FlowLayoutPanel
{
Dock = DockStyle.Top,
AutoSize = true,
ForeColor = SystemColors.GrayText,
Margin = new Padding(0, 0, 0, 8),
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).",
FlowDirection = FlowDirection.LeftToRight,
Margin = new Padding(0, 0, 0, 12),
};
contenuto.Controls.Add(lblIstruzioni);
_btnCreaRaw.Text = "Crea cartella raw";
_btnCreaRaw.Margin = new Padding(0, 0, 0, 12);
_btnCreaRaw.Margin = new Padding(0, 0, 8, 0);
UiStile.Primario(_btnCreaRaw);
_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.ReadOnly = true;
@@ -486,8 +559,7 @@ public partial class Form1 : Form
Margin = new Padding(0, 0, 0, 12),
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 "
+ "viene trovato nell'indice dentro 'raw'. Passo opzionale: se la distinta contiene "
+ "solo parti singole puoi anche passare direttamente alla fase successiva.",
+ "viene trovato nell'indice dentro 'raw'.",
};
contenuto.Controls.Add(lblIstruzioni);
@@ -516,27 +588,6 @@ public partial class Form1 : Form
};
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
{
AutoSize = true,
@@ -597,7 +648,7 @@ public partial class Form1 : Form
{
try
{
var (csv, righe) = DistintaBaseImporter.ConvertiInCsv(percorso, _cartellaLavoro!);
var (_, righe) = DistintaBaseImporter.ConvertiInCsv(percorso, _cartellaLavoro!);
_percorsoInputCorrente = percorso;
_righeCorrenti = righe;
@@ -610,7 +661,8 @@ public partial class Form1 : Form
_ultimaCartellaRaw = null;
_ultimaVerifica = null;
ImpostaStatoImport($"CSV generato ({righe.Count} righe): {csv}", Color.DarkGreen);
ImpostaStatoImport("", Color.Black);
_rigaVerifica.Visible = true;
}
catch (Exception ex)
{
@@ -618,6 +670,7 @@ public partial class Form1 : Form
_righeCorrenti = null;
_gridImport.Rows.Clear();
ImpostaStatoImport($"Errore durante la conversione: {ex.Message}", Color.DarkRed);
_rigaVerifica.Visible = false;
}
AggiornaStatoAvanti();
}
@@ -645,6 +698,7 @@ public partial class Form1 : Form
? Color.FromArgb(198, 239, 206)
: Color.FromArgb(255, 199, 206);
}
AggiornaStatoAvanti();
}
private void CreaCartellaRaw()
@@ -688,7 +742,8 @@ public partial class Form1 : Form
.ToList();
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;
}
@@ -715,7 +770,49 @@ 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>Elimina i CSV generati dall'import delle distinte (fase 2), che altrimenti si
/// accumulano uno per distinta nella cartella di lavoro. Solo i *.csv direttamente nella
/// cartella di lavoro, non in sottocartelle; i file originali del gestionale (.xls) restano.</summary>
private void EliminaCsv()
{
string radice = _cartellaLavoro!;
var files = Directory.Exists(radice) ? Directory.GetFiles(radice, "*.csv") : [];
if (files.Length == 0)
{
MessageBox.Show(this, $"Nessun file CSV trovato in {radice}.",
"Elimina CSV", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var nomi = files.Select(Path.GetFileName).Take(15).ToList();
if (files.Length > nomi.Count)
nomi.Add($"... e altri {files.Length - nomi.Count}");
var conferma = MessageBox.Show(this,
$"Eliminare {files.Length} file CSV da {radice}?\n\n{string.Join("\n", nomi)}\n\nL'operazione non è reversibile.",
"Elimina CSV", MessageBoxButtons.YesNo, MessageBoxIcon.Warning);
if (conferma != DialogResult.Yes)
return;
int eliminati = 0;
foreach (var file in files)
{
try
{
File.Delete(file);
eliminati++;
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
// ponytail: file in uso (es. aperto in Excel) -> si salta, non blocca gli altri
}
}
MessageBox.Show(this, $"{eliminati} file CSV eliminati da {radice}.",
"Elimina CSV", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
/// <summary>Tutti i codici prt/asm noti all'indice, con i relativi percorsi (ultima
@@ -743,6 +840,64 @@ public partial class Form1 : Form
return codiciNoti;
}
/// <summary>Tutti i file prt/asm dell'indice raggruppati per codice, tenendo per ogni
/// cartella la sola versione più alta. A differenza di <see cref="CaricaCodiciNoti"/> non
/// sceglie un vincitore fra le cartelle: lo stesso codice può esistere in più punti della
/// share (copie in cartelle di studio, di lavoro, temporanee) con contenuto diverso, e
/// quale sia quello giusto dipende da chi lo referenzia - vedi
/// <see cref="ScegliPerCartella"/>.</summary>
private static Dictionary<string, List<(string Tipo, string Percorso, int Versione, string Data)>> CaricaCandidati(string dbPath)
{
var candidati = new Dictionary<string, List<(string Tipo, string Percorso, int Versione, string Data)>>(StringComparer.OrdinalIgnoreCase);
using var connection = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly");
connection.Open();
using var command = connection.CreateCommand();
command.CommandText = "SELECT codice, tipo_file, percorso_completo, versione, data_modifica FROM files "
+ "WHERE LOWER(tipo_file) IN ('prt', 'asm')";
using var reader = command.ExecuteReader();
while (reader.Read())
{
string codice = reader.GetString(0);
string tipo = reader.GetString(1).ToLowerInvariant();
string percorso = reader.GetString(2);
int versione = reader.GetInt32(3);
var lista = candidati.TryGetValue(codice, out var l) ? l : candidati[codice] = new();
string cartella = Path.GetDirectoryName(percorso) ?? "";
int esistente = lista.FindIndex(x => x.Tipo == tipo
&& string.Equals(Path.GetDirectoryName(x.Percorso), cartella, StringComparison.OrdinalIgnoreCase));
if (esistente < 0)
lista.Add((tipo, percorso, versione, reader.GetString(4)));
else if (versione > lista[esistente].Versione)
lista[esistente] = (tipo, percorso, versione, reader.GetString(4));
}
return candidati;
}
/// <summary>Fra più copie dello stesso codice sulla share sceglie quella che userebbe Creo:
/// prima cerca nella cartella dell'assieme che la referenzia (Creo risolve i componenti
/// partendo da lì), poi ripiega sulla versione più alta e, a parità, sulla più recente.
/// Senza questo criterio si finisce per copiare in "raw" una copia omonima ma di un altro
/// modello - Creo la carica e i vincoli di accoppiamento non si risolvono, quindi l'assieme
/// non si apre affatto.</summary>
private static List<(string Tipo, string Percorso)> ScegliPerCartella(
List<(string Tipo, string Percorso, int Versione, string Data)> candidati, string? cartellaPadre)
{
return candidati
.GroupBy(c => c.Tipo)
.Select(perTipo =>
{
var scelto = perTipo
.OrderByDescending(c => cartellaPadre is not null
&& string.Equals(Path.GetDirectoryName(c.Percorso), cartellaPadre, StringComparison.OrdinalIgnoreCase))
.ThenByDescending(c => c.Versione)
.ThenByDescending(c => c.Data)
.First();
return (scelto.Tipo, scelto.Percorso);
})
.ToList();
}
/// <summary>Codici referenziati direttamente in un assieme: prova prima il parser esatto
/// dei record binari (<see cref="AsmRecordReader"/>, vedi docs/formato-asm-record.md), che
/// non ha i falsi positivi/negativi noti dell'euristica testuale (docs/formato-asm.md); se
@@ -763,6 +918,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
/// (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
@@ -771,42 +970,93 @@ public partial class Form1 : Form
private (List<(string Codice, string Tipo, string Percorso)> Trovati, List<string> NonTrovati) RisolviAlberoComponenti(
IEnumerable<string> asmIniziali, IProgress<(int Fatti, int Totale)>? progress = null)
{
var daScansionare = new Queue<string>(asmIniziali);
var codiciIniziali = daScansionare.Select(f => Path.GetFileName(f).Split('.')[0]);
// Ogni assieme viaggia con la cartella della share da cui proviene: e' li' che Creo
// 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>(
codiciIniziali.Concat(codiciIniziali.Select(AssemblyComponentScanner.CodiceRadice)),
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 codiciNonTrovati = new List<string>();
if (daScansionare.Count == 0 || _dbPath is null)
return (trovati, codiciNonTrovati);
var codiciNoti = CaricaCodiciNoti(_dbPath);
var candidati = CaricaCandidati(_dbPath);
int processati = 0;
while (daScansionare.Count > 0)
{
var asm = daScansionare.Dequeue();
var (asm, cartellaOrigine, esterno) = daScansionare.Dequeue();
cartellaOrigine ??= CartellaDiOrigine(asm, candidati);
processati++;
foreach (var codice in EstraiCodiciReferenziati(asm, codiciNoti.Keys))
{
if (!codiciVisti.Add(codice))
continue;
if (!codiciNoti.TryGetValue(codice, out var componenti))
if (!candidati.TryGetValue(codice, out var perCodice))
{
codiciNonTrovati.Add(codice);
if (!esterno)
codiciNonTrovati.Add(codice);
continue;
}
foreach (var (tipo, percorso) in componenti)
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)));
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 +1104,6 @@ public partial class Form1 : Form
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.
/// 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
@@ -1021,21 +1175,19 @@ public partial class Form1 : Form
private static readonly string CartellaImpostazioni = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "PrepStep");
private static readonly string PercorsoImpostazioni = Path.Combine(CartellaImpostazioni, "ultimo_db.txt");
private static readonly string PercorsoImpostazioniCartella = Path.Combine(CartellaImpostazioni, "ultima_cartella_lavoro.txt");
/// <summary>Propone l'ultima cartella di lavoro usata (se ancora esiste su disco) o, se non
/// 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,
/// non dov'è installata la app). Richiede quindi che InizializzaDatabase sia già stata
/// chiamata. Sempre modificabile con "Cambia cartella..." in fase 1.</summary>
/// c'è ancora una preferenza salvata (o punta a una cartella cancellata), la cartella di
/// installazione (%LOCALAPPDATA%\Programs\PrepStep col setup). Sempre modificabile con
/// "File → Scegli cartella di lavoro...".</summary>
private void InizializzaCartellaLavoro()
{
var ultima = LeggiUltimaCartellaLavoro();
_cartellaLavoro = ultima is not null && Directory.Exists(ultima)
? ultima
: (_dbPath is not null ? Path.GetDirectoryName(_dbPath) : null) ?? AppContext.BaseDirectory;
AggiornaLabelCartellaLavoro();
: AppContext.BaseDirectory;
AggiornaStatoAvanti();
}
private void ScegliCartellaLavoro()
@@ -1050,13 +1202,10 @@ public partial class Form1 : Form
string percorso = dialog.SelectedPath;
_cartellaLavoro = percorso;
AggiornaLabelCartellaLavoro();
SalvaUltimaCartellaLavoro(percorso);
AggiornaStatoAvanti();
}
private void AggiornaLabelCartellaLavoro() => _lblCartellaLavoro.Text = $"Cartella di lavoro: {_cartellaLavoro}";
private static string? LeggiUltimaCartellaLavoro()
{
try
@@ -1084,24 +1233,21 @@ public partial class Form1 : Form
private void InizializzaDatabase()
{
var candidati = new[]
{
LeggiUltimoDbUsato(),
Path.Combine(AppContext.BaseDirectory, "cad_index.db"),
Path.Combine(Directory.GetCurrentDirectory(), "cad_index.db"),
};
_dbPath = candidati.FirstOrDefault(p => p is not null && File.Exists(p));
if (_dbPath is null)
// Il DB sta sempre nella cartella di installazione; "Apri DB..." ne carica un altro
// solo per la sessione corrente, senza ricordarlo al prossimo avvio.
string predefinito = Path.Combine(AppContext.BaseDirectory, "cad_index.db");
if (!File.Exists(predefinito))
{
_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.";
_lblUltimoAggiornamento.Text = "";
_dbCaricato = false;
_indicatoreDb.Invalidate();
AggiornaStatoAvanti();
return;
}
CaricaDatabase(_dbPath);
CaricaDatabase(predefinito);
}
private void ScegliDatabase()
@@ -1134,43 +1280,17 @@ public partial class Form1 : Form
? $"Ultimo aggiornamento indice: {ultimaScansione.ToLocalTime():dd/MM/yyyy HH:mm}"
: "Ultimo aggiornamento indice: sconosciuto";
_lblStatus.Text = "Pronto.";
SalvaUltimoDbUsato(path);
}
catch (Exception ex)
{
MessageBox.Show(this, $"Impossibile aprire il database:\n{ex.Message}", "Errore",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
_dbCaricato = _dbPath is not null;
_indicatoreDb.Invalidate();
AggiornaStatoAvanti();
}
/// <summary>Ricorda l'ultimo DB caricato con successo, così al prossimo avvio si
/// ricarica automaticamente anche se l'eseguibile gira da una cartella diversa.</summary>
private static string? LeggiUltimoDbUsato()
{
try
{
return File.Exists(PercorsoImpostazioni) ? File.ReadAllText(PercorsoImpostazioni).Trim() : null;
}
catch (IOException)
{
return null;
}
}
private static void SalvaUltimoDbUsato(string path)
{
try
{
Directory.CreateDirectory(CartellaImpostazioni);
File.WriteAllText(PercorsoImpostazioni, path);
}
catch (IOException)
{
// ponytail: non critico se non si riesce a salvare, si torna a chiedere "Apri DB..." la prossima volta
}
}
private async void AggiornaIndice()
{
string dbPath = _dbPath ?? Path.Combine(AppContext.BaseDirectory, "cad_index.db");
+349
View File
@@ -0,0 +1,349 @@
using System.Runtime.InteropServices;
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",
"PrepStep - Cevolani 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 lo stato di distinta/raw/verifica per ripartire " +
"con una nuova distinta base, senza dover ricaricare il database."),
new("Dove finiscono i file",
"Tutto sta nella cartella di lavoro, che di default è la cartella di " +
"installazione (%LOCALAPPDATA%\\Programs\\PrepStep):",
Passi:
[
"Il database cad_index.db sta sempre nella cartella di installazione.",
"Il CSV di ogni distinta importata, \"raw\" (file CAD delle fasi 3-4) e " +
"\"step\" (STEP esportati da Creo, cartella che indichi tu a Creo) stanno " +
"nella cartella di lavoro. Il file .xls del gestionale resta dove l'hai " +
"preso.",
"Per cambiare la cartella di lavoro: \"File ▸ Scegli cartella di lavoro...\" " +
"(viene ricordata agli avvii successivi).",
"Per fare pulizia: \"File ▸ Pulisci raw e step...\" svuota \"raw\" e " +
"\"step\"; \"File ▸ Elimina CSV delle distinte...\" elimina i CSV, che " +
"altrimenti si accumulano uno per distinta. Nessuno dei due tocca il " +
"database.",
],
NotaTesto: "AppData è una cartella nascosta in Esplora file: per aprirla scrivi " +
"%LOCALAPPDATA%\\Programs\\PrepStep nella barra degli indirizzi (vale anche " +
"per scegliere \"step\" in Creo). Disinstallando il programma questi file " +
"restano: cancellali a mano se non servono più."),
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.",
"Se non vuoi usare la cartella di installazione, scegli la cartella di " +
"lavoro (\"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",
"All'avvio l'app carica da sola \"cad_index.db\" dalla cartella di installazione. " +
"Un pallino verde/rosso indica se un database è caricato.",
Passi:
[
"Pallino rosso (nessun database): premi \"Aggiorna indice\" per scansionare " +
"le cartelle di primo livello della share (X:\\) e crearlo. La prima volta " +
"richiede qualche minuto, i giri successivi aggiornano solo quello che è " +
"cambiato.",
"Database da un'altra postazione: \"Apri DB...\" lo carica solo finché non " +
"chiudi il programma. Per usarlo sempre, copialo nella cartella di " +
"installazione come cad_index.db.",
]),
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): svuota " +
"le griglie e la verifica delle fasi 2-5 e riporta al passo 2, senza dover " +
"ricaricare il database né cambiare la cartella di lavoro. Non cancella nessun " +
"file su disco: CSV, \"raw\" e \"step\" restano.",
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...\"; per eliminare i CSV che ogni distinta importata lascia nella " +
"cartella di lavoro, usa \"File ▸ Elimina CSV delle distinte...\"."),
};
public GuidaForm()
{
Text = "Guida";
ClientSize = new Size(1000, 680);
MinimumSize = new Size(640, 440);
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 TestoSolaLettura
{
Dock = DockStyle.Fill,
ReadOnly = true,
TabStop = false,
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;
}
/// <summary>RichTextBox da sola lettura che non si comporta come un editor: niente cursore
/// lampeggiante, niente cursore a "I" e niente selezione col mouse. I clic nell'area di testo
/// vengono ignorati (così non prende il fuoco e non mostra il caret); la barra di scorrimento
/// sta nell'area non-client e resta utilizzabile, la rotella scorre al passaggio del mouse.</summary>
private sealed class TestoSolaLettura : RichTextBox
{
private const int WM_SETFOCUS = 0x0007;
private const int WM_SETCURSOR = 0x0020;
private const int WM_LBUTTONDOWN = 0x0201;
private const int WM_LBUTTONDBLCLK = 0x0203;
private const int WM_RBUTTONDOWN = 0x0204;
[DllImport("user32.dll")]
private static extern bool HideCaret(IntPtr hWnd);
protected override void WndProc(ref Message m)
{
switch (m.Msg)
{
case WM_LBUTTONDOWN or WM_LBUTTONDBLCLK or WM_RBUTTONDOWN:
return;
case WM_SETCURSOR:
Cursor.Current = Cursors.Arrow;
m.Result = 1;
return;
}
base.WndProc(ref m);
if (m.Msg == WM_SETFOCUS)
HideCaret(Handle);
}
}
}
+11 -2
View File
@@ -40,13 +40,22 @@ public static class Indexer
CREATE INDEX IF NOT EXISTS idx_files_tipo ON files(tipo_file);
CREATE INDEX IF NOT EXISTS idx_files_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,
codice, tipo_file, versione, dimensione_byte, data_modifica, scansionato_il
FROM (
SELECT f.*, ROW_NUMBER() OVER (
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
FROM files f
) WHERE rn = 1;
+4 -3
View File
@@ -8,10 +8,11 @@
<ImplicitUsings>enable</ImplicitUsings>
<ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode>
<ApplicationIcon>app.ico</ApplicationIcon>
<AssemblyTitle>Cevolani StepPrep</AssemblyTitle>
<Product>Cevolani StepPrep</Product>
<AssemblyTitle>PrepStep - Cevolani</AssemblyTitle>
<Product>PrepStep - Cevolani</Product>
<Company>Cevolani</Company>
<Version>0.1.0</Version>
<Copyright>Copyright (c) 2026 Davide Grilli</Copyright>
<Version>1.0.0</Version>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
</PropertyGroup>
+20 -85
View File
@@ -2,23 +2,15 @@ namespace PrepStep;
public sealed class ProgressForm : Form
{
private readonly Label _lblTitolo = new();
private readonly Label _lblCartella = new();
private readonly Panel _barraTraccia = new();
private readonly Panel _barraRiempimento = new();
private readonly Label _lblPercentuale = new();
private readonly ProgressBar _bar = new();
private readonly Label _lblConteggio = new();
private readonly Label _lblDettaglio = new();
private readonly Label _lblEta = new();
private readonly Button _btnAnnulla = new();
private readonly Panel _separatore = new();
private readonly Panel _piePagina = new();
private readonly ToolTip _tipCartella = new();
private const int Margine = 24;
// 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);
private const int Margine = 16;
public event EventHandler? Annullato;
@@ -30,84 +22,42 @@ public sealed class ProgressForm : Form
MinimizeBox = false;
MaximizeBox = false;
ControlBox = false;
BackColor = Color.White;
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);
ClientSize = new Size(420, 190);
_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 "..."
// invece di sforare il bordo del dialog, che è FixedDialog e non si può allargare.
// Il testo completo resta disponibile nel tooltip (stesso pattern di Form1._lblImportStatus).
// invece di sforare il bordo del dialog, che è FixedDialog e non si può allargare. Il
// testo completo resta disponibile nel tooltip.
_lblCartella.AutoSize = false;
_lblCartella.AutoEllipsis = true;
_lblCartella.Size = new Size(ClientSize.Width - Margine * 2, 20);
_lblCartella.ForeColor = GrigioTesto;
// Barra piatta disegnata a mano (traccia grigia + riempimento) invece della ProgressBar
// nativa: quella di Windows è verde e in stile "a blocchi", stona con il blu del resto
// dell'app (UiStile.Primario). Larghezza = margine destro reale meno lo spazio per
// "100%" in grassetto, così barra+percentuale rispettano lo stesso margine destro del
// 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);
_bar.Location = new Point(Margine, 40);
_bar.Size = new Size(ClientSize.Width - Margine * 2, 24);
_bar.Minimum = 0;
_bar.Maximum = 100;
_lblPercentuale.AutoSize = true;
_lblPercentuale.Font = new Font(Font, FontStyle.Bold);
_lblPercentuale.TextAlign = ContentAlignment.MiddleRight;
_lblConteggio.Location = new Point(Margine, 122);
_lblConteggio.Location = new Point(Margine, 72);
_lblConteggio.AutoSize = true;
_lblDettaglio.Location = new Point(Margine, 146);
_lblDettaglio.Location = new Point(Margine, 96);
_lblDettaglio.AutoSize = true;
_lblDettaglio.ForeColor = GrigioTesto;
_lblEta.Location = new Point(Margine, 174);
_lblEta.Location = new Point(Margine, 120);
_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";
UiStile.Secondario(_btnAnnulla);
UiStile.Arrotonda(_btnAnnulla);
_btnAnnulla.Size = new Size(90, 28);
_btnAnnulla.Click += (_, e) => Annullato?.Invoke(this, e);
_piePagina.Controls.Add(_btnAnnulla);
Controls.AddRange(new Control[]
{
_lblTitolo, _lblCartella, _barraTraccia, _lblPercentuale,
_lblConteggio, _lblDettaglio, _lblEta, _separatore, _piePagina
_lblCartella, _bar, _lblConteggio, _lblDettaglio, _lblEta, _btnAnnulla
});
// Posizionato dopo l'aggiunta ai Controls: con AutoSize il Width vero si conosce
// solo a layout avvenuto, non subito dopo la creazione del bottone.
_btnAnnulla.Location = new Point(_piePagina.Width - Margine - _btnAnnulla.Width,
(_piePagina.Height - _btnAnnulla.Height) / 2);
_btnAnnulla.Anchor = AnchorStyles.Right | AnchorStyles.Top;
_btnAnnulla.Location = new Point(
ClientSize.Width - Margine - _btnAnnulla.Width, ClientSize.Height - Margine - _btnAnnulla.Height);
}
public void Aggiorna(ScanProgress p)
@@ -115,24 +65,9 @@ public sealed class ProgressForm : Form
_lblCartella.Text = $"Scansione: {p.Cartella}";
_tipCartella.SetToolTip(_lblCartella, _lblCartella.Text);
if (p.PrevistiTotali > 0)
{
double frazione = Math.Min(1.0, (double)p.FattiTotali / p.PrevistiTotali);
_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);
_bar.Value = p.PrevistiTotali > 0
? (int)Math.Clamp(100.0 * p.FattiTotali / p.PrevistiTotali, 0, 100)
: 0;
_lblConteggio.Text = p.PrevistiTotali > 0
? $"{p.FattiTotali:N0} / ~{p.PrevistiTotali:N0} file"
+25 -3
View File
@@ -15,11 +15,20 @@ public class PrtMancantiForm : Form
ClientSize = new Size(720, 440);
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 lblInfo = new Label
{
Dock = DockStyle.Top,
AutoSize = true,
MaximumSize = new Size(ClientSize.Width - 24, 0),
Padding = new Padding(12, 8, 12, 8),
Text = $"Copiati in \"{cartellaRaw}\": {numPrt} PRT e {numAsm} sotto-assiemi trovati nell'indice."
+ (codiciNonTrovati.Count > 0
@@ -55,8 +64,17 @@ public class PrtMancantiForm : Form
grid.Rows[i].DefaultCellStyle.ForeColor = Color.DarkRed;
}
var btnApriCartella = new Button { Text = "Apri cartella raw", Dock = DockStyle.Bottom, Height = 34 };
UiStile.Secondario(btnApriCartella);
var (pannelloAzioni, flowAzioni) = UiStile.FloatBar();
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 += (_, _) =>
{
try { Process.Start("explorer.exe", cartellaRaw); }
@@ -66,9 +84,13 @@ public class PrtMancantiForm : Form
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
};
flowAzioni.Controls.Add(btnApriCartella);
Controls.Add(grid);
Controls.Add(lblInfo);
Controls.Add(btnApriCartella);
Controls.Add(pannelloAzioni);
CancelButton = btnChiudi;
Resize += (_, _) => lblInfo.MaximumSize = new Size(ClientSize.Width - 24, 0);
}
}
-61
View File
@@ -1,61 +0,0 @@
namespace PrepStep;
/// <summary>Mostra, per ogni assieme della distinta base, se tutti i suoi componenti
/// (ricorsivamente fino ai PRT) sono presenti nell'indice (v) o mancano (x).</summary>
public class ReportCompletezzaForm : Form
{
public ReportCompletezzaForm(List<(string Componente, string Descrizione, bool? Completo)> righe)
{
int completi = righe.Count(r => r.Completo == true);
int incompleti = righe.Count(r => r.Completo == false);
int nonTrovati = righe.Count(r => r.Completo is null);
Text = "Completezza assiemi";
ClientSize = new Size(720, 440);
StartPosition = FormStartPosition.CenterParent;
Font = new Font("Segoe UI", 9.5f);
var lblInfo = new Label
{
Dock = DockStyle.Top,
AutoSize = true,
Padding = new Padding(12, 8, 12, 8),
Text = $"{completi} completi, {incompleti} incompleti, {nonTrovati} non trovati nell'indice "
+ $"su {righe.Count} componenti. Nota: la scansione dei .asm è euristica, qualche "
+ "falso 'incompleto' è possibile.",
};
var grid = new DataGridView
{
Dock = DockStyle.Fill,
ReadOnly = true,
AllowUserToAddRows = false,
AllowUserToDeleteRows = false,
RowHeadersVisible = false,
SelectionMode = DataGridViewSelectionMode.FullRowSelect,
AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill,
};
grid.Columns.Add("componente", "Componente");
grid.Columns.Add("descrizione", "Descrizione");
grid.Columns.Add("completo", "Completo");
grid.Columns["componente"]!.FillWeight = 20;
grid.Columns["descrizione"]!.FillWeight = 55;
grid.Columns["completo"]!.FillWeight = 25;
foreach (var (componente, descrizione, completo) in righe.OrderBy(r => r.Completo).ThenBy(r => r.Componente))
{
string testo = completo switch { true => "✓", false => "✗", null => "?" };
int i = grid.Rows.Add(componente, descrizione, testo);
grid.Rows[i].DefaultCellStyle.ForeColor = completo switch
{
true => Color.DarkGreen,
false => Color.DarkRed,
null => Color.Gray,
};
grid.Rows[i].Cells["completo"].Style.Alignment = DataGridViewContentAlignment.MiddleCenter;
}
Controls.Add(grid);
Controls.Add(lblInfo);
}
}
+111 -12
View File
@@ -8,9 +8,16 @@ namespace PrepStep;
internal static class UiStile
{
/// <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);
/// <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
// 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.
@@ -47,22 +54,38 @@ internal static class UiStile
b.Cursor = Cursors.Hand;
}
/// <summary>Arrotonda gli angoli del bottone. 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(Button b, int raggio = 6)
/// <summary>Come Secondario, ma pieno invece che a bordo: sulle card bianche delle floatbar
/// (vedi FloatBar) il bordo sottile di Secondario è quasi invisibile su sfondo bianco, e il
/// bottone risulterebbe visivamente diverso dal Primario pieno accanto — stessa costruzione
/// (nessun bordo, riempimento pieno), solo grigio chiaro invece che blu.</summary>
public static void SecondarioPieno(Button b)
{
b.SizeChanged += (_, _) => AggiornaRegione(b, raggio);
AggiornaRegione(b, raggio);
DimensionamentoAutomatico(b);
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;
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();
percorso.AddArc(rect.X, rect.Y, d, d, 180, 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.CloseFigure();
b.Region?.Dispose();
b.Region = new Region(percorso);
c.Region?.Dispose();
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);
}
}
+78 -24
View File
@@ -1,4 +1,4 @@
# Cevolani StepPrep
# PrepStep - Cevolani
App Windows Forms (.NET 10, progetto `PrepStep`) per indicizzare, cercare e
recuperare i file CAD Pro-ENGINEER/Creo di un disco/share di rete, e per trasformare
@@ -9,32 +9,39 @@ passaggio, non lo sostituisce.
## Funzionalità
1. **Indice CAD** — all'avvio carica automaticamente `cad_index.db` (se
presente accanto all'eseguibile). Se manca, si può selezionare un DB
esistente con **Apri DB...** o generarne uno nuovo con **Aggiorna
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
la **cartella di lavoro**: dove finiscono il CSV e la cartella `raw/` (gli STEP
esportati con Creo vanno in `step/`, dentro la stessa cartella). Proposta
automaticamente (ricordata da un avvio all'altro), modificabile con
**Cambia cartella...**.
1. **Indice CAD** — all'avvio carica automaticamente `cad_index.db` dalla
cartella di installazione (`%LOCALAPPDATA%\Programs\PrepStep`). Se manca, lo si
crea con **Aggiorna indice**, oppure si apre un altro DB per la sola sessione
con **Apri DB...**. **Aggiorna indice** scansiona il disco (di default `X:\`,
tutte le cartelle di primo livello che trova) e indicizza tutti i file. La
**cartella di lavoro** (dove finiscono il CSV e la cartella `raw/`; gli STEP
esportati con Creo vanno in `step/`, dentro la stessa cartella) è di default la
cartella di installazione, modificabile in qualunque momento da **File → Scegli
cartella di lavoro...** (la scelta viene ricordata). Dal menu **File** si
ripuliscono anche `raw/`/`step/` e i CSV accumulati.
2. **Importa Distinta Base** — trascina (o scegli da file) l'estrazione
`.xls`/`.xml` del gestionale (foglio "Distinta Base", colonne
Componente/Descrizione): viene generato un CSV nella cartella di lavoro, con
anteprima in griglia. Se il file segue la convenzione di nome
`DBP_<codice>_<timestamp>` del gestionale, l'assieme generale dell'estrazione
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
componente importato (assieme generale incluso) e copia i file trovati
in una cartella `raw/` dentro la cartella di lavoro.
4. **Trova componenti referenziati** — legge i metadati dei file `.asm` in
`raw/` per trovare ricorsivamente i sotto-assiemi/parti che li
compongono, copiandoli a loro volta in `raw/`.
5. **Verifica completezza assiemi** — confronta la distinta con l'indice per
capire se manca qualche sotto-componente, e mostra i file `.asm` della
distinta trascinabili su Creo/Esplora risorse per l'esportazione STEP con
Creo Distributed Batch.
compongono, copiandoli a loro volta in `raw/` (passo opzionale se la
distinta contiene solo parti singole).
5. **Verifica completezza** — mostra i file `.asm` della distinta già in `raw/`,
trascinabili su Creo Distributed Batch per l'esportazione STEP (passo
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
@@ -80,18 +87,59 @@ L'eseguibile risultante è in `PrepStep\publish\PrepStep.exe` — un unico
file, si copia e si lancia su un'altra macchina Windows senza bisogno di .NET
installato.
## Setup di installazione
Per distribuire l'app agli utenti si genera un setup con
[Inno Setup 6](https://jrsoftware.org/isinfo.php).
Una volta sola, per installare Inno Setup (per l'utente corrente, senza admin):
```powershell
winget install --id JRSoftware.InnoSetup -e --scope user
```
A ogni release, dalla root del repo:
```powershell
# 1. aggiorna <Version> in PrepStep\PrepStep.csproj e la voce in CHANGELOG.md
# 2. chiudi l'app se è aperta (l'exe in uso blocca la build)
dotnet publish PrepStep -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -o PrepStep\publish
& "$env:LOCALAPPDATA\Programs\Inno Setup 6\ISCC.exe" installer\PrepStep.iss
```
Produce `installer\Output\PrepStep-Setup-<versione>.exe` (versione letta dall'exe, cioè
`<Version>` del csproj): è l'unico file da distribuire. Per provarlo sul proprio PC basta
lanciarlo (`.\installer\Output\PrepStep-Setup-<versione>.exe`). Il setup:
- installa **solo per l'utente corrente**, in `%LOCALAPPDATA%\Programs\PrepStep`, senza
chiedere i diritti di amministratore;
- crea nel menu Start i collegamenti al programma e al disinstallatore (quello sul desktop
è facoltativo);
- compare in "Impostazioni → App installate", da cui si disinstalla.
Se l'app è già installata, il setup chiede prima cosa fare: **Sì** aggiorna/reinstalla (DB e
impostazioni restano), **No** disinstalla, **Annulla** esce. Installazione silenziosa (aggiorna
senza chiedere) — es. per distribuirla da script:
`PrepStep-Setup-<versione>.exe /VERYSILENT /SUPPRESSMSGBOXES`.
## Struttura del progetto
Tutto il codice è in `PrepStep/` (unico progetto nella solution, GUI e
indicizzazione integrate):
Il codice dell'app è tutto in `PrepStep/` (GUI e indicizzazione integrate); accanto ci sono
il progetto di test `PrepStep.Tests/`, gli script standalone `scripts/` e lo script del setup
`installer/`. Mappa file-per-file completa in [`docs/architettura.md`](docs/architettura.md).
- `Program.cs` — entry point.
- `Form1.cs` — finestra principale (indice CAD, import Distinta Base, griglia
risultati); UI costruita interamente in codice, nessun designer visuale.
- `Form1.cs` — finestra principale, wizard a 5 fasi e menu; UI costruita interamente in
codice (`Form1.Designer.cs` è solo lo scheletro del template).
- `Indexer.cs` — scansione del disco e scrittura/aggiornamento del DB SQLite.
- `DistintaBaseImporter.cs` — parsing del file Distinta Base (SpreadsheetML
XML o Excel binario/OpenXML tramite ExcelDataReader) e generazione CSV.
- `ProgressForm.cs` — dialog di avanzamento durante l'aggiornamento indice.
- `AsmRecordReader.cs` — lettura esatta dei record dei `.asm` (fase 4).
- `AssemblyComponentScanner.cs` — euristica testuale di fallback per i `.asm` fuori formato.
- `ProgressForm.cs`, `ProgressoComponentiForm.cs` — avanzamento di scansione e fase 4.
- `PrtMancantiForm.cs` — esito di "Trova componenti referenziati".
- `GuidaForm.cs` — guida in-app (Aiuto → Guida, F1).
- `UiStile.cs` — stile condiviso di bottoni, card e barre.
Il database generato (`cad_index.db`), i file Excel/CSV della distinta
trascinati nella root e le cartelle `raw/`/`step/` sono rigenerabili e non
@@ -99,12 +147,18 @@ sono tracciati in git (vedi `.gitignore`).
## Test
`PrepStep.Tests/` contiene unit test (regex di estrazione dei componenti
dai `.asm`, parsing del nome file Pro-E) e integration test (schema/vista SQL
reale, con focus sui bug di case-sensitivity/duplicati già risolti). Da
`PrepStep.Tests/` (xUnit) contiene unit test (lettore esatto e euristica di
estrazione dei componenti dai `.asm`, parsing del nome file Pro-E, import della
distinta) e integration test (scansione su disco, schema/vista SQL reale con focus
sui bug di case-sensitivity/duplicati già risolti, pipeline end-to-end). Da
eseguire:
```powershell
cd PrepStep.Tests
dotnet test
```
## Licenza
Software proprietario, © 2026 Davide Grilli, tutti i diritti riservati: vedi
[`LICENSE`](LICENSE).
+173 -37
View File
@@ -1,10 +1,11 @@
# Architettura di PrepStep (Cevolani StepPrep)
# Architettura di PrepStep
Panoramica di tutti i file sorgente e di come collaborano. `PrepStep` è il nome
tecnico del progetto (cartella, namespace, csproj, eseguibile `PrepStep.exe`);
**Cevolani StepPrep** è il nome mostrato all'utente finale (titolo finestra, icona,
**PrepStep - Cevolani** è 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
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`.
## Vista d'insieme
@@ -24,6 +25,57 @@ Distinta (.xls/.xml) → CSV (componente;descrizione) → raw/ (CAD copiati
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. La **cartella di lavoro**, che da qui in poi ospita tutto il resto, non si sceglie qui
ma da "File → Scegli cartella di lavoro..." (default: cartella di installazione).
**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
- **Program.cs** — `Main()` standard WinForms: `Application.Run(new Form1())`. Nessuna logica.
@@ -41,26 +93,42 @@ Distinta (.xls/.xml) → CSV (componente;descrizione) → raw/ (CAD copiati
| # | 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 |
| 2 | Importa Distinta Base | selezione file → `ImportaDistinta` → `DistintaBaseImporter.ConvertiInCsv` | Xls/Xml gestionale → CSV componente;descrizione, scritto nella 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/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 |
| 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/` |
| 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) |
| 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 | (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`,
`_percorsoInputCorrente`, `_righeCorrenti` (righe del CSV), `_ultimaVerifica` (cache percorsi
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); "File → Elimina CSV delle
distinte..." elimina i `*.csv` direttamente nella cartella di lavoro (`EliminaCsv`); "Aiuto →
Guida" (F1) apre `GuidaForm`, sintesi interattiva delle 5 fasi per chi non ha letto questi
docs.
Altre responsabilità in Form1.cs:
- `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
componenti sia da `AssemblyComponentScanner` per validare i token generici).
- `TrovaPercorsiCad` / `CaricaCodiciNoti` — query sul DB (`files_ultima_versione`): la
prima risolve i componenti del CSV (fase 3), la seconda carica tutti i codici prt/asm noti
in un colpo solo, usata in fase 4 solo per validare i token generici di
`AssemblyComponentScanner`.
- `CaricaCandidati` — in fase 4, tutte le copie di ogni codice dalla tabella `files` (non
dalla vista), da cui `ScegliPerCartella` sceglie quella da copiare in `raw/`.
- `CopiaFileTrovati` — copia (in parallelo, grado 6) i file trovati in `raw/`, saltando
silenziosamente quelli irraggiungibili (share offline/permessi).
- `PulisciRawEStep` — svuota `raw/` e `step/` nella cartella di lavoro (manutenzione,
richiede conferma).
- `InizializzaDatabase` / `ScegliDatabase` — localizzazione del DB (ultimo usato, salvato in
`%AppData%/PrepStep/ultimo_db.txt`, o `cad_index.db` accanto all'eseguibile).
- `EliminaCsv` — elimina i `*.csv` generati dall'import (uno per distinta) nella cartella di
lavoro, solo al primo livello; gli `.xls` del gestionale restano (richiede conferma, con
l'elenco dei file).
- `InizializzaDatabase` / `ScegliDatabase` — localizzazione del DB (sempre `cad_index.db` accanto
all'eseguibile; "Apri DB..." ne carica un altro solo per la sessione).
- `InizializzaCartellaLavoro` / `ScegliCartellaLavoro` — stesso pattern per la cartella di
lavoro (`ultima_cartella_lavoro.txt`), fallback `AppContext.BaseDirectory` se non c'è
ancora nulla di salvato: nessuna dipendenza da un marker di repo, funziona identica
@@ -81,8 +149,7 @@ Distinta (.xls/.xml) → CSV (componente;descrizione) → raw/ (CAD copiati
## Lettura dei componenti referenziati in un `.asm`
`Form1.EstraiCodiciReferenziati` (usata da `RisolviAlberoComponenti`, fase 4, e da
`AssiemeCompleto` dentro `VerificaCompletezzaAssiemi`, fase 5) prova prima
`Form1.EstraiCodiciReferenziati` (usata da `RisolviAlberoComponenti`, fase 4) prova prima
`AsmRecordReader.EstraiCodiciReferenziati` e ripiega su
`AssemblyComponentScanner.EstraiCodiciReferenziati` solo se il primo lancia
`InvalidDataException` (file che non rispetta il formato validato dal reverse engineering).
@@ -90,8 +157,12 @@ Distinta (.xls/.xml) → CSV (componente;descrizione) → raw/ (CAD copiati
- **AsmRecordReader.cs** — legge esattamente la sezione `members` del binario `.asm` (id/tipo/
nome dei componenti diretti, con risoluzione del nome istanza di family table dal parametro
`ConfigName`): nessuna euristica, struttura a record. Lancia `InvalidDataException` se il
file non ha una sezione `members` o se il conteggio dichiarato non torna. Dettagli in
`docs/formato-asm-record.md`. Condivisa con `scripts/CreoBom.cs` via `#:project`.
file non ha una sezione `members` o se il conteggio dichiarato non torna. Espone anche le
due liste "non di distinta": `EstraiCodiciSoppressi` (che passa da `members` per sapere
quali codici sono già noti, quindi può lanciare la stessa eccezione — `Form1.CodiciSoppressi`
la intercetta e restituisce vuoto) e `EstraiCodiciRiferimentiEsterni` (scansione a sé, non
lancia). Dettagli in `docs/formato-asm-record.md`. Condivisa con `scripts/CreoBom.cs` via
`#:project`.
- **AssemblyComponentScanner.cs** — estrae euristicamente i codici componente referenziati
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
@@ -100,6 +171,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
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
- **DistintaBaseImporter.cs** — converte l'export del gestionale (SpreadsheetML XML con
@@ -107,8 +201,8 @@ Distinta (.xls/.xml) → CSV (componente;descrizione) → raw/ (CAD copiati
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
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
file di input.
passata da `Form1` (la cartella di lavoro, impostabile da "File → Scegli cartella di lavoro..."),
non necessariamente accanto al file di input.
## Finestre di dialogo (modali, mostrate a fine step)
@@ -125,9 +219,22 @@ Distinta (.xls/.xml) → CSV (componente;descrizione) → raw/ (CAD copiati
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
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
`SizeChanged` (non una volta sola: con `AutoSize` la dimensione finale del bottone si
conosce solo a layout avvenuto), usata sui bottoni "Avanti"/"Indietro" del wizard.
larghezza scelta). `SecondarioPieno` è la variante senza bordo (sfondo grigio chiaro
pieno) per i bottoni secondari sopra le card bianche di `Card`/`FloatBar`, dove il
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
dell'exe in Esplora risorse/taskbar) e riletta esplicitamente in `Form1.BuildUi()`
con `Icon.ExtractAssociatedIcon(Application.ExecutablePath)` per la titlebar, senza
@@ -137,21 +244,45 @@ Distinta (.xls/.xml) → CSV (componente;descrizione) → raw/ (CAD copiati
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
positivi della scansione `.asm`).
- **ReportCompletezzaForm.cs** — per ogni assieme della distinta, se tutti i suoi componenti
(ricorsivamente fino ai PRT) sono nell'indice (✓/✗/? per "non trovato").
- **GuidaForm.cs** — guida interattiva ("Aiuto → Guida"/F1): elenco delle sezioni a sinistra
(Panoramica, Dove finiscono i file, Prima di iniziare, le 5 fasi, Ripartire da capo), testo
a destra; stessa guida di `docs/guida-utente.md`, il dettaglio tecnico resta solo in questi
docs.
## Persistenza
- **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
selezionabile con "Apri DB...". Radice disco di scansione di default `X:\`
(`Form1.RadiceDiscoDefault`, hardcoded).
- **Cartella di lavoro** — salvata in `%AppData%/PrepStep/ultima_cartella_lavoro.txt`
(stesso meccanismo del DB sopra), proposta di default `AppContext.BaseDirectory` la prima
volta; selezionabile/cambiabile con "Cambia cartella..." in fase 1. 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.
Sempre `cad_index.db` accanto all'eseguibile (`AppContext.BaseDirectory`, cioè la cartella
di installazione): `InizializzaDatabase` carica solo quello, e "Aggiorna indice" lo crea lì
se manca. "Apri DB..." carica un altro file solo per la sessione corrente, senza
ricordarlo. Radice disco di scansione
di default `X:\` (`Form1.RadiceDiscoDefault`, hardcoded).
- **Cartella di lavoro** — ricordata tra un avvio e l'altro in
`%AppData%/PrepStep/ultima_cartella_lavoro.txt`; se non c'è una preferenza salvata (o punta
a una cartella cancellata) si propone la cartella di installazione
(`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.
## Installazione
`installer/PrepStep.iss` (Inno Setup 6) impacchetta `PrepStep/publish/PrepStep.exe` in un
setup **per-utente** (`PrivilegesRequired=lowest`, quindi `{autopf}` =
`%LOCALAPPDATA%\Programs`, cartella `PrepStep`): niente admin, e la cartella resta scrivibile —
cosa che serve, perché `cad_index.db` e (di default) la cartella di lavoro con `raw/`
stanno accanto all'eseguibile (`AppContext.BaseDirectory`, vedi Persistenza). In `C:\Program Files` fallirebbe per un utente
normale. Versione del setup letta dall'`exe` (ProductVersion, tolto il `+<commit>`), quindi
basta cambiare `<Version>` nel csproj. L'`AppId` fisso fa riconoscere a Windows le versioni
nuove come aggiornamento della stessa app: non va cambiato
(`AppGuid` nello script). Se l'app risulta già installata (chiave `…\Uninstall\{AppGuid}_is1`
in HKCU), `InitializeSetup` nella sezione `[Code]` chiede se aggiornare o disinstallare (lancia
l'`UninstallString`), perché Inno da solo aggiornerebbe sempre; con `/VERYSILENT` non chiede.
La disinstallazione rimuove solo
i file installati: `cad_index.db`, `raw/`/`step/`/CSV creati accanto all'exe e
`%AppData%/PrepStep/` restano.
Comandi di build del setup nel `README.md`.
## Script standalone (`scripts/`)
@@ -159,11 +290,11 @@ C# file-based apps (.NET 10, `dotnet run scripts/Nome.cs -- arg1 arg2`, nessun `
dipendenze extra con `#:package Nome@versione` in testa al file):
- **ExtractCreoStrings.cs** — BOM di un `.asm` risolvendo ricorsivamente i sotto-assiemi
contro `cad_index.db` (stessa logica di `AssiemeCompleto` in Form1, solo codici numerici
contro `cad_index.db` con `AssemblyComponentScanner` (via `#:project`, solo codici numerici
Pro-E).
- **BomViaDb.cs** — come sopra ma con la tokenizzazione generica validata contro il DB, per
intercettare anche i nomi alfabetici delle parti commerciali.
- **CreoBom.cs** — usa `AsmRecordReader` (stesso lettore esatto della GUI, fase 4/5, via
- **CreoBom.cs** — usa `AsmRecordReader` (stesso lettore esatto della GUI, fase 4, via
`#:project`) invece dell'euristica testuale. Risolve i sottoassiemi cercandoli in cartelle
locali (`-p`), non contro `cad_index.db` come `ExtractCreoStrings.cs`/`BomViaDb.cs`.
- **Analyze.cs** — dump del contesto testo/hex attorno a un pattern in un file binario, per
@@ -181,6 +312,11 @@ dipendenze extra con `#:package Nome@versione` in testa al file):
- **PrepStep.Tests/** (xUnit) — riferimento al progetto principale via `ProjectReference`
+ `InternalsVisibleTo` (in `PrepStep.csproj`), così i test chiamano direttamente
`Indexer.ParseFilename` e `AssemblyComponentScanner.EstraiCodiciReferenziati` senza esporli
come API pubblica. File: `IndexerParseFilenameTests.cs`,
`AssemblyComponentScannerTests.cs`, `DistintaBaseImporterTests.cs`,
`CaseInsensitiveLookupIntegrationTests.cs`.
come API pubblica. File: `IndexerParseFilenameTests.cs`, `IndexerScanTests.cs`,
`AssemblyComponentScannerTests.cs`, `AsmRecordReaderTests.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.
+42 -26
View File
@@ -29,23 +29,42 @@ componenti e nel filtro per cartella).
Vista `files_ultima_versione`: stessa struttura di `files`, filtrata alla sola riga con
`versione` più alta per ogni `(cartella_radice, codice, tipo_file)`, tramite `ROW_NUMBER()
OVER (PARTITION BY ... ORDER BY versione DESC)`. Tutte le ricerche di componenti (`Form1.
TrovaPercorsiCad`, `Form1.CaricaCodiciNoti`) interrogano questa vista, non `files`
direttamente, così un componente con più versioni sulla share risolve sempre all'ultima.
Deliberatamente una `ROW_NUMBER()` invece di un self-join con `GROUP BY`: su ~200k righe un
self-join degenera in tempi quasi quadratici, `ROW_NUMBER()` resta lineare.
OVER (PARTITION BY ... ORDER BY versione DESC, data_modifica DESC, percorso_completo)`. Le
ricerche per codice (`Form1.TrovaPercorsiCad` in fase 3, `Form1.CaricaCodiciNoti`)
interrogano questa vista, non `files` direttamente, così un componente con più versioni sulla
share risolve sempre all'ultima. Fa eccezione la fase 4 (`Form1.CaricaCandidati`), che legge
`files` per avere tutte le copie di un codice in cartelle diverse e scegliere quella giusta
(`ScegliPerCartella`). Deliberatamente una `ROW_NUMBER()` invece di un self-join con
`GROUP BY`: su ~200k righe un self-join degenera in tempi quasi quadratici, `ROW_NUMBER()`
resta lineare.
La vista è ricreata (`DROP` + `CREATE`, non `CREATE IF NOT EXISTS`) a ogni "Aggiorna indice",
così un DB già esistente prende la definizione corrente senza dover reindicizzare la share.
Nota: `cartella_radice` fa parte della partition, quindi se lo stesso codice+tipo esiste
in **due cartelle radice diverse** (duplicati sulla share), la vista restituisce una riga per
ciascuna — non una sola in assoluto. `TrovaPercorsiCad` gestisce questo caso deduplicando per
`tipo_file` lato C# (vedi sotto).
> **I criteri di pareggio non sono decorativi.** Lo stesso codice può esistere **nella stessa
> cartella radice, con la stessa `versione`**, in due sottocartelle diverse (una copia in una
> cartella di studio, di lavoro o temporanea). Senza `data_modifica` nell'`ORDER BY` il
> pareggio veniva rotto in modo arbitrario, e poteva vincere la copia obsoleta — nascondendo
> del tutto quella buona, che non compariva più nella vista. Il file che finiva in `raw/` era
> allora un modello diverso da quello che l'assieme si aspetta: Creo lo carica, i vincoli di
> accoppiamento non si risolvono e **l'assieme non si apre affatto**.
>
> Il criterio deterministico però non basta a scegliere *bene*: è il minimo per non sbagliare
> a caso. Quale copia sia quella giusta dipende da chi la referenzia, non dalla data — vedi
> [`formato-asm-record.md`](formato-asm-record.md) §7.4 e `Form1.ScegliPerCartella`.
## Come viene popolato: `Indexer.ScanAsync`
Chiamato da "Aggiorna indice" (fase 1) e riutilizza lo stesso `dbPath` già caricato.
1. Crea lo schema se non esiste (`CREATE TABLE/VIEW IF NOT EXISTS`) — quindi funziona sia su
un DB vuoto (prima esecuzione) sia su uno esistente (aggiornamento incrementale).
1. Crea lo schema se non esiste (`CREATE TABLE/INDEX IF NOT EXISTS`; la vista invece è
`DROP`+`CREATE` a ogni scan) — quindi funziona sia su un DB vuoto (prima esecuzione) sia su
uno esistente (aggiornamento incrementale).
2. Cancella subito tutte le righe con `cartella_radice LIKE 'temp\_%'`: le cartelle
`temp_paris`, `temp_davide`, `temp_studi esterni`, ecc. sono lavori personali/temporanei che
possono contenere copie datate con lo stesso codice numerico di file "ufficiali" altrove
@@ -93,24 +112,20 @@ sulla share, non tipicamente cercato dalla UI).
## Come l'app trova il DB all'avvio
`Form1.InizializzaDatabase` prova, in ordine, i seguenti candidati e usa il primo che esiste
su disco:
`Form1.InizializzaDatabase` carica sempre e solo `cad_index.db` accanto all'eseguibile
(`AppContext.BaseDirectory`: con il setup è `%LOCALAPPDATA%\Programs\PrepStep`). Nessun
percorso viene ricordato fra un avvio e l'altro.
1. l'ultimo DB caricato con successo, salvato in
`%AppData%/PrepStep/ultimo_db.txt` (`Form1.LeggiUltimoDbUsato`/`SalvaUltimoDbUsato`)
2. `cad_index.db` accanto all'eseguibile (`AppContext.BaseDirectory`)
3. `cad_index.db` nella working directory corrente
Se nessuno dei tre esiste, la fase 1 resta bloccata su "Database non trovato" finché l'utente
non preme "Apri DB..." (`Form1.ScegliDatabase`, file picker libero) o "Aggiorna indice"
(`Form1.AggiornaIndice`, che crea `cad_index.db` da zero accanto all'eseguibile se non c'è
ancora un `_dbPath`).
Se non esiste, la fase 1 resta bloccata su "Database non trovato" finché l'utente non preme
"Aggiorna indice" (`Form1.AggiornaIndice`, che crea `cad_index.db` da zero accanto
all'eseguibile) o "Apri DB..." (`Form1.ScegliDatabase`, file picker libero: il DB scelto vale
solo per la sessione corrente, e "Aggiorna indice" aggiorna quello).
**Attenzione in sviluppo (dotnet run / IDE):** `AppContext.BaseDirectory` è la cartella di
build (`PrepStep/bin/Release/net10.0-windows` o `Debug/...`), non la radice del repo. Se in
quella cartella finisce per errore una copia di `cad_index.db` (es. creata lì da una vecchia
sessione), l'app la userà invece di quella nella radice del repo, e le due copie divergeranno
a ogni "Aggiorna indice" fatto nell'uno o nell'altro ambiente. Se i risultati di una ricerca
build (`PrepStep/bin/Release/net10.0-windows` o `Debug/...`), non la radice del repo. L'app usa
quindi la copia di `cad_index.db` in quella cartella (creandola lì al primo "Aggiorna
indice"), non quella nella radice del repo né quella installata: per usare un altro DB in
sviluppo va aperto con "Apri DB..." a ogni avvio. Se i risultati di una ricerca
sembrano incoerenti con quello che ti aspetti di vedere sul DB "giusto", controlla con "Apri
DB..." quale file è effettivamente caricato (mostrato in fase 1 accanto al conteggio dei file
indicizzati) prima di sospettare un bug nella ricerca.
@@ -126,10 +141,11 @@ indicizzati) prima di sospettare un bug nella ricerca.
riga con quell'esatto `codice`. In presenza di duplicati su più `cartella_radice`, tiene solo
il primo per `tipo_file` (il più recente, dato l'`ORDER BY data_modifica DESC`) e scarta gli
altri.
- **Risoluzione codici noti per lo scanner assiemi** (`Form1.CaricaCodiciNoti`, usata sia dalla
GUI in fase 4 sia da `scripts/BomViaDb.cs`): carica in un'unica query tutti i codici
prt/asm noti con i relativi percorsi, invece di una query per codice — usata per validare i
token generici trovati nei binari `.asm` (vedi `docs/formato-asm.md`).
- **Codici noti per lo scanner assiemi** (`Form1.CaricaCodiciNoti`, fase 4;
`scripts/BomViaDb.cs` fa una query equivalente per conto suo): carica in un'unica query tutti
i codici prt/asm noti, invece di una query per codice — usata solo per validare i token
generici trovati nei binari `.asm` (vedi `docs/formato-asm.md`). I percorsi da copiare
vengono invece da `Form1.CaricaCandidati` (tabella `files`, vedi sopra).
## Debug ad-hoc
+278 -54
View File
@@ -8,16 +8,28 @@ l'euristica testuale ("codice che compare almeno due volte") descritta in
struttura effettiva dei record.
Implementazione: lettura in [`PrepStep/AsmRecordReader.cs`](../PrepStep/AsmRecordReader.cs)
(condivisa con la GUI, `Form1.EstraiCodiciReferenziati` in fase "Trova componenti
referenziati"/"Verifica completezza" - con fallback su `AssemblyComponentScanner` per i file
(condivisa con la GUI, `Form1.EstraiCodiciReferenziati` in fase 4 "Trova componenti
referenziati" - con fallback su `AssemblyComponentScanner` per i file
che non rispettano il formato validato), resa in formato BOM e ricorsione sui sottoassiemi
in [`scripts/CreoBom.cs`](../scripts/CreoBom.cs) (via `#:project`).
**Stato della validazione.** Durante il reverse engineering, il livello 1 estratto da 3
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,
parsavano con il conteggio dichiarato uguale a quello trovato. Tutto ciò che segue oltre
questo perimetro (§7) è segnalato esplicitamente come ipotesi non dimostrata.
**Stato della validazione.** Due livelli di prova, da non confondere:
1. **Byte-per-byte contro BOM Creo di riferimento** (la prova forte): 3 assiemi del
reverse engineering iniziale, più 5 casi reali aggiunti in seguito, ciascuno con la sua BOM
esportata da Creo messa a confronto riga per riga con l'output di `CreoBom.cs` —
`9284218024b.asm` (albero profondo, multi-livello), `2546220101.asm`/`2546220101_c04.asm.1`
(istanza di family table isolata), `2546220101.asm` risalvato con un componente soppresso,
`9244218080_c01.asm` (sottoassieme montato due volte), `9284208092_D110.asm` (revisione del
figlio). Ognuno di questi è citato per nome nella sezione del documento a cui si riferisce,
così resta verificabile quale affermazione poggia su quale file.
2. **Su tutti i 350 `.asm` reali di `raw/`**, solo conteggi interni e coerenza con l'euristica
testuale (`AssemblyComponentScanner`), **non** contro BOM di riferimento: usato per
calibrare le soglie dei controlli di sicurezza (§7) su scala, non per dimostrare la
correttezza dei valori estratti file per file.
Un'affermazione verificata solo al livello 2 (conteggi su scala) è più debole di una
verificata al livello 1 (BOM reale) - il testo lo distingue sempre esplicitamente.
---
@@ -79,7 +91,7 @@ conteggio dei membri: con la lettura a 1 byte fisso i campioni grandi si
disallineavano. Non è confermata da una specifica; regge sui campioni
disponibili al momento del reverse engineering.
In `CreoBom.cs` è `AsmRecordReader.AsInt`, applicata al testo decodificato Latin1 (ogni `char`
Nel codice è `AsmRecordReader.AsInt` (privata, in `PrepStep/AsmRecordReader.cs`), applicata al testo decodificato Latin1 (ogni `char`
della stringa corrisponde 1:1 a un byte, quindi indicizzare la stringa o l'array di byte
originale dà lo stesso offset).
@@ -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
iniziale.
### 4.0.2 Istanza risolta direttamente in `members`, senza passare da `ConfigName`
Non tutti i membri che sono istanze di family table hanno un record `ConfigName` associato.
Per alcune parti commerciali/normalizzate non-UNI, il campo `name` (e allo stesso modo
`family_name`) del record `members` (§3) porta già per intero la notazione
`<nome istanza><generico>` invece del solo nome generico:
```
\xe3 <id> <type> "<istanza><generico>"\0 "<istanza><generico>"\0
```
È la stessa notazione istanza→generico della §4.1 sotto, ma qui compare **dentro** il record
`members` stesso, come valore diretto del componente - non nell'elenco di riferimenti
geometrici di cui parla la §4.1 (quello sì da ignorare per la BOM). Il generico fra `<` e `>`
è il file realmente indicizzato sul disco; il nome prima di `<` è quello che la BOM
mostrerebbe.
`AsmRecordReader.LeggiMembri` lo riconosce per i membri **senza** un `ConfigName`
corrispondente il cui campo `name` rispetta per intero il pattern `<testo><testo>`: il testo
dopo l'ultima `<` (fino alla `>` finale) diventa il generico da cercare nell'indice, quello
prima diventa il nome mostrato. Senza questo split, il codice cercato nell'indice sarebbe la
stringa intera con le parentesi comprese, mai presente nell'indice - il componente
risulterebbe erroneamente "non trovato".
### 4.1 Una seconda pista, che NON va usata per la BOM
Altrove nel file compaiono stringhe nella forma `istanza<generico>`:
@@ -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
più volte (deduplica per nome, `CreoAsm.Blocchi`).
> La deduplica è un'**assunzione**: nessuno dei campioni disponibili durante il
> reverse engineering montava lo stesso sottoassieme due volte, quindi non è
> dimostrata su un caso reale.
> **Verificato.** `9244218080_C01.asm` monta `2605410010_C01` due volte
> (`scripts/9244218080_c01.asm.1` + `scripts/9244218080_c01.bom.2` di riferimento): la BOM
> Creo mostra un solo blocco `Sottoassieme 2605410010_C01 contiene:` con quantità 2 in testa,
> non due blocchi separati, e `CreoBom.cs` riproduce l'output esatto senza modifiche. Confermata
> anche la moltiplicazione delle quantità nel riepilogo (§6.3): le parti di `2605410010_C01`
> compaiono raddoppiate (1→2, 1→2, 16→32).
### 6.3 Riepilogo finale
@@ -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ù
alta** presente.
> Scelta discutibile: la revisione più alta in cartella non è
> necessariamente quella con cui l'assieme padre è stato salvato. Il `.asm`
> padre non sembra registrare la revisione attesa del figlio, o non è stata
> trovata. Vedi `formato-asm.md` §"Risoluzione ricorsiva dell'albero" per le
> trappole già incontrate storicamente su questo punto (con l'euristica
> testuale, ma il problema della revisione attesa è indipendente dal metodo
> di estrazione).
> **Verificato: è la scelta giusta.** Il `.asm` padre non registra la revisione attesa del
> figlio (cercata e non trovata, vedi §7), ma non serve: confermato che **Creo stesso apre
> sempre la revisione più alta disponibile** di un componente, indipendentemente da quale
> fosse presente quando l'assieme padre fu salvato l'ultima volta. Caso reale controllato:
> `9284208092_D110.asm` (salvato 2018-08-01) referenzia `9284202481.prt`, che sulla share ha
> revisioni sia del 2014 (v1-v3, presenti all'epoca) sia del 2024 (v4-v6, arrivate anni dopo) -
> Creo carica v6, non v3. `CreaFinder` che prende sempre la revisione più alta in cartella
> replica esattamente questo comportamento, non è un'assunzione azzardata. Vedi `formato-
> asm.md` §"Risoluzione ricorsiva dell'albero" per le trappole storiche su questo punto (con
> l'euristica testuale - restano valide, sono su un problema diverso: trovare la cartella
> giusta, non quale revisione preferire).
Se un sottoassieme non si trova, il blocco viene emesso con
`*** NON RISOLTO: file .asm non trovato ***` e le sue parti restano fuori dal
@@ -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.**
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
(nell'esempio, 3 cuscinetti + 1 sotto-assieme), 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 proprio contenuto potenzialmente diverso. Il nome
istanza qui **non** passa da un record `ConfigName` (verificato: nessuna occorrenza
letterale di `ConfigName` nel file) ma da un meccanismo diverso, imparentato con la
"seconda pista" del §4.1 (`\xe3<id>\xe2\x02<stringa>\0`, senza il bracket
`<generico>` in questo caso) - non decodificato. **`AsmRecordReader.LeggiMembri` non lo
rileva e non lancia eccezione**: restituisce silenziosamente il contenuto della
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
nessun modo al chiamante (né un conteggio che non torna, né un'eccezione). Scoperto sullo
stesso test end-to-end del punto `#AF` sopra, mai osservato negli 8 campioni originali.
**Prima di fidarsi del parser esatto su un albero con assiemi a family table, verificare a
mano contro la BOM Creo.**
- **Stato soppresso / escluso.** Nessuna evidenza cercata o trovata. Non si sa
se un componente soppresso compaia in `members`, né con quale marcatore. Sui
campioni disponibili il totale coincideva con la BOM, il che *suggerisce* che
non ce ne fossero, non che siano gestiti.
- **La matrice di posizionamento** (`e1`, `e2`, `e3`, `origin`): formato
numerico non decodificato.
- **`family_name`**: sempre uguale a `name` sui campioni; non si sa in quale
situazione divergerebbe.
- **La costante `\x82\x7b`** nei record `ConfigName`.
- **I campi numerici dell'header** dopo `ASSEMBLY`.
- **Occorrenze multiple di `members`.** `AsmRecordReader.LeggiMembri` prende la prima.
Sui campioni disponibili funziona, ma non è stato verificato se le
rappresentazioni semplificate (simplified rep) generino sezioni `members`
aggiuntive.
- **La deduplica dei blocchi** e **la revisione attesa dei figli** (§6.2, §6.4).
- **Quale revisione del figlio** il padre si aspetta: non trovata nel `.asm`.
| Punto | Stato | Prova |
|---|---|---|
| Deduplica blocchi + moltiplicazione quantità (§6.2/§6.3) | Risolto | BOM reale (§7.1) |
| Revisione del figlio che il padre si aspetta (§6.4) | Risolto | BOM reale (§7.1) |
| Stato soppresso/escluso in `members` | Risolto | BOM reale (§7.1) |
| Family table di **assiemi** (rilevamento) | Risolto: serve la conferma del nome proprio (`objname`), il solo `model_name` dava falsi positivi | BOM reali + albero `raw/` completo (§7.1) |
| Family table di **assiemi** (contenuto di ogni istanza) | Non risolto, nessun impatto sulla BOM (fallback già corretto) | Scan sull'intera share (§7.2) |
| Costante `\x82\x7b` in `ConfigName` | Significato ignoto, rischio coperto | Scala 350 file (§7.2) |
| Occorrenze multiple di `members` (simplified rep) | Mai osservato, rischio coperto | Scala 350 file (§7.2) |
| Matrice di posizionamento (`e1`/`e2`/`e3`/`origin`) | Non letto, nessun impatto sulla BOM | — (§7.2) |
| Campi numerici dell'header dopo `ASSEMBLY` | Non letto, nessun impatto sulla BOM | — (§7.2) |
| Divergenza `family_name` ≠ `name` | Mai osservata, non letta | — (§7.2) |
"Rischio coperto" vuol dire: il codice non sa cosa significhi il byte, ma verifica un
invariante che lo renderebbe visibile (eccezione) se mai smettesse di valere - non è una prova
che il byte non conti mai, è la garanzia che se contasse il programma lo segnalerebbe invece
di sbagliare in silenzio.
### 7.1 Risolti con una BOM Creo di riferimento
- **Deduplica blocchi e moltiplicazione quantità.** `9244218080_C01.asm` monta
`2605410010_C01` due volte (`scripts/9244218080_c01.asm.1` +
`scripts/9244218080_c01.bom.2`): la BOM Creo mostra un solo blocco
`Sottoassieme 2605410010_C01 contiene:` con quantità 2 in testa (non due blocchi separati),
e le sue parti compaiono raddoppiate nel riepilogo (1→2, 1→2, 16→32). `CreoBom.cs` riproduce
l'output esatto senza modifiche: la deduplica per nome (`CreoAsm.Blocchi`) e la
moltiplicazione lungo l'albero (`CreoAsm.Sommario`) sono il comportamento reale di Creo, non
solo un'assunzione.
- **Revisione del figlio.** `9284208092_D110.asm` (salvato 2018-08-01) referenzia
`9284202481.prt`, che sulla share ha revisioni sia del 2014 (v1-v3, presenti quando il padre
fu salvato) sia del 2024 (v4-v6, arrivate anni dopo). Confermato direttamente in Creo: apre
v6, non v3 - **sempre la revisione più alta disponibile**, indipendentemente da quale fosse
presente all'ultimo salvataggio del padre. `CreoAsm.CreaFinder`, che prende sempre la
revisione più alta trovata in cartella, replica esattamente questo comportamento.
- **Stato soppresso/escluso.** Stesso assieme `2546220101.asm` risalvato da Creo con
`2546220101_C02` soppresso (`scripts/2546220101.asm.1` + `scripts/2546220101.bom.1`, con le
parti/sottoassiemi correlati). Un componente soppresso **non produce alcun record** in
`members`: sparisce e basta, il conteggio dichiarato scende di conseguenza (`\xf8 03` invece
di `\xf8 04`). Nessun marcatore di stato da gestire: `CreoBom.cs` sul file risalvato
riproduce esattamente la BOM di riferimento senza modifiche al parser.
> **Sparisce da `members`, non dal file.** La feature che lo posiziona resta, e con essa la
> stringa `Componente ID <n> (<codice>.PRT)` (localizzata come in
> `AssemblyComponentScanner.HaPlacement`). Per la BOM va ignorata - Creo non elenca i
> soppressi - ma **il file di quel componente serve comunque per aprire l'assieme**: se manca,
> Creo fallisce il caricamento con "dati di input non completi" (`[-4]`, cioè
> `PRO_TK_E_NOT_FOUND`). Verificato su un caso reale: un assieme che non si apriva dalla
> cartella `raw/`, e che si è aperto senza errori copiandoci dentro quel solo file.
> `AsmRecordReader.EstraiCodiciSoppressi` li restituisce a parte, apposta per questo - vedi
> §7.3.
- **Family table di assiemi — rilevamento.** Non solo le parti normalizzate (§4) possono
essere family table: un intero **assieme** può esserlo. Osservato su `2546220101.asm`
(dentro l'albero di `9284218024b.asm`): il file generico ha una propria sezione `members`
che elenca il contenuto di UNA rappresentazione (3 cuscinetti + 1 sotto-assieme, dati
arbitrari), ma la BOM Creo per lo stesso file mostra 4 **istanze diverse**
(`2546220101_C01`.."_C04", tre "Parte" e una "Sottoassieme" - tipi diversi fra loro),
ciascuna con un contenuto diverso. Il nome istanza qui **non** passa da `ConfigName`
(verificato: nessuna occorrenza letterale nel file), ma da una coppia di record distinta
subito dopo l'header del modello:
```
\xe0\x01model_t_type\0<int>\xe0\x0amodel_name\0<codice>_C0n\0\xe0\x0ageneric_name\0\0
...stamp\0\xe2\xf2\xf7<idSchema>
(\xe3<type>\x02<codice>_C0n\0\0\xe2)* -- un'occorrenza per ogni istanza sorella
\xf4
```
Quando questo succede il file generico è già di per sé un'istanza (`model_name` porta il
suffisso `_C0n`, non è neutro) e `stamp` elenca le sorelle.
> **Il record `model_name` da solo non è un'identità.** Su file reali quel record nomina
> quasi sempre **un altro modello** - tipicamente uno dei membri del file stesso, a volte un
> codice imparentato - e in quei casi `members` è già corretto e va usato. Serve una
> conferma indipendente: il **nome proprio del modello** (`objname`, §5). `LeggiMembri`
> segnala la family table solo se `objname` esiste **e** coincide con `model_name` privato
> del suffisso `_C0n`; negli altri casi legge `members` normalmente.
>
> Il controllo più largo (senza la conferma di `objname`) era la causa di
> **falsi "componenti mancanti"**: il parser esatto rifiutava file perfettamente leggibili,
> si ripiegava sull'euristica testuale e quella estraeva codici che non esistono, riportati
> poi come componenti non trovati nell'indice. Verificato contro BOM Creo di riferimento:
> sui file dove il segnale scattava senza conferma di `objname`, `members` elenca
> **esattamente** i componenti della BOM reale (anche nello stesso ordine); uno di questi
> file è stato inoltre aperto in Creo, e l'albero del modello mostra gli stessi componenti,
> senza alcuna family table. Con la conferma di `objname` richiesta, tutti i `.asm` di una
> cartella `raw/` reale (351 file, alberi completi di più macchine) si leggono col parser
> esatto, zero ricadute sull'euristica.
Da tenere presente: il sintomo dipende da **come il file è stato salvato**, non solo dal
modello. Rileggendo una revisione più recente dello stesso assieme generico, il record di
identità può non esserci più e `members` elencare direttamente tutte le istanze, col tipo
giusto per ciascuna - cioè esattamente quello che mostra la BOM. Il blocco quindi non è una
proprietà stabile del codice: va valutato file per file, revisione per revisione.
Verificato anche che il fallback su `AssemblyComponentScanner` **risolve già correttamente**
il caso vero: i nomi delle istanze compaiono come testo puro nel campo `stamp`, e
l'euristica ("codice che compare almeno due volte") li trova senza bisogno di decodificare
`stamp`. E che una singola istanza, se esportata da Creo come file a sé (Family Table →
istanza → "Salva una copia"), è un `.asm`/`.prt` normale senza traccia di family table al suo
interno - letto correttamente dal parser esatto (`2546220101_c04.asm.1`, verificato contro
`2546220101.bom.1`).
### 7.2 Non risolti
- **Family table di assiemi — contenuto di ogni istanza.** Come recuperare il contenuto
specifico di un'istanza **senza** che esista già un file separato per essa sul disco (il
caso comune, se nessuno l'ha mai esportata a parte con "Salva una copia"): il meccanismo
dietro `stamp` non è stato decodificato oltre a nome+tipo di ciascuna istanza sorella:
`stamp` porta **solo i nomi** delle sorelle, non il loro contenuto, quindi non è la
struttura da cui partire per risolvere questo punto.
Resta aperto solo per completezza: sui file dove il segnale scatta davvero (con conferma
del nome proprio, §7.1) il parser si ferma invece di restituire un contenuto sbagliato, e
il chiamante ripiega sull'euristica testuale, che i nomi delle istanze li trova comunque
come testo puro. Nessun comportamento dell'app dipende da questa decodifica.
- **Costante `\x82\x7b`** nei record `ConfigName`: significato ignoto. Rischio coperto -
`LeggiMembri` conta le occorrenze grezze di `ConfigName` e le confronta col numero di match
del regex; se un campione avesse un valore diverso, il regex non farebbe più match (prima:
in silenzio) e ora lancia eccezione invece di restituire il nome generico al posto
dell'istanza senza dirlo. Verificato su tutti i 350 `.asm` di `raw/`: 215 usano davvero
`ConfigName`, 0 mismatch.
- **Occorrenze multiple di `members`** (rappresentazioni semplificate, mai osservate). Rischio
coperto - `LeggiMembri` lancia eccezione se ne trova più di una invece di prendere
silenziosamente la prima senza sapere se sia quella giusta. Verificato su tutti i 350 `.asm`
di `raw/`: sempre esattamente una occorrenza.
- **La matrice di posizionamento** (`e1`, `e2`, `e3`, `origin`): formato numerico non
decodificato, nessun impatto sulla BOM (non letta da nessuna parte del codice).
- **I campi numerici dell'header** dopo `ASSEMBLY`: non letti, nessun impatto sulla BOM.
- **`family_name`**: sempre uguale a `name` su tutti i campioni disponibili; non si sa in
quale situazione divergerebbe, né se il codice dovrebbe fare qualcosa se succedesse (oggi
non la usa in nessun modo, legge solo `name`).
### 7.3 Due domande diverse: cosa va in distinta, cosa serve per aprire
È la lezione che costa di più riscoprire, quindi sta qui in chiaro: **la lista dei componenti
della BOM e la lista dei file che servono a Creo per aprire l'assieme non coincidono.** La
seconda è più lunga.
| Lista | Serve per | Si ottiene con |
| --- | --- | --- |
| Componenti della distinta | BOM, verifica completezza | `LeggiMembri` / `EstraiCodiciReferenziati` (sezione `members`) |
| File necessari al caricamento | popolare `raw/` per l'export STEP | i precedenti **più** `EstraiCodiciSoppressi` e `EstraiCodiciRiferimentiEsterni` (§7.5) |
La differenza sono i **componenti soppressi** (§7.1) e i **riferimenti esterni** (§7.5): fuori
dalla BOM, ma senza il loro file Creo non apre l'assieme. Chi popola `raw/` deve quindi usare
l'unione; chi genera la BOM o verifica la completezza deve usare **solo** i membri, altrimenti
quelle voci diventano falsi "componenti mancanti".
Il sintomo, se si sbaglia, non assomiglia per niente alla causa: Creo Distributed Batch
fallisce con `Carica modello in Creo Pro | Non riuscito | [-4] Errore: Dati di input non
completi` - un messaggio che non nomina il file mancante. Il modo più rapido per trovarlo è
aprire l'assieme in Creo **dalla cartella `raw/`** (non dalla share, dove i file ci sono tutti
e quindi si apre senza errori) e cercare nell'albero la voce con l'icona rossa.
### 7.4 Lo stesso codice esiste in più cartelle della share
Sulla share **13% dei codici** (5.193 su 39.274 fra prt e asm) ha la versione più alta in più
cartelle, con contenuto diverso. Non sono revisioni: sono copie parallele, lasciate da studi
preliminari, allestimenti fiera, cartelle di lavoro datate, raccolte temporanee.
Il `.asm` **non dice quale copia usa**: verificato che non registra né il percorso, né
l'`UOBJ_ID` del figlio (che pure distingue le copie), né la revisione attesa. Cita solo il
nome del modello. Quale copia venga caricata lo decide Creo al momento dell'apertura, e la
regola pratica è: **prima la cartella del modello che la referenzia**.
Non esiste quindi un criterio globale corretto - e i numeri lo confermano: nel 79% dei
conflitti nessuna delle copie sta in una cartella "ufficiale" (`…\M_B`, `…\A`), perché il
conflitto è fra due istantanee parallele, ognuna autoconsistente con i propri componenti. Per
data, per versione e per posizione sono equivalenti: solo il contesto di chi referenzia
discrimina.
Conseguenze per il codice:
- `Form1.ScegliPerCartella` risolve ogni componente preferendo la copia nella **stessa cartella
dell'assieme che lo referenzia**, e solo in mancanza ripiega su versione più alta e data più
recente. `Form1.CartellaDiOrigine` ricostruisce quella cartella per gli assiemi di partenza,
che arrivano dalla fase precedente senza portarsela dietro.
- La vista `files_ultima_versione` (vedi [`db-cad-index.md`](db-cad-index.md)) rompe il
pareggio fra copie di pari versione in modo deterministico invece che arbitrario.
- Resta un rischio noto: quando il componente **non** sta nella cartella del suo assieme, la
scelta torna a essere una scommessa. Se un assieme non si apre da `raw/`, è la prima cosa da
verificare - `9244206098` è il caso reale che ha portato a questa sezione: due copie di pari
versione, una del 2011 in una cartella di file sciolti e una del 2026 accanto all'assieme,
e in `raw/` finiva quella del 2011.
### 7.5 Riferimenti esterni: la tabella dei modelli citati
Terzo modo in cui un `.asm` dipende da un file che non è fra i suoi membri, dopo i soppressi
(§7.1) e le copie ambigue (§7.4). Un assieme che porta **assembly feature** o **datum copiati**
da un altro modello lo cita in una tabella a parte, che non è `members` e non lascia una stringa
`Componente ID …`: i due lettori esistenti non la vedevano.
Struttura: un array aperto da `\xf2\xf7\x05` e chiuso da `\xf3`, con elementi
`\xe3\xe2<tipo><nome>\x00`. Il nome può portare il suffisso `#AF<n>` di §4.0.1 e/o la notazione
`istanza<generico>` di §4.0.2 - il file su disco è il generico:
```
f2 f7 05 e3 e2 01 "9255708017#AF2<9255708017>" 00 ... -> 9255708017
e3 e2 02 "9255700181" 00 ... -> 9255700181
f3
```
Va letta con `EstraiCodiciRiferimentiEsterni`, e - a differenza dei soppressi - **si ricorre
dentro**: Creo carica il modello citato con tutti i suoi componenti, che quindi servono anch'essi
in `raw/`. La tabella contiene anche nomi di *feature* (`MIRROR3`, `CHAMFER1`,
`M10_TAPPED_HOLE1`), indistinguibili dai modelli guardando il record: non è un problema perché
chi la usa cerca ogni nome nell'indice e ignora quelli che non ci sono - per lo stesso motivo
questi codici non devono finire fra i "non trovati".
Il caso reale: `9255728001` falliva in Distributed Batch con lo stesso `[-4] Dati di input non
completi` di §7.3 perché il suo sottoassieme `9255728001_200` referenzia per assembly feature
`9255708017` e `9255708018` (e con loro `9255700181/182/183`), e monta una vite
`V6-12--_-_U5933_C<UNI5933>` il cui generico `uni5933.prt` non era in `raw/`. Nessuno dei sei
file compariva in `members` né fra i piazzamenti.
## 8. Uso
+7 -6
View File
@@ -12,8 +12,8 @@ Un reverse engineering successivo ha decodificato la **struttura a record** del
solo pattern testuali): legge esattamente la sezione `members` di un assieme, senza contare
occorrenze né rischiare i falsi positivi descritti in questo documento. Implementato in
`PrepStep/AsmRecordReader.cs`, documentato in [`formato-asm-record.md`](formato-asm-record.md).
È il metodo **preferito** in GUI (fase "Trova componenti referenziati"/"Verifica
completezza", `Form1.EstraiCodiciReferenziati`) e nello script `CreoBom.cs`:
È il metodo **preferito** in GUI (fase 4 "Trova componenti referenziati",
`Form1.EstraiCodiciReferenziati`) e nello script `CreoBom.cs`:
`AssemblyComponentScanner` (l'euristica di questo documento) resta come **fallback**, usato
solo quando `AsmRecordReader` lancia `InvalidDataException` (file che non rispetta il
formato validato dal reverse engineering — copertura limitata agli 8 campioni disponibili al
@@ -55,8 +55,9 @@ regex: \b\d{8,12}(?:_[A-Za-z]\d{2,4}|[A-Za-z]{0,2})\b
occorrenze: >= 2
```
`scripts/ExtractCreoStrings.cs` ha una versione più raffinata della regex e un filtro
aggiuntivo (vedi sotto), non ancora portati in `AssemblyComponentScanner.cs`:
La regex effettiva in `AssemblyComponentScanner.cs` (usata anche da
`scripts/ExtractCreoStrings.cs` via `#:project`) è una versione più raffinata, con in più il
filtro sui falsi positivi descritto sotto:
```
regex: \b\d{8,12}[A-Za-z]{0,2}(?:_[A-Za-z]?\d{2,4})?\b
@@ -285,8 +286,8 @@ livello) serve una BFS:
Stessa logica di estrazione candidati ed euristica del codice radice (sopra),
duplicata in due punti del codice per scopi diversi:
- [`AssemblyComponentScanner.EstraiCodiciReferenziati`](../PrepStep/AssemblyComponentScanner.cs) + `CodiceRadice` — estrazione dei candidati da un file, usata dalla BFS in [`Form1.RisolviAlberoComponenti`](../PrepStep/Form1.cs) (fase "Trova componenti referenziati") e [`Form1.AssiemeCompleto`](../PrepStep/Form1.cs) (fase "Verifica completezza"). Entrambe seminano l'insieme dei codici visitati con codice + radice di ogni file di partenza, e caricano una volta sola tutti i codici noti all'indice (`Form1.CaricaCodiciNoti`) sia per validare i token generici delle parti commerciali sia per risolvere i percorsi dei componenti trovati, senza una query SQL per candidato.
- [`scripts/ExtractCreoStrings.cs`](../scripts/ExtractCreoStrings.cs) e [`scripts/BomViaDb.cs`](../scripts/BomViaDb.cs) — stessa BFS ricorsiva standalone da riga di comando (copia indipendente della stessa euristica, nessun `ProjectReference` verso la GUI). Usati per confrontare l'euristica contro l'albero Creo di un singolo file durante il reverse engineering, senza dover passare dalla UI/dal DB `raw`.
- [`AssemblyComponentScanner.EstraiCodiciReferenziati`](../PrepStep/AssemblyComponentScanner.cs) + `CodiceRadice` — estrazione dei candidati da un file, usata come fallback dalla BFS in [`Form1.RisolviAlberoComponenti`](../PrepStep/Form1.cs) (fase 4 "Trova componenti referenziati"). La BFS semina l'insieme dei codici visitati con codice + radice di ogni file di partenza e carica una volta sola i codici noti all'indice (`Form1.CaricaCodiciNoti`, per validare i token generici delle parti commerciali) e tutte le copie di ogni codice (`Form1.CaricaCandidati`, per scegliere il percorso da copiare), senza una query SQL per candidato.
- [`scripts/ExtractCreoStrings.cs`](../scripts/ExtractCreoStrings.cs) e [`scripts/BomViaDb.cs`](../scripts/BomViaDb.cs) — stessa BFS ricorsiva standalone da riga di comando, che riusa `AssemblyComponentScanner` del progetto principale via `#:project` (nessuna copia dell'euristica). Usati per confrontare l'euristica contro l'albero Creo di un singolo file durante il reverse engineering, senza dover passare dalla UI/dal DB `raw`.
Nota: le query contro `files_ultima_versione` filtrano solo per
`(cartella_radice, codice, tipo_file)` - se lo stesso codice esiste in più
+153
View File
@@ -0,0 +1,153 @@
# Guida utente
Questa guida descrive, in ordine, tutti i passi da fare in **PrepStep - Cevolani** 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).
## Dove finiscono i file
Tutto sta nella **cartella di lavoro**, che di default è la cartella di installazione
`%LOCALAPPDATA%\Programs\PrepStep`:
| Cosa | Dove |
|---|---|
| database `cad_index.db` | sempre nella cartella di installazione |
| CSV di ogni distinta importata | cartella di lavoro |
| `raw/` — file CAD copiati nelle fasi 3-4 | cartella di lavoro |
| `step/` — file STEP esportati da Creo | cartella di lavoro (la indichi tu a Creo) |
Il file `.xls` del gestionale resta dove l'hai preso: l'app lo legge soltanto.
- **Cambiare la cartella di lavoro**: **File → Scegli cartella di lavoro...** (viene ricordata
agli avvii successivi). Il database resta comunque nella cartella di installazione.
- **Aprire la cartella**: `AppData` è nascosta in Esplora file; scrivi
`%LOCALAPPDATA%\Programs\PrepStep` nella barra degli indirizzi (vale anche per scegliere
`step/` in Creo). Dalla fase 4 si apre `raw/` con **Apri cartella raw**.
- **Fare pulizia** (menu **File**): **Pulisci raw e step...** svuota `raw/` e `step/`;
**Elimina CSV delle distinte...** elimina i CSV, che altrimenti si accumulano uno per
distinta. Nessuno dei due tocca il database. Entrambi chiedono conferma.
- **Disinstallando** il programma questi file restano: cancellali a mano se non servono più.
## 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.
- Se non vuoi usare la cartella di installazione, scegli la **cartella di lavoro**
("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
Se nella cartella di installazione c'è già un database (`cad_index.db`), l'app lo carica
da sola all'avvio: 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. Vale solo finché non chiudi il programma: al prossimo avvio si riparte
dal database nella cartella di installazione. Per usarlo sempre, copialo in
`%LOCALAPPDATA%\Programs\PrepStep` come `cad_index.db`.
- La **cartella di lavoro** non si imposta qui: vedi [Dove finiscono i file](#dove-finiscono-i-file).
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 presenza**, 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. Verifica completezza e 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): svuota le griglie e
la verifica delle fasi 2-5 e riporta al passo 2, senza dover ricaricare il database né
cambiare la cartella di lavoro. Non cancella nessun file su disco: CSV, `raw/` e `step/`
restano (vedi sotto per ripulirli).
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...**.
Ogni distinta importata lascia un CSV nella cartella di lavoro: per eliminarli tutti usa
**File → Elimina CSV delle distinte...** (i file `.xls` del gestionale non vengono toccati).
+85
View File
@@ -0,0 +1,85 @@
; Setup per-utente di PrepStep - Cevolani: installa in %LOCALAPPDATA%\Programs\PrepStep
; senza richiedere diritti di amministratore.
; Prima: dotnet publish (vedi README), poi: iscc installer\PrepStep.iss
#define Exe "..\PrepStep\publish\PrepStep.exe"
; Versione letta dall'exe (<Version> nel csproj), tolto l'eventuale "+<commit>" di .NET.
#define AppVersion GetStringFileInfo(Exe, "ProductVersion")
#if Pos("+", AppVersion) > 0
#define AppVersion Copy(AppVersion, 1, Pos("+", AppVersion) - 1)
#endif
; Non cambiare AppGuid: è quello che fa riconoscere a Windows gli aggiornamenti come la stessa app.
#define AppGuid "FAC1B760-4735-422F-86F0-888484CF2830"
[Setup]
AppId={{{#AppGuid}}
AppName=PrepStep - Cevolani
AppVersion={#AppVersion}
AppPublisher=Cevolani
AppCopyright=Copyright (c) 2026 Davide Grilli
; Con PrivilegesRequired=lowest {autopf} diventa %LOCALAPPDATA%\Programs.
PrivilegesRequired=lowest
DefaultDirName={autopf}\PrepStep
DisableDirPage=yes
DisableProgramGroupPage=yes
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
SetupIconFile=..\PrepStep\app.ico
UninstallDisplayIcon={app}\PrepStep.exe
UninstallDisplayName=PrepStep - Cevolani
OutputDir=Output
OutputBaseFilename=PrepStep-Setup-{#AppVersion}
Compression=lzma2
SolidCompression=yes
[Languages]
Name: "it"; MessagesFile: "compiler:Languages\Italian.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
[Files]
Source: "{#Exe}"; DestDir: "{app}"; Flags: ignoreversion
[InstallDelete]
; Collegamenti col vecchio nome dell'app (fino a 0.9.0), altrimenti restano nel menu Start.
Type: files; Name: "{autoprograms}\Cevolani StepPrep.lnk"
Type: files; Name: "{autoprograms}\Disinstalla Cevolani StepPrep.lnk"
Type: files; Name: "{autodesktop}\Cevolani StepPrep.lnk"
[Icons]
Name: "{autoprograms}\PrepStep - Cevolani"; Filename: "{app}\PrepStep.exe"
Name: "{autodesktop}\PrepStep - Cevolani"; Filename: "{app}\PrepStep.exe"; Tasks: desktopicon
Name: "{autoprograms}\Disinstalla PrepStep - Cevolani"; Filename: "{uninstallexe}"
[Run]
Filename: "{app}\PrepStep.exe"; Description: "{cm:LaunchProgram,PrepStep - Cevolani}"; Flags: nowait postinstall skipifsilent
[Code]
{ Se l'app è già installata, chiede se aggiornarla o disinstallarla (Inno da solo aggiornerebbe
sempre, senza offrire la disinstallazione). In modalità silenziosa aggiorna senza chiedere. }
function InitializeSetup(): Boolean;
var
Disinstallatore: String;
Codice: Integer;
begin
Result := True;
if WizardSilent then
Exit;
if not RegQueryStringValue(HKCU,
'Software\Microsoft\Windows\CurrentVersion\Uninstall\{' + '{#AppGuid}' + '}_is1',
'UninstallString', Disinstallatore) then
Exit;
case MsgBox('PrepStep - Cevolani è già installato.' + #13#10#13#10 +
'Sì: aggiorna / reinstalla' + #13#10 +
'No: disinstalla' + #13#10 +
'Annulla: esci senza fare nulla', mbConfirmation, MB_YESNOCANCEL) of
IDNO:
begin
Exec(RemoveQuotes(Disinstallatore), '', '', SW_SHOW, ewWaitUntilTerminated, Codice);
Result := False;
end;
IDCANCEL:
Result := False;
end;
end;