Author SHA1 Message Date
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
davideandClaude Sonnet 5.5 1eac3ccaa7 Documenta posizioni, fori e facce dei .prt (9239902104)
Matrici 4x3 di fori e feature (regola degli zeri corretta), foro a punti
di schizzo con facce piane di Estrusione 1, profilo a L, quote per feature
e tipi 928/1104/1052. Corregge l'ipotesi errata sul codice 12266.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 16:21:18 +02:00
davideandClaude Sonnet 5.5 5b11b3e8ca Documenta la decodifica dei .prt: albero delle feature, quote e geometria
Reverse engineering di test.prt.1, 9252000507.prt e 9239901907.prt contro
il model tree di Creo: contenitore e sezioni, ricostruzione dell'albero
(nomi per contatore, gruppi, sezioni), numeri compatti, quote, foro, smusso
e vincoli dei piani, con la tabella di cosa resta da decodificare.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 15:58:15 +02:00
davideandClaude Sonnet 5.5 d5fb20a34e Rilascia la 1.1.0
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 08:52:57 +02:00
davideandClaude Sonnet 5.5 6d1e36a2ae Aggiungi la lista personalizzata di codici nella fase 2
In alternativa al file del gestionale si possono digitare i codici uno alla
volta (Invio/Aggiungi, Canc per togliere). Tutto a valle legge solo
_righeCorrenti, quindi basta una nuova sorgente; la selezione della griglia
mantiene il colore verde/rosso della verifica.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 08:52:05 +02:00
davideandClaude Sonnet 5.5 ccfa00ff52 Aggiungi docs-as-code: indice Diátaxis, nuovi doc e controllo automatico
Nuovi doc: tutorial-primo-giro, risoluzione-problemi, sviluppo, rilascio,
glossario e registro delle decisioni (docs/decisioni, 6 ADR). docs/README.md
e' l'indice per categoria e la tabella delle fonti uniche; il README di
root rimanda a sviluppo/rilascio invece di duplicarli.

scripts/CheckDocs.cs verifica link e ancore, <Version> contro CHANGELOG,
copertura di architettura.md e presenza di ogni doc nell'indice;
.markdownlint-cli2.jsonc configura il lint Markdown.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 08:05:10 +02:00
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
43 changed files with 3706 additions and 373 deletions

No files matched your search

