Files
prep-step/docs/architettura.md
T
davideandClaude 2d7ace07e2 Documenta il formato PSB dei file Creo e aggiungi il parser di riferimento
Reverse engineering di 9253000117a.prt.35: contenitore (offset dall'ultimo
campo dell'intestazione, indice a catena), grammatica completa delle sezioni
(id progressivi dei campi, tabella "dopo X viene Y", marcatori di chiusura),
tabella dei double verificata sulla norma delle matrici, sezioni compresse e
catalogo del contenuto. Parser Python in scripts/psb/. docs/history.md
raccoglie stato, metodo e punti aperti per riprendere il lavoro.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-11-10 14:34:29 +01:00

22 KiB

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); 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 e la guida-utente.md; per il 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

Un solo eseguibile WinForms (nessuna CLI, nessun processo esterno): indicizza i file CAD di una share di rete in un DB SQLite locale, poi guida l'utente in un wizard a 5 fasi che, a partire da una distinta base esportata dal gestionale, raccoglie in locale (raw/) tutti i file CAD dei componenti e dei loro sotto-assiemi e verifica che l'indice li copra, come pre-process prima dell'esportazione STEP con Creo Distributed Batch (manuale, fuori dall'app: i file esportati finiscono in step/, dentro la cartella di lavoro).

Distinta (.xls/.xml)  →  CSV (componente;descrizione)  →  raw/ (CAD copiati dall'indice)
                                                              │
                                          scansione .asm ─────┘  (sotto-assiemi/parti mancanti)
                                                              │
                            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.

Finestra principale (wizard)

  • Form1.cs — tutta la UI è costruita in codice (BuildUi/BuildFase*), non con il designer visuale. È un wizard: _panelFasi[] contiene un Panel per fase, uno solo visibile alla volta (VaiAllaFase); si avanza con "Avanti" solo se AvantiConsentito ritorna vero per la fase corrente. Form1.Designer.cs contiene solo lo scheletro minimo generato dal template (InitializeComponent/Dispose), non è usato per costruire i controlli.

    Le 5 fasi e i metodi principali dietro ciascuna:

    # Fase Bottone → metodo Cosa fa
    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 (oppure "Lista personalizzata": codici digitati uno alla volta con AggiungiCodice, solo in memoria, senza CSV; tutto a valle legge solo _righeCorrenti); "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/, 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 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).
    • 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 sull'exe pubblicato standalone spostato su un'altra macchina.

Indicizzazione

  • Indexer.cs — ScanAsync cammina tutte le cartelle di primo livello sotto la radice (scoperte dinamicamente, non un elenco fisso) con un walk manuale a stack (Walk, più veloce di EnumerateFiles(..., AllDirectories) su share con centinaia di migliaia di file), esclude le cartelle temp_*, estrae da ogni nome file il pattern Pro-E <codice>.<tipo>.<versione> (ParseFilename, regex), e fa un upsert incrementale riga per riga confrontando percorso_completo + dimensione + data modifica. Progresso riportato via IProgress<ScanProgress> (marshalato sul thread UI). Contiene anche lo schema SQL: tabella files e vista files_ultima_versione (ultima versione per codice+tipo_file via ROW_NUMBER() OVER (...), non self-join+GROUP BY per evitare tempi quasi quadratici su ~200k righe).

Lettura dei componenti referenziati in un .asm

Form1.EstraiCodiciReferenziati (usata da RisolviAlberoComponenti, fase 4) prova prima AsmRecordReader.EstraiCodiciReferenziati e ripiega su AssemblyComponentScanner.EstraiCodiciReferenziati solo se il primo lancia InvalidDataException (file che non rispetta il formato validato dal reverse engineering).

  • AsmRecordReader.cs — legge esattamente la sezione members del binario .asm (id/tipo/ nome dei componenti diretti, con risoluzione del nome istanza di family table dal parametro ConfigName): nessuna euristica, struttura a record. Lancia InvalidDataException se il file non ha una sezione members o se il conteggio dichiarato non torna. Espone anche le due liste "non di distinta": EstraiCodiciSoppressi (che passa da members per sapere quali codici sono già noti, quindi può lanciare la stessa eccezione — Form1.CodiciSoppressi la intercetta e restituisce vuoto) e EstraiCodiciRiferimentiEsterni (scansione a sé, non lancia). Dettagli in docs/formato-asm-record.md. Condivisa con scripts/CreoBom.cs via #:project.
  • AssemblyComponentScanner.cs — estrae euristicamente i codici componente referenziati 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 parti commerciali tokenizzati genericamente e validati contro l'elenco di codici noti del DB (altrimenti il rumore binario sarebbe ingestibile). Dettagli sull'euristica e i falsi 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 estensione .xls/.xml, oppure .xls/.xlsx binario vero, distinti dai primi byte del file) nelle sole colonne Componente/Descrizione, foglio "Distinta Base". Se il nome del file segue la convenzione DBP_<codice>_<timestamp>, aggiunge l'assieme generale come riga in più (il foglio elenca solo i componenti sotto l'assieme). Scrive il CSV nella cartella 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)

  • ProgressForm.cs — durante l'aggiornamento indice: barra di progresso, conteggi nuovi/aggiornati/invariati/rimossi, tempo trascorso e stima del tempo rimanente (dal ritmo file/secondo rispetto al totale atteso, preso dal conteggio della scansione precedente salvato nel DB).
  • ProgressoComponentiForm.cs — dialog minimale (etichetta + barra) durante RisolviAlberoComponenti in fase 4.

Stile UI condiviso

  • UiStile.cs — Primario/Secondario: stile piatto per i bottoni (bordo sottile 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). 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 copiare il file .ico nell'output. Sfondo trasparente (nessuna card bianca), 7 risoluzioni da 16 a 256px.
  • PrtMancantiForm.cs — esito della risoluzione ricorsiva dell'albero componenti: PRT e 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).
  • 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. 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 in rilascio.md.

Script standalone (scripts/)

scripts/psb/ fa eccezione: sono tre file Python (prt.py, psb.py, unz.py), lo strumento del reverse engineering descritto in formato-psb.md. Non referenziano il progetto e l'app non li usa.

C# file-based apps (.NET 10, dotnet run scripts/Nome.cs -- arg1 arg2, nessun .csproj, 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 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, 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 investigare a mano nuovi falsi positivi prima di toccare la regex di AssemblyComponentScanner.
  • Query.cs — query SQL ad-hoc contro cad_index.db da riga di comando.
  • CheckDocs.cs — controllo dei doc (docs-as-code): link relativi e ancore, <Version> del csproj uguale all'ultima voce del CHANGELOG.md, ogni .cs di PrepStep/ e scripts/ descritto qui. Exit 1 se qualcosa non torna.

⚠️ Nota importante di sicurezza: se dotnet run viene lanciato dentro PrepStep/ (che contiene un .csproj di tipo WinExe), esegue l'app GUI vera invece dello script standalone e resta bloccato in attesa che una finestra si chiuda. Lanciare gli script da una cartella senza .csproj (es. la root del repo o scripts/ stessa).

Test

  • PrepStep.Tests/ (xUnit) — riferimento al progetto principale via ProjectReference e InternalsVisibleTo (in PrepStep.csproj), così i test chiamano direttamente Indexer.ParseFilename e AssemblyComponentScanner.EstraiCodiciReferenziati senza esporli 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.