+7
View File
@@ -37,3 +37,10 @@ publish/
# Output di scripts/ExtractCreoStrings.cs # Output di scripts/ExtractCreoStrings.cs
/BOM.txt /BOM.txt
# Impostazioni locali dell'editor
.vscode/
# Setup generato da Inno Setup (installer/PrepStep.iss)
installer/Output/
__pycache__/
+15
View File
@@ -0,0 +1,15 @@
{
// Regole base; disattivate quelle che non servono a prosa italiana con tabelle larghe
"config": {
"default": true,
"MD013": false, // lunghezza riga
"MD033": false, // HTML inline
"MD024": { "siblings_only": true }, // CHANGELOG ripete "Aggiunto/Modificato"
"MD036": false,
"MD040": false, // dump esadecimali/testuali senza linguaggio
"MD060": false, // stile colonne tabelle
"MD041": false
},
"globs": ["**/*.md"],
"ignores": ["node_modules", "raw", "step", "**/bin", "**/obj"]
}
+96
View File
@@ -5,6 +5,102 @@ 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 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/). aderisce a [Semantic Versioning](https://semver.org/lang/it/).
## [Non rilasciato]
### Aggiunto
- `docs/formato-prt-feature.md`: decodifica (da reverse engineering) dell'albero delle feature di un `.prt`.
- `docs/formato-prt-geometria.md`: numeri, quote e geometria degli schizzi di un `.prt`, con la tabella di cosa resta da decodificare.
- `docs/formato-psb.md` e `scripts/psb/`: formato di serializzazione completo delle sezioni dei file Creo (reverse engineering su `9253000117a.prt.35`) e parser Python di riferimento.
## [1.1.0] - 2026-10-07
### Aggiunto
- Fase 2: modalità "Lista personalizzata" per inserire i codici a mano, uno alla volta, in alternativa al file del gestionale.
- Docs-as-code: `scripts/CheckDocs.cs`, markdownlint, `docs/README.md` (indice Diátaxis e fonte unica).
- Nuovi doc: `tutorial-primo-giro`, `risoluzione-problemi`, `sviluppo`, `rilascio`, `glossario` e registro delle decisioni `docs/decisioni/` (6 ADR).
### Modificato
- Il README rimanda a `docs/sviluppo.md` e `docs/rilascio.md` per build, test e setup, invece di duplicarli.
- Fase 2: dopo "Verifica presenza" la riga selezionata mantiene il verde/rosso dell'esito invece di diventare blu.
## [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 ## [0.1.0] - 2026-09-22
### Aggiunto ### Aggiunto
+40 -15
View File
@@ -5,9 +5,9 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Cos'è questo progetto ## Cos'è questo progetto
`PrepStep` è il nome tecnico del progetto (cartella, namespace, csproj, eseguibile `PrepStep` è il nome tecnico del progetto (cartella, namespace, csproj, eseguibile
`PrepStep.exe`); il nome mostrato all'utente finale è **Cevolani StepPrep** (`<Product>`/ `PrepStep.exe`); il nome mostrato all'utente finale è **PrepStep - Cevolani** (`<Product>`/
`<AssemblyTitle>` in `PrepStep/PrepStep.csproj`, usato nel titolo della finestra insieme `<AssemblyTitle>` in `PrepStep/PrepStep.csproj`, usato nel titolo della finestra insieme
alla versione, `<Version>`, attualmente `0.1.0`). È un'app Windows Forms (.NET 10, WinForms) che indicizza e permette di alla versione, `<Version>`, attualmente `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 cercare i file CAD Pro-ENGINEER/Creo contenuti in tutte le cartelle di primo livello
di un disco/share di rete (radice di default `X:\`), salvando i metadati in un DB di un disco/share di rete (radice di default `X:\`), salvando i metadati in un DB
SQLite locale (`cad_index.db`). Il flusso a valle (fasi 2-5, vedi sotto) prepara i file SQLite locale (`cad_index.db`). Il flusso a valle (fasi 2-5, vedi sotto) prepara i file
@@ -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 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 `PrepStep.Tests/` (xUnit) contiene unit e integration test — vedi
`dotnet test` da dentro quella cartella. Riferimento al progetto principale `dotnet test` da dentro quella cartella. Riferimento al progetto principale
via `ProjectReference` + `InternalsVisibleTo` (in `PrepStep.csproj`), così via `ProjectReference` + `InternalsVisibleTo` (in `PrepStep.csproj`), così
@@ -51,12 +58,19 @@ Dove scrivere cosa:
|---|---| |---|---|
| `CLAUDE.md` (questo file) | il flusso essenziale e i punti che si sbaglierebbero senza saperli | | `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/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-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/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/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/` | | [`docs/copia-raw-parallela.md`](docs/copia-raw-parallela.md) | parallelizzazione della copia in `raw/` |
| [`docs/README.md`](docs/README.md) | indice Diátaxis di tutti i doc (tutorial, how-to, reference, decisioni) e regola di fonte unica |
| [`CHANGELOG.md`](CHANGELOG.md) | storico delle versioni (formato Keep a Changelog); ogni release che cambia `<Version>` nel csproj aggiunge qui la sua voce | | [`CHANGELOG.md`](CHANGELOG.md) | storico delle versioni (formato Keep a Changelog); ogni release che cambia `<Version>` nel csproj aggiunge qui la sua voce |
I doc sono trattati come codice: stanno in git e si verificano con
`dotnet run scripts/CheckDocs.cs` (link, versione/CHANGELOG, copertura di `architettura.md`) e
`npx markdownlint-cli2` (config `.markdownlint-cli2.jsonc`) — da lanciare prima di ogni commit che
tocca i doc. Organizzazione (Diátaxis) e regola di fonte unica: [`docs/README.md`](docs/README.md).
Quando il cambiamento nasce da un fallimento reale di Creo, scrivere anche **il sintomo**: i 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 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 riparte da zero (i §7.3/§7.4/§7.5 di `formato-asm-record.md` sono l'esempio del formato
@@ -64,7 +78,8 @@ atteso).
## Architettura ## 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 completo file-per-file in [`docs/architettura.md`](docs/architettura.md); qui solo il flusso
che conta per capire il prodotto. che conta per capire il prodotto.
@@ -73,15 +88,21 @@ che conta per capire il prodotto.
designer visuale): designer visuale):
1. **Database** — carica/aggiorna `cad_index.db` (`Indexer.ScanAsync`, radice disco di 1. **Database** — carica/aggiorna `cad_index.db` (`Indexer.ScanAsync`, radice disco di
default `X:\` hardcoded in `RadiceDiscoDefault`); qui vive anche la **cartella di lavoro** default `X:\` hardcoded in `RadiceDiscoDefault`); un pallino verde/rosso accanto al titolo
(destinazione di CSV/`raw/`/`step/`), proposta dall'ultima usata o dalla cartella del DB se indica se un DB è caricato. La **cartella di lavoro** (destinazione di CSV/`raw/`/`step/`)
non c'è ancora nulla di salvato — vedi `docs/db-cad-index.md` per il dettaglio di schema e non è più mostrata qui — proposta dall'ultima usata o dalla cartella di installazione se
risoluzione del percorso DB. 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 2. **Importa Distinta Base** — `DistintaBaseImporter.ConvertiInCsv` genera il CSV
componente;descrizione nella cartella di lavoro; se il file segue la convenzione del componente;descrizione nella cartella di lavoro (da file scelto o trascinato); se il file
gestionale (`DBP_<codice>_<timestamp>`), aggiunge l'assieme generale come riga in più (da segue la convenzione del gestionale (`DBP_<codice>_<timestamp>`), aggiunge l'assieme
quel momento è un componente come tutti gli altri, nessun caso speciale nel resto del generale come riga in più (da quel momento è un componente come tutti gli altri, nessun
codice). caso speciale nel resto del codice). Il bottone "Verifica presenza" compare solo dopo un
import riuscito e cerca i componenti nell'indice (`VerificaCodiciNelDb`, righe colorate in
verde/rosso in griglia): è quello — non il solo import — a sbloccare "Avanti"
(`AvantiConsentito`).
3. **Crea cartella raw** — `TrovaPercorsiCad` + `CopiaFileTrovati` (parallela, vedi 3. **Crea cartella raw** — `TrovaPercorsiCad` + `CopiaFileTrovati` (parallela, vedi
`docs/copia-raw-parallela.md`) copiano in `raw/` i file CAD dei componenti del CSV trovati `docs/copia-raw-parallela.md`) copiano in `raw/` i file CAD dei componenti del CSV trovati
nell'indice. nell'indice.
@@ -103,9 +124,13 @@ designer visuale):
5. **Verifica completezza** — mostra i `.asm` della distinta già in `raw/`, trascinabili su 5. **Verifica completezza** — mostra i `.asm` della distinta già in `raw/`, trascinabili su
Creo per l'esportazione STEP (Distributed Batch, manuale, fuori dall'app). Creo per l'esportazione STEP (Distributed Batch, manuale, fuori dall'app).
Menu in cima alla finestra: "File → Nuova estrazione" (Ctrl+N) azzera lo stato delle fasi 2-5 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; per ripartire con una nuova distinta, senza toccare DB/cartella di lavoro della fase 1 (va
"Aiuto → Guida" (F1) apre una sintesi interattiva delle 5 fasi (`GuidaForm.cs`). 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/`) ## Script standalone (`scripts/`)
@@ -122,5 +147,5 @@ intercambiabili:
- `ExtractCreoStrings.cs`/`BomViaDb.cs` usano `AssemblyComponentScanner` (euristica testuale, - `ExtractCreoStrings.cs`/`BomViaDb.cs` usano `AssemblyComponentScanner` (euristica testuale,
`docs/formato-asm.md`) e risolvono i sottoassiemi contro `cad_index.db`. `docs/formato-asm.md`) e risolvono i sottoassiemi contro `cad_index.db`.
- `CreoBom.cs` usa `AsmRecordReader` (parser esatto dei record binari, - `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`. 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.
+321 -160
View File
@@ -1,4 +1,5 @@
using System.Diagnostics; using System.Diagnostics;
using System.Drawing.Drawing2D;
using Microsoft.Data.Sqlite; using Microsoft.Data.Sqlite;
namespace PrepStep; namespace PrepStep;
@@ -10,19 +11,27 @@ public partial class Form1 : Form
private readonly Button _btnCreaRaw = new(); private readonly Button _btnCreaRaw = new();
private readonly Button _btnTrovaPrt = new(); private readonly Button _btnTrovaPrt = new();
private readonly Button _btnVerifica = new(); private readonly Button _btnVerifica = new();
private readonly FlowLayoutPanel _rigaVerifica = new();
private readonly FlowLayoutPanel _rigaFile = new();
private readonly FlowLayoutPanel _rigaCustom = new();
private readonly RadioButton _rbFile = new() { Text = "Da file del gestionale", Checked = true, AutoSize = true };
private readonly RadioButton _rbCustom = new() { Text = "Lista personalizzata", AutoSize = true };
private readonly TextBox _txtCodice = new() { Width = 220, Margin = new Padding(0, 3, 8, 0) };
private readonly ListView _lstAnteprimaAsm = new(); private readonly ListView _lstAnteprimaAsm = new();
private readonly Button _btnPulisci = new();
private readonly Label _lblStatus = new(); private readonly Label _lblStatus = new();
private readonly Label _lblDb = new(); private readonly Label _lblDb = new();
private readonly Label _lblUltimoAggiornamento = new(); private readonly Label _lblUltimoAggiornamento = new();
private readonly Label _lblCartellaLavoro = new(); private readonly Panel _indicatoreDb = new();
private readonly Button _btnCambiaCartella = new(); private bool _dbCaricato;
private readonly DataGridView _gridImport = new(); private readonly DataGridView _gridImport = new();
private readonly DataGridView _gridRaw = new(); private readonly DataGridView _gridRaw = new();
private readonly Label _lblImportStatus = new(); private readonly Label _lblImportStatus = new();
private readonly ToolTip _tipImportStatus = new(); private readonly ToolTip _tipImportStatus = new();
private readonly MenuStrip _menu = new(); private readonly MenuStrip _menu = new();
private readonly ToolStripMenuItem _mnuNuovaEstrazione = new("Nuova estrazione"); 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 // Wizard: una fase alla volta occupa tutta la finestra, si avanza solo quando quella
// corrente è "pronta" (vedi AvantiConsentito). NomiFasi/_panelFasi sono paralleli per indice. // corrente è "pronta" (vedi AvantiConsentito). NomiFasi/_panelFasi sono paralleli per indice.
@@ -46,7 +55,7 @@ public partial class Form1 : Form
private string? _dbPath; private string? _dbPath;
private string? _cartellaLavoro; private string? _cartellaLavoro;
private string? _percorsoInputCorrente; private bool _ripristinoModalita;
private List<(string Componente, string Descrizione)>? _righeCorrenti; private List<(string Componente, string Descrizione)>? _righeCorrenti;
private Dictionary<string, List<string>>? _ultimaVerifica; private Dictionary<string, List<string>>? _ultimaVerifica;
private string? _ultimaCartellaRaw; private string? _ultimaCartellaRaw;
@@ -71,7 +80,7 @@ public partial class Form1 : Form
AllowDrop = true; AllowDrop = true;
// Versione letta dall'assembly (<Version> nel csproj) invece che duplicata qui a mano. // Versione letta dall'assembly (<Version> nel csproj) invece che duplicata qui a mano.
var versione = typeof(Form1).Assembly.GetName().Version; var versione = typeof(Form1).Assembly.GetName().Version;
Text = $"Cevolani StepPrep {versione?.ToString(3)}"; Text = $"PrepStep - Cevolani {versione?.ToString(3)}";
MinimumSize = new Size(760, 480); MinimumSize = new Size(760, 480);
Font = new Font("Segoe UI", 9.5f); Font = new Font("Segoe UI", 9.5f);
// Stessa icona di ApplicationIcon nel csproj: la si rilegge dall'exe compilato invece // Stessa icona di ApplicationIcon nel csproj: la si rilegge dall'exe compilato invece
@@ -92,38 +101,27 @@ public partial class Form1 : Form
_lblFase.Font = new Font(Font, FontStyle.Bold); _lblFase.Font = new Font(Font, FontStyle.Bold);
headerPanel.Controls.Add(_lblFase); headerPanel.Controls.Add(_lblFase);
// Barra di navigazione come footer separato dal contenuto: linea sottile in cima e // Floatbar di navigazione: card staccata dai bordi della finestra invece di un footer
// sfondo bianco, invece dei bottoni appoggiati direttamente sul contenuto/sulla barra // a tutta larghezza appoggiato direttamente sul contenuto/sulla barra di stato.
// di stato senza alcuna divisione visiva. var (navContainer, navPanel) = UiStile.FloatBar();
// Padding destro = 16, lo stesso usato dai pannelli di ogni fase (Padding(16)): prima
// era 12, quindi "Avanti" sporgeva 4px oltre il margine destro del contenuto sopra.
var navContainer = new Panel { Dock = DockStyle.Bottom, Height = 64, BackColor = Color.White };
var navSeparatore = new Panel
{
Dock = DockStyle.Top,
Height = 1,
BackColor = Color.FromArgb(220, 220, 220),
};
var navPanel = new FlowLayoutPanel
{
Dock = DockStyle.Fill,
FlowDirection = FlowDirection.RightToLeft,
Padding = new Padding(16, 14, 16, 14),
};
_btnAvanti.Text = "Avanti ›"; _btnAvanti.Text = "Avanti ›";
UiStile.Primario(_btnAvanti); UiStile.Primario(_btnAvanti);
UiStile.Arrotonda(_btnAvanti);
_btnAvanti.Click += (_, _) => VaiAllaFase(_faseAttuale + 1);
navPanel.Controls.Add(_btnAvanti);
_btnIndietro.Text = "‹ Indietro"; _btnIndietro.Text = "‹ Indietro";
_btnIndietro.Margin = new Padding(8, 0, 0, 0); _btnIndietro.Margin = new Padding(8, 0, 0, 0);
UiStile.Secondario(_btnIndietro); UiStile.SecondarioPieno(_btnIndietro);
UiStile.Arrotonda(_btnIndietro);
// Dimensioni uguali decise PRIMA di arrotondare: se si arrotonda un bottone e solo dopo
// lo si ridimensiona, la Region calcolata sulla dimensione vecchia lascia un bordo
// "fantasma" finché non arriva un repaint completo della finestra.
UiStile.LarghezzaUguale(_btnAvanti, _btnIndietro);
UiStile.Arrotonda(_btnAvanti, 10);
UiStile.Arrotonda(_btnIndietro, 10);
_btnAvanti.Click += (_, _) => VaiAllaFase(_faseAttuale + 1);
navPanel.Controls.Add(_btnAvanti);
_btnIndietro.Click += (_, _) => VaiAllaFase(_faseAttuale - 1); _btnIndietro.Click += (_, _) => VaiAllaFase(_faseAttuale - 1);
navPanel.Controls.Add(_btnIndietro); navPanel.Controls.Add(_btnIndietro);
navContainer.Controls.Add(navPanel);
navContainer.Controls.Add(navSeparatore);
var statusPanel = new Panel { Dock = DockStyle.Bottom, Height = 28, BackColor = SystemColors.ControlLight }; var statusPanel = new Panel { Dock = DockStyle.Bottom, Height = 28, BackColor = SystemColors.ControlLight };
_lblStatus.Dock = DockStyle.Fill; _lblStatus.Dock = DockStyle.Fill;
@@ -152,6 +150,15 @@ public partial class Form1 : Form
_mnuNuovaEstrazione.Click += (_, _) => NuovaEstrazione(); _mnuNuovaEstrazione.Click += (_, _) => NuovaEstrazione();
mnuFile.DropDownItems.Add(_mnuNuovaEstrazione); 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 mnuAiuto = new ToolStripMenuItem("Aiuto");
var mnuGuida = new ToolStripMenuItem("Guida") { ShortcutKeys = Keys.F1 }; var mnuGuida = new ToolStripMenuItem("Guida") { ShortcutKeys = Keys.F1 };
mnuGuida.Click += (_, _) => new GuidaForm().ShowDialog(this); mnuGuida.Click += (_, _) => new GuidaForm().ShowDialog(this);
@@ -244,7 +251,7 @@ public partial class Form1 : Form
private bool AvantiConsentito(int fase) => fase switch private bool AvantiConsentito(int fase) => fase switch
{ {
0 => _dbPath is not null && _cartellaLavoro is not null, 0 => _dbPath is not null && _cartellaLavoro is not null,
1 => _righeCorrenti is { Count: > 0 }, 1 => _righeCorrenti is { Count: > 0 } && _ultimaVerifica is not null,
2 => _ultimaCartellaRaw is not null, 2 => _ultimaCartellaRaw is not null,
3 => true, // risoluzione ricorsiva opzionale: non tutti gli assiemi hanno sotto-componenti 3 => true, // risoluzione ricorsiva opzionale: non tutti gli assiemi hanno sotto-componenti
_ => true, _ => true,
@@ -259,12 +266,17 @@ public partial class Form1 : Form
{ {
if (_dbPath is null || _cartellaLavoro is null) if (_dbPath is null || _cartellaLavoro is null)
{ {
MessageBox.Show(this, "Carica prima il database in fase 1.", "Nuova estrazione", MessageBox.Show(this, "Carica prima il database in fase 1.", "Nuovo",
MessageBoxButtons.OK, MessageBoxIcon.Information); MessageBoxButtons.OK, MessageBoxIcon.Information);
return; return;
} }
_percorsoInputCorrente = null; AzzeraDistinta();
VaiAllaFase(1);
}
private void AzzeraDistinta()
{
_righeCorrenti = null; _righeCorrenti = null;
_ultimaCartellaRaw = null; _ultimaCartellaRaw = null;
_ultimaVerifica = null; _ultimaVerifica = null;
@@ -272,8 +284,69 @@ public partial class Form1 : Form
_gridRaw.Rows.Clear(); _gridRaw.Rows.Clear();
_lstAnteprimaAsm.Items.Clear(); _lstAnteprimaAsm.Items.Clear();
ImpostaStatoImport("", Color.Black); ImpostaStatoImport("", Color.Black);
_rigaVerifica.Visible = false;
}
VaiAllaFase(1); private void CambiaModalitaDistinta()
{
if (_ripristinoModalita)
return;
if (_gridImport.Rows.Count > 0 && MessageBox.Show(this,
"Cambiando modalità la distinta corrente viene azzerata. Continuare?", "Distinta",
MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes)
{
_ripristinoModalita = true;
(_rbCustom.Checked ? _rbFile : _rbCustom).Checked = true;
_ripristinoModalita = false;
return;
}
AzzeraDistinta();
_rigaFile.Visible = _rbFile.Checked;
_rigaCustom.Visible = _rbCustom.Checked;
AggiornaStatoAvanti();
}
private void AggiungiCodice()
{
string codice = _txtCodice.Text.Trim();
if (codice.Length == 0)
return;
_righeCorrenti ??= new();
if (_righeCorrenti.Any(r => string.Equals(r.Componente, codice, StringComparison.OrdinalIgnoreCase)))
{
_txtCodice.SelectAll();
return;
}
_righeCorrenti.Add((codice, ""));
_gridImport.Rows.Add(codice, "");
InvalidaVerifica();
_txtCodice.Clear();
_txtCodice.Focus();
}
private void RimuoviCodiceSelezionato()
{
if (_gridImport.CurrentRow is not { } riga || _righeCorrenti is null)
return;
_righeCorrenti.RemoveAt(riga.Index); // griglia e lista hanno lo stesso ordine
_gridImport.Rows.Remove(riga);
InvalidaVerifica();
}
/// <summary>La lista è cambiata: la verifica presenza e il raw già creato non valgono più.</summary>
private void InvalidaVerifica()
{
_ultimaVerifica = null;
_ultimaCartellaRaw = null;
_gridRaw.Rows.Clear();
foreach (DataGridViewRow r in _gridImport.Rows)
{
r.DefaultCellStyle.BackColor = Color.Empty;
r.DefaultCellStyle.SelectionBackColor = Color.Empty;
r.DefaultCellStyle.SelectionForeColor = Color.Empty;
}
_rigaVerifica.Visible = _righeCorrenti is { Count: > 0 };
AggiornaStatoAvanti();
} }
private Panel BuildFaseDatabase() private Panel BuildFaseDatabase()
@@ -295,78 +368,78 @@ public partial class Form1 : Form
Dock = DockStyle.Top, Dock = DockStyle.Top,
AutoSize = true, AutoSize = true,
ForeColor = SystemColors.GrayText, ForeColor = SystemColors.GrayText,
Margin = new Padding(0, 0, 0, 12), Margin = new Padding(0, 0, 0, 16),
Text = "L'indice CAD viene caricato automaticamente all'avvio, se già presente. " Text = "L'indice CAD viene caricato automaticamente all'avvio, se già presente.",
+ "Usa 'Apri DB...' per selezionarne un altro, oppure 'Aggiorna indice' per "
+ "scansionare di nuovo il disco (X:\\).",
}; };
contenuto.Controls.Add(lblIstruzioni); contenuto.Controls.Add(lblIstruzioni);
var cardDb = UiStile.Card();
var contenutoCardDb = new TableLayoutPanel
{
Dock = DockStyle.Top,
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
ColumnCount = 1,
};
contenutoCardDb.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
var rigaTitoloDb = new FlowLayoutPanel
{
AutoSize = true,
FlowDirection = FlowDirection.LeftToRight,
Margin = new Padding(0, 0, 0, 6),
};
_indicatoreDb.Size = new Size(10, 10);
_indicatoreDb.Margin = new Padding(0, 5, 8, 0);
_indicatoreDb.Paint += (_, e) =>
{
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
var colore = _dbCaricato ? Color.FromArgb(30, 160, 90) : Color.FromArgb(200, 60, 60);
using var pennello = new SolidBrush(colore);
e.Graphics.FillEllipse(pennello, 0, 0, _indicatoreDb.Width - 1, _indicatoreDb.Height - 1);
};
rigaTitoloDb.Controls.Add(_indicatoreDb);
var lblTitoloDb = new Label
{
AutoSize = true,
Font = new Font(Font, FontStyle.Bold),
Text = "Database",
};
rigaTitoloDb.Controls.Add(lblTitoloDb);
contenutoCardDb.Controls.Add(rigaTitoloDb);
_lblDb.Dock = DockStyle.Top; _lblDb.Dock = DockStyle.Top;
_lblDb.AutoSize = true; _lblDb.AutoSize = true;
_lblDb.ForeColor = SystemColors.GrayText; _lblDb.ForeColor = SystemColors.ControlText;
_lblDb.Margin = new Padding(0, 0, 0, 4); _lblDb.Margin = new Padding(0, 0, 0, 2);
contenuto.Controls.Add(_lblDb); contenutoCardDb.Controls.Add(_lblDb);
_lblUltimoAggiornamento.AutoSize = true; _lblUltimoAggiornamento.AutoSize = true;
_lblUltimoAggiornamento.ForeColor = SystemColors.GrayText; _lblUltimoAggiornamento.ForeColor = SystemColors.GrayText;
_lblUltimoAggiornamento.Margin = new Padding(0, 0, 0, 12); _lblUltimoAggiornamento.Margin = new Padding(0, 0, 0, 14);
contenuto.Controls.Add(_lblUltimoAggiornamento); contenutoCardDb.Controls.Add(_lblUltimoAggiornamento);
var bottoni = new FlowLayoutPanel { AutoSize = true, FlowDirection = FlowDirection.LeftToRight }; var bottoniDb = new FlowLayoutPanel { AutoSize = true, FlowDirection = FlowDirection.LeftToRight };
_btnApriDb.Text = "Apri DB..."; _btnApriDb.Text = "Apri DB...";
_btnApriDb.Margin = new Padding(0, 0, 8, 0); _btnApriDb.Margin = new Padding(0, 0, 8, 0);
UiStile.Secondario(_btnApriDb); UiStile.Secondario(_btnApriDb);
UiStile.Arrotonda(_btnApriDb);
_btnApriDb.Click += (_, _) => ScegliDatabase(); _btnApriDb.Click += (_, _) => ScegliDatabase();
bottoni.Controls.Add(_btnApriDb); bottoniDb.Controls.Add(_btnApriDb);
_btnAggiorna.Text = "Aggiorna indice"; _btnAggiorna.Text = "Aggiorna indice";
UiStile.Secondario(_btnAggiorna); UiStile.Secondario(_btnAggiorna);
UiStile.Arrotonda(_btnAggiorna);
_btnAggiorna.Click += (_, _) => AggiornaIndice(); _btnAggiorna.Click += (_, _) => AggiornaIndice();
bottoni.Controls.Add(_btnAggiorna); bottoniDb.Controls.Add(_btnAggiorna);
contenuto.Controls.Add(bottoni); contenutoCardDb.Controls.Add(bottoniDb);
cardDb.Controls.Add(contenutoCardDb);
var lblIstruzioniCartella = new Label contenuto.Controls.Add(cardDb);
{
Dock = DockStyle.Top,
AutoSize = true,
ForeColor = SystemColors.GrayText,
Margin = new Padding(0, 20, 0, 4),
Text = "Cartella di lavoro: qui vengono scritti il CSV della distinta e la "
+ "cartella 'raw' (gli STEP esportati con Creo vanno invece in 'step', "
+ "dentro la stessa cartella). Proposta automaticamente (ricordata da un "
+ "avvio all'altro), modificabile con 'Cambia cartella...'.",
};
contenuto.Controls.Add(lblIstruzioniCartella);
_lblCartellaLavoro.Dock = DockStyle.Top;
_lblCartellaLavoro.AutoSize = true;
_lblCartellaLavoro.ForeColor = SystemColors.GrayText;
_lblCartellaLavoro.Margin = new Padding(0, 0, 0, 4);
contenuto.Controls.Add(_lblCartellaLavoro);
_btnCambiaCartella.Text = "Cambia cartella...";
UiStile.Secondario(_btnCambiaCartella);
_btnCambiaCartella.Click += (_, _) => ScegliCartellaLavoro();
contenuto.Controls.Add(_btnCambiaCartella);
var lblManutenzione = new Label
{
Dock = DockStyle.Top,
AutoSize = true,
ForeColor = SystemColors.GrayText,
Margin = new Padding(0, 20, 0, 8),
Text = "Manutenzione: svuota le cartelle 'raw' e 'step' nella cartella di lavoro "
+ "prima di un nuovo giro (non tocca l'indice, solo le copie locali).",
};
contenuto.Controls.Add(lblManutenzione);
_btnPulisci.Text = "Pulisci raw e step";
UiStile.Secondario(_btnPulisci);
_btnPulisci.Click += (_, _) => PulisciRawEStep();
contenuto.Controls.Add(_btnPulisci);
var panel = new Panel(); var panel = new Panel();
panel.Controls.Add(contenuto); panel.Controls.Add(contenuto);
@@ -397,20 +470,86 @@ public partial class Form1 : Form
}; };
contenuto.Controls.Add(lblIstruzioni); contenuto.Controls.Add(lblIstruzioni);
var rigaModalita = new FlowLayoutPanel
{
AutoSize = true,
FlowDirection = FlowDirection.LeftToRight,
Margin = new Padding(0, 0, 0, 8),
};
rigaModalita.Controls.Add(_rbFile);
rigaModalita.Controls.Add(_rbCustom);
_rbCustom.CheckedChanged += (_, _) => CambiaModalitaDistinta();
contenuto.Controls.Add(rigaModalita);
_rigaCustom.AutoSize = true;
_rigaCustom.FlowDirection = FlowDirection.LeftToRight;
_rigaCustom.Margin = new Padding(0, 0, 0, 8);
_rigaCustom.Visible = false;
var btnAggiungi = new Button { Text = "Aggiungi", Margin = new Padding(0, 0, 8, 0) };
UiStile.Secondario(btnAggiungi);
btnAggiungi.Click += (_, _) => AggiungiCodice();
_txtCodice.KeyDown += (_, e) =>
{
if (e.KeyCode != Keys.Enter)
return;
e.SuppressKeyPress = true;
AggiungiCodice();
};
_rigaCustom.Controls.Add(_txtCodice);
_rigaCustom.Controls.Add(btnAggiungi);
_rigaCustom.Controls.Add(new Label
{
AutoSize = true,
ForeColor = SystemColors.GrayText,
Margin = new Padding(0, 9, 0, 0),
Text = "Digita un codice e premi Invio (Canc sulla riga per toglierlo)",
});
contenuto.Controls.Add(_rigaCustom);
_gridImport.KeyDown += (_, e) =>
{
if (e.KeyCode == Keys.Delete && _rbCustom.Checked)
RimuoviCodiceSelezionato();
};
var rigaImporta = _rigaFile;
rigaImporta.AutoSize = true;
rigaImporta.FlowDirection = FlowDirection.LeftToRight;
rigaImporta.Margin = new Padding(0, 0, 0, 8);
var btnSfoglia = new Button var btnSfoglia = new Button
{ {
Text = "Scegli file distinta...", Text = "Scegli file distinta...",
Margin = new Padding(0, 0, 0, 8), Margin = new Padding(0, 0, 8, 0),
}; };
UiStile.Secondario(btnSfoglia); UiStile.Secondario(btnSfoglia);
btnSfoglia.Click += (_, _) => ScegliFileDistinta(); btnSfoglia.Click += (_, _) => ScegliFileDistinta();
contenuto.Controls.Add(btnSfoglia); rigaImporta.Controls.Add(btnSfoglia);
rigaImporta.Controls.Add(new Label
{
AutoSize = true,
ForeColor = SystemColors.GrayText,
Margin = new Padding(0, 9, 0, 0),
Text = "1. Carica la distinta",
});
contenuto.Controls.Add(rigaImporta);
_rigaVerifica.AutoSize = true;
_rigaVerifica.FlowDirection = FlowDirection.LeftToRight;
_rigaVerifica.Margin = new Padding(0, 0, 0, 8);
_rigaVerifica.Visible = false; // compare solo dopo un import riuscito (file o drag&drop)
_btnVerifica.Text = "Verifica presenza"; _btnVerifica.Text = "Verifica presenza";
_btnVerifica.Margin = new Padding(0, 0, 0, 8); _btnVerifica.Margin = new Padding(0, 0, 8, 0);
UiStile.Secondario(_btnVerifica); UiStile.Secondario(_btnVerifica);
_btnVerifica.Click += (_, _) => VerificaCodiciNelDb(); _btnVerifica.Click += (_, _) => VerificaCodiciNelDb();
contenuto.Controls.Add(_btnVerifica); _rigaVerifica.Controls.Add(_btnVerifica);
_rigaVerifica.Controls.Add(new Label
{
AutoSize = true,
ForeColor = SystemColors.GrayText,
Margin = new Padding(0, 9, 0, 0),
Text = "2. Verifica la presenza dei file sul server",
});
contenuto.Controls.Add(_rigaVerifica);
_lblImportStatus.Dock = DockStyle.Top; _lblImportStatus.Dock = DockStyle.Top;
_lblImportStatus.AutoSize = false; _lblImportStatus.AutoSize = false;
@@ -460,22 +599,25 @@ public partial class Form1 : Form
}; };
contenuto.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); contenuto.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
var lblIstruzioni = new Label var rigaCreaRaw = new FlowLayoutPanel
{ {
Dock = DockStyle.Top,
AutoSize = true, AutoSize = true,
ForeColor = SystemColors.GrayText, FlowDirection = FlowDirection.LeftToRight,
Margin = new Padding(0, 0, 0, 8), Margin = new Padding(0, 0, 0, 12),
Text = "Cerca nell'indice il percorso CAD di ogni componente importato e copia i "
+ "file trovati in una cartella 'raw' dentro la cartella di lavoro (fase 1).",
}; };
contenuto.Controls.Add(lblIstruzioni);
_btnCreaRaw.Text = "Crea cartella raw"; _btnCreaRaw.Text = "Crea cartella raw";
_btnCreaRaw.Margin = new Padding(0, 0, 0, 12); _btnCreaRaw.Margin = new Padding(0, 0, 8, 0);
UiStile.Primario(_btnCreaRaw); UiStile.Primario(_btnCreaRaw);
_btnCreaRaw.Click += (_, _) => CreaCartellaRaw(); _btnCreaRaw.Click += (_, _) => CreaCartellaRaw();
contenuto.Controls.Add(_btnCreaRaw); rigaCreaRaw.Controls.Add(_btnCreaRaw);
rigaCreaRaw.Controls.Add(new Label
{
AutoSize = true,
ForeColor = SystemColors.GrayText,
Margin = new Padding(0, 9, 0, 0),
Text = "Copia i file dal server nella cartella 'raw'.",
});
contenuto.Controls.Add(rigaCreaRaw);
_gridRaw.Dock = DockStyle.Fill; _gridRaw.Dock = DockStyle.Fill;
_gridRaw.ReadOnly = true; _gridRaw.ReadOnly = true;
@@ -526,8 +668,7 @@ public partial class Form1 : Form
Margin = new Padding(0, 0, 0, 12), Margin = new Padding(0, 0, 0, 12),
Text = "Gli assiemi referenziano altri PRT e sotto-assiemi al loro interno: premi " Text = "Gli assiemi referenziano altri PRT e sotto-assiemi al loro interno: premi "
+ "qui per risolvere l'albero componenti (ricorsivamente) e copiare tutto ciò che " + "qui per risolvere l'albero componenti (ricorsivamente) e copiare tutto ciò che "
+ "viene trovato nell'indice dentro 'raw'. Passo opzionale: se la distinta contiene " + "viene trovato nell'indice dentro 'raw'.",
+ "solo parti singole puoi anche passare direttamente alla fase successiva.",
}; };
contenuto.Controls.Add(lblIstruzioni); contenuto.Controls.Add(lblIstruzioni);
@@ -614,10 +755,15 @@ public partial class Form1 : Form
private void ImportaDistinta(string percorso) private void ImportaDistinta(string percorso)
{ {
if (_rbCustom.Checked)
{
_rbFile.Checked = true; // chiede conferma se c'è una lista; se rifiuta resta in custom
if (_rbCustom.Checked)
return;
}
try try
{ {
var (csv, righe) = DistintaBaseImporter.ConvertiInCsv(percorso, _cartellaLavoro!); var (_, righe) = DistintaBaseImporter.ConvertiInCsv(percorso, _cartellaLavoro!);
_percorsoInputCorrente = percorso;
_righeCorrenti = righe; _righeCorrenti = righe;
_gridImport.Rows.Clear(); _gridImport.Rows.Clear();
@@ -629,14 +775,15 @@ public partial class Form1 : Form
_ultimaCartellaRaw = null; _ultimaCartellaRaw = null;
_ultimaVerifica = null; _ultimaVerifica = null;
ImpostaStatoImport($"CSV generato ({righe.Count} righe): {csv}", Color.DarkGreen); ImpostaStatoImport("", Color.Black);
_rigaVerifica.Visible = true;
} }
catch (Exception ex) catch (Exception ex)
{ {
_percorsoInputCorrente = null;
_righeCorrenti = null; _righeCorrenti = null;
_gridImport.Rows.Clear(); _gridImport.Rows.Clear();
ImpostaStatoImport($"Errore durante la conversione: {ex.Message}", Color.DarkRed); ImpostaStatoImport($"Errore durante la conversione: {ex.Message}", Color.DarkRed);
_rigaVerifica.Visible = false;
} }
AggiornaStatoAvanti(); AggiornaStatoAvanti();
} }
@@ -660,15 +807,20 @@ public partial class Form1 : Form
foreach (DataGridViewRow riga in _gridImport.Rows) foreach (DataGridViewRow riga in _gridImport.Rows)
{ {
string componente = (string)riga.Cells["componente"].Value!; string componente = (string)riga.Cells["componente"].Value!;
riga.DefaultCellStyle.BackColor = trovati.ContainsKey(componente) var colore = trovati.ContainsKey(componente)
? Color.FromArgb(198, 239, 206) ? Color.FromArgb(198, 239, 206)
: Color.FromArgb(255, 199, 206); : Color.FromArgb(255, 199, 206);
// la selezione mantiene il colore verde/rosso, altrimenti il blu nasconde l'esito
riga.DefaultCellStyle.BackColor = colore;
riga.DefaultCellStyle.SelectionBackColor = colore;
riga.DefaultCellStyle.SelectionForeColor = Color.Black;
} }
AggiornaStatoAvanti();
} }
private void CreaCartellaRaw() private void CreaCartellaRaw()
{ {
if (_percorsoInputCorrente is null || _righeCorrenti is null) if (_righeCorrenti is null)
return; return;
// L'assieme generale è già una riga di _righeCorrenti (aggiunta da // L'assieme generale è già una riga di _righeCorrenti (aggiunta da
@@ -707,7 +859,8 @@ public partial class Form1 : Form
.ToList(); .ToList();
if (cartelle.Count == 0) if (cartelle.Count == 0)
{ {
ImpostaStatoImport($"Nessuna cartella 'raw' o 'step' trovata in {radice}.", Color.DarkRed); MessageBox.Show(this, $"Nessuna cartella 'raw' o 'step' trovata in {radice}.",
"Pulisci raw e step", MessageBoxButtons.OK, MessageBoxIcon.Information);
return; return;
} }
@@ -734,7 +887,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 /// <summary>Tutti i codici prt/asm noti all'indice, con i relativi percorsi (ultima
@@ -1097,21 +1292,19 @@ public partial class Form1 : Form
private static readonly string CartellaImpostazioni = Path.Combine( private static readonly string CartellaImpostazioni = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "PrepStep"); 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"); 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 /// <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 /// c'è ancora una preferenza salvata (o punta a una cartella cancellata), la cartella di
/// trova il DB (più sensata dell'eseguibile: quella è dove l'utente tiene il proprio lavoro, /// installazione (%LOCALAPPDATA%\Programs\PrepStep col setup). Sempre modificabile con
/// non dov'è installata la app). Richiede quindi che InizializzaDatabase sia già stata /// "File → Scegli cartella di lavoro...".</summary>
/// chiamata. Sempre modificabile con "Cambia cartella..." in fase 1.</summary>
private void InizializzaCartellaLavoro() private void InizializzaCartellaLavoro()
{ {
var ultima = LeggiUltimaCartellaLavoro(); var ultima = LeggiUltimaCartellaLavoro();
_cartellaLavoro = ultima is not null && Directory.Exists(ultima) _cartellaLavoro = ultima is not null && Directory.Exists(ultima)
? ultima ? ultima
: (_dbPath is not null ? Path.GetDirectoryName(_dbPath) : null) ?? AppContext.BaseDirectory; : AppContext.BaseDirectory;
AggiornaLabelCartellaLavoro(); AggiornaStatoAvanti();
} }
private void ScegliCartellaLavoro() private void ScegliCartellaLavoro()
@@ -1126,13 +1319,10 @@ public partial class Form1 : Form
string percorso = dialog.SelectedPath; string percorso = dialog.SelectedPath;
_cartellaLavoro = percorso; _cartellaLavoro = percorso;
AggiornaLabelCartellaLavoro();
SalvaUltimaCartellaLavoro(percorso); SalvaUltimaCartellaLavoro(percorso);
AggiornaStatoAvanti(); AggiornaStatoAvanti();
} }
private void AggiornaLabelCartellaLavoro() => _lblCartellaLavoro.Text = $"Cartella di lavoro: {_cartellaLavoro}";
private static string? LeggiUltimaCartellaLavoro() private static string? LeggiUltimaCartellaLavoro()
{ {
try try
@@ -1160,24 +1350,21 @@ public partial class Form1 : Form
private void InizializzaDatabase() private void InizializzaDatabase()
{ {
var candidati = new[] // Il DB sta sempre nella cartella di installazione; "Apri DB..." ne carica un altro
{ // solo per la sessione corrente, senza ricordarlo al prossimo avvio.
LeggiUltimoDbUsato(), string predefinito = Path.Combine(AppContext.BaseDirectory, "cad_index.db");
Path.Combine(AppContext.BaseDirectory, "cad_index.db"), if (!File.Exists(predefinito))
Path.Combine(Directory.GetCurrentDirectory(), "cad_index.db"),
};
_dbPath = candidati.FirstOrDefault(p => p is not null && File.Exists(p));
if (_dbPath is null)
{ {
_lblStatus.Text = "Database non trovato. Usa 'Apri DB...' per selezionarlo oppure 'Aggiorna indice' per crearlo."; _lblStatus.Text = "Database non trovato. Usa 'Apri DB...' per selezionarlo oppure 'Aggiorna indice' per crearlo.";
_lblDb.Text = "Nessun database caricato. Premi 'Aggiorna indice' per crearlo da X:\\, oppure 'Apri DB...' se ne hai gi\u00e0 uno."; _lblDb.Text = "Nessun database caricato. Premi 'Aggiorna indice' per crearlo da X:\\, oppure 'Apri DB...' se ne hai gi\u00e0 uno.";
_lblUltimoAggiornamento.Text = ""; _lblUltimoAggiornamento.Text = "";
_dbCaricato = false;
_indicatoreDb.Invalidate();
AggiornaStatoAvanti(); AggiornaStatoAvanti();
return; return;
} }
CaricaDatabase(_dbPath); CaricaDatabase(predefinito);
} }
private void ScegliDatabase() private void ScegliDatabase()
@@ -1210,43 +1397,17 @@ public partial class Form1 : Form
? $"Ultimo aggiornamento indice: {ultimaScansione.ToLocalTime():dd/MM/yyyy HH:mm}" ? $"Ultimo aggiornamento indice: {ultimaScansione.ToLocalTime():dd/MM/yyyy HH:mm}"
: "Ultimo aggiornamento indice: sconosciuto"; : "Ultimo aggiornamento indice: sconosciuto";
_lblStatus.Text = "Pronto."; _lblStatus.Text = "Pronto.";
SalvaUltimoDbUsato(path);
} }
catch (Exception ex) catch (Exception ex)
{ {
MessageBox.Show(this, $"Impossibile aprire il database:\n{ex.Message}", "Errore", MessageBox.Show(this, $"Impossibile aprire il database:\n{ex.Message}", "Errore",
MessageBoxButtons.OK, MessageBoxIcon.Error); MessageBoxButtons.OK, MessageBoxIcon.Error);
} }
_dbCaricato = _dbPath is not null;
_indicatoreDb.Invalidate();
AggiornaStatoAvanti(); AggiornaStatoAvanti();
} }
/// <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() private async void AggiornaIndice()
{ {
string dbPath = _dbPath ?? Path.Combine(AppContext.BaseDirectory, "cad_index.db"); string dbPath = _dbPath ?? Path.Combine(AppContext.BaseDirectory, "cad_index.db");
+310 -49
View File
@@ -1,70 +1,239 @@
using System.Runtime.InteropServices;
namespace PrepStep; namespace PrepStep;
/// <summary>Guida interattiva richiamabile da "Aiuto ▸ Guida" (F1): sintesi delle 5 fasi del /// <summary>Guida interattiva richiamabile da "Aiuto ▸ Guida" (F1): stessa guida completa di
/// wizard, per chi apre l'app senza aver letto i docs. Il dettaglio completo (formato .asm, /// docs/guida-utente.md (passo-passo fino a Creo Distributed Batch), pensata per chi apre
/// schema DB, ecc.) resta solo in docs/architettura.md e affini, per non duplicarlo qui.</summary> /// 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 public class GuidaForm : Form
{ {
private static readonly (string Titolo, string Testo)[] Sezioni = private sealed record Sezione(
{ string Titolo,
("Panoramica", """ string Intro,
Cevolani StepPrep prepara i file CAD Pro-ENGINEER/Creo da passare a Creo string[]? Passi = null,
Distributed Batch per l'esportazione STEP: indicizza i file di una share di string? NotaTesto = null,
rete e guida attraverso 5 fasi, una alla volta, con "Avanti"/"Indietro". bool NotaAttenzione = false);
"File ▸ Nuova estrazione" (Ctrl+N) azzera distinta/raw/verifica per // Palette: blu primario dell'app per titoli/passi, ambra tenue solo per gli avvisi che
ripartire con una nuova distinta base, senza dover ricaricare il database. // 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.
("1. Database", """ private static readonly Color ColoreTesto = Color.FromArgb(40, 40, 40);
Carica o aggiorna l'indice "cad_index.db" a partire dalle cartelle di primo private static readonly Color ColoreNotaInfoSfondo = Color.FromArgb(232, 240, 250);
livello del disco/share (radice di default X:\). Qui si sceglie anche la private static readonly Color ColoreNotaInfoTesto = Color.FromArgb(15, 65, 120);
cartella di lavoro, dove finiscono CSV, "raw" e "step". private static readonly Color ColoreNotaAttenzioneSfondo = Color.FromArgb(253, 240, 219);
"""), private static readonly Color ColoreNotaAttenzioneTesto = Color.FromArgb(140, 90, 10);
("2. Importa Distinta Base", """
Converte il file della distinta (trascinabile nella finestra) in un CSV private static readonly Sezione[] Sezioni =
componente;descrizione nella cartella di lavoro. Se il file segue la {
convenzione del gestionale, aggiunge anche l'assieme generale come riga. new("Panoramica",
"""), "PrepStep - Cevolani prepara i file CAD Pro-ENGINEER/Creo da passare a Creo " +
("3. Crea cartella raw", """ "Distributed Batch per l'esportazione STEP: indicizza i file di una share di rete " +
Cerca nell'indice i file CAD dei componenti del CSV e li copia nella "e guida attraverso 5 fasi, una alla volta, con \"Avanti\"/\"Indietro\".",
sottocartella "raw" della cartella di lavoro. NotaTesto: "\"File ▸ Nuovo\" (Ctrl+N) azzera lo stato di distinta/raw/verifica per ripartire " +
"""), "con una nuova distinta base, senza dover ricaricare il database."),
("4. Trova componenti referenziati", """
Legge i file .asm già in "raw" per risolvere ricorsivamente sotto-assiemi e new("Dove finiscono i file",
parti citate, copiando anche quelli in "raw": componenti soppressi (non in "Tutto sta nella cartella di lavoro, che di default è la cartella di " +
distinta ma necessari per aprire l'assieme) e riferimenti esterni (assembly "installazione (%LOCALAPPDATA%\\Programs\\PrepStep):",
feature, datum copiati) inclusi. Passi:
"""), [
("5. Verifica completezza", """ "Il database cad_index.db sta sempre nella cartella di installazione.",
Mostra i file .asm della distinta già in "raw", trascinabili su Creo per "Il CSV di ogni distinta importata, \"raw\" (file CAD delle fasi 3-4) e " +
l'esportazione STEP (passo manuale, fuori da quest'app). "Verifica "\"step\" (STEP esportati da Creo, cartella che indichi tu a Creo) stanno " +
completezza assiemi" controlla che tutti i sotto-componenti risultino "nella cartella di lavoro. Il file .xls del gestionale resta dove l'hai " +
presenti nell'indice. "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. " +
"In alternativa scegli \"Lista personalizzata\" e digita i codici uno alla " +
"volta (Invio per aggiungere, Canc per togliere): nessun CSV, l'assieme " +
"generale va inserito a mano.",
"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() public GuidaForm()
{ {
Text = "Guida"; Text = "Guida";
ClientSize = new Size(640, 420); ClientSize = new Size(1000, 680);
MinimumSize = new Size(480, 320); MinimumSize = new Size(640, 440);
StartPosition = FormStartPosition.CenterParent; StartPosition = FormStartPosition.CenterParent;
Font = new Font("Segoe UI", 9.5f); Font = new Font("Segoe UI", 9.5f);
try
{
Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath);
}
catch (Exception ex) when (ex is IOException or ArgumentException)
{
// ponytail: nessuna icona nell'exe (es. lanciato da dotnet run) -> resta quella di default
}
var 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 var lista = new ListBox
{ {
Dock = DockStyle.Left, Dock = DockStyle.Left,
Width = 200, Width = 230,
IntegralHeight = false, IntegralHeight = false,
BorderStyle = BorderStyle.None, 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);
}; };
foreach (var (titolo, _) in Sezioni)
lista.Items.Add(titolo);
var testo = new RichTextBox var testo = new TestoSolaLettura
{ {
Dock = DockStyle.Fill, Dock = DockStyle.Fill,
ReadOnly = true, ReadOnly = true,
TabStop = false,
BorderStyle = BorderStyle.None, BorderStyle = BorderStyle.None,
BackColor = Color.White, BackColor = Color.White,
Font = new Font(Font.FontFamily, 10f), Font = new Font(Font.FontFamily, 10f),
@@ -73,19 +242,111 @@ public class GuidaForm : Form
lista.SelectedIndexChanged += (_, _) => lista.SelectedIndexChanged += (_, _) =>
{ {
if (lista.SelectedIndex >= 0) if (lista.SelectedIndex >= 0)
testo.Text = Sezioni[lista.SelectedIndex].Testo.Trim(); ScriviSezione(testo, Sezioni[lista.SelectedIndex]);
}; };
var btnChiudi = new Button { Text = "Chiudi", Dock = DockStyle.Bottom, Height = 34 }; var (barraChiudi, azioniChiudi) = UiStile.FloatBar();
UiStile.Secondario(btnChiudi); var btnChiudi = new Button { Text = "Chiudi" };
UiStile.Primario(btnChiudi);
UiStile.Arrotonda(btnChiudi, 10);
btnChiudi.Click += (_, _) => Close(); btnChiudi.Click += (_, _) => Close();
azioniChiudi.Controls.Add(btnChiudi);
CancelButton = btnChiudi;
var contenuto = new Panel { Dock = DockStyle.Fill, Padding = new Padding(16) }; var contenuto = new Panel { Dock = DockStyle.Fill, Padding = new Padding(24, 20, 24, 20), BackColor = Color.White };
contenuto.Controls.Add(testo); contenuto.Controls.Add(testo);
Controls.Add(contenuto); Controls.Add(contenuto);
Controls.Add(lista); Controls.Add(lista);
Controls.Add(btnChiudi); Controls.Add(barraChiudi);
Controls.Add(headerPanel);
lista.SelectedIndex = 0; 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);
}
}
} }
+4 -3
View File
@@ -8,10 +8,11 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode> <ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode>
<ApplicationIcon>app.ico</ApplicationIcon> <ApplicationIcon>app.ico</ApplicationIcon>
<AssemblyTitle>Cevolani StepPrep</AssemblyTitle> <AssemblyTitle>PrepStep - Cevolani</AssemblyTitle>
<Product>Cevolani StepPrep</Product> <Product>PrepStep - Cevolani</Product>
<Company>Cevolani</Company> <Company>Cevolani</Company>
<Version>0.1.0</Version> <Copyright>Copyright (c) 2026 Davide Grilli</Copyright>
<Version>1.1.0</Version>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract> <IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
</PropertyGroup> </PropertyGroup>
+22 -3
View File
@@ -15,6 +15,14 @@ public class PrtMancantiForm : Form
ClientSize = new Size(720, 440); ClientSize = new Size(720, 440);
StartPosition = FormStartPosition.CenterParent; StartPosition = FormStartPosition.CenterParent;
Font = new Font("Segoe UI", 9.5f); Font = new Font("Segoe UI", 9.5f);
try
{
Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath);
}
catch (Exception ex) when (ex is IOException or ArgumentException)
{
// ponytail: nessuna icona nell'exe (es. lanciato da dotnet run) -> resta quella di default
}
var lblInfo = new Label var lblInfo = new Label
{ {
@@ -56,8 +64,17 @@ public class PrtMancantiForm : Form
grid.Rows[i].DefaultCellStyle.ForeColor = Color.DarkRed; grid.Rows[i].DefaultCellStyle.ForeColor = Color.DarkRed;
} }
var btnApriCartella = new Button { Text = "Apri cartella raw", Dock = DockStyle.Bottom, Height = 34 }; var (pannelloAzioni, flowAzioni) = UiStile.FloatBar();
UiStile.Secondario(btnApriCartella);
var btnChiudi = new Button { Text = "Chiudi" };
UiStile.Primario(btnChiudi);
UiStile.Arrotonda(btnChiudi, 10);
btnChiudi.Click += (_, _) => Close();
flowAzioni.Controls.Add(btnChiudi);
var btnApriCartella = new Button { Text = "Apri cartella raw", Margin = new Padding(8, 0, 0, 0) };
UiStile.SecondarioPieno(btnApriCartella);
UiStile.Arrotonda(btnApriCartella, 10);
btnApriCartella.Click += (_, _) => btnApriCartella.Click += (_, _) =>
{ {
try { Process.Start("explorer.exe", cartellaRaw); } try { Process.Start("explorer.exe", cartellaRaw); }
@@ -67,10 +84,12 @@ public class PrtMancantiForm : Form
MessageBoxButtons.OK, MessageBoxIcon.Error); MessageBoxButtons.OK, MessageBoxIcon.Error);
} }
}; };
flowAzioni.Controls.Add(btnApriCartella);
Controls.Add(grid); Controls.Add(grid);
Controls.Add(lblInfo); Controls.Add(lblInfo);
Controls.Add(btnApriCartella); Controls.Add(pannelloAzioni);
CancelButton = btnChiudi;
Resize += (_, _) => lblInfo.MaximumSize = new Size(ClientSize.Width - 24, 0); Resize += (_, _) => lblInfo.MaximumSize = new Size(ClientSize.Width - 24, 0);
} }
+109 -11
View File
@@ -12,6 +12,12 @@ internal static class UiStile
/// l'RGB.</summary> /// l'RGB.</summary>
public static readonly Color ColorePrimario = Color.FromArgb(0, 99, 177); public static readonly Color ColorePrimario = Color.FromArgb(0, 99, 177);
/// <summary>Sfondo/bordo delle card di stato (es. riepilogo Database/cartella di lavoro in
/// fase 1): un grigio-azzurro appena percettibile rispetto al bianco del resto della
/// pagina, sufficiente a separare "stato attuale" da "istruzioni" senza ombre/gradienti.</summary>
public static readonly Color ColoreSfondoCard = Color.FromArgb(247, 249, 251);
public static readonly Color ColoreBordoCard = Color.FromArgb(226, 231, 236);
// AutoSize+Padding invece di una Size fissa in pixel: il bottone si dimensiona sempre // AutoSize+Padding invece di una Size fissa in pixel: il bottone si dimensiona sempre
// al proprio testo (a qualunque lunghezza/font), niente più testo che va a capo e viene // al proprio testo (a qualunque lunghezza/font), niente più testo che va a capo e viene
// tagliato in verticale quando non ci sta nella larghezza indovinata a mano. // tagliato in verticale quando non ci sta nella larghezza indovinata a mano.
@@ -48,22 +54,38 @@ internal static class UiStile
b.Cursor = Cursors.Hand; b.Cursor = Cursors.Hand;
} }
/// <summary>Arrotonda gli angoli del bottone. Si aggancia a SizeChanged invece di calcolare /// <summary>Come Secondario, ma pieno invece che a bordo: sulle card bianche delle floatbar
/// la Region una volta sola: con AutoSize la dimensione finale si conosce solo a layout /// (vedi FloatBar) il bordo sottile di Secondario è quasi invisibile su sfondo bianco, e il
/// avvenuto (dopo Text/Padding), non subito dopo la creazione del controllo.</summary> /// bottone risulterebbe visivamente diverso dal Primario pieno accanto — stessa costruzione
public static void Arrotonda(Button b, int raggio = 6) /// (nessun bordo, riempimento pieno), solo grigio chiaro invece che blu.</summary>
public static void SecondarioPieno(Button b)
{ {
b.SizeChanged += (_, _) => AggiornaRegione(b, raggio); DimensionamentoAutomatico(b);
AggiornaRegione(b, raggio); b.FlatStyle = FlatStyle.Flat;
b.FlatAppearance.BorderSize = 0;
b.FlatAppearance.MouseOverBackColor = Color.FromArgb(230, 233, 237);
b.FlatAppearance.MouseDownBackColor = Color.FromArgb(213, 217, 222);
b.BackColor = Color.FromArgb(241, 243, 245);
b.Cursor = Cursors.Hand;
} }
private static void AggiornaRegione(Button b, int raggio) /// <summary>Arrotonda gli angoli del controllo (bottone o card). Si aggancia a SizeChanged
/// invece di calcolare la Region una volta sola: con AutoSize la dimensione finale si
/// conosce solo a layout avvenuto (dopo Text/Padding), non subito dopo la creazione del
/// controllo.</summary>
public static void Arrotonda(Control c, int raggio = 6)
{ {
if (b.Width <= 0 || b.Height <= 0) c.SizeChanged += (_, _) => AggiornaRegione(c, raggio);
AggiornaRegione(c, raggio);
}
private static void AggiornaRegione(Control c, int raggio)
{
if (c.Width <= 0 || c.Height <= 0)
return; return;
int d = raggio * 2; int d = raggio * 2;
var rect = new Rectangle(Point.Empty, b.Size); var rect = new Rectangle(Point.Empty, c.Size);
using var percorso = new GraphicsPath(); using var percorso = new GraphicsPath();
percorso.AddArc(rect.X, rect.Y, d, d, 180, 90); percorso.AddArc(rect.X, rect.Y, d, d, 180, 90);
percorso.AddArc(rect.Right - d, rect.Y, d, d, 270, 90); percorso.AddArc(rect.Right - d, rect.Y, d, d, 270, 90);
@@ -71,7 +93,83 @@ internal static class UiStile
percorso.AddArc(rect.X, rect.Bottom - d, d, d, 90, 90); percorso.AddArc(rect.X, rect.Bottom - d, d, d, 90, 90);
percorso.CloseFigure(); percorso.CloseFigure();
b.Region?.Dispose(); c.Region?.Dispose();
b.Region = new Region(percorso); c.Region = new Region(percorso);
}
/// <summary>Card di stato: sfondo/bordo leggermente distinti dalla pagina, angoli arrotondati,
/// padding interno. Il bordo va disegnato a mano (Paint) perché Region ritaglia il
/// controllo ma non lascia spazio a un Border WinForms nativo sugli angoli arrotondati.</summary>
public static Panel Card()
{
var card = new Panel
{
Dock = DockStyle.Top,
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
BackColor = ColoreSfondoCard,
Padding = new Padding(18, 16, 18, 16),
Margin = new Padding(0, 0, 0, 16),
};
card.Paint += (_, e) =>
{
using var penna = new Pen(ColoreBordoCard);
e.Graphics.DrawRectangle(penna, 0, 0, card.Width - 1, card.Height - 1);
};
Arrotonda(card, 8);
return card;
}
/// <summary>Barra di bottoni "flottante" (Avanti/Indietro, Apri cartella raw/Chiudi, ecc.):
/// una card bianca staccata su tutti e 4 i lati dal bordo della finestra — non un footer a
/// tutta larghezza — con angoli arrotondati e bordo sottile, sospesa sopra lo sfondo grigio
/// della pagina invece che ancorata al bordo. Lo sfondo del contenitore esterno deve
/// combaciare con quello di default della Form (nessun BackColor impostato altrove:
/// SystemColors.Control) perché il "distacco" si veda solo nel margine attorno alla card,
/// non in una fascia di colore diverso.</summary>
public static (Panel Contenitore, FlowLayoutPanel Bottoni) FloatBar()
{
// Altezza minima per non tagliare il contenuto: bottone (MinimumSize.Height = 32) +
// padding verticale della riga bottoni (12+12) + padding verticale del contenitore
// esterno (10+16) = 82, con un po' di margine in più.
var contenitore = new Panel
{
Dock = DockStyle.Bottom,
Height = 88,
BackColor = SystemColors.Control,
Padding = new Padding(16, 10, 16, 16),
};
var card = new Panel { Dock = DockStyle.Fill, BackColor = Color.White };
card.Paint += (_, e) =>
{
using var penna = new Pen(ColoreBordoCard);
e.Graphics.DrawRectangle(penna, 0, 0, card.Width - 1, card.Height - 1);
};
Arrotonda(card, 14);
var bottoni = new FlowLayoutPanel
{
Dock = DockStyle.Fill,
FlowDirection = FlowDirection.RightToLeft,
Padding = new Padding(16, 12, 16, 12),
};
card.Controls.Add(bottoni);
contenitore.Controls.Add(card);
return (contenitore, bottoni);
}
/// <summary>Forza le stesse dimensioni per due bottoni della stessa barra (es. Avanti/
/// Indietro): AutoSize li dimensiona in base al proprio testo, e con testi di lunghezza
/// diversa (o solo uno dei due in grassetto, come Primario/Secondario) risulterebbero
/// disallineati sia in larghezza che in altezza — un dettaglio che un designer non
/// lascerebbe passare. Va chiamato dopo aver impostato Text/Font su entrambi.</summary>
public static void LarghezzaUguale(params Button[] bottoni)
{
int larghezza = bottoni.Max(b => b.PreferredSize.Width);
int altezza = bottoni.Max(b => b.PreferredSize.Height);
foreach (var b in bottoni)
b.MinimumSize = new Size(larghezza, altezza);
} }
} }
+45 -64
View File
@@ -1,4 +1,4 @@
# Cevolani StepPrep # PrepStep - Cevolani
App Windows Forms (.NET 10, progetto `PrepStep`) per indicizzare, cercare e 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 recuperare i file CAD Pro-ENGINEER/Creo di un disco/share di rete, e per trasformare
@@ -9,32 +9,40 @@ passaggio, non lo sostituisce.
## Funzionalità ## Funzionalità
1. **Indice CAD** — all'avvio carica automaticamente `cad_index.db` (se 1. **Indice CAD** — all'avvio carica automaticamente `cad_index.db` dalla
presente accanto all'eseguibile). Se manca, si può selezionare un DB cartella di installazione (`%LOCALAPPDATA%\Programs\PrepStep`). Se manca, lo si
esistente con **Apri DB...** o generarne uno nuovo con **Aggiorna crea con **Aggiorna indice**, oppure si apre un altro DB per la sola sessione
indice**, che scansiona il disco (di default `X:\`, tutte le cartelle di primo con **Apri DB...**. **Aggiorna indice** scansiona il disco (di default `X:\`,
livello che trova) e indicizza tutti i file. In questa fase si vede/cambia anche tutte le cartelle di primo livello che trova) e indicizza tutti i file. La
la **cartella di lavoro**: dove finiscono il CSV e la cartella `raw/` (gli STEP **cartella di lavoro** (dove finiscono il CSV e la cartella `raw/`; gli STEP
esportati con Creo vanno in `step/`, dentro la stessa cartella). Proposta esportati con Creo vanno in `step/`, dentro la stessa cartella) è di default la
automaticamente (ricordata da un avvio all'altro), modificabile con cartella di installazione, modificabile in qualunque momento da **File → Scegli
**Cambia cartella...**. 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 2. **Importa Distinta Base** — trascina (o scegli da file) l'estrazione
`.xls`/`.xml` del gestionale (foglio "Distinta Base", colonne `.xls`/`.xml` del gestionale (foglio "Distinta Base", colonne
Componente/Descrizione): viene generato un CSV nella cartella di lavoro, con Componente/Descrizione): viene generato un CSV nella cartella di lavoro, con
anteprima in griglia. Se il file segue la convenzione di nome anteprima in griglia. Se il file segue la convenzione di nome
`DBP_<codice>_<timestamp>` del gestionale, l'assieme generale dell'estrazione `DBP_<codice>_<timestamp>` del gestionale, l'assieme generale dell'estrazione
viene aggiunto come riga in più nel CSV (il foglio elenca solo i componenti viene aggiunto come riga in più nel CSV (il foglio elenca solo i componenti
sotto l'assieme, non l'assieme stesso). sotto l'assieme, non l'assieme stesso). Il bottone **Verifica presenza**
(compare solo dopo un import riuscito) cerca i componenti nel database — è
questo passo, non il solo import, a sbloccare la fase successiva.
3. **Crea cartella raw** — cerca nell'indice il percorso CAD di ogni 3. **Crea cartella raw** — cerca nell'indice il percorso CAD di ogni
componente importato (assieme generale incluso) e copia i file trovati componente importato (assieme generale incluso) e copia i file trovati
in una cartella `raw/` dentro la cartella di lavoro. in una cartella `raw/` dentro la cartella di lavoro.
4. **Trova componenti referenziati** — legge i metadati dei file `.asm` in 4. **Trova componenti referenziati** — legge i metadati dei file `.asm` in
`raw/` per trovare ricorsivamente i sotto-assiemi/parti che li `raw/` per trovare ricorsivamente i sotto-assiemi/parti che li
compongono, copiandoli a loro volta in `raw/`. compongono, copiandoli a loro volta in `raw/` (passo opzionale se la
5. **Verifica completezza assiemi** — confronta la distinta con l'indice per distinta contiene solo parti singole).
capire se manca qualche sotto-componente, e mostra i file `.asm` della 5. **Verifica completezza** — mostra i file `.asm` della distinta già in `raw/`,
distinta trascinabili su Creo/Esplora risorse per l'esportazione STEP con trascinabili su Creo Distributed Batch per l'esportazione STEP (passo
Creo Distributed Batch. manuale, fuori da quest'app): scrive in `step/`, dentro la cartella di
lavoro.
Primo uso: [`docs/tutorial-primo-giro.md`](docs/tutorial-primo-giro.md). Guida utente completa in
[`docs/guida-utente.md`](docs/guida-utente.md)
(stessa sintesi anche in-app da **Aiuto → Guida**, F1).
## Requisiti ## Requisiti
@@ -43,68 +51,41 @@ passaggio, non lo sostituisce.
- Accesso alla share di rete con i file CAD (`X:\` di default) per - Accesso alla share di rete con i file CAD (`X:\` di default) per
l'indicizzazione l'indicizzazione
## Build ## Build, test e rilascio
```powershell Build, avvio e test: [`docs/sviluppo.md`](docs/sviluppo.md). Pubblicazione dell'eseguibile e
cd PrepStep generazione del setup di installazione: [`docs/rilascio.md`](docs/rilascio.md).
dotnet build -c Release
```
## Avvio Il setup installa **solo per l'utente corrente**, in `%LOCALAPPDATA%\Programs\PrepStep`, senza
diritti di amministratore; si disinstalla da "Impostazioni → App installate".
Da sorgente:
```powershell
cd PrepStep
dotnet run -c Release
```
Oppure l'eseguibile già compilato:
```powershell
cd PrepStep\bin\Release\net10.0-windows
.\PrepStep.exe
```
## Pubblicazione (eseguibile standalone)
Genera un `.exe` self-contained per Windows x64, senza bisogno del runtime
.NET installato sulla macchina di destinazione:
```powershell
cd PrepStep
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -o publish
```
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.
## Struttura del progetto ## Struttura del progetto
Tutto il codice è in `PrepStep/` (unico progetto nella solution, GUI e Il codice dell'app è tutto in `PrepStep/` (GUI e indicizzazione integrate); accanto ci sono
indicizzazione integrate): 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); indice di tutti i doc in
[`docs/README.md`](docs/README.md).
- `Program.cs` — entry point. - `Program.cs` — entry point.
- `Form1.cs` — finestra principale (indice CAD, import Distinta Base, griglia - `Form1.cs` — finestra principale, wizard a 5 fasi e menu; UI costruita interamente in
risultati); UI costruita interamente in codice, nessun designer visuale. codice (`Form1.Designer.cs` è solo lo scheletro del template).
- `Indexer.cs` — scansione del disco e scrittura/aggiornamento del DB SQLite. - `Indexer.cs` — scansione del disco e scrittura/aggiornamento del DB SQLite.
- `DistintaBaseImporter.cs` — parsing del file Distinta Base (SpreadsheetML - `DistintaBaseImporter.cs` — parsing del file Distinta Base (SpreadsheetML
XML o Excel binario/OpenXML tramite ExcelDataReader) e generazione CSV. 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 Il database generato (`cad_index.db`), i file Excel/CSV della distinta
trascinati nella root e le cartelle `raw/`/`step/` sono rigenerabili e non trascinati nella root e le cartelle `raw/`/`step/` sono rigenerabili e non
sono tracciati in git (vedi `.gitignore`). sono tracciati in git (vedi `.gitignore`).
## Test Per i test vedi [`docs/sviluppo.md`](docs/sviluppo.md#test).
`PrepStep.Tests/` contiene unit test (regex di estrazione dei componenti ## Licenza
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
eseguire:
```powershell Software proprietario, © 2026 Davide Grilli, tutti i diritti riservati: vedi
cd PrepStep.Tests [`LICENSE`](LICENSE).
dotnet test
```
+61
View File
@@ -0,0 +1,61 @@
# Documentazione di PrepStep
Indice dei doc, organizzati secondo [Diátaxis](https://diataxis.fr/): ogni file ha **uno** scopo,
e cambia solo quando cambia il programma.
## Tutorial — imparare facendo
- [`tutorial-primo-giro.md`](tutorial-primo-giro.md) — il primo giro completo con una distinta vera.
## How-to — svolgere un compito
- [`guida-utente.md`](guida-utente.md) — i passi dell'utente fino a Creo Distributed Batch.
- [`risoluzione-problemi.md`](risoluzione-problemi.md) — sintomo → causa → cosa fare.
- [`sviluppo.md`](sviluppo.md) — build, test, script e regole per scrivere i doc.
- [`rilascio.md`](rilascio.md) — pubblicare e generare il setup di una nuova versione.
## Reference — consultare
- [`architettura.md`](architettura.md) — il processo delle 5 fasi e la mappa file-per-file.
- [`db-cad-index.md`](db-cad-index.md) — schema, vista e query di `cad_index.db`.
- [`formato-asm-record.md`](formato-asm-record.md) — parser esatto dei record `.asm`, casi limite (§7).
- [`formato-asm.md`](formato-asm.md) — euristica testuale di fallback.
- [`formato-prt-feature.md`](formato-prt-feature.md) — come leggere l'albero delle feature di un `.prt` (reverse engineering, non ancora nel codice).
- [`formato-prt-geometria.md`](formato-prt-geometria.md) — numeri, quote e geometria degli schizzi di un `.prt`, con la tabella di cosa manca per determinarlo per intero.
- [`formato-psb.md`](formato-psb.md) — il formato di serializzazione di tutte le sezioni (`.prt` e `.asm`): contenitore, grammatica, numeri, cosa c'è in ogni sezione, con un parser di riferimento in `scripts/psb/`.
- [`history.md`](history.md) — stato del reverse engineering dei file Creo: cosa è provato, cosa manca, da dove ripartire.
- [`glossario.md`](glossario.md) — i termini usati nei doc e nell'app.
- [`../CHANGELOG.md`](../CHANGELOG.md) — storico delle versioni.
## Explanation — capire il perché
- [`decisioni/`](decisioni/README.md) — registro delle scelte di progetto (ADR).
- [`copia-raw-parallela.md`](copia-raw-parallela.md) — perché e come la copia in `raw/` è parallela.
## Fonte unica (SSOT)
Ogni informazione vive in **un** posto; altrove si mette un link, non una copia.
| Informazione | Fonte |
|---|---|
| Versione e nome prodotto | `PrepStep/PrepStep.csproj` (`<Version>`, `<Product>`) |
| Schema e vista del DB | [`db-cad-index.md`](db-cad-index.md) |
| Formato dei record `.asm` e casi limite | [`formato-asm-record.md`](formato-asm-record.md) |
| Albero delle feature di un `.prt` | [`formato-prt-feature.md`](formato-prt-feature.md) |
| Quote, coordinate e geometria degli schizzi di un `.prt` | [`formato-prt-geometria.md`](formato-prt-geometria.md) |
| Come è scritta una sezione qualsiasi di un `.prt`/`.asm` | [`formato-psb.md`](formato-psb.md) |
| Processo delle 5 fasi, mappa dei file | [`architettura.md`](architettura.md) |
| Passi dell'utente | [`guida-utente.md`](guida-utente.md) |
| Build, test, regole sui doc | [`sviluppo.md`](sviluppo.md) |
| Procedura di rilascio e setup | [`rilascio.md`](rilascio.md) |
| Perché una scelta è fatta così | [`decisioni/`](decisioni/README.md) |
| Storico release | [`../CHANGELOG.md`](../CHANGELOG.md) |
`CLAUDE.md` e `README.md` riassumono soltanto e rimandano a questi file.
## Verifica
```powershell
dotnet run scripts/CheckDocs.cs
npx markdownlint-cli2
```
+90 -34
View File
@@ -1,9 +1,9 @@
# Architettura di PrepStep (Cevolani StepPrep) # Architettura di PrepStep
Panoramica di tutti i file sorgente e di come collaborano. `PrepStep` è il nome Panoramica di tutti i file sorgente e di come collaborano. `PrepStep` è il nome
tecnico del progetto (cartella, namespace, csproj, eseguibile `PrepStep.exe`); tecnico del progetto (cartella, namespace, csproj, eseguibile `PrepStep.exe`);
**Cevolani StepPrep** è il nome mostrato all'utente finale (titolo finestra, icona, **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 versione — vedi `<Version>`/`<Product>` in `PrepStep.csproj`). Per il "come si usa" vedi il `README.md` in root e la [`guida-utente.md`](guida-utente.md); per il
formato binario `.asm` vedi `docs/formato-asm.md` (euristica testuale) e formato binario `.asm` vedi `docs/formato-asm.md` (euristica testuale) e
`docs/formato-asm-record.md` (parser esatto dei record); per la parallelizzazione della `docs/formato-asm-record.md` (parser esatto dei record); per la parallelizzazione della
copia in `raw/` vedi `docs/copia-raw-parallela.md`. copia in `raw/` vedi `docs/copia-raw-parallela.md`.
@@ -35,7 +35,8 @@ successiva.
**1. Indice della share → `cad_index.db`.** `Indexer.ScanAsync` cammina tutte le cartelle di **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 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 incrementale (confronto su percorso+dimensione+data), così i giri successivi costano solo il
delta. Qui si sceglie anche la **cartella di lavoro**, che da qui in poi ospita tutto il resto. 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 **2. Distinta del gestionale → CSV.** `DistintaBaseImporter.ConvertiInCsv` riduce l'export a
due colonne `componente;descrizione`. Se il nome del file segue la convenzione due colonne `componente;descrizione`. Se il nome del file segue la convenzione
@@ -92,8 +93,8 @@ trascinano i `.asm` su Creo Distributed Batch, che scrive in `step/`.
| # | Fase | Bottone → metodo | Cosa fa | | # | Fase | Bottone → metodo | Cosa fa |
|---|---|---|---| |---|---|---|---|
| 1 | Database | "Aggiorna indice" → `Indexer.ScanAsync`; "Cambia cartella..." → `ScegliCartellaLavoro` | Crea/aggiorna `cad_index.db` scansionando la share; sceglie/mostra la cartella di lavoro | | 1 | Database | "Aggiorna indice" → `Indexer.ScanAsync` | Crea/aggiorna `cad_index.db` scansionando la share; un pallino verde/rosso accanto al titolo indica se un DB è caricato. La cartella di lavoro non è mostrata qui (cambiabile in qualunque momento da "File → Scegli cartella di lavoro...") |
| 2 | Importa Distinta Base | selezione file → `ImportaDistinta` → `DistintaBaseImporter.ConvertiInCsv` | Xls/Xml gestionale → CSV componente;descrizione, scritto nella cartella di lavoro | | 2 | Importa Distinta Base | selezione file/drag&drop → `ImportaDistinta` → `DistintaBaseImporter.ConvertiInCsv`, poi "Verifica presenza" → `VerificaCodiciNelDb` | Xls/Xml gestionale → CSV componente;descrizione, scritto nella cartella di lavoro (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 | | 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 | | 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) | | 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) |
@@ -102,21 +103,32 @@ trascinano i `.asm` su Creo Distributed Batch, che scrive in `step/`.
`_percorsoInputCorrente`, `_righeCorrenti` (righe del CSV), `_ultimaVerifica` (cache percorsi `_percorsoInputCorrente`, `_righeCorrenti` (righe del CSV), `_ultimaVerifica` (cache percorsi
CAD), `_ultimaCartellaRaw`. CAD), `_ultimaCartellaRaw`.
Menu (`MenuStrip`, in cima alla finestra): "File → Nuova estrazione" (Ctrl+N) azzera lo Menu (`MenuStrip`, in cima alla finestra): "File → Nuovo" (Ctrl+N) azzera lo
stato delle fasi 2-5 (`NuovaEstrazione`) per ripartire con una nuova distinta senza stato delle fasi 2-5 (`NuovaEstrazione`) e va direttamente alla fase 2, senza passare dalla
ricaricare il database/la cartella di lavoro (fase 1, che resta invariata); "Aiuto → Guida" fase 1: database e cartella di lavoro restano quelli già caricati, non serve rifare lo scan;
(F1) apre `GuidaForm`, sintesi interattiva delle 5 fasi per chi non ha letto questi docs. "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: Altre responsabilità in Form1.cs:
- `TrovaPercorsiCad` / `CaricaCodiciNoti` — query sul DB (`files_ultima_versione`), la - `TrovaPercorsiCad` / `CaricaCodiciNoti` — query sul DB (`files_ultima_versione`): la
seconda carica tutti i codici prt/asm noti in un colpo solo (usata sia per risolvere i prima risolve i componenti del CSV (fase 3), la seconda carica tutti i codici prt/asm noti
componenti sia da `AssemblyComponentScanner` per validare i token generici). 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 - `CopiaFileTrovati` — copia (in parallelo, grado 6) i file trovati in `raw/`, saltando
silenziosamente quelli irraggiungibili (share offline/permessi). silenziosamente quelli irraggiungibili (share offline/permessi).
- `PulisciRawEStep` — svuota `raw/` e `step/` nella cartella di lavoro (manutenzione, - `PulisciRawEStep` — svuota `raw/` e `step/` nella cartella di lavoro (manutenzione,
richiede conferma). richiede conferma).
- `InizializzaDatabase` / `ScegliDatabase` — localizzazione del DB (ultimo usato, salvato in - `EliminaCsv` — elimina i `*.csv` generati dall'import (uno per distinta) nella cartella di
`%AppData%/PrepStep/ultimo_db.txt`, o `cad_index.db` accanto all'eseguibile). 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 - `InizializzaCartellaLavoro` / `ScegliCartellaLavoro` — stesso pattern per la cartella di
lavoro (`ultima_cartella_lavoro.txt`), fallback `AppContext.BaseDirectory` se non c'è lavoro (`ultima_cartella_lavoro.txt`), fallback `AppContext.BaseDirectory` se non c'è
ancora nulla di salvato: nessuna dipendenza da un marker di repo, funziona identica ancora nulla di salvato: nessuna dipendenza da un marker di repo, funziona identica
@@ -189,8 +201,8 @@ completi".
file) nelle sole colonne Componente/Descrizione, foglio `"Distinta Base"`. Se il nome del file) nelle sole colonne Componente/Descrizione, foglio `"Distinta Base"`. Se il nome del
file segue la convenzione `DBP_<codice>_<timestamp>`, aggiunge l'assieme generale come riga file segue la convenzione `DBP_<codice>_<timestamp>`, aggiunge l'assieme generale come riga
in più (il foglio elenca solo i componenti sotto l'assieme). Scrive il CSV nella cartella in più (il foglio elenca solo i componenti sotto l'assieme). Scrive il CSV nella cartella
passata da `Form1` (la cartella di lavoro scelta in fase 1), non necessariamente accanto al passata da `Form1` (la cartella di lavoro, impostabile da "File → Scegli cartella di lavoro..."),
file di input. non necessariamente accanto al file di input.
## Finestre di dialogo (modali, mostrate a fine step) ## Finestre di dialogo (modali, mostrate a fine step)
@@ -207,9 +219,22 @@ completi".
per i secondari, blu pieno per l'azione principale di ogni fase), entrambi con per i secondari, blu pieno per l'azione principale di ogni fase), entrambi con
`AutoSize`+`Padding`+`MinimumSize` invece di una `Size` fissa in pixel indovinata a `AutoSize`+`Padding`+`MinimumSize` invece di una `Size` fissa in pixel indovinata a
mano (altrimenti il testo va a capo e si taglia in verticale se non ci sta nella mano (altrimenti il testo va a capo e si taglia in verticale se non ci sta nella
larghezza scelta). `Arrotonda` applica angoli arrotondati via `Region` calcolata su larghezza scelta). `SecondarioPieno` è la variante senza bordo (sfondo grigio chiaro
`SizeChanged` (non una volta sola: con `AutoSize` la dimensione finale del bottone si pieno) per i bottoni secondari sopra le card bianche di `Card`/`FloatBar`, dove il
conosce solo a layout avvenuto), usata sui bottoni "Avanti"/"Indietro" del wizard. bordo sottile di `Secondario` è troppo poco visibile su sfondo bianco. `Arrotonda`
applica angoli arrotondati a un `Control` qualsiasi (bottone o card) via `Region`
calcolata su `SizeChanged`, non una volta sola: con `AutoSize` la dimensione finale si
conosce solo a layout avvenuto — va chiamata dopo aver fissato le dimensioni finali del
controllo (es. dopo `LarghezzaUguale`), altrimenti la `Region` vecchia resta visibile
come bordo residuo finché non arriva un repaint completo. `Card` costruisce una card di
stato (sfondo/bordo distinti, angoli arrotondati), usata in fase 1 per il riepilogo
Database. `FloatBar` costruisce la barra di navigazione "flottante" in fondo alle
finestre (card bianca staccata su tutti e 4 i lati, non un footer a tutta larghezza):
usata per Avanti/Indietro nel wizard e per Chiudi/Apri cartella raw in
`PrtMancantiForm`. `LarghezzaUguale` forza la stessa larghezza e altezza tra due
bottoni della stessa barra (es. Avanti/Indietro, che altrimenti avrebbero dimensioni
diverse: testi di lunghezza diversa, e il font in grassetto di `Primario` richiede più
altezza di quello normale di `Secondario`/`SecondarioPieno`).
- **app.ico** — icona dell'app, referenziata da `<ApplicationIcon>` nel csproj (icona - **app.ico** — icona dell'app, referenziata da `<ApplicationIcon>` nel csproj (icona
dell'exe in Esplora risorse/taskbar) e riletta esplicitamente in `Form1.BuildUi()` dell'exe in Esplora risorse/taskbar) e riletta esplicitamente in `Form1.BuildUi()`
con `Icon.ExtractAssociatedIcon(Application.ExecutablePath)` per la titlebar, senza con `Icon.ExtractAssociatedIcon(Application.ExecutablePath)` per la titlebar, senza
@@ -219,39 +244,70 @@ completi".
sotto-assiemi trovati e copiati in `raw/`, più i codici referenziati che non corrispondono a sotto-assiemi trovati e copiati in `raw/`, più i codici referenziati che non corrispondono a
nessun file indicizzato (possono essere disegni, componenti davvero mancanti, o falsi nessun file indicizzato (possono essere disegni, componenti davvero mancanti, o falsi
positivi della scansione `.asm`). positivi della scansione `.asm`).
- **GuidaForm.cs** — guida interattiva ("Aiuto → Guida"/F1): lista delle 5 fasi a sinistra, - **GuidaForm.cs** — guida interattiva ("Aiuto → Guida"/F1): elenco delle sezioni a sinistra
testo a destra; sintesi, il dettaglio resta solo in questi docs. (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 ## Persistenza
- **DB** (`cad_index.db`, SQLite) — vedi tabella `files`/vista `files_ultima_versione` sopra. - **DB** (`cad_index.db`, SQLite) — vedi tabella `files`/vista `files_ultima_versione` sopra.
Percorso: cercato accanto all'eseguibile o nella working directory all'avvio; altrimenti Sempre `cad_index.db` accanto all'eseguibile (`AppContext.BaseDirectory`, cioè la cartella
selezionabile con "Apri DB...". Radice disco di scansione di default `X:\` di installazione): `InizializzaDatabase` carica solo quello, e "Aggiorna indice" lo crea lì
(`Form1.RadiceDiscoDefault`, hardcoded). se manca. "Apri DB..." carica un altro file solo per la sessione corrente, senza
- **Cartella di lavoro** — salvata in `%AppData%/PrepStep/ultima_cartella_lavoro.txt` ricordarlo. Radice disco di scansione
(stesso meccanismo del DB sopra), proposta di default `AppContext.BaseDirectory` la prima di default `X:\` (`Form1.RadiceDiscoDefault`, hardcoded).
volta; selezionabile/cambiabile con "Cambia cartella..." in fase 1. Tutti i file generati a - **Cartella di lavoro** — ricordata tra un avvio e l'altro in
valle ci finiscono dentro: il CSV della distinta, `raw/` (CAD dei componenti e `%AppData%/PrepStep/ultima_cartella_lavoro.txt`; se non c'è una preferenza salvata (o punta
sotto-componenti); `step/` non è scritta dall'app ma è dove l'utente esporta gli STEP a a una cartella cancellata) si propone la cartella di installazione
mano con Creo Distributed Batch, a partire dai file trascinati da `raw/` in fase 5. (`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`](rilascio.md).
## Script standalone (`scripts/`) ## 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`](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`, 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): dipendenze extra con `#:package Nome@versione` in testa al file):
- **ExtractCreoStrings.cs** — BOM di un `.asm` risolvendo ricorsivamente i sotto-assiemi - **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). Pro-E).
- **BomViaDb.cs** — come sopra ma con la tokenizzazione generica validata contro il DB, per - **BomViaDb.cs** — come sopra ma con la tokenizzazione generica validata contro il DB, per
intercettare anche i nomi alfabetici delle parti commerciali. intercettare anche i nomi alfabetici delle parti commerciali.
- **CreoBom.cs** — usa `AsmRecordReader` (stesso lettore esatto della GUI, fase 4/5, via - **CreoBom.cs** — usa `AsmRecordReader` (stesso lettore esatto della GUI, fase 4, via
`#:project`) invece dell'euristica testuale. Risolve i sottoassiemi cercandoli in cartelle `#:project`) invece dell'euristica testuale. Risolve i sottoassiemi cercandoli in cartelle
locali (`-p`), non contro `cad_index.db` come `ExtractCreoStrings.cs`/`BomViaDb.cs`. locali (`-p`), non contro `cad_index.db` come `ExtractCreoStrings.cs`/`BomViaDb.cs`.
- **Analyze.cs** — dump del contesto testo/hex attorno a un pattern in un file binario, per - **Analyze.cs** — dump del contesto testo/hex attorno a un pattern in un file binario, per
investigare a mano nuovi falsi positivi prima di toccare la regex di investigare a mano nuovi falsi positivi prima di toccare la regex di
`AssemblyComponentScanner`. `AssemblyComponentScanner`.
- **Query.cs** — query SQL ad-hoc contro `cad_index.db` da riga di comando. - **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/` > ⚠️ 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 > (che contiene un `.csproj` di tipo `WinExe`), esegue l'app GUI vera invece dello script
@@ -261,7 +317,7 @@ dipendenze extra con `#:package Nome@versione` in testa al file):
## Test ## Test
- **PrepStep.Tests/** (xUnit) — riferimento al progetto principale via `ProjectReference` - **PrepStep.Tests/** (xUnit) — riferimento al progetto principale via `ProjectReference`
+ `InternalsVisibleTo` (in `PrepStep.csproj`), così i test chiamano direttamente e `InternalsVisibleTo` (in `PrepStep.csproj`), così i test chiamano direttamente
`Indexer.ParseFilename` e `AssemblyComponentScanner.EstraiCodiciReferenziati` senza esporli `Indexer.ParseFilename` e `AssemblyComponentScanner.EstraiCodiciReferenziati` senza esporli
come API pubblica. File: `IndexerParseFilenameTests.cs`, `IndexerScanTests.cs`, come API pubblica. File: `IndexerParseFilenameTests.cs`, `IndexerScanTests.cs`,
`AssemblyComponentScannerTests.cs`, `AsmRecordReaderTests.cs`, `AssemblyComponentScannerTests.cs`, `AsmRecordReaderTests.cs`,
+25 -25
View File
@@ -29,10 +29,12 @@ componenti e nel filtro per cartella).
Vista `files_ultima_versione`: stessa struttura di `files`, filtrata alla sola riga con Vista `files_ultima_versione`: stessa struttura di `files`, filtrata alla sola riga con
`versione` più alta per ogni `(cartella_radice, codice, tipo_file)`, tramite `ROW_NUMBER() `versione` più alta per ogni `(cartella_radice, codice, tipo_file)`, tramite `ROW_NUMBER()
OVER (PARTITION BY ... ORDER BY versione DESC, data_modifica DESC, percorso_completo)`. Tutte OVER (PARTITION BY ... ORDER BY versione DESC, data_modifica DESC, percorso_completo)`. Le
le ricerche di componenti (`Form1.TrovaPercorsiCad`, `Form1.CaricaCodiciNoti`) interrogano ricerche per codice (`Form1.TrovaPercorsiCad` in fase 3, `Form1.CaricaCodiciNoti`)
questa vista, non `files` direttamente, così un componente con più versioni sulla share interrogano questa vista, non `files` direttamente, così un componente con più versioni sulla
risolve sempre all'ultima. Deliberatamente una `ROW_NUMBER()` invece di un self-join con 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()` `GROUP BY`: su ~200k righe un self-join degenera in tempi quasi quadratici, `ROW_NUMBER()`
resta lineare. resta lineare.
@@ -60,8 +62,9 @@ ciascuna — non una sola in assoluto. `TrovaPercorsiCad` gestisce questo caso d
Chiamato da "Aggiorna indice" (fase 1) e riutilizza lo stesso `dbPath` già caricato. Chiamato da "Aggiorna indice" (fase 1) e riutilizza lo stesso `dbPath` già caricato.
1. Crea lo schema se non esiste (`CREATE TABLE/VIEW IF NOT EXISTS`) — quindi funziona sia su 1. Crea lo schema se non esiste (`CREATE TABLE/INDEX IF NOT EXISTS`; la vista invece è
un DB vuoto (prima esecuzione) sia su uno esistente (aggiornamento incrementale). `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 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 `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 possono contenere copie datate con lo stesso codice numerico di file "ufficiali" altrove
@@ -109,24 +112,20 @@ sulla share, non tipicamente cercato dalla UI).
## Come l'app trova il DB all'avvio ## Come l'app trova il DB all'avvio
`Form1.InizializzaDatabase` prova, in ordine, i seguenti candidati e usa il primo che esiste `Form1.InizializzaDatabase` carica sempre e solo `cad_index.db` accanto all'eseguibile
su disco: (`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 Se non esiste, la fase 1 resta bloccata su "Database non trovato" finché l'utente non preme
`%AppData%/PrepStep/ultimo_db.txt` (`Form1.LeggiUltimoDbUsato`/`SalvaUltimoDbUsato`) "Aggiorna indice" (`Form1.AggiornaIndice`, che crea `cad_index.db` da zero accanto
2. `cad_index.db` accanto all'eseguibile (`AppContext.BaseDirectory`) all'eseguibile) o "Apri DB..." (`Form1.ScegliDatabase`, file picker libero: il DB scelto vale
3. `cad_index.db` nella working directory corrente solo per la sessione corrente, e "Aggiorna indice" aggiorna quello).
Se nessuno dei tre esiste, la fase 1 resta bloccata su "Database non trovato" finché l'utente
non preme "Apri DB..." (`Form1.ScegliDatabase`, file picker libero) o "Aggiorna indice"
(`Form1.AggiornaIndice`, che crea `cad_index.db` da zero accanto all'eseguibile se non c'è
ancora un `_dbPath`).
**Attenzione in sviluppo (dotnet run / IDE):** `AppContext.BaseDirectory` è la cartella di **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 build (`PrepStep/bin/Release/net10.0-windows` o `Debug/...`), non la radice del repo. L'app usa
quella cartella finisce per errore una copia di `cad_index.db` (es. creata lì da una vecchia quindi la copia di `cad_index.db` in quella cartella (creandola lì al primo "Aggiorna
sessione), l'app la userà invece di quella nella radice del repo, e le due copie divergeranno indice"), non quella nella radice del repo né quella installata: per usare un altro DB in
a ogni "Aggiorna indice" fatto nell'uno o nell'altro ambiente. Se i risultati di una ricerca 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 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 DB..." quale file è effettivamente caricato (mostrato in fase 1 accanto al conteggio dei file
indicizzati) prima di sospettare un bug nella ricerca. indicizzati) prima di sospettare un bug nella ricerca.
@@ -142,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 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 il primo per `tipo_file` (il più recente, dato l'`ORDER BY data_modifica DESC`) e scarta gli
altri. altri.
- **Risoluzione codici noti per lo scanner assiemi** (`Form1.CaricaCodiciNoti`, usata sia dalla - **Codici noti per lo scanner assiemi** (`Form1.CaricaCodiciNoti`, fase 4;
GUI in fase 4 sia da `scripts/BomViaDb.cs`): carica in un'unica query tutti i codici `scripts/BomViaDb.cs` fa una query equivalente per conto suo): carica in un'unica query tutti
prt/asm noti con i relativi percorsi, invece di una query per codice — usata per validare i i codici prt/asm noti, invece di una query per codice — usata solo per validare i token
token generici trovati nei binari `.asm` (vedi `docs/formato-asm.md`). 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 ## Debug ad-hoc
@@ -0,0 +1,22 @@
# 0001 — Indice dei file in un DB SQLite locale
**Stato:** accettata
## Contesto
La share `X:\` contiene centinaia di migliaia di file (ordine di ~200k righe indicizzate).
Cercare un codice direttamente sul disco di rete a ogni distinta è lento: il costo è dominato
dalla latenza di rete.
## Decisione
Scansionare la share una volta in un DB SQLite locale (`cad_index.db`) e interrogare quello.
L'aggiornamento è incrementale (confronto su percorso, dimensione e data), la vista
`files_ultima_versione` usa `ROW_NUMBER()` e non un self-join con `GROUP BY`.
## Conseguenze
- Le ricerche sono istantanee e non richiedono la share (la copia sì).
- L'indice può essere vecchio: per questo la fase 2 ha "Verifica presenza".
- Il DB è rigenerabile, quindi non è tracciato in git.
- Schema e criteri di pareggio: [`../db-cad-index.md`](../db-cad-index.md).
@@ -0,0 +1,18 @@
# 0002 — UI WinForms costruita interamente in codice
**Stato:** accettata
## Contesto
L'app è un wizard a 5 fasi con stile condiviso (bottoni, card, barre) e stato comune tra fasi.
## Decisione
Nessun designer visuale: `Form1.BuildUi` e i `BuildFase*` costruiscono i controlli, `UiStile`
centralizza lo stile. `Form1.Designer.cs` resta lo scheletro del template.
## Conseguenze
- Layout e stile modificabili e rivedibili come codice normale (diff leggibili).
- Dimensioni con `AutoSize`+`Padding` invece di pixel fissi, per non tagliare il testo.
- `Form1.cs` è grande (~1400 righe) perché contiene wizard e logica delle fasi.
@@ -0,0 +1,29 @@
# 0003 — Parser esatto dei `.asm` con fallback euristico
**Stato:** accettata
## Contesto
Il `.asm` Creo è un binario proprietario non documentato. La prima soluzione, un'euristica
testuale (`AssemblyComponentScanner`), contava le occorrenze di stringhe candidate e produceva
falsi positivi (riferimenti fantasma, componenti soppressi con placement residuo).
Inoltre la lista dei componenti della distinta **non coincide** con i file necessari a Creo per
aprire l'assieme: ne servono di più (soppressi, riferimenti esterni). Se ne manca uno Creo
risponde solo `[-4] Dati di input non completi`.
## Decisione
Per reverse engineering si è decodificata la struttura a record (`AsmRecordReader`), validata
contro BOM esportate da Creo. È il metodo preferito. L'euristica resta come **fallback** quando
il parser lancia `InvalidDataException`, perché la validazione copre solo un campione di file.
La fase 4 tiene tre liste separate (distinta, soppressi, riferimenti esterni) con regole di
ricorsione diverse.
## Conseguenze
- Estrazione esatta sui file che rispettano il formato; nessun falso positivo da conteggio.
- Due implementazioni da mantenere, per ora; l'euristica serve anche a due script.
- Ogni caso strano diventa un test minimale costruito byte per byte.
- Dettagli: [`../formato-asm-record.md`](../formato-asm-record.md),
[`../formato-asm.md`](../formato-asm.md).
@@ -0,0 +1,27 @@
# 0004 — Setup per-utente con DB nella cartella di installazione
**Stato:** accettata
## Contesto
Gli utenti non hanno diritti di amministratore. L'app scrive `cad_index.db` e, di default, la
cartella di lavoro accanto all'eseguibile. In `C:\Program Files` non sarebbe scrivibile.
Prima il DB era scelto dall'utente e ricordato in `ultimo_db.txt`: si finiva con DB diversi tra
postazioni e ricerche incoerenti.
## Decisione
- Setup Inno Setup 6 con `PrivilegesRequired=lowest`: installa in
`%LOCALAPPDATA%\Programs\PrepStep`.
- Il DB è sempre `cad_index.db` nella cartella di installazione; "Apri DB..." vale solo per la
sessione e nulla viene ricordato.
- `AppId` fisso per riconoscere gli aggiornamenti; se l'app è già installata il setup chiede se
aggiornare o disinstallare.
## Conseguenze
- Nessun admin necessario; aggiornare non tocca DB e impostazioni.
- La disinstallazione lascia DB, `raw/`, `step/`, CSV e `%AppData%\PrepStep\`.
- In sviluppo il DB è nella cartella di build ([`../db-cad-index.md`](../db-cad-index.md)).
- Procedura: [`../rilascio.md`](../rilascio.md).
@@ -0,0 +1,22 @@
# 0005 — Scegliere la copia di un componente dalla cartella dell'assieme
**Stato:** accettata
## Contesto
Per il 13% dei codici esiste più di una copia sulla share, con contenuto diverso e stessa
versione. Il `.asm` non registra quale copia usa. Scegliere a caso, o per data, può mettere in
`raw/` un modello diverso da quello atteso: Creo lo carica, i vincoli non si risolvono e
l'assieme non si apre, con il solito `[-4]`.
## Decisione
`ScegliPerCartella` preferisce la copia nella stessa cartella dell'assieme che la referenzia
(replica la regola di Creo) e solo in mancanza ripiega su versione più alta e data più recente.
Nella vista `files_ultima_versione` il pareggio è deterministico (`data_modifica` e percorso
nell'`ORDER BY`), ma serve solo a non sbagliare a caso.
## Conseguenze
- La fase 4 legge `files` (tutte le copie) e non la vista.
- Dettaglio e casi reali: [`../formato-asm-record.md`](../formato-asm-record.md) §7.4.
@@ -0,0 +1,21 @@
# 0006 — Script standalone che referenziano il progetto principale
**Stato:** accettata
## Contesto
Servono strumenti da riga di comando (BOM, query, analisi di binari) senza duplicare la logica di
lettura dei `.asm`.
## Decisione
Gli script in `scripts/` sono C# file-based apps (.NET 10) che referenziano
`PrepStep/PrepStep.csproj` con `#:project`. La logica vive **solo** nel progetto principale: una
sola fonte di verità per GUI e script.
## Conseguenze
- Si lanciano dalla root del repo; da dentro `PrepStep/` partirebbe la GUI.
- Due famiglie non intercambiabili: euristica + DB (`ExtractCreoStrings`, `BomViaDb`) e parser
esatto + cartelle locali (`CreoBom`). Vedi [`../architettura.md`](../architettura.md).
- `CheckDocs.cs` segue lo stesso schema per i doc.
+14
View File
@@ -0,0 +1,14 @@
# Registro delle decisioni
Perché il programma è fatto così. Ogni voce spiega il contesto, la scelta e le conseguenze, così
chi ci rimette mano non rifà il ragionamento da zero. Se cambia una scelta non si riscrive la
vecchia voce: se ne aggiunge una nuova che la sostituisce.
| N. | Decisione | Stato |
|---|---|---|
| [0001](0001-indice-sqlite-locale.md) | Indice dei file in un DB SQLite locale | accettata |
| [0002](0002-ui-winforms-in-codice.md) | UI WinForms costruita interamente in codice | accettata |
| [0003](0003-parser-esatto-con-fallback.md) | Parser esatto dei `.asm` con fallback euristico | accettata |
| [0004](0004-installazione-per-utente.md) | Setup per-utente con DB nella cartella di installazione | accettata |
| [0005](0005-copia-nella-cartella-dell-assieme.md) | Scegliere la copia di un componente dalla cartella dell'assieme | accettata |
| [0006](0006-script-che-riusano-il-progetto.md) | Script standalone che referenziano il progetto principale | accettata |
+3 -3
View File
@@ -8,8 +8,8 @@ l'euristica testuale ("codice che compare almeno due volte") descritta in
struttura effettiva dei record. struttura effettiva dei record.
Implementazione: lettura in [`PrepStep/AsmRecordReader.cs`](../PrepStep/AsmRecordReader.cs) Implementazione: lettura in [`PrepStep/AsmRecordReader.cs`](../PrepStep/AsmRecordReader.cs)
(condivisa con la GUI, `Form1.EstraiCodiciReferenziati` in fase "Trova componenti (condivisa con la GUI, `Form1.EstraiCodiciReferenziati` in fase 4 "Trova componenti
referenziati"/"Verifica completezza" - con fallback su `AssemblyComponentScanner` per i file referenziati" - con fallback su `AssemblyComponentScanner` per i file
che non rispettano il formato validato), resa in formato BOM e ricorsione sui sottoassiemi che non rispettano il formato validato), resa in formato BOM e ricorsione sui sottoassiemi
in [`scripts/CreoBom.cs`](../scripts/CreoBom.cs) (via `#:project`). in [`scripts/CreoBom.cs`](../scripts/CreoBom.cs) (via `#:project`).
@@ -91,7 +91,7 @@ conteggio dei membri: con la lettura a 1 byte fisso i campioni grandi si
disallineavano. Non è confermata da una specifica; regge sui campioni disallineavano. Non è confermata da una specifica; regge sui campioni
disponibili al momento del reverse engineering. disponibili al momento del reverse engineering.
In `CreoBom.cs` è `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 della stringa corrisponde 1:1 a un byte, quindi indicizzare la stringa o l'array di byte
originale dà lo stesso offset). originale dà lo stesso offset).
+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 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 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). `PrepStep/AsmRecordReader.cs`, documentato in [`formato-asm-record.md`](formato-asm-record.md).
È il metodo **preferito** in GUI (fase "Trova componenti referenziati"/"Verifica È il metodo **preferito** in GUI (fase 4 "Trova componenti referenziati",
completezza", `Form1.EstraiCodiciReferenziati`) e nello script `CreoBom.cs`: `Form1.EstraiCodiciReferenziati`) e nello script `CreoBom.cs`:
`AssemblyComponentScanner` (l'euristica di questo documento) resta come **fallback**, usato `AssemblyComponentScanner` (l'euristica di questo documento) resta come **fallback**, usato
solo quando `AsmRecordReader` lancia `InvalidDataException` (file che non rispetta il solo quando `AsmRecordReader` lancia `InvalidDataException` (file che non rispetta il
formato validato dal reverse engineering — copertura limitata agli 8 campioni disponibili al 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 occorrenze: >= 2
``` ```
`scripts/ExtractCreoStrings.cs` ha una versione più raffinata della regex e un filtro La regex effettiva in `AssemblyComponentScanner.cs` (usata anche da
aggiuntivo (vedi sotto), non ancora portati in `AssemblyComponentScanner.cs`: `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 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), Stessa logica di estrazione candidati ed euristica del codice radice (sopra),
duplicata in due punti del codice per scopi diversi: 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. - [`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 (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`. - [`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 Nota: le query contro `files_ultima_versione` filtrano solo per
`(cartella_radice, codice, tipo_file)` - se lo stesso codice esiste in più `(cartella_radice, codice, tipo_file)` - se lo stesso codice esiste in più
Binary file not shown.
+538
View File
@@ -0,0 +1,538 @@
# Decodifica del `.prt` Creo: numeri, quote e geometria degli schizzi
Seguito di [`formato-prt-feature.md`](formato-prt-feature.md), che spiega come trovare le sezioni
del file e leggere l'**albero** delle feature (ordine, tipo, nome). Qui: come leggere i **numeri**
(quote, coordinate) e come ricostruire da quelli la forma di una feature.
> **Aggiornamento.** La lettura campo per campo di tutte le sezioni, e la tabella completa dei
> numeri, sono in [`formato-psb.md`](formato-psb.md), che corregge la §1 di questo doc (vedi lì §9).
**Stato, da leggere prima di fidarsi.** Questo doc contiene *tutto quello che è stato decodificato
e verificato*, e dice per ogni cosa quanto è provata. **Non basta ancora a determinare in modo
univoco com'è fatto un `.prt`**: manca il posizionamento delle feature nello spazio e il
significato certo di alcuni codici (§7 elenca cosa manca, dove sta nel file, e come finire il
lavoro). Nessun codice del progetto lo usa: è reverse engineering fatto con script di prova.
Livelli di prova, come negli altri doc:
1. **Contro Creo** (schermate del modello aperto): `9252000507.prt.11`, feature *Rivoluzione 1*
(§5). Quote e coordinate dello schizzo coincidono con quelle mostrate da Creo.
2. **Coerenza e plausibilità**: `test.prt.1` e le altre feature di `9252000507.prt.11`. I valori
sono numeri "tondi" e coerenti fra sezioni diverse, ma nessuno li ha confrontati con Creo.
## 1. Come sono scritti i numeri reali
Nelle sezioni a marcatori ([`formato-asm-record.md`](formato-asm-record.md) §2) i `double` sono
scritti in forma compatta. Si distinguono per il primo byte, `tag`:
| Tag (hex) | Byte dopo il tag | Valore | Prova |
|---|---|---|---|
| `18` | 0 | `0.0` | in ogni sezione |
| `0f` | 0 | `1.0` | diagonale delle matrici identità |
| `0e` | 0 | `0.5` | **livello 1**: la quota di *Smusso 1* (D = 0.500) è il solo byte `0e` |
| `10` | 0 | `-1.0` | ipotesi dal contesto |
| `2f` | 2 | `40 b1 b2 00 00 00 00 00` | **livello 1** (12,5; 69,5; 100; 139) e 2 (8, 20, 25, 75) |
| `2e` | 2 | `40 b1 b2 b2 b2 b2 b2 b2` (l'ultimo byte si ripete) | **livello 1**: `2e 1b 33` = 6,8, il diametro della punta mostrato da Creo |
| `2d` | 7 | `40 b1 .. b7` (precisione piena) | livello 2: `2d 39 7f ff ff ff ff ff` = 25,5 |
| `2a` | 2 | `3f b1 b2 00...` | livello 2: `2a f4 00` = 1,25, il passo di M8x1.25 |
| `48` | 2 | `c0 b1 b2 00...` (negativo) | **livello 1** (−12,5; −69,5) |
| `47` / `46` | 2 / 7 | stessa famiglia di `48`, con ripetizione / precisione piena | non isolati |
| `e4`-`e7` | 0 | `K − 3` zeri di seguito, `K` = byte − `e0` (solo dentro le matrici, §6.1) | **livello 1** (matrici di `9239902104`) |
| `ed`, `32`... | 8-9 | altri byte alti | non decodificato |
**La regola, ricavata dai dati** (non da una specifica): a ogni *byte alto* `T` del `double`
corrispondono **tre tag consecutivi**, e la base si sposta di 5 per ogni passo di `T`:
```
tag_zero(T) = 0x2f + 5 * (T - 0x40) per i positivi (negativi: 0x48 + 5 * (T - 0xc0))
tag_zero -> T, b1, b2 e 5 byte a zero (valori "tondi")
tag_zero - 1 -> T, b1, b2 e b2 ripetuto 5 volte (es. 6,8 = 40 1b 33 33 33 33 33 33)
tag_zero - 2 -> T e 7 byte pieni (precisione piena)
```
Verifica: `T = 0x40` dà `2f`, `2e`, `2d`; `T = 0x3f` dà `2a` (1,25 con `f4 00`); `T = 0xc0` dà
`48`. Funziona solo finché i tag non si sovrappongono: i byte alti `0x41`-`0x43` e `0xb8`-`0xbf`
non li ho incontrati e non so se la regola regge (i tag di `0x45` collidono con quelli dei
negativi). Per i valori "non tondi" con ripetizione (come 6,8 o 3,4) serve il tag `2e`: senza
quello si leggerebbe `6.7998`.
Come si ricostruisce un valore a 3 byte: si prende il `double` big-endian `40 b1 b2 00...` (per
`2f`) oppure `c0 b1 b2 00...` (per `48`). Esempi verificati (`struct.unpack('>d', ...)`):
| Byte | Valore | Dove |
|---|---|---|
| `2f 59 00` | `100.0` | quota |
| `2f 52 c0` | `75.0` | quota |
| `2f 51 60` | `69.5` | coordinata y dello schizzo di Rivoluzione 1 (= 139 / 2) |
| `2f 29 00` | `12.5` | coordinata x (= 25 / 2) |
| `48 29 00` | `-12.5` | idem, lato opposto all'asse |
| `48 51 60` | `-69.5` | idem |
| `2f 61 60` | `139.0` | quota (lunghezza) |
| `48 59 00` | `-100.0` | estremo della linea d'asse |
Le unità non sono scritte nel record. Nei campioni sono millimetri, ma il file ha una sezione
`UnitSystemDef` (nel contenitore compresso, §1.2 di `formato-prt-feature.md`) che non ho letto:
l'unità va presa da lì prima di dare un numero come "mm".
## 2. Le quote: `dim_array` in `BasFullData`
Ogni quota (dimensione guida) di ogni feature e di ogni schizzo è un record della sezione
`BasFullData`, array `dim_array`. Campi, nell'ordine in cui compaiono:
```
\xe1 nome della quota (qui sempre nullo: il nome "d12" si calcola altrove)
\x08 tipo del record
<dim_type> 1 byte: classe della quota (tabella sotto)
<feat_id> intero (formato asm-record §2.1): la feature a cui appartiene
<attributi> \xf8 \x02 ... flag
\xe3 <valore> <valore_di_backup> \xe3 ... due numeri uguali: value e bck_value
\xe3 \x09 \x01 ... <tolleranza> ... blocco della tolleranza (nominal_value, bound...)
```
Per leggere solo valore e feature basta ancorarsi a `\xe1 \x08 <dim_type> <feat_id>` e leggere il
primo numero che segue (§1). Il primo record dell'array è scritto con i nomi di campo per esteso
(layout diverso: non l'ho agganciato con lo stesso pattern, quindi **la prima quota dell'array può
mancare**).
`feat_id` è lo stesso id di `AllFeatur` e dello storico (`formato-prt-feature.md` §2 e §5): è il
modo per attribuire la quota a "Foro 1" o a "Estrusione 2".
### 2.1 Le quote lette sui due campioni
`test.prt.1` (id → feature come in `formato-prt-feature.md` §6):
| Feature | `dim_type` | Valore | Interpretazione |
|---|---|---|---|
| Estrusione 1 (79) | 62 | 75 | profondità (ipotesi) |
| Estrusione 1 (79) | 4 | 100 | diametro del cerchio dello schizzo: nello schizzo il raggio è 50 (`entity(circle)`, `radius`) |
| Foro 1 (100) | 4 | 19 | diametro (ipotesi) |
| Foro 1 (100) | 62 | 40 | profondità (ipotesi) |
| Arrotondamento 1 (125) | 3 | 3,5 | raggio (ipotesi) |
| Estrusione 2 (149) | 62 / 1 / 1 | 11 / 25 / 53 | profondità e due quote dello schizzo (ipotesi) |
| Smusso 1 (188) | 9 | 6 | distanza dello smusso (ipotesi) |
`9252000507.prt.11`:
| Feature | `dim_type` | Valore | Interpretazione |
|---|---|---|---|
| Rivoluzione 1 (73) | 2 | **25** | **diametro, confermato da Creo** (Ø25 h7) |
| Rivoluzione 1 (73) | 1 | **139** | **lunghezza, confermata da Creo** |
| Estrusione 1 / taglio (118) | 62 / 14 / 5 / 20 | 4 / 45 / 8 / 3 | non verificate |
| Foro 1 (157) | 4 / 9 / 2 / 10 / 2 | 8 / 20 / 6,8 / 118 / 25 | **confermate da Creo** (§6.4): 8 = diametro nominale, 20 = profondità filetto, 118 = angolo punta, 25 = profondità foro |
`9239902104.prt.1` (piastra con tre fori, §6.4.1):
| Feature | `dim_type` | Valore | Interpretazione |
|---|---|---|---|
| Sketch 1 / Estrusione 1 (84) | 1 / 5 / 1 | 21 / 54 / 125 (+ 46) | quote del profilo a L (§3.2) |
| Estrusione 1 (84) | 62 | 6 | profondità, coerente con y = 0 ... 6 delle facce |
| Sketch 2 / Foro 1 (124 / 131) | 8 / 2 / 2 / 7 | 38 / 8 / 8 / 30 | quote dei 4 punti (§6.4.1) |
| Foro 1 (131) | 4 | 5,5 | diametro |
| Sketch 3 / Foro 2 (1095 / 1099) | 2 / 2 | 4 / 5 | distanze del punto dai bordi: l'origine `(67, 6, -5)` = 71 − 4 e 5 |
| Foro 2 (1099) | 4 | 5 | diametro |
| Foro 3 (1132) | 4 / 9 / 2 / 2 | 3 / 6 / 2,5 / 137,5 | diametro nominale M3, profondità filetto, punta, lunghezza (distanza fino all'estremo, §6.1) |
| Sketch 4 / Estrusione 2 (1903 / 1911) | 1 / 62 | 3,5 / 3 | quota dello schizzo e profondità |
| Arrotondamento 1 (2777) | 3 | 3 | raggio, confermato dallo storico |
**Cosa è provato e cosa no.** I valori `25` e `139` di *Rivoluzione 1* sono confermati da Creo
(§5). Per tutte le altre righe il *numero* è decodificato in modo plausibile ma il *significato*
(profondità? diametro?) è dedotto dal codice `dim_type`, da cui non ho una tabella certa. I codici
visti: `1` e `2` quote lineari/diametrali di schizzo, `3` raggio, `4` diametro, `9` distanza
(smusso), `62` profondità/estensione, altri (`5`, `10`, `14`, `37`) non interpretati. Per
distinguere con certezza serve un confronto riga per riga con un modello noto, o leggere il
nome della quota (`d0`, `d1`... del Creo).
La *tolleranza* (es. `h7` di Ø25) sta nello stesso record, ma non l'ho decodificata: nel campo
`tol_class`/`tol_table_index` compaiono indici, non la stringa `h7`.
## 3. La geometria di uno schizzo: `p_saved_result`
Ogni schizzo (sezione) ha, dentro `BasFullData`, un blocco:
```
gsec_ptr ... name "Sezione 1" <- nome mostrato nel tree
gsec2d_ptr name "S2D0001"
segtab_ptr / dimtab_ptr / dim_ref <- segmenti, quote e a cosa si riferiscono
...
p_saved_result
entity(line) ... <- le linee dello schizzo con le coordinate
entity(circle) ... <- i cerchi (centro, raggio)
local_sys 4x3 <- sistema di coordinate dello schizzo
```
Le coordinate delle entità sono **nel sistema dello schizzo**, non assolute (§6).
Ogni `entity(line)` ha due estremi con tre coordinate ciascuno `(x, y, z)`; con z sempre `0` nello
schizzo. Le coordinate sono numeri come in §1.
### 3.1 Esempio verificato: *Sezione 1* di Rivoluzione 1
Decodificando i numeri del blocco `entity(line)` di `9252000507.prt.11` si ottengono quattro
linee chiuse e una linea d'asse:
| Linea | Da `(x, y)` | A `(x, y)` |
|---|---|---|
| 1 | (0, 69,5) | (−12,5, 69,5) |
| 2 | (−12,5, 69,5) | (−12,5, −69,5) |
| 3 | (−12,5, −69,5) | (0, −69,5) |
| 4 | (0, −69,5) | (0, 69,5) |
| asse | (0, −100) | (0, 100) |
È il rettangolo di larghezza 12,5 (= raggio) e altezza 139, centrato in y sul piano di
riferimento, con il lato destro sull'asse x = 0, e una linea d'asse più lunga della figura.
**Coincide con quello che Creo mostra** nella vista dello schizzo (rettangolo di 139 di altezza,
quotato 25 come diametro rispetto all'asse tratteggiato, simmetrico rispetto a `TOP`).
Nota: nel file la quota `25` è un **diametro** (Ø25), anche se in geometria il lato è 12,5. Per
questo non si può dedurre la forma dalle sole quote: servono anche le linee.
### 3.2 Secondo esempio: il profilo a L di *Estrusione 1* (`9239902104.prt.1`)
Le linee del profilo (`entity(line)`, estremi `(x, y)` nel sistema dello schizzo, `z = 0`):
| Linea | Da | A |
|---|---|---|
| 1 | (71, 0) | (−54, 0) |
| 2 | (−54, 0) | (−54, 46) |
| 3 | (−54, 46) | (0, 46) |
| 4 | (0, 46) | (0, 21) |
| 5 | (0, 21) | (71, 21) |
| 6 | (71, 21) | (71, 0) |
più una linea d'asse. Le quote 125 (= 54 + 71), 54, 46 e 21 sono tutte spiegate dal profilo. Qui le
linee stanno in `FeatDefs` (record della feature, sezione `S2D0006`), non in `BasFullData` come in
§3: la posizione del blocco `p_saved_result` dipende dal tipo di feature.
## 4. Da schizzo + quote alla forma: la rivoluzione
Con le due cose sopra, *Rivoluzione 1* è determinata così:
```
profilo (nel piano dello schizzo): x in [-12,5 ; 0], y in [-69,5 ; 69,5]
asse di rotazione: la linea x = 0 (la linea d'asse dello schizzo)
angolo: 360 (non decodificato, vedi §7)
risultato: cilindro Ø25 x 139, centrato sull'origine lungo y dello schizzo
```
Nell'albero di Creo il cilindro risulta con il fondo sul piano `DTM1` e l'asse come `A_1`: la
posizione assoluta (quale piano contiene lo schizzo, come è orientato `y`) **non è nello schizzo**
ma nella definizione della feature (§7).
## 5. Verifica di *Rivoluzione 1* contro Creo
`9252000507.prt` aperto in Creo (schermate del model tree e dello schizzo):
| Cosa | Creo | Letto dal file |
|---|---|---|
| Lunghezza dello schizzo | 139.000 | quota `dim_type 1` = 139 |
| Diametro | Ø25 (h7) | quota `dim_type 2` = 25 |
| Rettangolo, simmetrico rispetto a `TOP` | sì | y da −69,5 a 69,5 |
| Rettangolo a sinistra dell'asse | sì | x da −12,5 a 0 |
| Asse (linea di costruzione) | più lungo della figura | da y = −100 a 100 |
| Assi A_3, A_4, A_5, punto PNT0 | presenti nel tree | `AllFeatur` + storico (`formato-prt-feature.md`) |
## 6. Posizione nello spazio
### 6.1 Matrici 4×3
Piani di riferimento, assi, sistemi di coordinate, schizzi e feature hanno un `local_sys`: array
`f9 04 03` = 4 righe per 3 colonne = 12 numeri: **tre versori e, per ultima, l'origine**. Si leggono
con i numeri di §1 più un codice di ripetizione: un byte `e4`-`e7` vale **"altri `K − 3` zeri"**,
con `K` = byte − `e0` (`e4` = 1 zero, `e5` = 2, `e6` = 3, `e7` = 4). L'identità è quindi
`0f 18 e5 0f 18 e5 0f 18 e5` = `1,0,0,0, 1,0,0,0, 1,0,0,0`.
**Verificato su `9239902104.prt.1`** (livello 1: i valori coincidono con quelli mostrati da Creo,
sotto), con le 4 righe `(e1, e2, e3, origine)`:
| Feature | Matrice letta | Significato |
|---|---|---|
| Foro 1 | `(1,0,0) (0,0,-1) (0,1,0)`, origine `(-46, 6, -38)` | uno dei 4 punti dello schizzo `(-46, 38)`: x = x dello schizzo, z = **−**y dello schizzo, y = 6 = faccia superiore (la piastra è spessa 6) |
| Foro 2 | `(1,0,0) (0,0,-1) (0,1,0)`, origine `(67, 6, -5)` | 71 − 4 e 5 dal bordo: le due quote 4 e 5 della feature |
| Foro 3 | `(0,1,0) (-1,0,0) (0,0,1)`, origine `(-66.5, 3, -5)` | y = 3: l'**offset 3,00** dalla faccia superiore (y = 6) che Creo mostra; z = −5: allineato all'asse di Foro 2 (**"Allinea" con `A_7`**); x = −66,5 = 71 − 137,5, cioè il foro parte 137,5 prima dell'estremo destro (71) |
Quindi **la posizione di un foro si legge dall'origine della sua matrice**: le quote di
posizionamento (4, 5, 3,00) non servono, sono già il risultato. Nei tre fori l'`y` e la `z` tornano
esattamente con le quote e con i riferimenti mostrati da Creo.
Dalla stessa matrice: nel modello di questa piastra lo schizzo di Estrusione 1 sta nel piano `x–z`
(x dello schizzo = x, y dello schizzo = −z) e l'estrusione cresce lungo +y fino a 6.
**Resta incerta l'interpretazione dell'asse del foro.** Per Foro 1 e 2 il terzo versore è `(0,1,0)`
(perpendicolare alla piastra, come deve essere). Per Foro 3 è `(0,0,1)`, ma il foro in Creo entra
dalla faccia laterale: o la convenzione del versore dell'asse è diversa, o la direzione di foratura
sta in un altro campo. Non risolto.
### 6.2 Dove stanno
| Cosa | Dove nel file |
|---|---|
| Piani di riferimento (RIGHT, TOP, FRONT, DTM1) | `ActDatums` (`srf_prim_ptr(plane)` con `local_sys`), `feat_id` = id della feature; vincoli di posizionamento in `FeatDefsDtm` (§6.3) |
| Sistema di coordinate (`PRT_CSYS_DEF`) | `MdlRefInfo` (`ent_list(coord_sys)`) e `GeomDepen` |
| Assi, punti (A_3, PNT0...) | `ent_list(arc_z)`, `ent_list(point)` in `MdlRefInfo` |
| Piano che contiene lo schizzo | `gsec3d_ptr` -> `plane_id`, `ref_planes`; il piano è identificato da un id di geometria |
| Orientamento dello schizzo | `sec_orient_data`, `place_instruction_ptrs` (nella definizione di feature `feat_defs_<tipo>`) |
| Offset di un piano da un altro (DTM1) | quota di tipo "offset" nel `dim_array` della feature, se esiste |
### 6.3 Come è posizionato un piano: i vincoli di `FeatDefsDtm`
La finestra "Piano di riferimento → Posizionamento" di Creo (riferimenti con il loro vincolo,
es. *Tangente* a una superficie, *Parallelo* a un piano) **si legge dal file**. Sta nella sezione
`FeatDefsDtm`, record `feat_defs_923` (un record per piano; `feat_defs_926` assi, `feat_defs_931`
punti, `feat_defs_979` sistemi di assi). Ogni record ha:
```
id id della feature (come in AllFeatur)
local_sys matrice 4x3 del piano risultante (§6.1)
place_instruction_ptrs una istruzione per ogni riferimento della finestra:
type codice del vincolo (intero a 2 byte)
offset double, offset del vincolo
dim_id, ref_id quota dell'offset e riferimento, se esistono
geometry1_id id della geometria a cui ci si riferisce
geometry2_id id della geometria che si sta creando (il piano stesso)
```
Gli `id` di geometria (`geom_id`) si risolvono nelle sezioni `ActDatums` (piani e assi di
riferimento), `VisibGeom` e `NovisGeom` (superfici del solido, visibili e no). Ogni voce di
`srf_array` ha `geom_id`, `geom_type` (`"`, 0x22 = piano; `$`, 0x24 = cilindro) e **`feat_id`**: è il
legame con la feature che l'ha generata.
**Verifica sul caso che hai mostrato**: *DTM1* di `9252000507.prt` (feature 116), finestra di Creo
"Supfce:F5(RIVOLUZIONE_1) Tangente / FRONT:F3 Parallelo". Dal file:
| Istruzione | `type` | `geometry1_id` | Si risolve in | Corrisponde a |
|---|---|---|---|---|
| 1 | `81 fb` (507) | 88 | `VisibGeom`: cilindro (`$`), `feat_id` 73 = Rivoluzione 1 | "Supfce:F5(RIVOLUZIONE_1)", **Tangente** |
| 2 | `81 f9` (505) | 6 | `ActDatums`: piano (`"`), `feat_id` 5 = FRONT | "FRONT:F3", **Parallelo** |
`geometry2_id` è 117 in entrambe: `ActDatums` lo dà come piano con `feat_id` 116 = DTM1 (il piano
risultante). Nella sua matrice compare il valore `12.5` (`2f 29 00`): è il raggio del cilindro,
come deve essere per un piano tangente ad esso. Il campo `Traslazione` della finestra era vuoto.
Quindi **quali sono i riferimenti** (quale feature/superficie e quale piano) si legge con certezza.
Resta da fissare:
- **Il significato di `type`.** 507 ↔ Tangente e 505 ↔ Parallelo è dedotto da un solo esempio e
dall'ordine delle righe della finestra: non ho una tabella dei codici. Gli altri visti
(`82 01`, `82 02`, `82 03`, `81 fd`, `81 ff`) sono quelli dei piani di default e degli altri
piani del campione, non interpretati.
- **`Feature number` di Creo** (`F3`, `F5`): non è l'`id` ma la **posizione** nel tree
(RIGHT = F1, TOP = F2, FRONT = F3, PRT_CSYS_DEF = F4, Rivoluzione 1 = F5). I piani interni non
contano. Per tradurre un `id` in `F<n>` si numerano le feature visibili in ordine
(`formato-prt-feature.md` §3).
- **L'offset.** Se un vincolo ha un offset (quota di tipo offset) il valore sta nel `dim_array`
della feature (§2) con il `dim_id` dell'istruzione; non ho un esempio con offset non nullo.
### 6.4 Il foro: `feat_defs_911`
Riferimento: *Foro 1* di `9252000507.prt.11` (feature 157), confrontato con le schermate di Creo.
Creo mostra: tipo **Standard**, profilo **Diritto** con **Maschiato**, filetto ISO **M8x1.25**,
**profondità 25**, **Ø6,8**, **profondità del filetto 20**, **angolo della punta 118°**,
"Includi la superficie del filetto" attivo, posizionamento **Coassiale** con riferimenti
`Supfce:F5(RIVOLUZIONE_1)` e `A_1(ASSE):F5(RIVOLUZIONE_1)`.
La definizione del foro sta nella sezione `FeatDefs`, record `feat_defs_911`. Ha due parti.
**a) I parametri del filetto, in chiaro** (`data(ddt_plinfo_hole_fstn_param)`):
| Campo | Valore letto | In Creo |
|---|---|---|
| `name`, `series` | `ISO`, `ISO` | Tipo filetto: ISO |
| `scrsize` | `M8x1.25` | Dimensione vite: M8x1.25 |
| `scrsize_val` | `0x1d` (29) | indice di M8x1.25 nella tabella delle viti di Creo (nel segnaposto `M1x.25` vale 0) |
| `hole_class` | `H` | (classe del foro) |
| `drillnum` | `6.8` | Ø6,8 |
**b) Le istruzioni** (`place_instruction_ptrs`): una per ogni parametro numerico, nella forma
`type`, `offset` (un `double`, §1), `dim_id`, `ref_id`, `geometry1_id`, `geometry2_id`. Quelle
del foro filettato, con il valore atteso da Creo:
| `type` | `offset` letto | In Creo | Prova |
|---|---|---|---|
| 5104 (`0x13f0`) | 6,8 | Ø6,8 (anche nel foro semplice, vedi sotto) | **livello 1** |
| 5528 (`0x1598`) | 118 | angolo della punta | **livello 1** |
| 5538 (`0x15a2`) | 25 | profondità del foro | **livello 1** |
| 5536 (`0x15a0`) | 1,25 | passo del filetto (M8**x1.25**) | livello 2 |
| 6617 (`0x19d9`) | 11 | non corrisponde a nulla di mostrato | non interpretato |
| 5542, 10103, 5533, 5534 | 0 | non mostrati | non interpretati |
Il codice `type` identifica il parametro; il valore è `offset`. La profondità del filetto (20) e il
diametro nominale (8) **non** sono istruzioni ma quote del `dim_array` (§2), con `dim_type` 9 e 4
e `feat_id` 157. In tutto il `dim_array` ha per il foro cinque quote: 8 (`dim_type` 4), 20 (9),
Ø6,8 (2, scritto con il tag `2e`), 118 (10) e 25 (2). Tre di queste (6,8, 118, 25) compaiono
quindi due volte, come quota e come parametro di un'istruzione.
**Foro semplice e foro standard.** `test.prt.1` ha un foro **semplice**: il suo `feat_defs_911`
è lungo 862 byte e ha solo due istruzioni (`5104` con `dim_id` 7, e `1853`); i parametri del
filetto ci sono comunque, ma con i valori segnaposto di Creo (`M1x.25`, `drillnum 0.75`,
`scrsize_val 0`): **quindi le stringhe `scrsize`/`drillnum` da sole non dicono se il foro è
filettato**. Quello di Fata ha 3212 byte e 12 istruzioni, valori diversi dal segnaposto. Il segno
affidabile oggi è: **foro standard = presenza delle istruzioni 5536 (passo) e 5528 (angolo punta)**,
e il foro è filettato quando anche i valori del filetto non sono quelli segnaposto.
**Non individuato nel file** (per ognuno servono due salvataggi che differiscono solo per
quell'opzione):
- *Maschiato / Trapanato / Gioco*, *Diritto / Rastremato*: non so quale istruzione o flag le
codifichi. `plin_info_hole_flag.flags` vale `2, 2` in entrambi i fori.
- *Includi la superficie del filetto*: idem.
- Il tipo *Standard*/*Semplice* come campo esplicito: oggi si deduce solo dal numero di istruzioni.
**Posizionamento Coassiale.** Gli ingredienti ci sono: un blocco `data(plin_info_ref)` per ogni
riferimento, con gli id di geometria. Il piano del riferimento `Supfce:F5` dovrebbe essere la
faccia piana di *Rivoluzione 1* (feature 73, in `VisibGeom` con `feat_id` 73) e il secondo
riferimento l'asse `A_1`: non ho agganciato quegli id perché le coppie `id_type1`/`id_type2` non le
ho decifrate. La geometria generata dal foro (cilindro, cono, piani con `feat_id` 157) è invece
in `VisibGeom` e `NovisGeom`, e `geometry2_id` 175 delle istruzioni è un suo piano.
### 6.4.1 Foro a punti di schizzo e facce piane: `9239902104.prt.1`
*Foro 1* è definito da *Sketch 2* con **4 punti**. Cosa si legge, tutto da `local_sys` (§6.1):
**I punti.** Nello schizzo (`entity(point)`, campo `pnt`, un `[x, y, z]` per punto) sono
`(-46, 8)`, `(-8, 8)`, `(-8, 38)`, `(-46, 38)`, con quote 8, 8, 30 e 38. Nel modello ogni punto
diventa un'**istanza** del foro, e ogni istanza ha un piano proprio in `NovisGeom` (superficie
piana con `feat_id` 131, §6.3) con la matrice del punto: l'**origine** è la posizione assoluta.
| Piano (`geom_id`) | Origine nel modello |
|---|---|
| 135 | `(-46, 6, -8)` |
| 147 | `(-8, 6, -8)` |
| 159 | `(-8, 6, -38)` |
| 171 | `(-46, 6, -38)` |
Quindi `x` = `x` dello schizzo, `z` = **−**`y` dello schizzo (confermato dai quattro punti e da Foro
2, §6.1) e `y` = 6.
**La faccia.** La superficie su cui stanno è la faccia piana superiore di *Estrusione 1*: ha
`y = 6` e la stessa matrice del sistema di coordinate dello schizzo (origine `(0, 6, 0)`,
versori `(1,0,0)`, `(0,0,1)`, `(0,-1,0)`). Le facce piane di *Estrusione 1* (`feat_id` 84, in
`VisibGeom`) sono, con origine e normale (il terzo versore):
| `geom_id` | Origine | Normale | Faccia |
|---|---|---|---|
| 95 | `(0, 6, 0)` | `(0,-1,0)` | **superiore** (`y = 6`), dove sono i fori |
| 107 | `(71, 0, 0)` | `(-1,0,0)` | estremo destro (`x = 71`) |
| 113 | `(-54, 0, -46)` | `(1,0,0)` | estremo sinistro (`x = -54`) |
| 115 | `(-54, 0, 0)` | `(0,0,1)` | lato `z = 0` |
| 2999 | `(71, 0, -21)` | `(0,0,-1)` | lato del gradino (`z = -21`) |
| 3001 | `(0, 0, -21)` | `(-1,0,0)` | alzata del gradino (`x = 0`) |
| 3003 | `(0, 0, -46)` | `(0,0,-1)` | lato `z = -46` |
La piastra a L di Estrusione 1 (§3.2, profilo `x` da −54 a 71, `y` dello schizzo da 0 a 46, gradino
a 21) torna in coordinate modello con `z = −y` dello schizzo, e spessore 6 lungo `y`. Gli altri
tre fori cadono dentro il materiale (`x` −46 e −8, `z` −8 e −38).
**Non risolto:** quale `geom_id` il foro usi *formalmente* come faccia di riferimento (95, per
coincidenza di piano e di origine) non è scritto in un campo che io abbia identificato: è
un'identificazione per posizione, non una lettura.
### 6.5 Lo smusso: `feat_defs_914`
Riferimento: *Smusso 1* di `9252000507.prt.11` (feature 252), in modifica in Creo: modalità
**Insiemi**, schema quote **D x D** con **D = 0.500**, un insieme ("Insieme 1") con **due
riferimenti**, entrambi `Spigolo:F5(RIVOLUZIONE_1)`, e l'opzione "Superfici con offset".
| Cosa | Dove | Letto | Prova |
|---|---|---|---|
| Valore D | `dim_array`, `feat_id` 252, `dim_type` 9 | `0e` = **0,5** | **livello 1** |
| Valore in `test.prt.1` | `dim_array`, feat 188, `dim_type` 9 | `2f 18 00` = 6,0 | livello 2 |
| Superfici del bordo | `feat_defs_914`, istruzioni di tipo 4530 | `geometry1_id` = 88, 86, 106, 90 | vedi sotto |
| Superfici generate dallo smusso | `VisibGeom`, `feat_id` 252 | superfici coniche (`geom_id` 253, 258, 261, 266) | presenti |
Il `dim_type` 9 è lo stesso del foro (profondità del filetto): dice la *classe* della quota, non
cosa misura. Per lo smusso vale D, ma la stessa classe in un foro vale 20.
**I riferimenti.** Le istruzioni di tipo `4530` hanno nel campo `geometry1_id` gli id 88, 86, 106 e
90, ripetuti a coppie. Risolti con `VisibGeom`: **88** e **106** sono superfici cilindriche con
`feat_id` 73 (cioè *Rivoluzione 1*, F5), **86** è il piano di estremità della stessa rivoluzione
(visto in §3.1 e §6.3). Quindi i riferimenti `Spigolo:F5(RIVOLUZIONE_1)` sono spigoli ricavati dalle
superfici di Rivoluzione 1, come mostra Creo. Non so ancora quale coppia di id formi quale
spigolo, né a quale dei due "Spigolo" della lista corrisponda (le istruzioni non sono in un ordine
che io abbia decifrato), e **90** non l'ho risolto.
**Non decodificato:**
- lo schema quote (D x D, D1 x D2, angolo x D...): non ho trovato dove sta; compaiono vari tipi di
istruzione (`7350`, `7360`, `4549`, `4552`, `4557`, `4558`, `1125`, `7265`, `14741`) senza
corrispondenza con le voci mostrate;
- "Insieme 1" e la sua composizione: compare la struttura (`data(plin_info_chut_info)`, array di
riferimenti) ma non la ho letta;
- "Superfici con offset" e il tipo di transizione;
- l'istruzione `14741`, con un valore `11,476` in precisione piena: non corrisponde a niente di
mostrato.
**Foro filettato con posizionamento lineare** (*Foro 3* di `9239902104.prt.1`). Creo mostra: tipo
Standard, Diritto, Maschiato, ISO **M3x.5**, Ø2,50 (punta), campo profondità vuoto, posizionamento
**Lineare** con riferimento primario `Supfce:F6(ESTRUSIONE_1)` e due riferimenti di offset
(`Supfce:F6(ESTRUSIONE_1)` con offset 3,00 e `A_7(ASSE):F10(FORO 2)` allineato). Dal file:
| Informazione | Letto | Prova |
|---|---|---|
| Filetto | stringhe `ISO`, `M3x.5`, punta `2.5`; `scrsize_val` = 12 | concorda con Creo |
| Ø della punta | istruzione 5104, offset 2,5 | concorda con Creo (Ø2,50) |
| Quote del foro | 3 (`dim_type` 4), 6 (9), 2,5 (2), 137,5 (2) | numeri letti; 137,5 con campo "Profondità" vuoto in Creo: probabilmente un foro passante, valore non verificato |
| Posizione risultante (offset 3,00 e allineamento a `A_7`) | **letta dalla matrice** `local_sys` (§6.1): origine `(-66.5, 3, -5)`, con y = 3 e z = −5 uguali a quelli di Foro 2 | concorda con Creo; la quota 3,00 non è una quota a sé nel `dim_array` |
| Riferimenti `F6` e `A_7` | non risolti | gli id stanno in blocchi `plin_info_ref` come per DTM1 (§6.3) |
Correzione: il primo codice delle istruzioni è 5542 anche qui, come nel foro coassiale di
`9252000507`; il `12266` che avevo letto nella prima versione di questo doc veniva da un'istruzione
di un altro record. Il tipo di posizionamento (coassiale / lineare) **non** è quel codice. Quale sia
non lo so: la posizione finale si legge comunque dalla matrice (§6.1).
## 7. Cosa manca per determinare un `.prt` in modo univoco
Tabella di verità: ogni informazione necessaria, se l'ho decodificata e quanto è provata.
| Informazione | Stato | Dove si trova |
|---|---|---|
| Elenco e ordine delle feature | **fatto**, provato (`test.prt.1`, `9252000507.prt.11` contro Creo) | `AllFeatur` |
| Tipo di ogni feature | **fatto**; estrusione vs rivoluzione solo con lo storico | `AllFeatur`, storico |
| Nome di ogni feature | **fatto**, ma solo se esiste lo storico (10 file su 31 leggibili) | `MdlStatus` |
| Quote (valori numerici) | **fatto per i valori interi/semplici**, tag `2f`/`48` | `dim_array` |
| Significato di ogni quota | **parziale**: provato solo per Rivoluzione 1 | `dim_type` (§2.1) |
| Geometria dello schizzo (linee) | **fatto** per le linee, provato su Rivoluzione 1 | `p_saved_result` |
| Cerchi, archi, spline dello schizzo | non verificato (cerchio visto in `test.prt.1`, raggio 50) | `entity(circle)`, `entity(arc_z)`, `entity(spline)` |
| Valori non "tondi" | **non decodificato** (tag diversi da `2f`/`48`) | ovunque |
| Piano e orientamento dello schizzo | **non decodificato** | `gsec3d_ptr`, `sec_orient_data` |
| Posizione/rotazione di fori e feature | **fatto**: matrice 4×3 (§6.1) | `local_sys` |
| Posizione/rotazione di piani, assi, punti | stessa matrice, da verificare su un piano ruotato | `ActDatums`, `MdlRefInfo` |
| Vincoli e riferimenti di un piano (come DTM1) | **fatto per DTM1 di `9252000507`** (riferimenti sì, nome del vincolo da un esempio) | `FeatDefsDtm`, §6.3 |
| Offset di un piano da un altro | **non decodificato**: mai visto un offset non nullo | quota in `dim_array` (`dim_id`) |
| Direzione/lato e angolo di una rivoluzione, profondità e direzione di un'estrusione | **non decodificato** | `feat_defs_917`/`916` (`place_instruction_ptrs`, `plin_info_arr`) |
| Parametri del foro (misura, punta, passo, profondità, angolo) | **fatto**, provato contro Creo su `9252000507` | `feat_defs_911`, §6.4 |
| Opzioni del foro (maschiato, diritto, superficie del filetto) | **non individuato** | §6.4 |
| Posizione assoluta del foro (origine e versori) | **fatto**, provato contro i riferimenti di Creo su `9239902104` (§6.1) | `local_sys` del record |
| Riferimenti del posizionamento (quale faccia/asse) | **non decodificato** | `plin_info_ref` in `feat_defs_911` |
| Valore di raccordi e smussi | **fatto**, provato contro Creo su `9252000507` (D = 0,5) | `dim_array` |
| Spigoli a cui si applicano raccordi/smussi | **parziale**: si risolvono le superfici (feature di origine), non lo spigolo né l'insieme | `feat_defs_914`, §6.5 |
| Schema quote e insiemi dello smusso | **non decodificato** | `feat_defs_914` |
| Tolleranze (h7...) | **non decodificato** | blocco `tol_ptr` del `dim_array` |
| Unità di misura | **non letto** | `UnitSystemDef` (sezione compressa) |
| Feature nascoste / soppresse | **non decodificato** | non individuato nel file |
| Parametri e materiale | parziale, via `ND:0:Model_*` (XML) | sezione compressa `.Z` |
| File Wildfire (generazioni 1322/1375/1376) | **illeggibile** (sezioni di tipo `11`) | `formato-prt-feature.md` §1.3 |
**Conclusione onesta:** oggi si ricava con affidabilità *quali* feature ci sono, *che tipo* sono,
*come si chiamano* (se c'è lo storico), *quanto misurano* le quote semplici e *che forma ha lo
schizzo* se fatto di linee con numeri semplici. Non si ricava ancora *dove* stanno nello spazio né
*come* sono orientate, né si può ricostruire il solido completo.
## 8. Come finire il lavoro
Per ogni riga "non decodificato" della §7 il metodo è lo stesso, ed è quello che ha dato Rivoluzione
1:
1. Scegliere un `.prt` piccolo con **una sola feature** del tipo da decodificare, e annotare da
Creo i valori veri (quote, posizione dei piani, angolo, direzione).
2. Estrarre da `BasFullData`/`ActDatums` i record di quella feature e cercarvi quei numeri con il
decodificatore di §1: i valori noti ancorano i byte.
3. Variare **una cosa alla volta** nel modello (spostare il piano, cambiare l'angolo), risalvare e
confrontare i due file: i byte che cambiano sono il campo cercato.
4. Registrare in questo doc il campo, con il file di prova e il valore atteso, come in §5.
Tre modelli di prova che chiuderebbero la maggior parte della §7: un piano `DTM1` a offset non nullo
dal piano `TOP` (e con un vincolo *Attraverso*, *Normale*, *Offset*...); una estrusione di profondità nota su un piano ruotato; una rivoluzione di 90°.
Una volta chiusa la §7 il passo successivo è un `PrtModelReader` in C# accanto ad `AsmRecordReader`,
con le stesse regole: se un valore non si decodifica si solleva `InvalidDataException`, non si
restituisce un modello parziale in silenzio.
+371
View File
@@ -0,0 +1,371 @@
# Il formato PSB: leggere tutte le sezioni di un `.prt`/`.asm` senza Creo
Come è scritto **per intero** il contenuto di un file Creo, e come leggerlo campo per campo con
nome e valore, senza àncore a pattern. Ricavato per reverse engineering su
`9253000117a.prt.35` (Creo 2.0 M200, generazione `1632`, 2 MB) e poi provato sugli 89 file di
`raw/`. Il nome "PSB" è quello che il formato dà a se stesso (`PSB_Tokens`, §7).
Questo doc **sostituisce** le parti di [`formato-prt-feature.md`](formato-prt-feature.md) e
[`formato-prt-geometria.md`](formato-prt-geometria.md) che leggevano i record cercando sequenze
di byte: quelle àncore restano valide come esempi, ma il modo giusto di leggere è quello qui
sotto. Le correzioni puntuali a quei due doc sono in §9.
Implementazione di riferimento: [`scripts/psb/psb.py`](../scripts/psb/psb.py) (parser),
[`scripts/psb/prt.py`](../scripts/psb/prt.py) (contenitore),
[`scripts/psb/unz.py`](../scripts/psb/unz.py) (sezioni compresse). È Python (solo libreria
standard), non C#: è lo strumento con cui è stato fatto il reverse engineering, non codice
dell'app. Nessun codice di `PrepStep/` lo usa.
**Livelli di prova** (come negli altri doc):
1. **Verifica interna forte**: la sezione si legge fino all'ultimo byte, con i conteggi degli
array rispettati; i numeri reali sono controllati su vincoli geometrici (righe delle matrici
di rotazione a norma 1: 5397 su 5397 in `9253000117a.prt.35`).
2. **Su scala**: 2152 sezioni su 2226 lette fino in fondo negli 89 file di `raw/` (tutte le
generazioni, da `1322` a `2063`). Le 74 che falliscono sono in §8.
3. **Contro Creo**: nessuna. Nessun valore di questo doc è stato confrontato con il modello
aperto in Creo. I nomi dei campi sono quelli scritti nel file; il loro *significato* è
dedotto dal nome.
## 1. Il contenitore
```
#UGC:2 PART 1632 1060 805 1 1 15 3300 2015440 000002cc \
...
#-END_OF_UGC_HEADER
#Creo TM 2.0 (c) 2020 by PTC Inc. All Rights Reserved. M200
#UGC_TOC 2 32 81 17####...
DEPDB_DATA ab1 19 d 1632 9 -1 05e1 260f####
...
NEXT_TOC_ENTRY 1f8679 3cc 0 1632 1 -1 0000 0000####
```
- **L'ultimo campo della prima riga** (`000002cc`) è la lunghezza dell'intestazione: è la
costante da sommare a tutti gli offset dell'indice. `inizio = 0x2cc + offset`.
- Ogni sezione comincia con una riga `#<Nome>\n`, **compresa** nell'offset e nella lunghezza.
Il corpo è quindi `[0x2cc + offset + len(nome) + 2, 0x2cc + offset + lunghezza)`.
- **L'indice è a catena**: la voce `NEXT_TOC_ENTRY` punta (stessa regola) a un secondo
`#UGC_TOC`, in fondo al file, con altre sezioni. Vanno seguiti tutti fino a `#END_OF_UGC`.
- Campi di una riga: `nome offset lunghezza dimensione generazione tipo metodo crc crc`.
`dimensione` è la lunghezza del corpo **decompresso** (per le sezioni in chiaro è
`lunghezza − len(nome) − 2`).
Il campo `tipo` in questo file:
| Tipo | Contenuto | Sezioni |
|---|---|---|
| `9` | PSB in chiaro | quasi tutte |
| `a` | PSB compresso Unix `compress` (`1f 9d 10`, LZW a 16 bit): si decomprime e si legge come le altre; la dimensione ottenuta coincide con quella dichiarata | `ND:0:*`, `SolidPersistTable`, `SolidPrimdata` |
| `c` | comincia con `1f 9d 10` ma **non** è un LZW standard | `DispDataTable` (non letto) |
| `11` | JPEG (`ff d8 ff e0 ... JFIF`): la miniatura del modello | `THMB_IMG_MAIN` |
| `b` | non decodificato: primi 4 byte in chiaro (`e0 00 Fi`, `e0 00 Fe`), poi entropia massima, stessa lunghezza del testo in chiaro | `IndentTag`, `FeatRefData` |
Il tipo `b` ha lo stesso aspetto delle sezioni "tipo `11`" dei file Wildfire descritte in
`formato-prt-feature.md` §1.3. Resta il blocco principale.
## 2. La grammatica
Una sezione è un albero di campi. I byte `e0`-`ff` sono marcatori, gli altri dati.
| Byte | Significato |
|---|---|
| `e0 <tipo> <nome>\0` | **dichiara** un campo e ne dà subito il valore |
| `e1` | puntatore nullo |
| `e2`, `e3` | apre una struttura (non distinte dal lettore) |
| `e4`, `e5`, `e6` | dentro un array: ripeti il valore precedente 1, 2, 3 volte |
| `e7 <n>` | dentro un array: ripeti il valore precedente `n` volte |
| `e8`-`ed` | forme "grezze" dei numeri (§3) |
| `f0`-`f3` | chiudi 0, 1, 2, 3 contenitori (§2.3) |
| `f4 <n>` | chiudi `n` contenitori |
| `f5` | prefisso di un intero: bit 31 acceso |
| `f6` | intero −1 |
| `f7 <id>` | il prossimo valore è del campo numero `id` (§2.2) |
| `f8 <n>` | array a una dimensione di `n` elementi |
| `f9 <a> <b>` | array a due dimensioni (`a`×`b` numeri; per gli array di strutture `a` è il totale) |
| `fa <k> <d1>..<dk>` | array a `k` dimensioni |
| `fb` | segue l'intestazione di un array di strutture, prima del primo elemento |
| `fc <n>` | array a due dimensioni: cambia solo il numero di righe, colonne come l'ultima volta |
Tipi di campo (il byte dopo `e0`):
| Tipo | Valore |
|---|---|
| `00` | puntatore: `e1`, struttura (`e2`/`e3`), o array di strutture (`f8 n [f7 id] [fb]`) |
| `01`, `05`, `08`, `09` | intero (§3.1). `09` è senza segno (flag); `05`/`08` non distinti |
| `02` | `double` (§3.2) |
| `06` | `float` a 32 bit (§3.3) |
| `04`, `07` | array di byte: ogni byte vale se stesso; un byte ≥ `e0` è preceduto da `e8` |
| `03`, `0a` | stringa Latin1 terminata da `\0`, o `e1` |
Un campo numerico o stringa può essere un array: `f8`/`f9`/`fa` prima dei valori.
### 2.1 Gli id dei campi
Ogni `e0` riceve un **numero progressivo** (dal primo `e0` della sezione = 1). Lo stesso nome in
due strutture diverse ha due id diversi. È la chiave di tutto: `f7 <id>` si riferisce a quel
numero. Verifica: l'intestazione di ogni array di strutture ripete l'id del proprio campo
(`unit_arr` è il 20° campo di `BasicText` e l'array è `f8 03 f7 14 fb`).
### 2.2 Modalità posizionale: la tabella "dopo X viene Y"
Un campo è scritto col nome **una sola volta**. Dopo, i valori arrivano nudi. Per sapere di
quale campo è un valore nudo, chi scrive e chi legge tengono la stessa tabella:
```
next[id] = l'id del campo scritto subito dopo, l'ultima volta che id è stato scritto
```
- Se il prossimo campo è `next[ultimo]`, il valore è scritto e basta.
- Altrimenti è preceduto da `f7 <id>`.
- La tabella è **piatta** (non tiene conto dell'annidamento) e si aggiorna a ogni campo, anche
dopo un `f7` e dopo un campo dichiarato per nome.
- Ogni elemento di un array di strutture è una nuova occorrenza del campo-array: per questo
dopo il primo elemento si trova `f1 f7 <id array>` (insegna che "dopo l'ultimo campo
dell'elemento viene un altro elemento") e dopo gli altri non c'è nulla.
- Un campo mai dichiarato e nullo **non viene scritto**: compare col nome la prima volta che ha
un valore, anche in mezzo a un record posizionale.
**Lunghezza degli array in modalità posizionale**: `f8 <n>` compare solo se `n` è diverso
dall'ultima volta che quel campo è stato scritto. Eccezione osservata: un array di stringhe più
corto del precedente arriva senza `f8` (`edit_header` in `BasicData`: 6 stringhe, poi 5); il
lettore si ferma al primo byte < `20`. È un'euristica, non una regola capita.
### 2.3 Dove va un campo: la pila dei contenitori
Strutture, array e loro elementi sono contenitori su una pila. Tre regole:
1. **Valore nudo senza `f7`**: va nel contenitore più vicino alla cima che possiede quel campo;
quelli sopra si chiudono in silenzio.
2. **Campo con nome o `f7`, con marcatore `f0`-`f4`**: si chiudono esattamente quel numero di
contenitori (`f0` = nessuno). Il marcatore sta **dopo** il nome del campo.
3. **Campo con nome o `f7`, senza marcatore**: se il campo atteso (`next[ultimo]`) appartiene al
contenitore **padre** di quello in cui è stato scritto l'ultimo campo, quest'ultimo si chiude;
altrimenti non si chiude nulla.
Casi particolari, tutti osservati:
- **Alias di tipo**: `e3 f7 <id>` con `id` di un'altra struttura vuol dire "questa struttura ha
lo stesso schema di quella" (`next_feat_ptr 00 f2 e3 f7 03`: il record successivo usa i campi
del primo).
- **Puntatori "al successivo"** (`next_feat_ptr`, `next_cntr_ptr`, `relcode_tree_next`): sono
scritti **dopo aver chiuso** la struttura a cui appartengono, quindi al livello del padre. Le
liste concatenate risultano così sequenze piatte, non annidamenti.
- **Campi a livello di array**: `next` in `ref_part_table` è dichiarato dopo aver chiuso il
primo elemento e ricompare (`e1`) dopo ogni elemento.
- **Varianti di unione**: il nome porta la variante tra parentesi (`data(sld_trace)`,
`srf_prim_ptr(cylinder)`, `typed_data(MTTyped_CreateData)`). Lo stesso id può comparire in
contenitori diversi.
**Cosa è solido e cosa no.** La sequenza *campo → valore* dipende solo da `next[]` e dai tipi:
è la parte provata (tutte le sezioni lette fino all'ultimo byte). L'annidamento dipende dalle
regole 1-3, che sono ricavate per tentativi: in `AllFeatur`, `MdlStatus` e `FeatDefs` di
questo file la pila alla fine non torna a zero (un contenitore in più o in meno). I valori sono
giusti, il livello di qualche campo può non esserlo.
## 3. I numeri
### 3.1 Interi
| Primo byte | Valore |
|---|---|
| `00`-`7f` | il byte |
| `80`-`bf` | 2 byte, 14 bit |
| `c0`-`df` | 3 byte, 21 bit |
| `e8` / `e9` / `ea` / `eb` | 1 / 2 / 3 / 4 byte grezzi (`e9` ipotizzato) |
| `f5 <intero>` | intero con il bit 31 acceso (`hostid` `f5 eb 2c 14 fd cf` = `ac14fdcf` = 172.20.253.207) |
| `f6` | −1 |
### 3.2 `double`
Il primo byte è un tag. `T` è il byte alto del `double` IEEE big-endian.
| Tag | Byte dopo | Valore |
|---|---|---|
| `00`-`03` | 1 costante + (tag+1) byte | la costante, con gli ultimi (tag+1) byte sostituiti: `00 0c 9a` = 0,1 esatto; `02 11 66 89 30` = π con rumore |
| `04`-`18` | 0 | costante (tabella sotto) |
| `19`-`2f` | vedi modi | positivi, 5 tag per ogni `T` da `3c` a `40`: `T = 0x3c + (tag − 0x17) / 5` |
| `30`-`48` | vedi modi | negativi, `T` da `bc` a `c0`: `T = 0xbc + (tag − 0x30) / 5` |
| `49`-`a4` | 6 | positivi: i primi 2 byte sono `0x3f75 + tag` (da `3fbe` a `4019`) |
| `a5`-`df` | 6 | negativi: i primi 2 byte sono, in ordine, `bfd0`...`c00d` **saltando** `bfd2`, `bfdf` e `bffe` |
| `ea` | 3 | 3 byte alti, il resto zero |
| `ec` | 7 | 7 byte alti, l'ultimo zero |
| `ed` | 8 | il `double` intero |
Modi dei tag a famiglie di 5 (resto di `(tag − base) / 5`):
| Resto | Byte dopo | Composizione |
|---|---|---|
| 4 | 2 | `T b1 b2` e 5 zeri (`2f 59 00` = 100) |
| 3 | 2 | `T b1 b2` e `b2` ripetuto (`2e 1b 33` = 6,8) |
| 2 | 7 | `T` e 7 byte |
| 1 | 6 | `T`, 6 byte, uno zero |
| 0 | 3 | `T`, 3 byte, 4 zeri |
La famiglia di `T = 3c` ha solo i tag `19`, `1a`, `1b`: `17` e `18` sono costanti.
Costanti a un byte:
| Tag | Valore | Prova |
|---|---|---|
| `07` / `0a` / `0c` | 0,001 / 0,01 / 0,1 | tolleranze di default per numero di decimali (`BasicData`), ISO 2768 sulle quote |
| `0d` | −0,5 | **ipotesi** (un solo caso) |
| `0e` / `0f` / `10` | 0,5 / 1 / −1 | matrici ortonormali |
| `11` / `12` / `13` / `14` | π / π/2 / 2π / 3π/2 | estremi angolari dei cilindri; stesso rumore (`+0x112e0c` sulla mantissa) nelle forme `02 xx ...` |
| `15` / `16` / `17` | −π / −π/2 / −2π | idem |
| `18` | 0 | ovunque |
| `04` | **ignoto** | compare come componente di tangente e come estremo di un inviluppo |
| `05`, `06`, `08`, `09`, `0b` | mai visti | |
Prove: le righe e colonne di tutte le matrici `local_sys` hanno norma 1 con questa tabella
(5397 su 5397 in questo file); i prefissi `3fd0`-`3fef` e `bfd0`-`bfef` sono verificati uno a
uno sugli 89 file risolvendo il modulo dalla norma. Fuori da quell'intervallo valgono solo
alcuni punti fermi: `86` = `3ffb` (√3), `8c` = `4001` (2,2046 lb/kg), `8f` = `4004` (5π/6),
`ce` = `bffb`, `d9` = `c007` (−3 + ε). Il buco `bffe` (e non `bffd`) è una **scelta non
provata**: uno dei due manca, non so quale.
### 3.3 `float` a 32 bit (tipo `06`)
Usati nei dati di visualizzazione (`SolidPrimdata`, `SolidPersistTable`).
| Tag | Byte dopo | Valore |
|---|---|---|
| `00` / `0b` / `28` | 0 | 0 / +1 / −1 (segno ricavato dalle normali delle facce estreme) |
| `30`-`3f` | 3 | byte alto `0x37 + (tag − 0x30)`, positivi |
| `40`-`4f` | 3 | byte alto `0xb7 + (tag − 0x40)`, negativi |
| `50`-`a3` | 2 | positivi: i primi 2 byte da una **tabella non lineare** |
| `a4`-`df` | 2 | negativi: idem |
| `e8` / `e9` / `ea` / `eb` | 1 / 2 / 3 / 4 | byte alti grezzi, il resto zero |
La tabella dei tag `50`-`df` **non è ricostruita**: sono prefissi scelti per frequenza, non una
formula. Voci ricavate dalle normali unitarie: `63`=`3e1d`, `67`=`3e46`, `68`=`3e80`,
`69`=`3e84`, `6a`=`3ea6`, `6d`=`3f08`, `6e`=`3f0a`, `6f`=`3f0b`, `70`=`3f0c`, `71`=`3f0d`,
`72`=`3f0e`, `73`=`3f0f`, `74`=`3f10`, `75`=`3f17`, `76`=`3f21`, `77`=`3f57`; `bc`=`be80`,
`bd`=`be84`, `bf`=`becf`, `c0`=`bee4`, `c1`=`beea`, `c4`=`bf0a`, `c5`=`bf1c`, `c6`=`bf20`,
`c7`=`bf27`, `c8`=`bf32`, `ca`=`bf60`, `cb`=`bf66`, `cc`=`bf7a`. Il parser di riferimento usa
una formula lineare **sbagliata** per questi tag: la lunghezza è giusta (la lettura prosegue),
il valore no. Finché la tabella non è completa la mesh non si può esportare.
## 4. Algoritmo di lettura
```
pila = [], ultimo = nessuno, next = {}, campi = {}
finché ci sono byte:
m = marcatore f0-f4 se presente
se il byte è e0: # campo con nome
leggi tipo e nome; m = marcatore dopo il nome, se presente
applica la regola 2 (m presente) o 3 (m assente) di §2.3
id = nuovo numero; il campo appartiene al contenitore in cima
altrimenti:
se m presente: chiudi m contenitori
se il byte è f7: id = intero che segue (poi regola 3 se m assente)
altrimenti: id = next[ultimo]
porta in cima il contenitore di id (regola 1; alias se la cima è una struttura vuota)
next[ultimo] = id; ultimo = id
leggi il valore secondo il tipo di id; strutture e array vanno sulla pila
```
Uso:
```powershell
python scripts/psb/prt.py 9253000117a.prt.35 # indice delle sezioni
python scripts/psb/psb.py 9253000117a.prt.35 '*' out # ogni sezione in out/<Nome>.txt
python scripts/psb/psb.py 9253000117a.prt.35 FullMData out
python scripts/psb/unz.py 9253000117a.prt.35 out # decomprime le sezioni .Z in out/*.bin
```
## 5. Cosa c'è in ogni sezione
Nomi di radice e campi come nel file. "Letto" = arriva all'ultimo byte.
| Sezione | Radice | Contenuto |
|---|---|---|
| `BasicText` | `Sld_BasicText` | proprietà di massa (`density`, `volume`, `cg`, `inertia`, `surfarea`), unità (`unit_arr`: `MM`, `KILOGRAM`, `SECS`; `principal_sys_units` = `millimeter Kilogram Sec (mmKs)`) |
| `BasicData` | `Sld_BasicData` | tolleranze di default, tabelle ISO (`iso_tol_tbls_ptr`, classi `Fine`...`Very Coarse`), tabelle dei fori (`holes_arr`) |
| `FullMData` | `Sld_FullData` | **quote** (`dim_array`, 788 voci: `dim_type`, `feat_id`, `value`, `tol_ptr.value`), modelli referenziati (`ref_part_table`) |
| `AllFeatur` | `Sld_Features` | lista delle feature (`first_feat_ptr`/`next_feat_ptr`): `id`, `feat_type_ptr.type`, `sect_name`, tabelle delle geometrie create, date |
| `FeatDefs` | `Sld_FeatDefs` | definizione di ogni feature (`feat_defs_<tipo>`), schizzi, istruzioni di posizionamento |
| `FeatDefsIndex` | `Sld_FeatDefsIndex` | i tipi di feature presenti (`feat_defs_index`: 923, 979, 1084, 917, 916, 914, 926, 1104, 1232, 911, 949, 1070, 913, 1052) |
| `MdlStatus` | `Sld_MdlStatus` | storico: un record per azione, con utente, data e nome (`Extrude 9`, `Specchiare 4`) |
| `VisibGeom` / `NovisGeom` | `Sld_VisGeom` / `Sld_NonVisGeom` | **B-rep**: `srf_array` (superfici: `geom_id`, `geom_type`, `feat_id`, `srf_prim_ptr(plane/cylinder/...)` con `local_sys` e raggio), contorni (`next_cntr_ptr`), `crv_array` (spigoli con i punti) |
| `ActEntity` | `Sld_Entities` | entità di riferimento: punti (`PNT0`...), linee, sistemi di coordinate |
| `GeomDepen` | `Sld_GeomDepend` | ultima feature rigenerata, data dell'ultima rigenerazione, proprietà delle geometrie |
| `LargeText` | `Sld_LargeText` | **nomi** (`names_table`), **parametri** (`symtbl`: `DENOMINAZIONE`, `PRO_MP_*`...), **relazioni** come albero (`relset_code_tree`) con il testo sorgente e i commenti |
| `DispCntrl` | `Sld_DispCntrl` | layer (`01___PRT_ALL_DTM_PLN`, ...) con gli id contenuti e le regole |
| `FamilyInf` | `Sld_FamilyInfo` | family table (`gen_name`, `info_items`) |
| `Xsections` | `Sld_Xsections` | sezioni trasversali, ognuna con la propria geometria (`xsec_geom`) |
| `ModelView#n` | `View` | viste salvate: matrici `orient`/`local_sys` 4×4, nome |
| `NeuPrtSld` | `Sld_NeutPart` | dati "neutral" della parte |
| `ColorSchemeInfo*`, `DispDataColor`, `AssMrgPrt`, `PipeInfos`, `NeuAsmSld`, `DEPDB_DATA`, `THMB_IMG_INDX` | | piccole o vuote |
| `SolidPrimdata` (compressa) | `pgl_primdata_group_table` | **geometria di visualizzazione**: archi (`prim_arc`: raggio, angoli, centro, piano), linee e la **mesh** (`mv_p_NxNyNzxyz`: normale + vertice, `float`) |
| `SolidPersistTable` (compressa) | `pgl_persist_table` | ingombro (`xyz_min_max`: x ±45,57, y −74,07..90, z −807..0), `bounding_radius` 414,27 |
| `ND:0:Model_*`, `ND:0:UnitSystemDef_*`, `ND:0:PDMTrail_*` (compresse) | `PSB_Tokens` | §7 |
Valori letti da questo file, come esempio (nessuno confrontato con Creo): densità
7,85·10⁻⁶ kg/mm³, volume 3,11·10⁶ mm³, dipendenze `9053004005` e `9253018001`.
## 6. Cosa cambia rispetto alla lettura a pattern
- **Il primo record non è più un caso speciale**: è solo quello che dichiara i campi. "La prima
quota dell'array può mancare" (`formato-prt-geometria.md` §2) non vale più.
- **I record posizionali si leggono con i nomi**: ogni valore ha il nome del suo campo.
- **Le quote**: `dim_array` → `dim_dat_ptr.value`, `tol_ptr.value` (i due scostamenti: 0,1 per
la quota 2; 0,2 per 20; 0,3 per 59, cioè ISO 2768-m), `dim_cosm_ptr`.
- **Le unità** si leggono (`BasicText`), non vanno più assunte.
## 7. Le sezioni `ND:0:*`: `PSB_Tokens`
Decompresse sono un PSB con una sola struttura:
```
PSB_Tokens:
format = 0
type_har = "MOMOIOIOIOIOFCOFOM..." un carattere per token
int_har = [0, 1, 2, 3, 4, 5, -1, ...]
double_xar = [-1.0, 0.001, 1e-09, ...]
string_xar = ['ndmodel_l05::Model_L05', 'Descriptor', 'Name', '9253000117a', 'Type', 'prt', ...]
```
È un secondo livello di serializzazione: `type_har` è la sequenza dei token, gli altri tre array
sono i valori a cui i token attingono in ordine. **Il significato delle lettere non è
decodificato** (`M`, `O`, `I`, `F`, `C`, `P`, `Q`, `N`, `H`, `R`, `S`, `U`). Le stringhe però si
leggono già: nome e tipo del modello, dipendenze, nomi dei parametri.
## 8. Dove non regge
| Problema | Dove | Stato |
|---|---|---|
| Sezioni tipo `b` (`IndentTag`, `FeatRefData`) e tipo `11` dei file Wildfire | tutti i file | **illeggibili** |
| `DispDataTable` (tipo `c`) | file recenti | non letto |
| Tabella dei prefissi `float` `50`-`df` | `SolidPrimdata` | parziale: la mesh non è esportabile |
| Costante `double` `04` | geometria | valore ignoto |
| Annidamento | `AllFeatur`, `MdlStatus`, `FeatDefs` | la pila non torna a zero a fine sezione |
| Alberi bilanciati (`lesser_child`/`greater_child`/`balance_factor`) | `LargeText` di 24 file di `raw/` | il lettore si ferma: la regola dei puntatori "al successivo" non basta |
| `srf_flip_dat`, `end2`, `backup_size` "contenitore non in pila" | 26 sezioni di `raw/` | stesso tipo di errore: è la pila, non i valori |
| Array di stringhe senza `f8` | `BasicData` | euristica (§2.2) |
| `PSB_Tokens` | `ND:0:*` | solo gli array, non la struttura |
| Significato dei campi | ovunque | dedotto dal nome, mai verificato in Creo |
Sugli 89 file di `raw/`: 2152 sezioni lette per intero, 74 no. Tutti i fallimenti sono del tipo
"contenitore non in pila" o "pila vuota": la sequenza dei valori fino a quel punto è corretta.
## 9. Correzioni agli altri doc
- `formato-prt-feature.md` §1.1: la costante `K` non va ricavata dall'ultima sezione: è
l'ultimo campo della prima riga del file, e la sezione comincia con la riga `#Nome`. L'indice
può essere a catena (`NEXT_TOC_ENTRY`). La formula data lì funziona solo per caso.
- `formato-prt-feature.md` §1.2: le sezioni `ND:0:*` decompresse non sono XML ma `PSB_Tokens`
(§7); i nomi tipo SOAP sono le stringhe di `string_xar`.
- `formato-prt-geometria.md` §1: la tabella dei tag è sostituita da §3.2. In particolare
`e4`-`e7` non sono "zeri" ma **ripetizioni del valore precedente**, e i tag `49`-`df`
("non decodificato") sono i prefissi a 2 byte.
- `formato-prt-geometria.md` §1: `UnitSystemDef` non serve per le unità, stanno in `BasicText`.
## 10. Come proseguire
1. **Confrontare con Creo** un file piccolo: è l'unica prova che manca a tutto il doc.
2. Completare la tabella dei `float` raccogliendo le normali su più file (stesso metodo di
§3.3): sblocca l'esportazione della mesh senza Creo.
3. Sistemare la pila sugli alberi di `LargeText` (§8), poi portare il parser in C# accanto ad
`AsmRecordReader`, con la stessa regola: se la sezione non si legge fino in fondo,
`InvalidDataException`.
4. Sezioni tipo `b`: servono due salvataggi dello stesso modello che differiscono di poco.
+20
View File
@@ -0,0 +1,20 @@
# Glossario
| Termine | Significato |
|---|---|
| Assieme generale | l'assieme di primo livello di una distinta; il foglio del gestionale non lo elenca, l'app lo aggiunge come riga se il file è `DBP_<codice>_<timestamp>` |
| `.asm` / `.prt` | file Creo/Pro-E di assieme e di parte; il nome è `<codice>.<tipo>.<versione>` |
| Cartella di lavoro | dove finiscono CSV, `raw/` e `step/`; di default la cartella di installazione |
| Codice | identificativo del componente, ricavato dal nome del file (può avere una lettera di revisione attaccata) |
| Creo Distributed Batch | strumento PTC che esporta in batch (qui in STEP); usato a mano, fuori dall'app |
| Distinta Base (DBP) | estrazione dal gestionale (Fata) dei componenti di un assieme |
| Family table | tabella di varianti di un modello; il nome dell'istanza sta nel parametro `ConfigName` |
| Indice | il DB `cad_index.db` con i metadati dei file della share |
| `members` | sezione del `.asm` con i componenti diretti dell'assieme |
| `raw/` | cartella con i file CAD da passare a Creo |
| Riferimento esterno | modello citato da un assembly feature o datum copiato; non è un membro, ma Creo lo carica con il suo albero |
| Share | disco di rete con i file CAD, `X:\` di default |
| Soppresso | componente tolto dalla distinta ma il cui file serve comunque ad aprire l'assieme |
| `step/` | cartella degli STEP esportati da Creo; non scritta dall'app |
| `temp_*` | cartelle personali della share, escluse dall'indice |
| Ultima versione | per ogni `(cartella_radice, codice, tipo_file)` la riga con versione più alta (vista `files_ultima_versione`) |
+159
View File
@@ -0,0 +1,159 @@
# 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.
In alternativa scegli **Lista personalizzata**: digita un codice e premi **Invio** (o
**Aggiungi**) per inserirlo, ripeti per ogni codice; **Canc** sulla riga selezionata lo
toglie. La lista non scrive nessun CSV e, passando da una modalità all'altra, viene azzerata
(con conferma). Inserisci anche l'assieme generale, se serve: qui non viene aggiunto da solo.
- **2. Verifica la presenza dei file sul server**: compare solo dopo un import riuscito
(o dopo il primo codice aggiunto alla lista personalizzata; ogni modifica della lista
richiede di rifare la verifica).
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).
+160
View File
@@ -0,0 +1,160 @@
# Reverse engineering dei file Creo: stato e ripresa
Diario di passaggio di consegne per il lavoro sul formato dei `.prt`/`.asm`. Serve a chi riprende
(anche in un'altra sessione) per sapere **dove siamo, cosa è provato, da dove ripartire**, senza
rifare la strada. Il *risultato* del lavoro sta in [`formato-psb.md`](formato-psb.md): qui c'è
solo lo stato, le decisioni e i vicoli ciechi. Quando un punto si chiude, il contenuto va nel doc
di formato e qui resta una riga.
Ultimo aggiornamento: 2026-11-10, ramo `reverse-engineering-creo`.
## Stato in una riga
Il formato di serializzazione (PSB) è decodificato e tutte le sezioni in chiaro di
`9253000117a.prt.35` si leggono fino all'ultimo byte con nome e valore di ogni campo; **nessun
valore è stato ancora confrontato con Creo**, e tre cose bloccano "tutti i dati senza Creo"
(vedi "Cosa manca").
## Da non perdere
- **Niente è committato.** In sospeso: `docs/formato-psb.md`, `docs/history.md`, `scripts/psb/`,
e le modifiche a `CHANGELOG.md`, `docs/README.md`, `docs/architettura.md`,
`docs/formato-prt-geometria.md`.
- `9253000117a.prt.35` è nella root, non tracciato: è il campione di tutto il lavoro. Decidere
se tenerlo nel repo (2 MB) o in `raw/`.
- `scripts/psb/__pycache__/` va ignorato, non committato.
- `docs/formato-prt-feature.md` contiene byte NUL: gli strumenti di modifica lo rifiutano. Le
correzioni che lo riguardano sono in `formato-psb.md` §9, non applicate al file. Va
ripulito dai NUL (conservando la codifica) prima di poterlo correggere.
## Come ripartire in cinque minuti
Dalla root del repo, con Python 3.12 (solo libreria standard):
```powershell
python scripts/psb/prt.py 9253000117a.prt.35 # indice delle sezioni
python scripts/psb/psb.py 9253000117a.prt.35 '*' out # dump di ogni sezione in out/
python scripts/psb/psb.py 9253000117a.prt.35 LargeText - t # traccia campo per campo (debug)
python scripts/psb/unz.py 9253000117a.prt.35 out # decomprime le sezioni .Z
python scripts/psb/pbin.py out/SolidPrimdata.bin # legge una sezione decompressa
python scripts/psb/batch.py "raw/*" # regressione sugli 89 file
python scripts/psb/norms.py 9253000117a.prt.35 # controllo norma 1 delle matrici
python scripts/psb/tagmap.py 9253000117a.prt.35 "raw/*" # tag double -> prefisso, dalle norme
python scripts/psb/ctx.py raw/x.prt.1,VisibGeom,cf8 # hexdump attorno a un offset
```
Valori attesi oggi, da usare come regressione prima e dopo ogni modifica al parser:
| Controllo | Atteso |
|---|---|
| `psb.py 9253000117a.prt.35` | 29 sezioni `OK`, 5 no: `AllFeatur` (`pop underflow` sull'ultimo byte), `MdlStatus` e `FeatDefs` (pila non a zero), più l'intestazione delle due ultime |
| `norms.py` | `good rows 5397 bad 0` |
| `batch.py "raw/*"` | 2152 sezioni lette, 74 no (misurato **prima** delle ultime correzioni ai tag: va rilanciato) |
Se `norms.py` dà righe "bad" dopo una modifica, si è rotta la tabella dei `double`: è il
controllo più sensibile che abbiamo.
## Cosa è provato, e come
| Cosa | Prova |
|---|---|
| Contenitore (offset = ultimo campo della prima riga, riga `#Nome` inclusa, indice a catena) | tutte le sezioni cominciano con `e0 00 <radice>` |
| Grammatica e tabella "dopo X viene Y" | lettura fino all'ultimo byte di tutte le sezioni; l'id nell'intestazione degli array coincide con quello calcolato |
| Tag dei `double` | norma 1 su 5397 righe; prefissi `3fd0`-`3fef` e `bfd0`-`bfef` verificati uno a uno sugli 89 file |
| Costanti π, π/2, 2π, 3π/2 e negative | stesso rumore di mantissa nelle forme modificate (`02 xx ...`) |
| Sezioni `.Z` | la dimensione decompressa coincide con quella dichiarata nell'indice |
| Significato dei campi | **nessuna**: dedotto dai nomi |
## Cosa manca, in ordine di valore
1. **Confronto con Creo.** Aprire un file piccolo, annotare quote, parametri, massa, posizione
di una faccia, e confrontare con il dump. Finché non c'è, tutto il doc è "livello 1 e 2".
2. **Tabella dei `float` (tag `50`-`df`).** È una tabella di prefissi scelti per frequenza, non
una formula. Il parser usa `FBASE` lineare, **sbagliato** per quei tag (lunghezza giusta,
valore no). Senza, la mesh di `SolidPrimdata` (`mv_p_NxNyNzxyz`) non si esporta. Metodo che
ha funzionato per le 29 voci note: normali unitarie, risolvere il modulo dalla norma. Va
ripetuto su tutti i file di `raw/` (ognuno ha `SolidPrimdata`), poi sostituire `FBASE` con
un dizionario e far fallire il parser sui tag ignoti invece di inventare.
3. **La pila dei contenitori.** I valori sono giusti, l'annidamento no in alcuni punti:
- `LargeText` di 24 file di `raw/`: alberi bilanciati (`lesser_child`, `greater_child`,
`balance_factor`, `backup_size`). La regola `post` (puntatore "al successivo" scritto al
livello del padre) copre un solo campo per struttura; qui i figli sono due.
- `srf_flip_dat` (geometria), `end2` (`FeatDefs`): "contenitore non in pila".
- fine di `AllFeatur`/`MdlStatus`/`FeatDefs`: pila non a zero.
Punto di partenza: `default_close` e `locate` in `psb.py`, con la traccia (`- t`).
4. **Sezioni tipo `b`** (`IndentTag`, `FeatRefData`) e **tipo `11` Wildfire**: illeggibili.
Stessa lunghezza del chiaro, primi 4 byte in chiaro. Già escluso (vedi
`formato-prt-feature.md` §1.3): XOR a chiave fissa, zlib, LZW. Non riprovare senza una
coppia chiaro/cifrato dello stesso modello.
5. **`DispDataTable`** (tipo `c`): comincia con `1f 9d 10` ma l'LZW standard fallisce al
secondo codice. Non indagato.
6. **`PSB_Tokens`** (`ND:0:*`): letti i quattro array, non il significato delle lettere di
`type_har`. Ipotesi da cui partire: `I` consuma un intero, `F` un double, una lettera per le
stringhe; `M`/`O` aprono oggetto e campo. Contare le occorrenze contro le lunghezze degli
array la conferma o la smentisce in pochi minuti.
7. **Costanti `double` ignote**: `04` (vista come componente di tangente e come estremo di
inviluppo), `0d` (−0,5 da un solo caso), buco `bffe` contro `bffd` non deciso.
8. **Portare il parser in C#** accanto ad `AsmRecordReader`: solo dopo i punti 1 e 3.
## Il ragionamento da adottare
È il metodo che ha portato fin qui. Va seguito anche per i punti aperti: ogni volta che è stato
saltato, si è perso tempo.
1. **Partire dal modello di chi scrive, non dai byte.** Il file è l'uscita di un serializzatore
generico che percorre strutture C. La domanda utile non è "cosa significano questi byte" ma
"che regola semplice userebbe un programma per scriverli". Ogni marcatore ridondante (`f7`,
`f0`-`f4`) esiste perché il lettore, da solo, avrebbe previsto un'altra cosa: capire la
previsione spiega il marcatore.
2. **Un'ipotesi alla volta, e un controllo che la possa smentire.** Prima di scrivere codice
dire cosa ci si aspetta di vedere se l'ipotesi è vera. Esempio: "se gli id sono progressivi,
l'id nell'intestazione di `unit_arr` deve essere 0x14" (contato a mano: lo era).
3. **Il parser è lo strumento di misura.** Una regola è buona se fa arrivare la lettura
all'ultimo byte di *tutte* le sezioni; l'errore dice dove guardare. Si corregge la regola
generale, mai il caso singolo: un'eccezione scritta per una sezione ha sempre rotto un'altra.
4. **Cercare i controesempi, non le conferme.** Quando una regola spiega cinque casi, elencare
per iscritto i casi noti (con i byte) e verificare che li spieghi tutti, compreso quello
scomodo. La regola sulle chiusure implicite è nata mettendo in tabella dieci casi e cercando
l'unica formulazione che non ne contraddiceva nessuno.
5. **Ancorare i numeri a vincoli fisici, non all'aspetto.** Un valore "plausibile" non è una
prova. Lo sono: la norma 1 delle righe di una matrice di rotazione, valori noti (π, √2/2,
0,1, 25,4), la coerenza tra sezioni (lo stesso punto scritto due volte). Per i tag dei
numeri: far risolvere il prefisso al vincolo (`tagmap.py`), non estrapolare una formula.
6. **Separare ciò che è provato da ciò che è dedotto.** Tokenizzazione (sequenza campo-valore),
annidamento e significato dei campi sono tre cose con tre livelli di prova diversi. Nel doc
ognuna va dichiarata col suo livello; "regge sui campioni" non è "è così".
7. **Allargare il campione appena una regola regge.** Un file solo nasconde le tabelle: i buchi
nei prefissi negativi e la tabella dei `float` sono emersi solo confrontando più file.
Dopo ogni modifica: `norms.py` sul campione, poi `batch.py` su `raw/`.
8. **Quando due spiegazioni reggono entrambe, cercare il dato che le distingue** invece di
scegliere la più elegante (esempio: i tag `91`-`a0` sembravano negativi; li ha decisi una
curva i cui parametri dovevano crescere da 3π/2).
9. **Scrivere subito nel doc, con il sintomo e i byte.** Una scoperta non scritta si rifà.
Scrivere anche ciò che è stato escluso e perché.
10. **Dire quello che non si sa.** Una costante ignota resta "ignota" (`<c04>` nel dump), non
riceve un valore inventato; un parser che non capisce si ferma, non restituisce un risultato
parziale in silenzio.
## Vicoli ciechi e trappole, per non rifarli
- **Modello "pop = chiudi N livelli" puro**: non basta. I marcatori `f0`-`f4` compaiono solo
quando la chiusura reale è diversa da quella che il lettore farebbe da solo; `f0` vuol dire
"resta qui". Ci sono volute quattro riscritture per arrivarci.
- **Ordine dei campi per struttura** (primo/successivo per schema): sbagliato. La tabella è
piatta, per id, e vale l'ultima transizione vista.
- **`e4`-`e7` come "zeri"**: sono ripetizioni del valore precedente.
- **Lo stesso id in contenitori diversi**: succede con le varianti di unione
(`typed_data(...)`); il parser le accetta e le conta in `lenient`.
- **Tag `double` negativi lineari**: no, ci sono tre buchi. Non fidarsi di un'estrapolazione
fuori dagli intervalli verificati.
- **Heredoc in Bash con `\\0`**: lo strumento collassa le barre e scrive un NUL nel sorgente.
Per creare file Python usare lo strumento di scrittura, non un heredoc.
- **Git Bash e `cd`**: gli script vanno lanciati dalla root del repo; `dotnet run` dentro
`PrepStep/` avvia la GUI.
## Cronologia
| Data | Fatto |
|---|---|
| 2026-10-21 | Albero delle feature, quote e geometria degli schizzi letti a pattern (`formato-prt-feature.md`, `formato-prt-geometria.md`) |
| 2026-11-10 | Contenitore corretto; grammatica PSB completa; tabella dei `double`; sezioni `.Z`; catalogo delle sezioni; parser di riferimento in `scripts/psb/`; `formato-psb.md` |
+47
View File
@@ -0,0 +1,47 @@
# Rilasciare una nuova versione
Procedura per produrre il setup `PrepStep-Setup-<versione>.exe`, l'unico file da distribuire.
Il comportamento del setup (installazione per-utente, aggiornamento, `/VERYSILENT`) è descritto
in [`architettura.md`](architettura.md#installazione); qui ci sono solo i passi da eseguire.
## Una volta sola
Installa Inno Setup 6 per l'utente corrente (niente admin):
```powershell
winget install --id JRSoftware.InnoSetup -e --scope user
```
## A ogni release
1. Aggiorna `<Version>` in `PrepStep/PrepStep.csproj` (SemVer).
2. In [`CHANGELOG.md`](../CHANGELOG.md) trasforma "Non rilasciato" nella nuova versione con la
data e riscrivi sotto le modifiche (Aggiunto/Modificato/Corretto/Rimosso).
3. Controlla i doc: `dotnet run scripts/CheckDocs.cs` deve confermare che versione e CHANGELOG
coincidono.
4. Esegui i test: `dotnet test` in `PrepStep.Tests/`.
5. Chiudi l'app se è aperta (l'exe in uso blocca la build), poi dalla root del repo:
```powershell
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
```
6. Il setup è in `installer\Output\PrepStep-Setup-<versione>.exe`; la versione è letta
dall'exe, quindi coincide con `<Version>`. Provalo lanciandolo.
7. Commit con codice, doc e CHANGELOG insieme (regola in [`README.md`](README.md)).
## Solo l'eseguibile, senza setup
```powershell
cd PrepStep
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -o publish
```
Produce `PrepStep\publish\PrepStep.exe`, un file unico che gira su un Windows x64 senza .NET
installato.
## Da non toccare
`AppGuid` in `installer/PrepStep.iss`: è ciò che fa riconoscere a Windows le versioni nuove
come aggiornamento della stessa app. Cambiarlo produce un'installazione doppia.
+46
View File
@@ -0,0 +1,46 @@
# Risoluzione dei problemi
Sintomo → causa probabile → cosa fare. I messaggi di Creo non nominano il file né la causa, per
questo il sintomo è il punto di partenza.
## Dentro l'app
| Sintomo | Causa probabile | Cosa fare |
|---|---|---|
| Pallino rosso in fase 1, "Database non trovato" | non c'è `cad_index.db` nella cartella di installazione | **Aggiorna indice** (lo crea lì), oppure **Apri DB...** per usarne un altro solo per la sessione |
| "Disco non raggiungibile: X:\" | la share non è montata o non hai accesso | monta `X:\` e riprova; la radice è fissa in `Form1.RadiceDiscoDefault` |
| Componente rosso in "Verifica presenza" | il codice non è nell'indice: indice vecchio, oppure il nome su disco ha una lettera di revisione in più o in meno | **Aggiorna indice**; poi cerca il codice con `LIKE` (vedi sotto) |
| "Avanti" resta grigio in fase 2 | manca il passo **Verifica presenza** | premila: non basta l'import |
| Alcuni file non compaiono in `raw/` dopo fase 3/4 | la copia salta in silenzio i file irraggiungibili (share offline, permessi) | controlla l'accesso alla share e ripeti la fase |
| In fase 4 compaiono codici "non trovati" | disegni, componenti davvero assenti dalla share o falsi positivi della scansione | controllali a mano; non bloccano il lavoro |
| Risultati incoerenti in sviluppo | l'app sta usando il DB della cartella di build, non quello che credi | guarda il percorso mostrato in fase 1; apri il DB giusto con **Apri DB...** |
Per capire se un codice è nell'indice:
```powershell
dotnet run scripts/Query.cs -- cad_index.db "SELECT codice, tipo_file, percorso_completo FROM files_ultima_versione WHERE codice LIKE '925%' LIMIT 20"
```
Nessuna riga nemmeno con `LIKE` → il file non è indicizzato. Una riga con codice leggermente
diverso → revisione, maiuscole o spazi.
## In Creo Distributed Batch
| Sintomo | Causa probabile | Cosa fare |
|---|---|---|
| `[-4] Dati di input non completi` | nell'albero manca un file che Creo deve caricare, ma non dice quale | rifai la fase 4 e leggi l'elenco dei "non trovati"; poi vedi sotto |
| L'assieme non si apre, nessun messaggio chiaro | in `raw/` c'è una copia sbagliata di un componente (lo stesso codice esiste in più cartelle, 13% dei casi) | l'app sceglie la copia nella cartella dell'assieme: [`formato-asm-record.md`](formato-asm-record.md) §7.4 |
| Assieme con un componente soppresso che non si apre | il file del soppresso non era in `raw/` | §7.3; oggi la fase 4 lo copia |
| Assieme con feature di assembly o datum copiati che non si apre | manca il modello citato per riferimento esterno | §7.5; oggi la fase 4 lo copia con il suo albero |
## Da sviluppatore
| Sintomo | Causa | Cosa fare |
|---|---|---|
| `dotnet run` apre la GUI e si blocca | lanciato dentro `PrepStep/` | lanciare gli script dalla root del repo |
| `dotnet publish` fallisce con file in uso | l'app è aperta | chiudila e ripeti |
| `CheckDocs.cs` segnala un file non descritto | nuovo `.cs` o doc non documentato | aggiungilo a `architettura.md` o a `docs/README.md` |
## Dopo la disinstallazione
Restano `cad_index.db`, `raw/`, `step/`, i CSV e `%AppData%\PrepStep\`: vanno cancellati a mano.
+66
View File
@@ -0,0 +1,66 @@
# Guida per chi sviluppa
Come lavorare sul codice e sui doc. Per capire com'è fatto il programma parti da
[`architettura.md`](architettura.md); per rilasciare vedi [`rilascio.md`](rilascio.md).
## Requisiti
- Windows e [.NET 10 SDK](https://dotnet.microsoft.com/download).
- Accesso alla share `X:\` solo per indicizzare davvero; i test non ne hanno bisogno.
- Node.js (per `npx markdownlint-cli2`, solo per i doc).
## Build e avvio
```powershell
cd PrepStep
dotnet build -c Release
dotnet run -c Release
```
In sviluppo `cad_index.db` si cerca nella cartella di build, non nella radice del repo: vedi
[`db-cad-index.md`](db-cad-index.md#come-lapp-trova-il-db-allavvio).
## Test
```powershell
cd PrepStep.Tests
dotnet test
```
I test xUnit chiamano gli `internal` del progetto principale grazie a `InternalsVisibleTo`. I
`.asm` dei test si costruiscono byte per byte con piccoli helper invece di versionare file
binari: ogni caso strano trovato sulla share diventa un test minimale. Elenco dei file di test in
[`architettura.md`](architettura.md#test).
## Script standalone
Si lanciano dalla **root del repo**, mai da dentro `PrepStep/` (che è un `WinExe`: `dotnet run`
avvierebbe la GUI e resterebbe bloccato). Elenco in
[`architettura.md`](architettura.md#script-standalone-scripts).
```powershell
dotnet run scripts/Query.cs -- cad_index.db "SELECT COUNT(*) FROM files"
```
## Documentazione come codice
I doc stanno in git, si modificano nello stesso commit del codice e si verificano a mano:
```powershell
dotnet run scripts/CheckDocs.cs # link, ancore, versione/CHANGELOG, copertura dei doc
npx markdownlint-cli2 # stile Markdown (.markdownlint-cli2.jsonc)
```
Regole di scrittura:
- **Una sola fonte per ogni informazione**, altrove un link (tabella in [`README.md`](README.md)).
- **Ogni doc ha un solo scopo** (Diátaxis): non mescolare istruzioni, riferimento e motivazioni.
- Un cambiamento strutturale (fasi, formato `.asm`, schema DB, criterio di scelta di un file)
aggiorna i doc e il CHANGELOG **nello stesso commit**.
- Se la modifica nasce da un fallimento reale di Creo scrivi **il sintomo**: i messaggi di Creo
non nominano il file né la causa (esempio: §7.3-7.5 di
[`formato-asm-record.md`](formato-asm-record.md)).
- Una scelta con alternative reali non banali merita una voce in
[`decisioni/`](decisioni/README.md).
- Un nuovo `.cs` va descritto in `architettura.md` e un nuovo doc elencato in `docs/README.md`:
`CheckDocs.cs` lo verifica.
+64
View File
@@ -0,0 +1,64 @@
# Tutorial: il tuo primo giro con PrepStep
Questo tutorial ti porta, con una distinta vera, dall'app appena installata ai file `raw/` pronti
per Creo. Alla fine saprai cosa produce ogni fase e come capire se è andata bene. Per il
dettaglio di ogni schermata e per l'esportazione in Creo vedi [`guida-utente.md`](guida-utente.md).
**Ti serve:** il programma installato (setup in [`rilascio.md`](rilascio.md)), la share `X:\`
raggiungibile e un'estrazione "Distinta Base" `.xls` del gestionale, meglio se piccola (poche
decine di righe) per il primo giro.
## 1. Avvia e carica l'indice
Apri **PrepStep - Cevolani**. Accanto a "Database" c'è un pallino.
- **Verde:** l'indice c'è già, vai al passo 2.
- **Rosso:** premi **Aggiorna indice**. La prima volta scansiona tutta la share e richiede
qualche minuto; vedi una barra di avanzamento con i conteggi.
Risultato atteso: pallino verde, percorso di `cad_index.db` e data dell'aggiornamento.
Premi **Avanti**.
## 2. Importa la distinta
Trascina il `.xls` nella finestra. Risultato atteso: la griglia si riempie con
Componente/Descrizione e nella cartella di lavoro compare un CSV con lo stesso nome base.
Poi premi **Verifica presenza**: le righe diventano verdi (trovato nell'indice) o rosse (non
trovato). Solo ora **Avanti** si sblocca.
Se vedi righe rosse non fermarti: nel primo giro è normale scoprire che l'indice è vecchio o
che un componente non è sulla share. Vedi [`risoluzione-problemi.md`](risoluzione-problemi.md).
## 3. Crea la cartella raw
Premi **Crea cartella raw**. Risultato atteso: nella cartella di lavoro nasce `raw/` con i file
`.prt`/`.asm` dei componenti trovati, e la griglia mostra il percorso CAD di ciascuno.
## 4. Trova i componenti referenziati
Premi **Trova componenti referenziati**. L'app legge i `.asm` in `raw/` e copia anche tutto
quello che serve a Creo per aprirli (sotto-assiemi, componenti soppressi, riferimenti esterni).
Risultato atteso: una finestra con l'elenco di quanto copiato e, in fondo, i codici "non
trovati". `raw/` ora contiene più file di prima: è giusto così, perché la lista dei file
necessari è più lunga di quella della distinta (il perché, in
[`decisioni/0003-parser-esatto-con-fallback.md`](decisioni/0003-parser-esatto-con-fallback.md)).
## 5. Porta i `.asm` a Creo
La fase 5 mostra i `.asm` della distinta come icone trascinabili. Trascinali su **Creo
Distributed Batch** (passi in
[`guida-utente.md`](guida-utente.md#5-verifica-completezza-e-creo-distributed-batch)):
i file STEP finiscono in `step/`.
## Cosa hai imparato
| Fase | Entra | Esce |
|---|---|---|
| 1 | share `X:\` | `cad_index.db` |
| 2 | `.xls` del gestionale | CSV + verifica sull'indice |
| 3 | CSV + indice | `raw/` con i componenti della distinta |
| 4 | `.asm` in `raw/` | `raw/` completa dell'albero |
| 5 | `raw/` | `.asm` da trascinare su Creo |
Per un secondo giro con un'altra distinta usa **File → Nuovo** (Ctrl+N), e prima ripulisci
`raw/`/`step/` da **File → Pulisci raw e step...**.
+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;
+59
View File
@@ -0,0 +1,59 @@
// dotnet run scripts/CheckDocs.cs (dalla root del repo; exit 1 se i doc sono fuori allineamento)
// Controlla anche che ogni doc di docs/ e docs/decisioni/ sia nell'indice.
// Controlla: link relativi (file e #ancora), <Version> del csproj == ultima voce del CHANGELOG,
// ogni .cs di PrepStep/ e scripts/ citato in docs/architettura.md.
using System.Text.RegularExpressions;
var errori = new List<string>();
var mdFiles = Directory.EnumerateFiles(".", "*.md", SearchOption.AllDirectories)
.Where(f => !f.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}")
&& !f.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}")
&& !f.Contains($"{Path.DirectorySeparatorChar}raw{Path.DirectorySeparatorChar}")
&& !f.Contains($"{Path.DirectorySeparatorChar}step{Path.DirectorySeparatorChar}")).ToList();
// slug delle intestazioni come lo calcola GitHub/Gitea
static HashSet<string> Ancore(string path) =>
File.ReadLines(path).Where(l => l.StartsWith('#'))
.Select(l => Regex.Replace(Regex.Replace(l.TrimStart('#').Trim().ToLowerInvariant(), @"[`*]", ""), @"[^\p{L}\p{N}\- _]", "").Replace(' ', '-'))
.ToHashSet();
// 1. link relativi
foreach (var f in mdFiles)
{
var testo = Regex.Replace(File.ReadAllText(f), @"```.*?```", "", RegexOptions.Singleline);
foreach (Match m in Regex.Matches(testo, @"\]\(([^)\s]+)\)"))
{
var url = m.Groups[1].Value;
if (Regex.IsMatch(url, @"^[a-z]+:")) continue; // http:, mailto:, ...
var parti = url.Split('#', 2);
var target = parti[0] == "" ? f : Path.GetFullPath(Path.Combine(Path.GetDirectoryName(f)!, parti[0]));
if (!File.Exists(target) && !Directory.Exists(target)) { errori.Add($"{f}: link rotto -> {url}"); continue; }
if (parti.Length == 2 && target.EndsWith(".md") && !Ancore(target).Contains(parti[1].ToLowerInvariant()))
errori.Add($"{f}: ancora inesistente -> {url}");
}
}
// 2. versione
var version = Regex.Match(File.ReadAllText("PrepStep/PrepStep.csproj"), @"<Version>(.+?)</Version>").Groups[1].Value;
var ultima = Regex.Match(File.ReadAllText("CHANGELOG.md"), @"^## \[(\d[^\]]*)\]", RegexOptions.Multiline).Groups[1].Value;
if (version != ultima) errori.Add($"CHANGELOG.md: ultima voce {ultima} != <Version> {version} del csproj");
// 3. ogni sorgente citato nella mappa file-per-file
var arch = File.ReadAllText("docs/architettura.md");
foreach (var cs in Directory.EnumerateFiles("PrepStep", "*.cs").Concat(Directory.EnumerateFiles("scripts", "*.cs")))
{
var nome = Path.GetFileName(cs);
if (!arch.Contains(nome) && !nome.EndsWith(".Designer.cs")) errori.Add($"docs/architettura.md: {cs} non è descritto");
}
// 4. ogni doc in docs/ è raggiungibile dall'indice della sua cartella
foreach (var dir in new[] { "docs", Path.Combine("docs", "decisioni") })
{
var indice = File.ReadAllText(Path.Combine(dir, "README.md")) + (dir == "docs" ? File.ReadAllText(Path.Combine(dir, "decisioni", "README.md")) : "");
foreach (var md in Directory.EnumerateFiles(dir, "*.md").Where(f => Path.GetFileName(f) != "README.md"))
if (!indice.Contains(Path.GetFileName(md))) errori.Add($"{dir}/README.md: {Path.GetFileName(md)} non è nell'indice");
}
errori.ForEach(Console.Error.WriteLine);
Console.WriteLine(errori.Count == 0 ? $"Doc OK ({mdFiles.Count} file md)" : $"{errori.Count} problemi");
return errori.Count == 0 ? 0 : 1;
+25
View File
@@ -0,0 +1,25 @@
import sys,os,glob,collections
sys.path.insert(0,os.path.dirname(os.path.abspath(__file__)))
import prt,psb
files=sorted(glob.glob(sys.argv[1]))
tot=collections.Counter(); errs=collections.Counter(); bygen=collections.defaultdict(collections.Counter)
samples={}
for f in files:
try: d,secs,order=prt.load(f)
except Exception as e:
print('LOADERR',f,e); continue
gen=d[:60].split()[2].decode()
for n in order:
o,l,fl=secs[n]
typ=fl[2]
if typ!='9': bygen[gen]['type_'+typ]+=1; continue
b=d[o:o+l]
p,root,e=psb.parse(b)
full = p.p==len(b)
if e is None and full: bygen[gen]['ok']+=1; bygen[gen]['okbytes']+=l
else:
bygen[gen]['err']+=1; bygen[gen]['errbytes']+=l
key=(n.split('#')[0], str(e).split('@')[0][:50])
errs[key]+=1; samples.setdefault(key,(os.path.basename(f),str(e)))
for g,c in sorted(bygen.items()): print(g,dict(c))
for k,v in errs.most_common(40): print(v,k,samples[k])
+9
View File
@@ -0,0 +1,9 @@
import sys,os
sys.path.insert(0,os.path.dirname(os.path.abspath(__file__)))
import prt
for a in sys.argv[1:]:
f,sec,off=a.split(',')
off=int(off,16)
d,secs,order=prt.load(f); o,l,_=secs[sec]; b=d[o:o+l]
print('==',a)
prt.hexdump(b,max(0,off-96),192)
+30
View File
@@ -0,0 +1,30 @@
import sys,os,math,collections
sys.path.insert(0,os.path.dirname(os.path.abspath(__file__)))
import prt,psb
d,secs,order=prt.load(sys.argv[1])
bad=collections.Counter(); good=0; unk=collections.Counter()
def walk(v,name=''):
global good
if isinstance(v,psb.Node):
for n,x in v: walk(x,n)
elif isinstance(v,psb.Arr):
for x in v: walk(x,name)
elif isinstance(v,list) and v and isinstance(v[0],list) and len(v) in (3,4) and len(v[0])==3 and name in ('local_sys','orient','matrix'):
for r in v[:3]:
if any(isinstance(c,psb.Unk) for c in r):
u=[c for c in r if isinstance(c,psb.Unk)]
if len(u)==1:
rest=sum(c*c for c in r if not isinstance(c,psb.Unk))
unk[(u[0].code, round(math.sqrt(max(0,1-rest)),6))]+=1
continue
n=math.sqrt(sum(c*c for c in r))
if abs(n-1)<1e-6: good+=1
else: bad[(name,tuple(round(c,4) for c in r))]+=1
for nme in order:
o,l,f=secs[nme]
if f[2]!='9': continue
p,root,e=psb.parse(d[o:o+l])
if root: walk(root[1])
print('good rows',good,'bad',sum(bad.values()))
for k,v in bad.most_common(15): print(v,k)
print('unknown consts solved by norm:',unk.most_common(20))
+11
View File
@@ -0,0 +1,11 @@
import sys,os
sys.path.insert(0,os.path.dirname(os.path.abspath(__file__)))
import prt,psb
for f in sys.argv[1:]:
b=open(f,'rb').read()
p,root,e=psb.parse(b)
print(os.path.basename(f),len(b),p.p,'fields',p.nid,'lenient',p.lenient,'OK' if e is None else 'ERR '+repr(e))
if e is not None: prt.hexdump(b,max(0,p.p-64),128)
if root:
out=[root[0]+':']; psb.fmt(root[1],1,out)
open(f[:-4]+'.txt','w',encoding='utf-8').write('\n'.join(out))
+40
View File
@@ -0,0 +1,40 @@
import sys,re
sys.stdout.reconfigure(encoding='utf-8',errors='replace')
def load(p):
d=open(p,'rb').read()
base=int(d[:d.find(b'\n')].split()[-2],16)
secs={}
order=[]
i=d.find(b'#UGC_TOC')
while True:
e=d.find(b'\n',i); pos=e+1; nxt=None
while True:
e2=d.find(b'\n',pos); line=d[pos:e2]
if line.startswith(b'#'): break
f=line.rstrip(b'#').split()
name=f[0].decode('latin1'); off=int(f[1],16); ln=int(f[2],16)
if name=='NEXT_TOC_ENTRY': nxt=base+off
else:
mk=len(name)+2
secs[name]=(base+off+mk,ln-mk,[x.decode() for x in f[3:]]); order.append(name)
pos=e2+1
if nxt is None: break
i=nxt
return d,secs,order
def sec(p,name):
d,secs,_=load(p); o,l,_=secs[name]; return d[o:o+l]
def hexdump(b,start=0,n=None,w=32):
b=b[start:start+n] if n else b[start:]
for i in range(0,len(b),w):
c=b[i:i+w]
print(f"{start+i:6x} "+' '.join(f"{x:02x}" for x in c).ljust(w*3)+' '+''.join(chr(x) if 32<=x<127 else '.' for x in c))
if __name__=='__main__':
d,secs,order=load(sys.argv[1])
if len(sys.argv)>2:
o,l,_=secs[sys.argv[2]]; b=d[o:o+l]
st=int(sys.argv[3],0) if len(sys.argv)>3 else 0
n=int(sys.argv[4],0) if len(sys.argv)>4 else None
hexdump(b,st,n)
else:
for nme in order:
o,l,f=secs[nme]; print(f"{nme:26} {o:8} {l:8} {' '.join(f):34} {d[o:o+20]!r}")
+529
View File
@@ -0,0 +1,529 @@
"""Parser generico delle sezioni a marcatori dei file Creo (.prt/.asm). Reverse engineering.
Modello: ogni campo dichiarato per nome (e0 <tipo> nome\\0) riceve un id progressivo.
In modalita' posizionale il campo atteso e' next[ultimo id letto]; f7 <id> lo forza.
"""
import struct, sys, os
class Err(Exception):
pass
class Node(list):
"""struct: lista di (nome, valore)"""
def get(s, k, d=None):
for n, v in s:
if n == k:
return v
return d
def all(s, k):
return [v for n, v in s if n == k]
class Arr(list):
pass
class Unk(float):
"""costante a un byte non ancora identificata"""
def __new__(cls, code):
o = float.__new__(cls, 'nan')
o.code = code
return o
def __repr__(s):
return f"<c{s.code:02x}>"
class P:
def __init__(s, b, trace=False):
s.b = b
s.p = 0
s.fields = {}
s.nid = 0
s.next = {}
s.alias = {}
s.stack = []
s.trace = trace
s.root = None
s.last = None
s.lenient = 0
s.lastc = None
s.popped = None
def u8(s):
v = s.b[s.p]
s.p += 1
return v
def peek(s):
return s.b[s.p] if s.p < len(s.b) else None
def rint(s):
b = s.u8()
if b < 0x80:
return b
if b < 0xc0:
return (b & 0x3f) << 8 | s.u8()
if b < 0xe0:
v = (b & 0x1f) << 16 | s.b[s.p] << 8 | s.b[s.p + 1]
s.p += 2
return v
if b == 0xe8:
return s.u8()
if b == 0xe9:
v = int.from_bytes(s.b[s.p:s.p + 2], 'big')
s.p += 2
return v
if b == 0xea:
v = int.from_bytes(s.b[s.p:s.p + 3], 'big')
s.p += 3
return v
if b == 0xeb:
v = int.from_bytes(s.b[s.p:s.p + 4], 'big')
s.p += 4
return v
if b == 0xf5:
return s.rint() | 0x80000000
if b == 0xf6:
return -1
raise Err(f"int tag {b:02x} @{s.p - 1:x}")
CONST = {0x17: -6.283185307179586, 0x14: 4.71238898038469, 0x0d: -0.5, 0x11: 3.141592653589793, 0x15: -3.141592653589793, 0x12: 1.5707963267948966, 0x16: -1.5707963267948966, 0x13: 6.283185307179586, 0x18: 0.0, 0x0f: 1.0, 0x0e: 0.5, 0x10: -1.0, 0x0c: 0.1, 0x0a: 0.01, 0x07: 0.001}
FBASE = 0x3f9d
FCONST = {0x0b: 1.0, 0x28: -1.0}
def rflt(s):
t = s.u8()
if 0x30 <= t <= 0x4f:
T = (0x37 + t - 0x30) if t < 0x40 else (0xb7 + t - 0x40)
raw = bytes([T]) + s.b[s.p:s.p + 3]
s.p += 3
elif t == 0xea:
raw = s.b[s.p:s.p + 3] + bytes(1)
s.p += 3
elif t == 0xeb:
raw = s.b[s.p:s.p + 4]
s.p += 4
elif t == 0xe9:
raw = s.b[s.p:s.p + 2] + bytes(2)
s.p += 2
elif t == 0xe8:
raw = s.b[s.p:s.p + 1] + bytes(3)
s.p += 1
elif t == 0x00:
return 0.0
elif t in s.FCONST:
return s.FCONST[t]
elif t < 0x30:
return Unk(t)
elif 0x50 <= t <= 0xdf:
raw = (s.FBASE + t).to_bytes(2, 'big') + s.b[s.p:s.p + 2]
s.p += 2
else:
raise Err(f"flt tag {t:02x} @{s.p - 1:x}")
return struct.unpack('>f', raw)[0]
def rbyte(s):
b = s.u8()
if b == 0xe8:
return s.u8()
return b
# prefissi negativi a 16 bit dei tag a5-df: bfd0..c00d meno tre buchi (bfd2, bfdf, bffe)
NEG = [x for x in range(0xbfd0, 0xc00e) if x not in (0xbfd2, 0xbfdf, 0xbffe)]
def rdbl(s):
t = s.u8()
if t in s.CONST:
return s.CONST[t]
if t <= 0x16 and t > 0x03:
return Unk(t)
if t <= 0x03:
c = s.u8()
k = t + 1
pl = s.b[s.p:s.p + k]
s.p += k
if c not in s.CONST:
if 0x04 <= c <= 0x16:
return Unk(c)
raise Err(f"dbl const {c:02x} after modifier @{s.p - 1:x}")
raw = bytearray(struct.pack('>d', s.CONST[c]))
raw[8 - k:] = pl
return struct.unpack('>d', bytes(raw))[0]
if t == 0xed:
v = struct.unpack('>d', s.b[s.p:s.p + 8])[0]
s.p += 8
return v
if t == 0xea:
v = struct.unpack('>d', s.b[s.p:s.p + 3] + bytes(5))[0]
s.p += 3
return v
if t == 0xec:
v = struct.unpack('>d', s.b[s.p:s.p + 7] + bytes(1))[0]
s.p += 7
return v
if 0x19 <= t <= 0x48:
if t <= 0x2f:
T = 0x3c + (t - 0x17) // 5
m = (t - 0x17) % 5
else:
T = 0xbc + (t - 0x30) // 5
m = (t - 0x30) % 5
if m == 4:
pl = s.b[s.p:s.p + 2]
s.p += 2
raw = bytes([T]) + pl + b'\x00' * 5
elif m == 3:
pl = s.b[s.p:s.p + 2]
s.p += 2
raw = bytes([T]) + pl + pl[1:] * 5
elif m == 2:
pl = s.b[s.p:s.p + 7]
s.p += 7
raw = bytes([T]) + pl
elif m == 1:
pl = s.b[s.p:s.p + 6]
s.p += 6
raw = bytes([T]) + pl + b'\x00'
else:
pl = s.b[s.p:s.p + 3]
s.p += 3
raw = bytes([T]) + pl + bytes(4)
return struct.unpack('>d', raw)[0]
if 0x49 <= t <= 0xdf:
hi = (0x3f75 + t) if t <= 0xa4 else s.NEG[t - 0xa5]
pl = s.b[s.p:s.p + 6]
s.p += 6
return struct.unpack('>d', hi.to_bytes(2, 'big') + pl)[0]
raise Err(f"dbl tag {t:02x} @{s.p - 1:x}")
def rstr(s):
if s.peek() == 0xe1:
s.p += 1
return None
e = s.b.index(0, s.p)
v = s.b[s.p:e].decode('latin1')
s.p = e + 1
return v
def rarr(s, n, rd):
out = []
while len(out) < n:
b = s.peek()
if 0xe4 <= b <= 0xe6 and out:
s.p += 1
out += [out[-1]] * (b - 0xe3)
elif b == 0xe7 and out:
s.p += 1
k = s.rint()
out += [out[-1]] * k
else:
out.append(rd())
if len(out) != n:
raise Err(f"array overrun {len(out)}>{n} @{s.p:x}")
return out
def scalar(s, f):
t = f['t']
if t in (0x0a, 0x03):
if s.peek() == 0xf8:
s.p += 1
f['dim'] = (s.rint(),)
return [s.rstr() for _ in range(f['dim'][0])]
if f.get('dim'):
out = []
while len(out) < f['dim'][0] and 0x20 <= s.peek() < 0xe0:
out.append(s.rstr())
return out
return s.rstr()
rd = s.rdbl if t == 0x02 else s.rbyte if t in (0x07, 0x04) else s.rflt if t == 0x06 else s.rint
b = s.peek()
if b == 0xe1:
s.p += 1
return None
if b == 0xf8:
s.p += 1
f['dim'] = (s.rint(),)
elif b == 0xf9:
s.p += 1
f['dim'] = (s.rint(), s.rint())
elif b == 0xfc:
s.p += 1
f['dim'] = (s.rint(),) + tuple(f['dim'][1:])
elif b == 0xfa:
s.p += 1
f['dim'] = tuple(s.rint() for _ in range(s.rint()))
d = f.get('dim')
if d is None:
return rd()
n = 1
for k in d:
n *= k
v = s.rarr(n, rd)
if len(d) == 2:
v = [v[i * d[1]:(i + 1) * d[1]] for i in range(d[0])]
return v
def sch(s, fid):
return s.alias.get(fid, fid)
# --- contenitori -------------------------------------------------
def owner_index(s, E, post=False):
own = s.fields[E]['own']
for i in range(len(s.stack) - 1, -1, -1):
c = s.stack[i]
if c['kind'] == 'struct':
if c['schema'] in own:
if post and s.fields[E].get('post') and i > 0:
return i - 1
return i
elif c['occ'] == E or ('A', c['occ']) in own:
return i
return None
def default_close(s):
"""chiusura implicita prima di un campo con nome o di un salto senza marcatore f0-f4:
se il campo atteso appartiene al contenitore padre di quello in cui e' stato scritto
l'ultimo campo, quest'ultimo contenitore si considera chiuso."""
if s.last is None or not s.stack or s.lastc is None:
return
E = s.next.get(s.last)
if E is None:
return
p = None
for k in range(len(s.stack) - 1, -1, -1):
if s.stack[k] is s.lastc:
p = k
break
if not p:
return
i = s.owner_index(E, True)
if i == p - 1:
del s.stack[p:]
def marker(s):
b = s.peek()
if b is None or not (0xf0 <= b <= 0xf4):
return None
s.p += 1
if b == 0xf4:
return s.rint()
return b - 0xf0
def explicit(s, n):
for _ in range(n):
if not s.stack:
raise Err(f"pop underflow @{s.p:x}")
s.popped = s.stack.pop()
def trans(s, E):
if s.last is not None:
s.next[s.last] = E
s.last = E
def locate(s, E, jump, stay):
"""porta in cima allo stack il contenitore che riceve il campo E. Ritorna 'elem' o 'field'."""
f = s.fields[E]
hdr = s.peek() in (0xf8, 0xf9)
top = s.stack[-1]
if not hdr and f['t'] == 0:
for i in range(len(s.stack) - 1, -1, -1):
c = s.stack[i]
if c['kind'] == 'array' and c['occ'] == E:
del s.stack[i + 1:]
return 'elem'
par = f['parent']
i = s.owner_index(E, not jump)
if i is None and jump and top['kind'] == 'struct' and top['node']:
# variante di unione riusata in un altro contenitore: resta dove siamo
f['own'].add(top['schema'])
s.lenient += 1
return 'field'
fresh = top['kind'] == 'struct' and not top['node']
if jump and fresh and i != len(s.stack) - 1:
s.alias[top['field']] = par
top['schema'] = par
return 'field'
if i is not None:
if jump and i != len(s.stack) - 1:
s.lenient += 1
del s.stack[i + 1:]
return 'field'
if jump and top['kind'] == 'array' and not top['node'] and f['t'] == 0 and not hdr:
top['occ'] = E
return 'elem'
raise Err(f"field {E} {f['name']}: container {par} not on stack @{s.p:x}")
def element(s, E):
c = s.stack[-1]
s.trans(E)
s.lastc = c
if s.peek() == 0xfb:
s.p += 1
b = s.u8()
if len(c['node']) >= c['n']:
raise Err(f"too many elements in {s.fields[c['field']]['name']} @{s.p:x}")
if b == 0xe1:
c['node'].append(None)
elif b in (0xe2, 0xe3):
v = Node()
c['node'].append(v)
s.stack.append(dict(kind='struct', field=c['field'], schema=s.sch(c['occ']), node=v, elem=True, occ=None, occ0=c['occ']))
else:
raise Err(f"array elem tag {b:02x} @{s.p - 1:x} in {s.fields[c['field']]['name']}")
def value(s, fid):
c = s.stack[-1] if s.stack else None
f = s.fields[fid]
s.trans(fid)
s.lastc = c
if f['t'] != 0:
v = s.scalar(f)
else:
b = s.u8()
if b == 0xe1:
v = None
elif b in (0xe2, 0xe3):
v = Node()
elif b == 0xf8:
v = Arr()
n = s.rint()
elif b == 0xf9:
v = Arr()
n = s.rint()
s.rint()
else:
raise Err(f"ptr tag {b:02x} @{s.p - 1:x} field {f['name']}")
if c is None:
s.root = (f['name'], v)
elif c['kind'] == 'array':
c.setdefault('tail', []).append((f['name'], v))
else:
c['node'].append((f['name'], v))
if isinstance(v, Arr):
s.stack.append(dict(kind='array', field=fid, occ=fid, n=n, node=v))
elif isinstance(v, Node):
s.stack.append(dict(kind='struct', field=fid, schema=s.sch(fid), node=v, occ=None))
def run(s):
b = s.b
while s.p < len(b):
m = s.marker()
x = s.peek()
if x is None:
s.explicit(m or 0)
break
if x == 0xe0:
s.p += 1
t = s.u8()
e = b.index(0, s.p)
name = b[s.p:e].decode('latin1')
s.p = e + 1
m2 = s.marker()
if m2 is not None:
m = (m or 0) + m2
s.popped = None
if m is None:
s.default_close()
else:
s.explicit(m)
c = s.stack[-1] if s.stack else None
s.nid += 1
fid = s.nid
if c is None:
par = 0
elif c['kind'] == 'array':
par = ('A', c['occ'])
else:
par = c['schema']
s.fields[fid] = dict(name=name, t=t, parent=par, own={par})
pp = s.popped
if pp is not None and c is not None and pp['kind'] == 'struct' and c['kind'] == 'struct' and pp['schema'] == c['schema'] and t == 0:
s.fields[fid]['post'] = True
if s.trace:
print(f"{s.p:6x} {' ' * len(s.stack)}#{fid} {name} t={t}")
s.value(fid)
continue
if m:
s.explicit(m)
jump = False
if x == 0xf7:
s.p += 1
E = s.rint()
jump = True
if m is None:
s.default_close()
if E not in s.fields:
raise Err(f"jump to unknown field {E} @{s.p:x}")
else:
E = s.next.get(s.last)
if E is None:
raise Err(f"no expected field @{s.p:x} after {s.last} "
f"{s.fields[s.last]['name'] if s.last else ''}")
kind = s.locate(E, jump or m is not None, m == 0)
if s.trace:
print(f"{s.p:6x} {' ' * len(s.stack)}{'>' if jump else '='}{E} {s.fields[E]['name']} {kind}")
if kind == 'elem':
s.element(E)
else:
s.value(E)
return s.root
def short(x, maxarr=40):
if isinstance(x, Unk):
return repr(x)
if isinstance(x, float):
return repr(round(x, 9))
if isinstance(x, list):
r = ', '.join(short(y) for y in x[:maxarr])
return f"[{r}{', ...' if len(x) > maxarr else ''}] (n={len(x)})"
return repr(x)
def fmt(v, ind=0, out=None, maxarr=40):
pad = ' ' * ind
if isinstance(v, Node):
for n, x in v:
if isinstance(x, (Node, Arr)):
out.append(f"{pad}{n}:")
fmt(x, ind + 1, out, maxarr)
else:
out.append(f"{pad}{n} = {short(x, maxarr)}")
elif isinstance(v, Arr):
for i, x in enumerate(v):
out.append(f"{pad}[{i}]")
fmt(x, ind + 1, out, maxarr)
def parse(b, trace=False):
p = P(b, trace)
try:
root = p.run()
return p, root, None
except (Err, IndexError, ValueError, KeyError) as e:
return p, p.root, e
if __name__ == '__main__':
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import prt
d, secs, order = prt.load(sys.argv[1])
names = sys.argv[2].split(',') if len(sys.argv) > 2 and sys.argv[2] != '*' else [n for n in order if secs[n][2][2] == '9']
outdir = sys.argv[3] if len(sys.argv) > 3 and sys.argv[3] != '-' else None
for nme in names:
o, l, f = secs[nme]
b = d[o:o + l]
p, root, e = parse(b, trace=len(sys.argv) > 4)
ok = e is None and not p.stack
print(f"{nme:22} len={l:7} parsed={p.p:7} fields={p.nid:5} lenient={p.lenient} {'OK' if ok else 'ERR ' + repr(e) + ' stack=' + str(len(p.stack))}")
if e is not None:
prt.hexdump(b, max(0, p.p - 64), 128)
if outdir and root:
out = [root[0] + ':']
fmt(root[1], 1, out)
open(f"{outdir}/{nme.replace(':', '_').replace('#', '_')}.txt", 'w', encoding='utf-8').write('\n'.join(out))
+47
View File
@@ -0,0 +1,47 @@
import sys,os,math,collections,struct,glob,itertools
sys.path.insert(0,os.path.dirname(os.path.abspath(__file__)))
import prt,psb
class TF(float):
pass
orig=psb.P.rdbl
def rdbl(s):
p0=s.p; t=s.b[p0]
v=orig(s)
if 0x49<=t<=0xdf and not isinstance(v,psb.Unk):
o=TF(v); o.tag=t; o.pl=bytes(s.b[p0+1:p0+7]); return o
return v
psb.P.rdbl=rdbl
OFFS=[0]+list(range(0x44,0x4d))
def mag(tf,off):
return struct.unpack('>d',(0x3f75+tf.tag-off).to_bytes(2,'big')+tf.pl)[0]
votes=collections.defaultdict(collections.Counter)
def walk(v,name=''):
if isinstance(v,psb.Node):
for n,x in v: walk(x,n)
elif isinstance(v,psb.Arr):
for x in v: walk(x,name)
elif isinstance(v,list) and v and isinstance(v[0],list) and len(v) in (3,4) and len(v[0])==3 and name in ('local_sys','orient','matrix','transf_to_local'):
rows=v[:3]
if any(isinstance(c,psb.Unk) for r in rows for c in r): return
vecs=rows+[[rows[i][j] for i in range(3)] for j in range(3)]
for r in vecs:
tf=[c for c in r if isinstance(c,TF)]
if not tf or len(tf)>2: continue
rest=sum(c*c for c in r if not isinstance(c,TF))
sols=[]
for offs in itertools.product(OFFS,repeat=len(tf)):
n=rest+sum(mag(c,o)**2 for c,o in zip(tf,offs))
if abs(n-1)<1e-9: sols.append(offs)
if len(sols)==1:
for c,o in zip(tf,sols[0]): votes[c.tag][o]+=1
for f in sys.argv[1:]:
for g in glob.glob(f):
try: d,secs,order=prt.load(g)
except Exception: continue
for nme in order:
o,l,fl=secs[nme]
if fl[2]!='9' or l<200: continue
p,root,e=psb.parse(d[o:o+l])
if root: walk(root[1])
for t in sorted(votes):
print(hex(t), {hex(o):n for o,n in votes[t].items()}, ' -> prefix', [hex((0x3f75+t-o) | (0x8000 if o else 0)) for o in votes[t]])
+75
View File
@@ -0,0 +1,75 @@
"""Decompressore Unix compress (.Z, LZW) minimale, come ncompress."""
import sys, os
def unz(data):
assert data[0] == 0x1f and data[1] == 0x9d, "non e' un flusso .Z"
flags = data[2]
maxbits = flags & 0x1f
block = bool(flags & 0x80)
out = bytearray()
n_bits = 9
maxcode = (1 << n_bits) - 1
table = {i: bytes([i]) for i in range(256)}
free = 257 if block else 256
pos = 3 * 8 # posizione in bit
total = len(data) * 8
start = pos # inizio del gruppo corrente di codici (per il riallineamento)
old = None
while pos + n_bits <= total:
byte = pos >> 3
chunk = int.from_bytes(data[byte:byte + 4], 'little')
code = (chunk >> (pos & 7)) & ((1 << n_bits) - 1)
pos += n_bits
if old is None:
old = table[code]
out += old
continue
if code == 256 and block:
table = {i: bytes([i]) for i in range(256)}
free = 256
# riallinea a multipli di 8 codici
grp = n_bits * 8
pos = start + ((pos - start + grp - 1) // grp) * grp
n_bits = 9
maxcode = (1 << n_bits) - 1
start = pos
old = None
free = 257
continue
if code in table:
entry = table[code]
elif code == free:
entry = old + old[:1]
else:
raise ValueError(f"codice LZW non valido {code} (free={free}) a bit {pos}")
out += entry
if free <= (1 << maxbits) - 1:
table[free] = old + entry[:1]
free += 1
old = entry
if free > maxcode and n_bits < maxbits:
grp = n_bits * 8
pos = start + ((pos - start + grp - 1) // grp) * grp
n_bits += 1
maxcode = (1 << n_bits) - 1
start = pos
return bytes(out)
if __name__ == '__main__':
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import prt
d, secs, order = prt.load(sys.argv[1])
outdir = sys.argv[2]
for n in order:
o, l, f = secs[n]
b = d[o:o + l]
if b[:2] == b'\x1f\x9d':
try:
u = unz(b)
except Exception as e:
print(n, 'ERR', e)
continue
print(n, 'compressa', l, '->', len(u), 'dichiarata', int(f[0], 16), u[:60])
open(os.path.join(outdir, n.replace(':', '_') + '.bin'), 'wb').write(u)