Compare commits
27
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d7ace07e2 | ||
|
|
1eac3ccaa7 | ||
|
|
5b11b3e8ca | ||
|
|
d5fb20a34e | ||
|
|
6d1e36a2ae | ||
|
|
ccfa00ff52 | ||
|
|
5fac2edf52 | ||
|
|
4ae3d0e087 | ||
|
|
64ff1d8770 | ||
|
|
57020f9964 | ||
|
|
206db85e12 | ||
|
|
d81fede4cc | ||
|
|
948937ba75 | ||
|
|
ff4e7a71e0 | ||
|
|
be4989fc8d | ||
|
|
e94331b69f | ||
|
|
3e9c78074c | ||
|
|
e29a1bf90a | ||
|
|
28aa16837b | ||
|
|
d9f16f2844 | ||
|
|
db4c444473 | ||
|
|
13f6de6402 | ||
|
|
54f267b508 | ||
|
|
0ae2a5e7cf | ||
|
|
b136e7f49b | ||
|
|
cf4c2e8e29 | ||
|
|
9af426ca1b |
No files matched your search
@@ -37,3 +37,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__/
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
+131
@@ -0,0 +1,131 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
Tutte le modifiche rilevanti di questo progetto sono documentate in questo file.
|
||||||
|
|
||||||
|
Il formato segue [Keep a Changelog](https://keepachangelog.com/it/1.1.0/), e il progetto
|
||||||
|
aderisce a [Semantic Versioning](https://semver.org/lang/it/).
|
||||||
|
|
||||||
|
## [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
|
||||||
|
|
||||||
|
### Aggiunto
|
||||||
|
|
||||||
|
- Wizard a 5 fasi (Database → Importa Distinta Base → Crea cartella raw → Trova componenti
|
||||||
|
referenziati → Verifica completezza) per preparare i file `raw/` da passare a Creo
|
||||||
|
Distributed Batch per l'esportazione STEP.
|
||||||
|
- Indicizzazione dei file CAD Pro-ENGINEER/Creo di un disco/share di rete (radice di default
|
||||||
|
`X:\`) in un DB SQLite locale (`cad_index.db`), con aggiornamento incrementale e barra di
|
||||||
|
progresso.
|
||||||
|
- Import della distinta base dall'export del gestionale (`.xls`/`.xml`) in CSV
|
||||||
|
componente;descrizione, incluso il riconoscimento della convenzione `DBP_<codice>_<timestamp>`
|
||||||
|
per aggiungere l'assieme generale come riga.
|
||||||
|
- Creazione della cartella `raw/` con copia parallela dei file CAD dei componenti trovati
|
||||||
|
nell'indice.
|
||||||
|
- Risoluzione ricorsiva dell'albero componenti dai file `.asm` (sotto-assiemi, parti,
|
||||||
|
componenti soppressi, riferimenti esterni), con parser esatto dei record binari
|
||||||
|
(`AsmRecordReader`) e fallback euristico testuale (`AssemblyComponentScanner`) per i file
|
||||||
|
fuori formato.
|
||||||
|
- Scelta automatica della copia corretta quando lo stesso codice esiste in più cartelle della
|
||||||
|
share, preferendo quella della cartella dell'assieme che la referenzia.
|
||||||
|
- Anteprima trascinabile (drag&drop) dei file `.asm` della distinta già in `raw/`, pronti per
|
||||||
|
Creo Distributed Batch.
|
||||||
|
- Menu "File → Nuova estrazione" (Ctrl+N) per ripartire con una nuova distinta senza ricaricare
|
||||||
|
database e cartella di lavoro, e "Aiuto → Guida" (F1) con una sintesi interattiva delle 5 fasi.
|
||||||
|
- Script standalone C# file-based (`scripts/`): `ExtractCreoStrings.cs`, `BomViaDb.cs`,
|
||||||
|
`CreoBom.cs`, per estrarre la distinta componenti da riga di comando.
|
||||||
|
- Pubblicazione come eseguibile standalone single-file self-contained (Windows x64).
|
||||||
@@ -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ì
|
||||||
@@ -36,9 +43,43 @@ i test possono chiamare direttamente `Indexer.ParseFilename` e
|
|||||||
`AssemblyComponentScanner.EstraiCodiciReferenziati` senza esporli come API
|
`AssemblyComponentScanner.EstraiCodiciReferenziati` senza esporli come API
|
||||||
pubblica.
|
pubblica.
|
||||||
|
|
||||||
|
## Documentazione: va tenuta allineata
|
||||||
|
|
||||||
|
`docs/` non è un archivio di note storiche: descrive **il programma com'è adesso**, ed è la
|
||||||
|
prima cosa che si legge per capirlo. Ogni modifica strutturale — un passo in più o in meno nel
|
||||||
|
flusso delle fasi, un nuovo modo di leggere il `.asm`, un cambio nello schema del DB o nel
|
||||||
|
criterio con cui si sceglie un file — va riflessa nei doc **nello stesso commit** del codice,
|
||||||
|
non "dopo". Un doc che descrive un comportamento che non esiste più è peggio di un doc
|
||||||
|
mancante: fa prendere decisioni sbagliate.
|
||||||
|
|
||||||
|
Dove scrivere cosa:
|
||||||
|
|
||||||
|
| Doc | Contenuto |
|
||||||
|
|---|---|
|
||||||
|
| `CLAUDE.md` (questo file) | il flusso essenziale e i punti che si sbaglierebbero senza saperli |
|
||||||
|
| [`docs/architettura.md`](docs/architettura.md) | il processo delle 5 fasi e la mappa file-per-file |
|
||||||
|
| [`docs/guida-utente.md`](docs/guida-utente.md) | cosa clicca l'utente, passo per passo, fino a Creo Distributed Batch |
|
||||||
|
| [`docs/formato-asm-record.md`](docs/formato-asm-record.md) | parser esatto dei record `.asm`, con §7 per i casi limite |
|
||||||
|
| [`docs/formato-asm.md`](docs/formato-asm.md) | euristica testuale di fallback |
|
||||||
|
| [`docs/db-cad-index.md`](docs/db-cad-index.md) | schema e vista di `cad_index.db` |
|
||||||
|
| [`docs/copia-raw-parallela.md`](docs/copia-raw-parallela.md) | parallelizzazione della copia in `raw/` |
|
||||||
|
| [`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 |
|
||||||
|
|
||||||
|
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
|
||||||
|
messaggi di Creo non nominano il file o la causa, e senza quel ponte la prossima volta si
|
||||||
|
riparte da zero (i §7.3/§7.4/§7.5 di `formato-asm-record.md` sono l'esempio del formato
|
||||||
|
atteso).
|
||||||
|
|
||||||
## Architettura
|
## Architettura
|
||||||
|
|
||||||
Il programma è tutto in `PrepStep/` (nessun altro progetto nella solution). Dettaglio
|
Il programma è tutto in `PrepStep/` (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.
|
||||||
|
|
||||||
@@ -47,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.
|
||||||
@@ -63,15 +110,27 @@ designer visuale):
|
|||||||
sotto-assiemi/parti (`RisolviAlberoComponenti`), copiandoli anch'essi in `raw/`. Usa
|
sotto-assiemi/parti (`RisolviAlberoComponenti`), copiandoli anch'essi in `raw/`. Usa
|
||||||
`AsmRecordReader` (parser esatto dei record binari, `docs/formato-asm-record.md`) e ripiega
|
`AsmRecordReader` (parser esatto dei record binari, `docs/formato-asm-record.md`) e ripiega
|
||||||
su `AssemblyComponentScanner` (euristica testuale, `docs/formato-asm.md`) solo se il file
|
su `AssemblyComponentScanner` (euristica testuale, `docs/formato-asm.md`) solo se il file
|
||||||
non rispetta il formato validato (`InvalidDataException`). ⚠️ Limite noto non coperto dal
|
non rispetta il formato validato (`InvalidDataException`). Tre punti da non confondere,
|
||||||
fallback: un **assieme** con family table (raro, non solo le parti normalizzate possono
|
tutti imparati facendo fallire Creo (`docs/formato-asm-record.md` §7.3, §7.4 e §7.5):
|
||||||
averla) può dare un risultato silenziosamente incompleto senza lanciare eccezione — vedi
|
i **componenti soppressi** non sono nella distinta ma il loro file serve per aprire
|
||||||
`docs/formato-asm-record.md` §7.
|
l'assieme (`EstraiCodiciSoppressi`, copiati senza finire fra i "non trovati"); i
|
||||||
|
**riferimenti esterni** (assembly feature, datum copiati) citano modelli che non sono né
|
||||||
|
membri né piazzamenti, e che Creo carica con tutto il loro albero
|
||||||
|
(`EstraiCodiciRiferimentiEsterni`, anche questi fuori dai "non trovati", ma con ricorsione);
|
||||||
|
e quando lo stesso codice esiste in più cartelle della share — succede per il 13% dei
|
||||||
|
codici — va copiata la copia nella **stessa cartella dell'assieme che la referenzia**
|
||||||
|
(`ScegliPerCartella`), perché il `.asm` non registra quale usa e sbagliarla impedisce a
|
||||||
|
Creo di aprire l'assieme.
|
||||||
5. **Verifica completezza** — mostra i `.asm` della distinta già in `raw/`, trascinabili su
|
5. **Verifica completezza** — mostra i `.asm` della distinta già in `raw/`, trascinabili su
|
||||||
Creo per l'esportazione STEP (Distributed Batch, manuale, fuori dall'app); "Verifica
|
Creo per l'esportazione STEP (Distributed Batch, manuale, fuori dall'app).
|
||||||
completezza assiemi" controlla ricorsivamente sull'indice (non su `raw/`, stessa
|
|
||||||
`AsmRecordReader`/fallback della fase 4) se tutti i sotto-componenti di ogni assieme sono
|
Menu in cima alla finestra: "File → Nuovo" (Ctrl+N) azzera lo stato delle fasi 2-5
|
||||||
presenti.
|
per ripartire con una nuova distinta, senza toccare DB/cartella di lavoro della fase 1 (va
|
||||||
|
volutamente alla fase 2, non alla 1: quei dati restano quelli già caricati); "File → Scegli
|
||||||
|
cartella di lavoro..." cambia la cartella di CSV/`raw/`/`step/` in qualunque momento; "File →
|
||||||
|
Pulisci raw e step..." svuota le cartelle `raw/`/`step/` nella cartella di lavoro (non tocca
|
||||||
|
l'indice); "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/`)
|
||||||
|
|
||||||
@@ -88,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`.
|
||||||
@@ -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.
|
||||||
@@ -37,6 +37,28 @@ public class AsmRecordReaderTests
|
|||||||
private static byte[] ConfigName(string istanza, int idMembro) =>
|
private static byte[] ConfigName(string istanza, int idMembro) =>
|
||||||
Bytes(0xe3, "ConfigName", 0, 0xe2, (byte)'3', istanza, 0, idMembro, 0x82, 0x7b);
|
Bytes(0xe3, "ConfigName", 0, 0xe2, (byte)'3', istanza, 0, idMembro, 0x82, 0x7b);
|
||||||
|
|
||||||
|
// \0objname\0 \xe2 \xe0\x01 object_type\0 <int> \xe0\x0a name\0 <nome>\0 - il nome proprio
|
||||||
|
// del modello (quello che la BOM intesta), vedi docs/formato-asm-record.md §5
|
||||||
|
private static byte[] ObjName(string nome) =>
|
||||||
|
Bytes(0, "objname", 0, 0xe2, 0xe0, 0x01, "object_type", 0, 2, 0xe0, 0x0a, "name", 0, nome, 0);
|
||||||
|
|
||||||
|
// identita' dichiarata dal file: \xe0\x01 model_t_type\0 <int> \xe0\x0a model_name\0 <nome>\0
|
||||||
|
// \xe0\x0a generic_name\0 \0 (vuoto)
|
||||||
|
private static byte[] ModelName(string nome) =>
|
||||||
|
Bytes(0xe0, 0x01, "model_t_type", 0, 2, 0xe0, 0x0a, "model_name", 0, nome, 0,
|
||||||
|
0xe0, 0x0a, "generic_name", 0, 0);
|
||||||
|
|
||||||
|
// array dei modelli referenziati: \xf2\xf7\x05 <elementi> \xf3, ogni elemento
|
||||||
|
// \xe3\xe2<tipo><nome>\0 - vedi docs/formato-asm-record.md §7.5
|
||||||
|
private static byte[] Riferimenti(params string[] nomi)
|
||||||
|
{
|
||||||
|
var parti = new List<object> { 0xf2, 0xf7, 0x05 };
|
||||||
|
foreach (var nome in nomi)
|
||||||
|
parti.AddRange(new object[] { 0xe3, 0xe2, 0x02, nome, 0 });
|
||||||
|
parti.Add(0xf3);
|
||||||
|
return Bytes(parti.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
private static byte[] Asm(int conteggioDichiarato, byte[] membri, byte[]? extra = null) =>
|
private static byte[] Asm(int conteggioDichiarato, byte[] membri, byte[]? extra = null) =>
|
||||||
Bytes("header ", extra ?? Array.Empty<byte>(), "\0members\0", 0xf1, 0xf8, conteggioDichiarato, membri, "#MdlStatus");
|
Bytes("header ", extra ?? Array.Empty<byte>(), "\0members\0", 0xf1, 0xf8, conteggioDichiarato, membri, "#MdlStatus");
|
||||||
|
|
||||||
@@ -87,6 +109,23 @@ public class AsmRecordReaderTests
|
|||||||
Assert.All(membri, m => Assert.Equal("9244250136_C03", m.Nome));
|
Assert.All(membri, m => Assert.Equal("9244250136_C03", m.Nome));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Membro_con_notazione_istanza_generico_diretta_nel_nome_usa_il_generico()
|
||||||
|
{
|
||||||
|
// Regressione: 9244248577a_c01.asm - alcuni membri di family table non-UNI non
|
||||||
|
// passano da ConfigName, il campo "name"/"family_name" del record members porta gia'
|
||||||
|
// la forma "<istanza><generico>" (stessa notazione di §4.1). Senza lo split, il codice
|
||||||
|
// cercato nell'indice sarebbe "V6-12--_-_U5931_GC<UNI5931>" per intero, mai trovato -
|
||||||
|
// va cercato invece il generico "UNI5931".
|
||||||
|
byte[] dati = Asm(1, Membro(1, 2, "V6-12--_-_U5931_GC<UNI5931>"));
|
||||||
|
|
||||||
|
var membri = AsmRecordReader.LeggiMembri(dati);
|
||||||
|
|
||||||
|
Assert.Single(membri);
|
||||||
|
Assert.Equal("V6-12--_-_U5931_GC", membri[0].Nome);
|
||||||
|
Assert.Equal("UNI5931", membri[0].Generico);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Componente_senza_ConfigName_non_e_toccato_da_istanze_di_altri_membri()
|
public void Componente_senza_ConfigName_non_e_toccato_da_istanze_di_altri_membri()
|
||||||
{
|
{
|
||||||
@@ -111,6 +150,118 @@ public class AsmRecordReaderTests
|
|||||||
Assert.Throws<InvalidDataException>(() => AsmRecordReader.LeggiMembri(dati));
|
Assert.Throws<InvalidDataException>(() => AsmRecordReader.LeggiMembri(dati));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Assieme_family_table_con_generic_name_lancia_eccezione()
|
||||||
|
{
|
||||||
|
// Regressione: un file che e' esso stesso un'istanza di family table (il suo nome
|
||||||
|
// proprio "2546220101" piu' il suffisso "_C01" in model_name) descrive in "members"
|
||||||
|
// solo quella rappresentazione, non le istanze sorelle. Va segnalato invece di
|
||||||
|
// restituire silenziosamente il contenuto sbagliato (docs/formato-asm-record.md §7).
|
||||||
|
byte[] dati = Asm(1, Membro(1, 2, "9244206098"),
|
||||||
|
extra: Bytes(ObjName("2546220101"), ModelName("2546220101_C01")));
|
||||||
|
|
||||||
|
Assert.Throws<InvalidDataException>(() => AsmRecordReader.LeggiMembri(dati));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Model_name_di_un_altro_modello_non_lancia_eccezione()
|
||||||
|
{
|
||||||
|
// Regressione: sui file reali della share il record "model_name" nomina spesso un
|
||||||
|
// modello diverso dal file (tipicamente un suo membro), non l'identita' del file. Il
|
||||||
|
// nome proprio (objname) non conferma, quindi non e' il sintomo della family table:
|
||||||
|
// "members" e' corretto e va usato. Verificato contro BOM Creo reali - senza questo
|
||||||
|
// controllo il file cadeva sull'euristica testuale, che produceva codici inesistenti
|
||||||
|
// segnalati poi come "componenti mancanti".
|
||||||
|
byte[] dati = Asm(1, Membro(1, 2, "9044207999A_C01"),
|
||||||
|
extra: Bytes(ObjName("9244207999A"), ModelName("9044207999A_C01")));
|
||||||
|
|
||||||
|
var membri = AsmRecordReader.LeggiMembri(dati);
|
||||||
|
|
||||||
|
Assert.Equal("9044207999A_C01", membri.Single().Nome);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Model_name_senza_nome_proprio_non_lancia_eccezione()
|
||||||
|
{
|
||||||
|
// Stesso caso del test sopra ma senza record "objname" (formato osservato su tutti i
|
||||||
|
// file reali che facevano scattare il falso positivo): senza conferma del nome proprio
|
||||||
|
// il record non e' un'identita', quindi "members" va letto normalmente.
|
||||||
|
byte[] dati = Asm(1, Membro(1, 2, "9244206098"), extra: ModelName("2546220101_C01"));
|
||||||
|
|
||||||
|
Assert.Single(AsmRecordReader.LeggiMembri(dati));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Generic_name_senza_suffisso_istanza_non_lancia_eccezione()
|
||||||
|
{
|
||||||
|
// Regressione: su ~7% dei file reali della share compare un record "model_name" +
|
||||||
|
// "generic_name" (vuoto) che NON e' il sintomo della family table di assiemi - il
|
||||||
|
// valore non ha il suffisso "_C0n" delle istanze (es. e' un codice qualunque non
|
||||||
|
// correlato al nome del file). In quei casi "members" e' letto correttamente e va
|
||||||
|
// usato normalmente, non bloccato per un falso positivo.
|
||||||
|
byte[] dati = Asm(1, Membro(1, 2, "9244206098"),
|
||||||
|
extra: Bytes(ObjName("9244230117"), ModelName("9244230117")));
|
||||||
|
|
||||||
|
var membri = AsmRecordReader.LeggiMembri(dati);
|
||||||
|
|
||||||
|
Assert.Single(membri);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Componente_soppresso_non_e_fra_i_membri_ma_e_segnalato_a_parte()
|
||||||
|
{
|
||||||
|
// Un componente soppresso sparisce da "members" (il conteggio scende) ma resta citato
|
||||||
|
// dalla feature che lo posiziona. Non va nella BOM, però senza il suo file Creo non
|
||||||
|
// apre l'assieme, quindi va copiato lo stesso.
|
||||||
|
byte[] dati = Asm(1, Membro(1, 2, "9244206098"),
|
||||||
|
extra: Bytes("Componente ID 52 (9230100043.PRT)", 0));
|
||||||
|
|
||||||
|
Assert.Equal(new[] { "9244206098" }, AsmRecordReader.LeggiMembri(dati).Select(m => m.Nome));
|
||||||
|
|
||||||
|
string percorso = ScriviFile(dati);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Assert.Equal(new[] { "9230100043" }, AsmRecordReader.EstraiCodiciSoppressi(percorso));
|
||||||
|
Assert.DoesNotContain("9230100043", AsmRecordReader.EstraiCodiciReferenziati(percorso));
|
||||||
|
}
|
||||||
|
finally { File.Delete(percorso); }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Componente_gia_fra_i_membri_non_e_segnalato_come_soppresso()
|
||||||
|
{
|
||||||
|
byte[] dati = Asm(1, Membro(1, 2, "9244206098"),
|
||||||
|
extra: Bytes("Componente ID 7 (9244206098.PRT)", 0));
|
||||||
|
string percorso = ScriviFile(dati);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Assert.Empty(AsmRecordReader.EstraiCodiciSoppressi(percorso));
|
||||||
|
}
|
||||||
|
finally { File.Delete(percorso); }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Riferimenti_esterni_non_sono_fra_i_membri_e_si_risolvono_sul_generico()
|
||||||
|
{
|
||||||
|
// Un modello legato da riferimento esterno (assembly feature "#AFn", datum copiati) non
|
||||||
|
// compare ne' in "members" ne' fra i piazzamenti, ma senza il suo file Creo non apre
|
||||||
|
// l'assieme: e' il caso che faceva fallire 9255728001_200 in Distributed Batch. Per le
|
||||||
|
// istanze di family table il file su disco e' il generico fra parentesi angolari.
|
||||||
|
byte[] dati = Asm(1, Membro(1, 2, "9244206098"),
|
||||||
|
extra: Bytes(Riferimenti("9255708017#AF2<9255708017>", "9255708018"),
|
||||||
|
Riferimenti("V6-12--_-_U5933_C<UNI5933>")));
|
||||||
|
|
||||||
|
Assert.Equal(new[] { "9244206098" }, AsmRecordReader.LeggiMembri(dati).Select(m => m.Nome));
|
||||||
|
|
||||||
|
string percorso = ScriviFile(dati);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Assert.Equal(new[] { "9255708017", "9255708018", "UNI5933" },
|
||||||
|
AsmRecordReader.EstraiCodiciRiferimentiEsterni(percorso).OrderBy(c => c, StringComparer.Ordinal));
|
||||||
|
}
|
||||||
|
finally { File.Delete(percorso); }
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Sezione_members_assente_lancia_eccezione()
|
public void Sezione_members_assente_lancia_eccezione()
|
||||||
{
|
{
|
||||||
|
|||||||
+141
-2
@@ -49,12 +49,47 @@ public static class AsmRecordReader
|
|||||||
// normalizzazione ogni istanza del pattern risulterebbe un componente diverso.
|
// normalizzazione ogni istanza del pattern risulterebbe un componente diverso.
|
||||||
private static readonly Regex SuffissoArrayFeatureRegex = new(@"#AF\d+$", RegexOptions.Compiled);
|
private static readonly Regex SuffissoArrayFeatureRegex = new(@"#AF\d+$", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
// record di identita' del modello, seguito da "generic_name" (vuoto): compare su una
|
||||||
|
// minoranza di assiemi (~7% dei campioni sulla share) per motivi diversi e nella
|
||||||
|
// maggioranza dei casi "members" resta corretto - la sola presenza di "generic_name" NON
|
||||||
|
// e' un segnale affidabile (verificato: su 144 file che lo contengono, solo 25 hanno
|
||||||
|
// anche il sintomo sotto; per gli altri 119 "members" letto normalmente coincide con
|
||||||
|
// l'euristica testuale, quindi e' un contesto diverso, non lo stesso problema).
|
||||||
|
private static readonly Regex OwnModelIdentityRegex = new(
|
||||||
|
@"\xe0\x01model_t_type\x00" + Int + @"\xe0\x0amodel_name\x00([\x20-\x7e]+)\x00\xe0\x0ageneric_name\x00\x00",
|
||||||
|
RegexOptions.Compiled);
|
||||||
|
|
||||||
|
// Alcuni membri di family table non-UNI (es. viti/dadi commerciali) non passano da
|
||||||
|
// ConfigName: il campo "name" del record members porta gia' la forma "<istanza><generico>"
|
||||||
|
// (stessa notazione di §4.1, li' sconsigliata per la BOM perche' e' un elenco di
|
||||||
|
// riferimenti geometrici - qui invece e' il valore vero del componente, va solo separato).
|
||||||
|
// Osservato su 9244248577a_c01.asm: "V6-12--_-_U5931_GC<UNI5931>" - senza questo split il
|
||||||
|
// codice cercato nell'indice sarebbe la stringa intera con le parentesi, mai trovata.
|
||||||
|
private static readonly Regex IstanzaConGenericoRegex = new(@"^([\x20-\x7e]+?)<([\x20-\x7e]+)>$", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
// Suffisso "_C0n" delle istanze di family table (stessa convenzione di
|
||||||
|
// SuffissoArrayFeatureRegex/ConfigName sopra). Usato insieme a OwnModelIdentityRegex per
|
||||||
|
// riconoscere un file che e' esso stesso un'istanza, il cui "members" descriverebbe solo
|
||||||
|
// quella rappresentazione invece delle istanze sorelle (docs/formato-asm-record.md §7).
|
||||||
|
// Da solo NON basta: su file reali quel record nomina spesso un altro modello (un membro
|
||||||
|
// del file, o un codice imparentato), non il file stesso - per quello serve la conferma
|
||||||
|
// del nome proprio (objname).
|
||||||
|
private static readonly Regex SuffissoIstanzaFamilyTableRegex = new(@"_C\d{2,}$", RegexOptions.Compiled);
|
||||||
|
|
||||||
// primo elemento dell'array "members": porta le chiavi per esteso.
|
// primo elemento dell'array "members": porta le chiavi per esteso.
|
||||||
private static readonly Regex FirstMemberRegex = new(
|
private static readonly Regex FirstMemberRegex = new(
|
||||||
@"\x01id\x00" + Int + @"\xe0\x01type\x00" + Int
|
@"\x01id\x00" + Int + @"\xe0\x01type\x00" + Int
|
||||||
+ @"\xe0\x0aname\x00([^\x00]+)\x00\xe0\x0afamily_name\x00([^\x00]+)\x00",
|
+ @"\xe0\x0aname\x00([^\x00]+)\x00\xe0\x0afamily_name\x00([^\x00]+)\x00",
|
||||||
RegexOptions.Compiled);
|
RegexOptions.Compiled);
|
||||||
|
|
||||||
|
private static int ContaOccorrenze(string testo, string marcatore)
|
||||||
|
{
|
||||||
|
int conteggio = 0;
|
||||||
|
for (int i = testo.IndexOf(marcatore, StringComparison.Ordinal); i >= 0; i = testo.IndexOf(marcatore, i + 1, StringComparison.Ordinal))
|
||||||
|
conteggio++;
|
||||||
|
return conteggio;
|
||||||
|
}
|
||||||
|
|
||||||
private static int AsInt(string bytes)
|
private static int AsInt(string bytes)
|
||||||
{
|
{
|
||||||
int mask = bytes.Length switch { 1 => 0x7F, 2 => 0x3F, 3 => 0x1F, _ => throw new ArgumentException("lunghezza intero non valida") };
|
int mask = bytes.Length switch { 1 => 0x7F, 2 => 0x3F, 3 => 0x1F, _ => throw new ArgumentException("lunghezza intero non valida") };
|
||||||
@@ -84,14 +119,26 @@ public static class AsmRecordReader
|
|||||||
{
|
{
|
||||||
string testo = Encoding.Latin1.GetString(dati);
|
string testo = Encoding.Latin1.GetString(dati);
|
||||||
|
|
||||||
|
var configNameMatches = ConfigNameRegex.Matches(testo);
|
||||||
|
int configNameGrezzi = ContaOccorrenze(testo, (char)0xe3 + "ConfigName\0");
|
||||||
|
if (configNameMatches.Count != configNameGrezzi)
|
||||||
|
throw new InvalidDataException(
|
||||||
|
$"trovati {configNameGrezzi} record 'ConfigName' ma solo {configNameMatches.Count} " +
|
||||||
|
"interpretati: formato del parametro diverso da quello atteso (vedi §4, costante " +
|
||||||
|
"\\x82\\x7b), lettura non affidabile");
|
||||||
|
|
||||||
var istanze = new Dictionary<int, string>();
|
var istanze = new Dictionary<int, string>();
|
||||||
foreach (Match m in ConfigNameRegex.Matches(testo))
|
foreach (Match m in configNameMatches)
|
||||||
istanze[AsInt(m.Groups[2].Value)] = SuffissoArrayFeatureRegex.Replace(m.Groups[1].Value, "");
|
istanze[AsInt(m.Groups[2].Value)] = SuffissoArrayFeatureRegex.Replace(m.Groups[1].Value, "");
|
||||||
|
|
||||||
const string chiaveMembers = "\0members\0";
|
const string chiaveMembers = "\0members\0";
|
||||||
int start = testo.IndexOf(chiaveMembers, StringComparison.Ordinal);
|
int start = testo.IndexOf(chiaveMembers, StringComparison.Ordinal);
|
||||||
if (start < 0)
|
if (start < 0)
|
||||||
throw new InvalidDataException("sezione 'members' assente");
|
throw new InvalidDataException("sezione 'members' assente");
|
||||||
|
if (testo.IndexOf(chiaveMembers, start + chiaveMembers.Length, StringComparison.Ordinal) >= 0)
|
||||||
|
throw new InvalidDataException(
|
||||||
|
"trovate piu' occorrenze di 'members': probabile rappresentazione semplificata, " +
|
||||||
|
"mai verificata (vedi §7) - non e' chiaro quale sezione descriva la BOM");
|
||||||
start += chiaveMembers.Length;
|
start += chiaveMembers.Length;
|
||||||
|
|
||||||
int conteggio = dati[start + 2]; // \xf1 \xf8 <conteggio>
|
int conteggio = dati[start + 2]; // \xf1 \xf8 <conteggio>
|
||||||
@@ -118,7 +165,30 @@ public static class AsmRecordReader
|
|||||||
if (grezzi.Count != conteggio)
|
if (grezzi.Count != conteggio)
|
||||||
throw new InvalidDataException($"attesi {conteggio} membri, trovati {grezzi.Count}: lettura disallineata");
|
throw new InvalidDataException($"attesi {conteggio} membri, trovati {grezzi.Count}: lettura disallineata");
|
||||||
|
|
||||||
return grezzi.Select(g => new Membro(g.Id, g.Tipo, istanze.GetValueOrDefault(g.Id, g.Nome), g.Nome)).ToList();
|
// Il record "model_name" e' l'identita' del file solo se il nome proprio del modello
|
||||||
|
// (objname, §5) lo conferma: da solo nomina spesso un altro modello - un membro del
|
||||||
|
// file, o un modello imparentato - e in quei casi "members" e' gia' corretto. Vedi
|
||||||
|
// docs/formato-asm-record.md §7.
|
||||||
|
var identita = OwnModelIdentityRegex.Match(testo);
|
||||||
|
var nomeProprio = ObjNameRegex.Match(testo);
|
||||||
|
if (identita.Success && nomeProprio.Success
|
||||||
|
&& SuffissoIstanzaFamilyTableRegex.IsMatch(identita.Groups[2].Value)
|
||||||
|
&& string.Equals(SuffissoIstanzaFamilyTableRegex.Replace(identita.Groups[2].Value, ""),
|
||||||
|
nomeProprio.Groups[1].Value, StringComparison.OrdinalIgnoreCase))
|
||||||
|
throw new InvalidDataException(
|
||||||
|
$"assieme con family table: il file e' esso stesso l'istanza '{identita.Groups[2].Value}', " +
|
||||||
|
"'members' descriverebbe solo questa rappresentazione, non le istanze sorelle - vedi " +
|
||||||
|
"docs/formato-asm-record.md §7");
|
||||||
|
|
||||||
|
return grezzi.Select(g =>
|
||||||
|
{
|
||||||
|
if (istanze.TryGetValue(g.Id, out var nomeIstanza))
|
||||||
|
return new Membro(g.Id, g.Tipo, nomeIstanza, g.Nome);
|
||||||
|
var bracket = IstanzaConGenericoRegex.Match(g.Nome);
|
||||||
|
return bracket.Success
|
||||||
|
? new Membro(g.Id, g.Tipo, bracket.Groups[1].Value, bracket.Groups[2].Value)
|
||||||
|
: new Membro(g.Id, g.Tipo, g.Nome, g.Nome);
|
||||||
|
}).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Codici dei componenti diretti (il generico su disco, non il nome istanza),
|
/// <summary>Codici dei componenti diretti (il generico su disco, non il nome istanza),
|
||||||
@@ -131,4 +201,73 @@ public static class AsmRecordReader
|
|||||||
var membri = LeggiMembri(File.ReadAllBytes(percorsoAsm));
|
var membri = LeggiMembri(File.ReadAllBytes(percorsoAsm));
|
||||||
return new HashSet<string>(membri.Select(m => m.Generico), StringComparer.OrdinalIgnoreCase);
|
return new HashSet<string>(membri.Select(m => m.Generico), StringComparer.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Codici che compaiono come piazzamento ma non fra i membri: sono i componenti
|
||||||
|
/// **soppressi**, che spariscono da "members" senza lasciare traccia (§7.1) ma restano
|
||||||
|
/// citati dalla feature che li posiziona.
|
||||||
|
///
|
||||||
|
/// Non fanno parte della BOM - Creo non li elenca - ma il loro file serve comunque per
|
||||||
|
/// **aprire** l'assieme: senza, il caricamento fallisce con "dati di input non completi".
|
||||||
|
/// Sono quindi due domande diverse: cosa compare nella distinta (<see
|
||||||
|
/// cref="EstraiCodiciReferenziati"/>) e cosa serve avere su disco (questo metodo in
|
||||||
|
/// aggiunta), e la seconda lista e' piu' lunga della prima.</summary>
|
||||||
|
public static HashSet<string> EstraiCodiciSoppressi(string percorsoAsm)
|
||||||
|
{
|
||||||
|
byte[] dati = File.ReadAllBytes(percorsoAsm);
|
||||||
|
var membri = LeggiMembri(dati);
|
||||||
|
var noti = new HashSet<string>(
|
||||||
|
membri.Select(m => m.Generico).Concat(membri.Select(m => m.Nome)), StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var soppressi = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (Match m in PlacementRegex.Matches(Encoding.Latin1.GetString(dati)))
|
||||||
|
if (!noti.Contains(m.Groups[1].Value))
|
||||||
|
soppressi.Add(m.Groups[1].Value);
|
||||||
|
return soppressi;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Modelli che l'assieme cita nella tabella dei riferimenti (§7.5): oltre ai propri
|
||||||
|
/// membri comprende quelli legati da **riferimenti esterni** - assembly feature, datum
|
||||||
|
/// copiati - che non compaiono né in "members" né come piazzamento, ma che Creo carica
|
||||||
|
/// comunque per aprire l'assieme.
|
||||||
|
///
|
||||||
|
/// Terza lista dopo <see cref="EstraiCodiciReferenziati"/> ("cosa c'è in distinta") e <see
|
||||||
|
/// cref="EstraiCodiciSoppressi"/>: come quest'ultima risponde a "cosa serve su disco", non a
|
||||||
|
/// "cosa va nella BOM. L'array contiene anche nomi di feature (MIRROR3, CHAMFER1...) e non
|
||||||
|
/// solo di modelli: non si riesce a distinguerli dal record, ma non danno fastidio perché il
|
||||||
|
/// chiamante li cerca nell'indice e ignora quelli che non ci sono.</summary>
|
||||||
|
public static HashSet<string> EstraiCodiciRiferimentiEsterni(string percorsoAsm)
|
||||||
|
{
|
||||||
|
string testo = Encoding.Latin1.GetString(File.ReadAllBytes(percorsoAsm));
|
||||||
|
var codici = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
for (int i = testo.IndexOf(InizioRiferimenti, StringComparison.Ordinal); i >= 0;
|
||||||
|
i = testo.IndexOf(InizioRiferimenti, i + 1, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
int max = Math.Min(LunghezzaMassimaRiferimenti, testo.Length - i);
|
||||||
|
int fine = testo.IndexOf(FineRiferimenti, i, max);
|
||||||
|
foreach (Match m in RiferimentoRegex.Matches(testo.Substring(i, fine >= 0 ? fine - i : max)))
|
||||||
|
{
|
||||||
|
var bracket = IstanzaConGenericoRegex.Match(m.Groups[1].Value);
|
||||||
|
codici.Add(bracket.Success
|
||||||
|
? bracket.Groups[2].Value
|
||||||
|
: SuffissoArrayFeatureRegex.Replace(m.Groups[1].Value, ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return codici;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Array dei modelli referenziati: "\xf2\xf7\x05" lo apre, "\xf3" lo chiude, ogni elemento e'
|
||||||
|
// "\xe3\xe2<tipo><nome>\x00" (§7.5). Il tetto di lunghezza limita il danno se il terminatore
|
||||||
|
// non si trova dove atteso: si perde qualche riferimento, non si raccoglie mezzo file.
|
||||||
|
private const string InizioRiferimenti = "ò÷\u0005ãâ";
|
||||||
|
private const char FineRiferimenti = 'ó';
|
||||||
|
private const int LunghezzaMassimaRiferimenti = 4096;
|
||||||
|
private static readonly Regex RiferimentoRegex = new(
|
||||||
|
@"\xe3\xe2[\x01\x02]([\x20-\x7e]{3,})\x00", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
// "Componente ID 52 (9230100043.PRT)" - la stringa cambia con la localizzazione Creo del
|
||||||
|
// file, come in AssemblyComponentScanner.HaPlacement. Minimo 3 caratteri per lo stesso
|
||||||
|
// motivo per cui li' i token generici partono da 3: sotto quella soglia si raccoglie
|
||||||
|
// rumore (sulla share esistono davvero modelli chiamati "1.prt", "2.prt"...).
|
||||||
|
private static readonly Regex PlacementRegex = new(
|
||||||
|
@"Component(?:e)? [Ii][Dd] \d+ \(([A-Za-z0-9_\-]{3,})\.(?:PRT|ASM)\)", RegexOptions.Compiled);
|
||||||
}
|
}
|
||||||
+518
-281
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,352 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace PrepStep;
|
||||||
|
|
||||||
|
/// <summary>Guida interattiva richiamabile da "Aiuto ▸ Guida" (F1): stessa guida completa di
|
||||||
|
/// docs/guida-utente.md (passo-passo fino a Creo Distributed Batch), pensata per chi apre
|
||||||
|
/// l'app senza aver letto i docs e senza esperienza pregressa. Il dettaglio tecnico (formato
|
||||||
|
/// .asm, schema DB, ecc.) resta solo in docs/architettura.md e affini, per non duplicarlo qui.
|
||||||
|
///
|
||||||
|
/// Gerarchia tipografica nel pannello di lettura (vedi ScriviSezione): titolo sezione in blu
|
||||||
|
/// primario/grassetto/14pt, corpo in grigio quasi-nero/10pt, passi numerati con il numero in
|
||||||
|
/// blu grassetto per farli scorrere a colpo d'occhio, callout con sfondo tinta piatta (nessun
|
||||||
|
/// bordo/ombra: RichTextBox non li supporta) — blu tenue per le note informative, ambra tenue
|
||||||
|
/// per gli avvisi importanti, con etichetta ("Nota"/"Attenzione") in grassetto a inizio blocco
|
||||||
|
/// invece di un colore isolato, così restano leggibili anche senza vedere i colori.</summary>
|
||||||
|
public class GuidaForm : Form
|
||||||
|
{
|
||||||
|
private sealed record Sezione(
|
||||||
|
string Titolo,
|
||||||
|
string Intro,
|
||||||
|
string[]? Passi = null,
|
||||||
|
string? NotaTesto = null,
|
||||||
|
bool NotaAttenzione = false);
|
||||||
|
|
||||||
|
// Palette: blu primario dell'app per titoli/passi, ambra tenue solo per gli avvisi che
|
||||||
|
// possono far perdere tempo se ignorati (es. l'errore Creo [-4]) — il rosso resta riservato
|
||||||
|
// ai messaggi di errore veri e propri altrove nell'app.
|
||||||
|
private static readonly Color ColoreTesto = Color.FromArgb(40, 40, 40);
|
||||||
|
private static readonly Color ColoreNotaInfoSfondo = Color.FromArgb(232, 240, 250);
|
||||||
|
private static readonly Color ColoreNotaInfoTesto = Color.FromArgb(15, 65, 120);
|
||||||
|
private static readonly Color ColoreNotaAttenzioneSfondo = Color.FromArgb(253, 240, 219);
|
||||||
|
private static readonly Color ColoreNotaAttenzioneTesto = Color.FromArgb(140, 90, 10);
|
||||||
|
|
||||||
|
private static readonly Sezione[] Sezioni =
|
||||||
|
{
|
||||||
|
new("Panoramica",
|
||||||
|
"PrepStep - Cevolani prepara i file CAD Pro-ENGINEER/Creo da passare a Creo " +
|
||||||
|
"Distributed Batch per l'esportazione STEP: indicizza i file di una share di rete " +
|
||||||
|
"e guida attraverso 5 fasi, una alla volta, con \"Avanti\"/\"Indietro\".",
|
||||||
|
NotaTesto: "\"File ▸ Nuovo\" (Ctrl+N) azzera lo stato di distinta/raw/verifica per ripartire " +
|
||||||
|
"con una nuova distinta base, senza dover ricaricare il database."),
|
||||||
|
|
||||||
|
new("Dove finiscono i file",
|
||||||
|
"Tutto sta nella cartella di lavoro, che di default è la cartella di " +
|
||||||
|
"installazione (%LOCALAPPDATA%\\Programs\\PrepStep):",
|
||||||
|
Passi:
|
||||||
|
[
|
||||||
|
"Il database cad_index.db sta sempre nella cartella di installazione.",
|
||||||
|
"Il CSV di ogni distinta importata, \"raw\" (file CAD delle fasi 3-4) e " +
|
||||||
|
"\"step\" (STEP esportati da Creo, cartella che indichi tu a Creo) stanno " +
|
||||||
|
"nella cartella di lavoro. Il file .xls del gestionale resta dove l'hai " +
|
||||||
|
"preso.",
|
||||||
|
"Per cambiare la cartella di lavoro: \"File ▸ Scegli cartella di lavoro...\" " +
|
||||||
|
"(viene ricordata agli avvii successivi).",
|
||||||
|
"Per fare pulizia: \"File ▸ Pulisci raw e step...\" svuota \"raw\" e " +
|
||||||
|
"\"step\"; \"File ▸ Elimina CSV delle distinte...\" elimina i CSV, che " +
|
||||||
|
"altrimenti si accumulano uno per distinta. Nessuno dei due tocca il " +
|
||||||
|
"database.",
|
||||||
|
],
|
||||||
|
NotaTesto: "AppData è una cartella nascosta in Esplora file: per aprirla scrivi " +
|
||||||
|
"%LOCALAPPDATA%\\Programs\\PrepStep nella barra degli indirizzi (vale anche " +
|
||||||
|
"per scegliere \"step\" in Creo). Disinstallando il programma questi file " +
|
||||||
|
"restano: cancellali a mano se non servono più."),
|
||||||
|
|
||||||
|
new("Prima di iniziare",
|
||||||
|
"Prima di importare la prima distinta:",
|
||||||
|
Passi:
|
||||||
|
[
|
||||||
|
"Serve accesso alla share di rete con i file CAD (di default X:\\).",
|
||||||
|
"Serve l'estrazione \"Distinta Base\" da Fata (il gestionale): il file .xls " +
|
||||||
|
"così come Fata lo esporta, senza risalvarlo o convertirlo in altri " +
|
||||||
|
"formati — l'app riconosce da sola se sotto l'estensione .xls c'è XML o " +
|
||||||
|
"Excel binario vero.",
|
||||||
|
"Se il file segue la convenzione di nome DBP_<codice>_<timestamp>, l'app " +
|
||||||
|
"aggiunge da sola l'assieme generale come componente in più: non serve " +
|
||||||
|
"fare nulla di diverso.",
|
||||||
|
"Se non vuoi usare la cartella di installazione, scegli la cartella di " +
|
||||||
|
"lavoro (\"File ▸ Scegli cartella di lavoro...\") prima di importare la " +
|
||||||
|
"distinta: è lì che finiscono CSV, \"raw\" e, dopo l'esportazione con " +
|
||||||
|
"Creo, \"step\".",
|
||||||
|
"Se la cartella di lavoro è già stata usata per un'altra distinta, conviene " +
|
||||||
|
"pulire \"raw\" e \"step\" prima di ripartire (\"File ▸ Pulisci raw e " +
|
||||||
|
"step...\"): \"File ▸ Nuovo\" azzera solo lo stato dell'app, non i file " +
|
||||||
|
"già copiati su disco.",
|
||||||
|
]),
|
||||||
|
|
||||||
|
new("1. Database",
|
||||||
|
"All'avvio l'app carica da sola \"cad_index.db\" dalla cartella di installazione. " +
|
||||||
|
"Un pallino verde/rosso indica se un database è caricato.",
|
||||||
|
Passi:
|
||||||
|
[
|
||||||
|
"Pallino rosso (nessun database): premi \"Aggiorna indice\" per scansionare " +
|
||||||
|
"le cartelle di primo livello della share (X:\\) e crearlo. La prima volta " +
|
||||||
|
"richiede qualche minuto, i giri successivi aggiornano solo quello che è " +
|
||||||
|
"cambiato.",
|
||||||
|
"Database da un'altra postazione: \"Apri DB...\" lo carica solo finché non " +
|
||||||
|
"chiudi il programma. Per usarlo sempre, copialo nella cartella di " +
|
||||||
|
"installazione come cad_index.db.",
|
||||||
|
]),
|
||||||
|
|
||||||
|
new("2. Importa Distinta Base",
|
||||||
|
"Due passi in sequenza:",
|
||||||
|
Passi:
|
||||||
|
[
|
||||||
|
"Carica la distinta: trascina il file .xls nella finestra, oppure premi " +
|
||||||
|
"\"Scegli file distinta...\". Le righe importate compaiono nella griglia " +
|
||||||
|
"(Componente/Descrizione) e viene scritto un CSV nella cartella di lavoro. " +
|
||||||
|
"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()
|
||||||
|
{
|
||||||
|
Text = "Guida";
|
||||||
|
ClientSize = new Size(1000, 680);
|
||||||
|
MinimumSize = new Size(640, 440);
|
||||||
|
StartPosition = FormStartPosition.CenterParent;
|
||||||
|
Font = new Font("Segoe UI", 9.5f);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is IOException or ArgumentException)
|
||||||
|
{
|
||||||
|
// ponytail: nessuna icona nell'exe (es. lanciato da dotnet run) -> resta quella di default
|
||||||
|
}
|
||||||
|
|
||||||
|
var headerPanel = new Panel { Dock = DockStyle.Top, Height = 40, BackColor = SystemColors.ControlLight };
|
||||||
|
var lblHeader = new Label
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Fill,
|
||||||
|
Text = "Guida",
|
||||||
|
TextAlign = ContentAlignment.MiddleLeft,
|
||||||
|
Padding = new Padding(16, 0, 0, 0),
|
||||||
|
Font = new Font(Font, FontStyle.Bold),
|
||||||
|
};
|
||||||
|
headerPanel.Controls.Add(lblHeader);
|
||||||
|
|
||||||
|
// Sidebar disegnata a mano (OwnerDrawFixed) invece della selezione blu di sistema:
|
||||||
|
// stessa identità visiva del resto dell'app (UiStile.ColorePrimario) invece del colore
|
||||||
|
// di selezione standard di Windows, che varia con il tema dell'utente.
|
||||||
|
var lista = new ListBox
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Left,
|
||||||
|
Width = 230,
|
||||||
|
IntegralHeight = false,
|
||||||
|
BorderStyle = BorderStyle.None,
|
||||||
|
DrawMode = DrawMode.OwnerDrawFixed,
|
||||||
|
ItemHeight = 34,
|
||||||
|
BackColor = UiStile.ColoreSfondoCard,
|
||||||
|
};
|
||||||
|
foreach (var sezione in Sezioni)
|
||||||
|
lista.Items.Add(sezione.Titolo);
|
||||||
|
lista.DrawItem += (_, e) =>
|
||||||
|
{
|
||||||
|
if (e.Index < 0)
|
||||||
|
return;
|
||||||
|
bool selezionato = (e.State & DrawItemState.Selected) != 0;
|
||||||
|
using var sfondo = new SolidBrush(selezionato ? UiStile.ColorePrimario : UiStile.ColoreSfondoCard);
|
||||||
|
e.Graphics.FillRectangle(sfondo, e.Bounds);
|
||||||
|
var colore = selezionato ? Color.White : ColoreTesto;
|
||||||
|
using var testoFont = new Font(lista.Font, selezionato ? FontStyle.Bold : FontStyle.Regular);
|
||||||
|
var rettangoloTesto = new Rectangle(e.Bounds.X + 16, e.Bounds.Y, e.Bounds.Width - 20, e.Bounds.Height);
|
||||||
|
TextRenderer.DrawText(e.Graphics, lista.Items[e.Index].ToString(), testoFont, rettangoloTesto,
|
||||||
|
colore, TextFormatFlags.VerticalCenter | TextFormatFlags.Left | TextFormatFlags.EndEllipsis);
|
||||||
|
};
|
||||||
|
|
||||||
|
var testo = new TestoSolaLettura
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Fill,
|
||||||
|
ReadOnly = true,
|
||||||
|
TabStop = false,
|
||||||
|
BorderStyle = BorderStyle.None,
|
||||||
|
BackColor = Color.White,
|
||||||
|
Font = new Font(Font.FontFamily, 10f),
|
||||||
|
};
|
||||||
|
|
||||||
|
lista.SelectedIndexChanged += (_, _) =>
|
||||||
|
{
|
||||||
|
if (lista.SelectedIndex >= 0)
|
||||||
|
ScriviSezione(testo, Sezioni[lista.SelectedIndex]);
|
||||||
|
};
|
||||||
|
|
||||||
|
var (barraChiudi, azioniChiudi) = UiStile.FloatBar();
|
||||||
|
var btnChiudi = new Button { Text = "Chiudi" };
|
||||||
|
UiStile.Primario(btnChiudi);
|
||||||
|
UiStile.Arrotonda(btnChiudi, 10);
|
||||||
|
btnChiudi.Click += (_, _) => Close();
|
||||||
|
azioniChiudi.Controls.Add(btnChiudi);
|
||||||
|
CancelButton = btnChiudi;
|
||||||
|
|
||||||
|
var contenuto = new Panel { Dock = DockStyle.Fill, Padding = new Padding(24, 20, 24, 20), BackColor = Color.White };
|
||||||
|
contenuto.Controls.Add(testo);
|
||||||
|
|
||||||
|
Controls.Add(contenuto);
|
||||||
|
Controls.Add(lista);
|
||||||
|
Controls.Add(barraChiudi);
|
||||||
|
Controls.Add(headerPanel);
|
||||||
|
|
||||||
|
lista.SelectedIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Costruisce il pannello di lettura con gerarchia tipografica esplicita: titolo
|
||||||
|
/// (blu primario, grassetto, 14pt), corpo (grigio quasi-nero, 10pt), passi numerati (numero
|
||||||
|
/// in blu grassetto, indentati) e callout finale con sfondo a tinta piatta ed etichetta in
|
||||||
|
/// grassetto — informativo (blu tenue) o di attenzione (ambra tenue). Applica la
|
||||||
|
/// formattazione DOPO aver scritto il testo, selezionando il range appena aggiunto: è il
|
||||||
|
/// solo modo affidabile di formattare i paragrafi in un RichTextBox (impostare
|
||||||
|
/// SelectionFont/Color su una selezione vuota prima di AppendText non è garantito che
|
||||||
|
/// "si attacchi" al testo che segue).</summary>
|
||||||
|
private static void ScriviSezione(RichTextBox box, Sezione sezione)
|
||||||
|
{
|
||||||
|
box.Clear();
|
||||||
|
var titoloFont = new Font(box.Font.FontFamily, 14f, FontStyle.Bold);
|
||||||
|
var corpoFont = new Font(box.Font.FontFamily, 10f, FontStyle.Regular);
|
||||||
|
var corpoGrassetto = new Font(box.Font.FontFamily, 10f, FontStyle.Bold);
|
||||||
|
|
||||||
|
void Scrivi(string testo, Font font, Color colore, int indent = 0, Color? sfondo = null)
|
||||||
|
{
|
||||||
|
int inizio = box.TextLength;
|
||||||
|
box.AppendText(testo);
|
||||||
|
box.Select(inizio, box.TextLength - inizio);
|
||||||
|
box.SelectionFont = font;
|
||||||
|
box.SelectionColor = colore;
|
||||||
|
box.SelectionBackColor = sfondo ?? box.BackColor;
|
||||||
|
box.SelectionIndent = indent;
|
||||||
|
}
|
||||||
|
|
||||||
|
Scrivi(sezione.Titolo + "\n\n", titoloFont, UiStile.ColorePrimario);
|
||||||
|
Scrivi(sezione.Intro + "\n", corpoFont, ColoreTesto);
|
||||||
|
|
||||||
|
if (sezione.Passi is { Length: > 0 })
|
||||||
|
{
|
||||||
|
Scrivi("\n", corpoFont, ColoreTesto);
|
||||||
|
for (int i = 0; i < sezione.Passi.Length; i++)
|
||||||
|
{
|
||||||
|
Scrivi($"{i + 1}. ", corpoGrassetto, UiStile.ColorePrimario, indent: 8);
|
||||||
|
Scrivi(sezione.Passi[i] + "\n\n", corpoFont, ColoreTesto, indent: 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sezione.NotaTesto is not null)
|
||||||
|
{
|
||||||
|
var sfondo = sezione.NotaAttenzione ? ColoreNotaAttenzioneSfondo : ColoreNotaInfoSfondo;
|
||||||
|
var coloreEtichetta = sezione.NotaAttenzione ? ColoreNotaAttenzioneTesto : ColoreNotaInfoTesto;
|
||||||
|
string etichetta = sezione.NotaAttenzione ? "Attenzione" : "Nota";
|
||||||
|
|
||||||
|
Scrivi("\n", corpoFont, ColoreTesto);
|
||||||
|
Scrivi(" " + etichetta + "\n", corpoGrassetto, coloreEtichetta, indent: 12, sfondo: sfondo);
|
||||||
|
Scrivi(" " + sezione.NotaTesto + " \n", corpoFont, ColoreTesto, indent: 12, sfondo: sfondo);
|
||||||
|
}
|
||||||
|
|
||||||
|
box.SelectionStart = 0;
|
||||||
|
box.SelectionLength = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>RichTextBox da sola lettura che non si comporta come un editor: niente cursore
|
||||||
|
/// lampeggiante, niente cursore a "I" e niente selezione col mouse. I clic nell'area di testo
|
||||||
|
/// vengono ignorati (così non prende il fuoco e non mostra il caret); la barra di scorrimento
|
||||||
|
/// sta nell'area non-client e resta utilizzabile, la rotella scorre al passaggio del mouse.</summary>
|
||||||
|
private sealed class TestoSolaLettura : RichTextBox
|
||||||
|
{
|
||||||
|
private const int WM_SETFOCUS = 0x0007;
|
||||||
|
private const int WM_SETCURSOR = 0x0020;
|
||||||
|
private const int WM_LBUTTONDOWN = 0x0201;
|
||||||
|
private const int WM_LBUTTONDBLCLK = 0x0203;
|
||||||
|
private const int WM_RBUTTONDOWN = 0x0204;
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern bool HideCaret(IntPtr hWnd);
|
||||||
|
|
||||||
|
protected override void WndProc(ref Message m)
|
||||||
|
{
|
||||||
|
switch (m.Msg)
|
||||||
|
{
|
||||||
|
case WM_LBUTTONDOWN or WM_LBUTTONDBLCLK or WM_RBUTTONDOWN:
|
||||||
|
return;
|
||||||
|
case WM_SETCURSOR:
|
||||||
|
Cursor.Current = Cursors.Arrow;
|
||||||
|
m.Result = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
base.WndProc(ref m);
|
||||||
|
if (m.Msg == WM_SETFOCUS)
|
||||||
|
HideCaret(Handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
-2
@@ -40,13 +40,22 @@ public static class Indexer
|
|||||||
CREATE INDEX IF NOT EXISTS idx_files_tipo ON files(tipo_file);
|
CREATE INDEX IF NOT EXISTS idx_files_tipo ON files(tipo_file);
|
||||||
CREATE INDEX IF NOT EXISTS idx_files_cartella ON files(cartella_radice);
|
CREATE INDEX IF NOT EXISTS idx_files_cartella ON files(cartella_radice);
|
||||||
|
|
||||||
CREATE VIEW IF NOT EXISTS files_ultima_versione AS
|
-- DROP+CREATE invece di "IF NOT EXISTS": la vista e' ricalcolata a ogni avvio di scan,
|
||||||
|
-- cosi' un DB gia' esistente prende la definizione corrente senza reindicizzare.
|
||||||
|
DROP VIEW IF EXISTS files_ultima_versione;
|
||||||
|
CREATE VIEW files_ultima_versione AS
|
||||||
SELECT percorso_completo, percorso_relativo, nome_file, cartella_radice, famiglia,
|
SELECT percorso_completo, percorso_relativo, nome_file, cartella_radice, famiglia,
|
||||||
codice, tipo_file, versione, dimensione_byte, data_modifica, scansionato_il
|
codice, tipo_file, versione, dimensione_byte, data_modifica, scansionato_il
|
||||||
FROM (
|
FROM (
|
||||||
SELECT f.*, ROW_NUMBER() OVER (
|
SELECT f.*, ROW_NUMBER() OVER (
|
||||||
PARTITION BY cartella_radice, codice, tipo_file
|
PARTITION BY cartella_radice, codice, tipo_file
|
||||||
ORDER BY versione DESC
|
-- A parita' di versione (stesso codice in due sottocartelle della stessa
|
||||||
|
-- radice, es. una copia vecchia in una cartella di lavoro) vince il file
|
||||||
|
-- modificato piu' di recente: senza questo criterio il pareggio e' rotto in
|
||||||
|
-- modo arbitrario e puo' vincere la copia obsoleta, nascondendo del tutto
|
||||||
|
-- quella buona. Il file copiato in "raw" sarebbe allora un modello diverso da
|
||||||
|
-- quello che l'assieme si aspetta, e Creo non riesce ad aprirlo.
|
||||||
|
ORDER BY versione DESC, data_modifica DESC, percorso_completo
|
||||||
) AS rn
|
) AS rn
|
||||||
FROM files f
|
FROM files f
|
||||||
) WHERE rn = 1;
|
) WHERE rn = 1;
|
||||||
|
|||||||
@@ -8,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>
|
||||||
|
|
||||||
|
|||||||
+20
-85
@@ -2,23 +2,15 @@ namespace PrepStep;
|
|||||||
|
|
||||||
public sealed class ProgressForm : Form
|
public sealed class ProgressForm : Form
|
||||||
{
|
{
|
||||||
private readonly Label _lblTitolo = new();
|
|
||||||
private readonly Label _lblCartella = new();
|
private readonly Label _lblCartella = new();
|
||||||
private readonly Panel _barraTraccia = new();
|
private readonly ProgressBar _bar = new();
|
||||||
private readonly Panel _barraRiempimento = new();
|
|
||||||
private readonly Label _lblPercentuale = new();
|
|
||||||
private readonly Label _lblConteggio = new();
|
private readonly Label _lblConteggio = new();
|
||||||
private readonly Label _lblDettaglio = new();
|
private readonly Label _lblDettaglio = new();
|
||||||
private readonly Label _lblEta = new();
|
private readonly Label _lblEta = new();
|
||||||
private readonly Button _btnAnnulla = new();
|
private readonly Button _btnAnnulla = new();
|
||||||
private readonly Panel _separatore = new();
|
|
||||||
private readonly Panel _piePagina = new();
|
|
||||||
private readonly ToolTip _tipCartella = new();
|
private readonly ToolTip _tipCartella = new();
|
||||||
|
|
||||||
private const int Margine = 24;
|
private const int Margine = 16;
|
||||||
// SystemColors.GrayText (~3.9:1 su bianco) è sotto la soglia WCAG AA 4.5:1 per testo
|
|
||||||
// normale: usiamo un grigio più scuro, leggibile ma comunque secondario.
|
|
||||||
private static readonly Color GrigioTesto = Color.FromArgb(90, 90, 90);
|
|
||||||
|
|
||||||
public event EventHandler? Annullato;
|
public event EventHandler? Annullato;
|
||||||
|
|
||||||
@@ -30,84 +22,42 @@ public sealed class ProgressForm : Form
|
|||||||
MinimizeBox = false;
|
MinimizeBox = false;
|
||||||
MaximizeBox = false;
|
MaximizeBox = false;
|
||||||
ControlBox = false;
|
ControlBox = false;
|
||||||
BackColor = Color.White;
|
ClientSize = new Size(420, 190);
|
||||||
ClientSize = new Size(480, 270);
|
|
||||||
|
|
||||||
_lblTitolo.Text = "Aggiornamento indice in corso...";
|
|
||||||
_lblTitolo.Location = new Point(Margine, 22);
|
|
||||||
_lblTitolo.AutoSize = true;
|
|
||||||
_lblTitolo.Font = new Font(Font.FontFamily, 11f, FontStyle.Bold);
|
|
||||||
|
|
||||||
_lblCartella.Text = "Preparazione...";
|
_lblCartella.Text = "Preparazione...";
|
||||||
_lblCartella.Location = new Point(Margine, 52);
|
_lblCartella.Location = new Point(Margine, Margine);
|
||||||
// AutoSize=false + AutoEllipsis: un percorso lungo (share di rete) va troncato con "..."
|
// AutoSize=false + AutoEllipsis: un percorso lungo (share di rete) va troncato con "..."
|
||||||
// invece di sforare il bordo del dialog, che è FixedDialog e non si può allargare.
|
// invece di sforare il bordo del dialog, che è FixedDialog e non si può allargare. Il
|
||||||
// Il testo completo resta disponibile nel tooltip (stesso pattern di Form1._lblImportStatus).
|
// testo completo resta disponibile nel tooltip.
|
||||||
_lblCartella.AutoSize = false;
|
_lblCartella.AutoSize = false;
|
||||||
_lblCartella.AutoEllipsis = true;
|
_lblCartella.AutoEllipsis = true;
|
||||||
_lblCartella.Size = new Size(ClientSize.Width - Margine * 2, 20);
|
_lblCartella.Size = new Size(ClientSize.Width - Margine * 2, 20);
|
||||||
_lblCartella.ForeColor = GrigioTesto;
|
|
||||||
|
|
||||||
// Barra piatta disegnata a mano (traccia grigia + riempimento) invece della ProgressBar
|
_bar.Location = new Point(Margine, 40);
|
||||||
// nativa: quella di Windows è verde e in stile "a blocchi", stona con il blu del resto
|
_bar.Size = new Size(ClientSize.Width - Margine * 2, 24);
|
||||||
// dell'app (UiStile.Primario). Larghezza = margine destro reale meno lo spazio per
|
_bar.Minimum = 0;
|
||||||
// "100%" in grassetto, così barra+percentuale rispettano lo stesso margine destro del
|
_bar.Maximum = 100;
|
||||||
// resto del contenuto (prima finivano ~25px prima, sbilanciate rispetto al margine
|
|
||||||
// sinistro di 24px).
|
|
||||||
_barraTraccia.Location = new Point(Margine, 90);
|
|
||||||
_barraTraccia.Size = new Size(ClientSize.Width - Margine * 2 - 46, 10);
|
|
||||||
_barraTraccia.BackColor = Color.FromArgb(230, 230, 230);
|
|
||||||
_barraRiempimento.Location = Point.Empty;
|
|
||||||
_barraRiempimento.Size = new Size(0, _barraTraccia.Height);
|
|
||||||
_barraRiempimento.BackColor = UiStile.ColorePrimario;
|
|
||||||
_barraTraccia.Controls.Add(_barraRiempimento);
|
|
||||||
|
|
||||||
_lblPercentuale.AutoSize = true;
|
_lblConteggio.Location = new Point(Margine, 72);
|
||||||
_lblPercentuale.Font = new Font(Font, FontStyle.Bold);
|
|
||||||
_lblPercentuale.TextAlign = ContentAlignment.MiddleRight;
|
|
||||||
|
|
||||||
_lblConteggio.Location = new Point(Margine, 122);
|
|
||||||
_lblConteggio.AutoSize = true;
|
_lblConteggio.AutoSize = true;
|
||||||
|
|
||||||
_lblDettaglio.Location = new Point(Margine, 146);
|
_lblDettaglio.Location = new Point(Margine, 96);
|
||||||
_lblDettaglio.AutoSize = true;
|
_lblDettaglio.AutoSize = true;
|
||||||
_lblDettaglio.ForeColor = GrigioTesto;
|
|
||||||
|
|
||||||
_lblEta.Location = new Point(Margine, 174);
|
_lblEta.Location = new Point(Margine, 120);
|
||||||
_lblEta.AutoSize = true;
|
_lblEta.AutoSize = true;
|
||||||
|
|
||||||
// Footer separato dal contenuto (linea + sfondo leggermente diverso) invece del
|
|
||||||
// bottone appiccicato al bordo inferiore del dialog: ancorato in basso, quindi la
|
|
||||||
// spaziatura sopra/sotto resta corretta anche se la ClientSize cambia. Altezza footer
|
|
||||||
// 60px e ClientSize 270 scelti per lasciare sotto l'ultima riga (eta, Bottom≈189) lo
|
|
||||||
// stesso margine (~20px) usato sopra il titolo, invece dei ~7px sbilanciati di prima.
|
|
||||||
_separatore.BackColor = Color.FromArgb(230, 230, 230);
|
|
||||||
_separatore.Size = new Size(ClientSize.Width, 1);
|
|
||||||
_separatore.Location = new Point(0, ClientSize.Height - 60);
|
|
||||||
_separatore.Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom;
|
|
||||||
|
|
||||||
_piePagina.BackColor = Color.FromArgb(247, 247, 248);
|
|
||||||
_piePagina.Size = new Size(ClientSize.Width, 59);
|
|
||||||
_piePagina.Location = new Point(0, ClientSize.Height - 59);
|
|
||||||
_piePagina.Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom;
|
|
||||||
|
|
||||||
_btnAnnulla.Text = "Annulla";
|
_btnAnnulla.Text = "Annulla";
|
||||||
UiStile.Secondario(_btnAnnulla);
|
_btnAnnulla.Size = new Size(90, 28);
|
||||||
UiStile.Arrotonda(_btnAnnulla);
|
|
||||||
_btnAnnulla.Click += (_, e) => Annullato?.Invoke(this, e);
|
_btnAnnulla.Click += (_, e) => Annullato?.Invoke(this, e);
|
||||||
_piePagina.Controls.Add(_btnAnnulla);
|
|
||||||
|
|
||||||
Controls.AddRange(new Control[]
|
Controls.AddRange(new Control[]
|
||||||
{
|
{
|
||||||
_lblTitolo, _lblCartella, _barraTraccia, _lblPercentuale,
|
_lblCartella, _bar, _lblConteggio, _lblDettaglio, _lblEta, _btnAnnulla
|
||||||
_lblConteggio, _lblDettaglio, _lblEta, _separatore, _piePagina
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Posizionato dopo l'aggiunta ai Controls: con AutoSize il Width vero si conosce
|
_btnAnnulla.Location = new Point(
|
||||||
// solo a layout avvenuto, non subito dopo la creazione del bottone.
|
ClientSize.Width - Margine - _btnAnnulla.Width, ClientSize.Height - Margine - _btnAnnulla.Height);
|
||||||
_btnAnnulla.Location = new Point(_piePagina.Width - Margine - _btnAnnulla.Width,
|
|
||||||
(_piePagina.Height - _btnAnnulla.Height) / 2);
|
|
||||||
_btnAnnulla.Anchor = AnchorStyles.Right | AnchorStyles.Top;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Aggiorna(ScanProgress p)
|
public void Aggiorna(ScanProgress p)
|
||||||
@@ -115,24 +65,9 @@ public sealed class ProgressForm : Form
|
|||||||
_lblCartella.Text = $"Scansione: {p.Cartella}";
|
_lblCartella.Text = $"Scansione: {p.Cartella}";
|
||||||
_tipCartella.SetToolTip(_lblCartella, _lblCartella.Text);
|
_tipCartella.SetToolTip(_lblCartella, _lblCartella.Text);
|
||||||
|
|
||||||
if (p.PrevistiTotali > 0)
|
_bar.Value = p.PrevistiTotali > 0
|
||||||
{
|
? (int)Math.Clamp(100.0 * p.FattiTotali / p.PrevistiTotali, 0, 100)
|
||||||
double frazione = Math.Min(1.0, (double)p.FattiTotali / p.PrevistiTotali);
|
: 0;
|
||||||
_barraRiempimento.Width = (int)(frazione * _barraTraccia.Width);
|
|
||||||
_lblPercentuale.Text = $"{frazione * 100:0}%";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// ponytail: prima del conteggio totale (poco probabile: richiede la scansione
|
|
||||||
// precedente salvata nel DB) la barra resta vuota invece di un'animazione
|
|
||||||
// indeterminata — se serve, aggiungere un Timer che pulsa _barraRiempimento.
|
|
||||||
_barraRiempimento.Width = 0;
|
|
||||||
_lblPercentuale.Text = "";
|
|
||||||
}
|
|
||||||
// Riallineato al margine destro reale ogni volta: AutoSize aggiorna Width non appena
|
|
||||||
// Text cambia, quindi la Location va ricalcolata qui (non solo una volta nel costruttore).
|
|
||||||
_lblPercentuale.Location = new Point(
|
|
||||||
ClientSize.Width - Margine - _lblPercentuale.Width, _barraTraccia.Top - 3);
|
|
||||||
|
|
||||||
_lblConteggio.Text = p.PrevistiTotali > 0
|
_lblConteggio.Text = p.PrevistiTotali > 0
|
||||||
? $"{p.FattiTotali:N0} / ~{p.PrevistiTotali:N0} file"
|
? $"{p.FattiTotali:N0} / ~{p.PrevistiTotali:N0} file"
|
||||||
|
|||||||
@@ -15,11 +15,20 @@ public class PrtMancantiForm : Form
|
|||||||
ClientSize = new Size(720, 440);
|
ClientSize = new Size(720, 440);
|
||||||
StartPosition = FormStartPosition.CenterParent;
|
StartPosition = FormStartPosition.CenterParent;
|
||||||
Font = new Font("Segoe UI", 9.5f);
|
Font = new Font("Segoe UI", 9.5f);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is IOException or ArgumentException)
|
||||||
|
{
|
||||||
|
// ponytail: nessuna icona nell'exe (es. lanciato da dotnet run) -> resta quella di default
|
||||||
|
}
|
||||||
|
|
||||||
var lblInfo = new Label
|
var lblInfo = new Label
|
||||||
{
|
{
|
||||||
Dock = DockStyle.Top,
|
Dock = DockStyle.Top,
|
||||||
AutoSize = true,
|
AutoSize = true,
|
||||||
|
MaximumSize = new Size(ClientSize.Width - 24, 0),
|
||||||
Padding = new Padding(12, 8, 12, 8),
|
Padding = new Padding(12, 8, 12, 8),
|
||||||
Text = $"Copiati in \"{cartellaRaw}\": {numPrt} PRT e {numAsm} sotto-assiemi trovati nell'indice."
|
Text = $"Copiati in \"{cartellaRaw}\": {numPrt} PRT e {numAsm} sotto-assiemi trovati nell'indice."
|
||||||
+ (codiciNonTrovati.Count > 0
|
+ (codiciNonTrovati.Count > 0
|
||||||
@@ -55,8 +64,17 @@ public class PrtMancantiForm : Form
|
|||||||
grid.Rows[i].DefaultCellStyle.ForeColor = Color.DarkRed;
|
grid.Rows[i].DefaultCellStyle.ForeColor = Color.DarkRed;
|
||||||
}
|
}
|
||||||
|
|
||||||
var btnApriCartella = new Button { Text = "Apri cartella raw", Dock = DockStyle.Bottom, Height = 34 };
|
var (pannelloAzioni, flowAzioni) = UiStile.FloatBar();
|
||||||
UiStile.Secondario(btnApriCartella);
|
|
||||||
|
var btnChiudi = new Button { Text = "Chiudi" };
|
||||||
|
UiStile.Primario(btnChiudi);
|
||||||
|
UiStile.Arrotonda(btnChiudi, 10);
|
||||||
|
btnChiudi.Click += (_, _) => Close();
|
||||||
|
flowAzioni.Controls.Add(btnChiudi);
|
||||||
|
|
||||||
|
var btnApriCartella = new Button { Text = "Apri cartella raw", Margin = new Padding(8, 0, 0, 0) };
|
||||||
|
UiStile.SecondarioPieno(btnApriCartella);
|
||||||
|
UiStile.Arrotonda(btnApriCartella, 10);
|
||||||
btnApriCartella.Click += (_, _) =>
|
btnApriCartella.Click += (_, _) =>
|
||||||
{
|
{
|
||||||
try { Process.Start("explorer.exe", cartellaRaw); }
|
try { Process.Start("explorer.exe", cartellaRaw); }
|
||||||
@@ -66,9 +84,13 @@ public class PrtMancantiForm : Form
|
|||||||
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
flowAzioni.Controls.Add(btnApriCartella);
|
||||||
|
|
||||||
Controls.Add(grid);
|
Controls.Add(grid);
|
||||||
Controls.Add(lblInfo);
|
Controls.Add(lblInfo);
|
||||||
Controls.Add(btnApriCartella);
|
Controls.Add(pannelloAzioni);
|
||||||
|
CancelButton = btnChiudi;
|
||||||
|
|
||||||
|
Resize += (_, _) => lblInfo.MaximumSize = new Size(ClientSize.Width - 24, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
namespace PrepStep;
|
|
||||||
|
|
||||||
/// <summary>Mostra, per ogni assieme della distinta base, se tutti i suoi componenti
|
|
||||||
/// (ricorsivamente fino ai PRT) sono presenti nell'indice (v) o mancano (x).</summary>
|
|
||||||
public class ReportCompletezzaForm : Form
|
|
||||||
{
|
|
||||||
public ReportCompletezzaForm(List<(string Componente, string Descrizione, bool? Completo)> righe)
|
|
||||||
{
|
|
||||||
int completi = righe.Count(r => r.Completo == true);
|
|
||||||
int incompleti = righe.Count(r => r.Completo == false);
|
|
||||||
int nonTrovati = righe.Count(r => r.Completo is null);
|
|
||||||
|
|
||||||
Text = "Completezza assiemi";
|
|
||||||
ClientSize = new Size(720, 440);
|
|
||||||
StartPosition = FormStartPosition.CenterParent;
|
|
||||||
Font = new Font("Segoe UI", 9.5f);
|
|
||||||
|
|
||||||
var lblInfo = new Label
|
|
||||||
{
|
|
||||||
Dock = DockStyle.Top,
|
|
||||||
AutoSize = true,
|
|
||||||
Padding = new Padding(12, 8, 12, 8),
|
|
||||||
Text = $"{completi} completi, {incompleti} incompleti, {nonTrovati} non trovati nell'indice "
|
|
||||||
+ $"su {righe.Count} componenti. Nota: la scansione dei .asm è euristica, qualche "
|
|
||||||
+ "falso 'incompleto' è possibile.",
|
|
||||||
};
|
|
||||||
|
|
||||||
var grid = new DataGridView
|
|
||||||
{
|
|
||||||
Dock = DockStyle.Fill,
|
|
||||||
ReadOnly = true,
|
|
||||||
AllowUserToAddRows = false,
|
|
||||||
AllowUserToDeleteRows = false,
|
|
||||||
RowHeadersVisible = false,
|
|
||||||
SelectionMode = DataGridViewSelectionMode.FullRowSelect,
|
|
||||||
AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill,
|
|
||||||
};
|
|
||||||
grid.Columns.Add("componente", "Componente");
|
|
||||||
grid.Columns.Add("descrizione", "Descrizione");
|
|
||||||
grid.Columns.Add("completo", "Completo");
|
|
||||||
grid.Columns["componente"]!.FillWeight = 20;
|
|
||||||
grid.Columns["descrizione"]!.FillWeight = 55;
|
|
||||||
grid.Columns["completo"]!.FillWeight = 25;
|
|
||||||
|
|
||||||
foreach (var (componente, descrizione, completo) in righe.OrderBy(r => r.Completo).ThenBy(r => r.Componente))
|
|
||||||
{
|
|
||||||
string testo = completo switch { true => "✓", false => "✗", null => "?" };
|
|
||||||
int i = grid.Rows.Add(componente, descrizione, testo);
|
|
||||||
grid.Rows[i].DefaultCellStyle.ForeColor = completo switch
|
|
||||||
{
|
|
||||||
true => Color.DarkGreen,
|
|
||||||
false => Color.DarkRed,
|
|
||||||
null => Color.Gray,
|
|
||||||
};
|
|
||||||
grid.Rows[i].Cells["completo"].Style.Alignment = DataGridViewContentAlignment.MiddleCenter;
|
|
||||||
}
|
|
||||||
|
|
||||||
Controls.Add(grid);
|
|
||||||
Controls.Add(lblInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+111
-12
@@ -8,9 +8,16 @@ namespace PrepStep;
|
|||||||
internal static class UiStile
|
internal static class UiStile
|
||||||
{
|
{
|
||||||
/// <summary>Blu del bottone primario, esposto qui per essere riusato anche da elementi
|
/// <summary>Blu del bottone primario, esposto qui per essere riusato anche da elementi
|
||||||
/// non-bottone (es. la barra di avanzamento di ProgressForm) invece di duplicare l'RGB.</summary>
|
/// non-bottone (es. il colore di selezione delle griglie in Form1) invece di duplicare
|
||||||
|
/// l'RGB.</summary>
|
||||||
public static readonly Color ColorePrimario = Color.FromArgb(0, 99, 177);
|
public static readonly Color ColorePrimario = Color.FromArgb(0, 99, 177);
|
||||||
|
|
||||||
|
/// <summary>Sfondo/bordo delle card di stato (es. riepilogo Database/cartella di lavoro in
|
||||||
|
/// fase 1): un grigio-azzurro appena percettibile rispetto al bianco del resto della
|
||||||
|
/// pagina, sufficiente a separare "stato attuale" da "istruzioni" senza ombre/gradienti.</summary>
|
||||||
|
public static readonly Color ColoreSfondoCard = Color.FromArgb(247, 249, 251);
|
||||||
|
public static readonly Color ColoreBordoCard = Color.FromArgb(226, 231, 236);
|
||||||
|
|
||||||
// AutoSize+Padding invece di una Size fissa in pixel: il bottone si dimensiona sempre
|
// AutoSize+Padding invece di una Size fissa in pixel: il bottone si dimensiona sempre
|
||||||
// al proprio testo (a qualunque lunghezza/font), niente più testo che va a capo e viene
|
// al proprio testo (a qualunque lunghezza/font), niente più testo che va a capo e viene
|
||||||
// tagliato in verticale quando non ci sta nella larghezza indovinata a mano.
|
// tagliato in verticale quando non ci sta nella larghezza indovinata a mano.
|
||||||
@@ -47,22 +54,38 @@ internal static class UiStile
|
|||||||
b.Cursor = Cursors.Hand;
|
b.Cursor = Cursors.Hand;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Arrotonda gli angoli del bottone. Si aggancia a SizeChanged invece di calcolare
|
/// <summary>Come Secondario, ma pieno invece che a bordo: sulle card bianche delle floatbar
|
||||||
/// la Region una volta sola: con AutoSize la dimensione finale si conosce solo a layout
|
/// (vedi FloatBar) il bordo sottile di Secondario è quasi invisibile su sfondo bianco, e il
|
||||||
/// avvenuto (dopo Text/Padding), non subito dopo la creazione del controllo.</summary>
|
/// bottone risulterebbe visivamente diverso dal Primario pieno accanto — stessa costruzione
|
||||||
public static void Arrotonda(Button b, int raggio = 6)
|
/// (nessun bordo, riempimento pieno), solo grigio chiaro invece che blu.</summary>
|
||||||
|
public static void SecondarioPieno(Button b)
|
||||||
{
|
{
|
||||||
b.SizeChanged += (_, _) => AggiornaRegione(b, raggio);
|
DimensionamentoAutomatico(b);
|
||||||
AggiornaRegione(b, raggio);
|
b.FlatStyle = FlatStyle.Flat;
|
||||||
|
b.FlatAppearance.BorderSize = 0;
|
||||||
|
b.FlatAppearance.MouseOverBackColor = Color.FromArgb(230, 233, 237);
|
||||||
|
b.FlatAppearance.MouseDownBackColor = Color.FromArgb(213, 217, 222);
|
||||||
|
b.BackColor = Color.FromArgb(241, 243, 245);
|
||||||
|
b.Cursor = Cursors.Hand;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AggiornaRegione(Button b, int raggio)
|
/// <summary>Arrotonda gli angoli del controllo (bottone o card). Si aggancia a SizeChanged
|
||||||
|
/// invece di calcolare la Region una volta sola: con AutoSize la dimensione finale si
|
||||||
|
/// conosce solo a layout avvenuto (dopo Text/Padding), non subito dopo la creazione del
|
||||||
|
/// controllo.</summary>
|
||||||
|
public static void Arrotonda(Control c, int raggio = 6)
|
||||||
{
|
{
|
||||||
if (b.Width <= 0 || b.Height <= 0)
|
c.SizeChanged += (_, _) => AggiornaRegione(c, raggio);
|
||||||
|
AggiornaRegione(c, raggio);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AggiornaRegione(Control c, int raggio)
|
||||||
|
{
|
||||||
|
if (c.Width <= 0 || c.Height <= 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
int d = raggio * 2;
|
int d = raggio * 2;
|
||||||
var rect = new Rectangle(Point.Empty, b.Size);
|
var rect = new Rectangle(Point.Empty, c.Size);
|
||||||
using var percorso = new GraphicsPath();
|
using var percorso = new GraphicsPath();
|
||||||
percorso.AddArc(rect.X, rect.Y, d, d, 180, 90);
|
percorso.AddArc(rect.X, rect.Y, d, d, 180, 90);
|
||||||
percorso.AddArc(rect.Right - d, rect.Y, d, d, 270, 90);
|
percorso.AddArc(rect.Right - d, rect.Y, d, d, 270, 90);
|
||||||
@@ -70,7 +93,83 @@ internal static class UiStile
|
|||||||
percorso.AddArc(rect.X, rect.Bottom - d, d, d, 90, 90);
|
percorso.AddArc(rect.X, rect.Bottom - d, d, d, 90, 90);
|
||||||
percorso.CloseFigure();
|
percorso.CloseFigure();
|
||||||
|
|
||||||
b.Region?.Dispose();
|
c.Region?.Dispose();
|
||||||
b.Region = new Region(percorso);
|
c.Region = new Region(percorso);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Card di stato: sfondo/bordo leggermente distinti dalla pagina, angoli arrotondati,
|
||||||
|
/// padding interno. Il bordo va disegnato a mano (Paint) perché Region ritaglia il
|
||||||
|
/// controllo ma non lascia spazio a un Border WinForms nativo sugli angoli arrotondati.</summary>
|
||||||
|
public static Panel Card()
|
||||||
|
{
|
||||||
|
var card = new Panel
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Top,
|
||||||
|
AutoSize = true,
|
||||||
|
AutoSizeMode = AutoSizeMode.GrowAndShrink,
|
||||||
|
BackColor = ColoreSfondoCard,
|
||||||
|
Padding = new Padding(18, 16, 18, 16),
|
||||||
|
Margin = new Padding(0, 0, 0, 16),
|
||||||
|
};
|
||||||
|
card.Paint += (_, e) =>
|
||||||
|
{
|
||||||
|
using var penna = new Pen(ColoreBordoCard);
|
||||||
|
e.Graphics.DrawRectangle(penna, 0, 0, card.Width - 1, card.Height - 1);
|
||||||
|
};
|
||||||
|
Arrotonda(card, 8);
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Barra di bottoni "flottante" (Avanti/Indietro, Apri cartella raw/Chiudi, ecc.):
|
||||||
|
/// una card bianca staccata su tutti e 4 i lati dal bordo della finestra — non un footer a
|
||||||
|
/// tutta larghezza — con angoli arrotondati e bordo sottile, sospesa sopra lo sfondo grigio
|
||||||
|
/// della pagina invece che ancorata al bordo. Lo sfondo del contenitore esterno deve
|
||||||
|
/// combaciare con quello di default della Form (nessun BackColor impostato altrove:
|
||||||
|
/// SystemColors.Control) perché il "distacco" si veda solo nel margine attorno alla card,
|
||||||
|
/// non in una fascia di colore diverso.</summary>
|
||||||
|
public static (Panel Contenitore, FlowLayoutPanel Bottoni) FloatBar()
|
||||||
|
{
|
||||||
|
// Altezza minima per non tagliare il contenuto: bottone (MinimumSize.Height = 32) +
|
||||||
|
// padding verticale della riga bottoni (12+12) + padding verticale del contenitore
|
||||||
|
// esterno (10+16) = 82, con un po' di margine in più.
|
||||||
|
var contenitore = new Panel
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Bottom,
|
||||||
|
Height = 88,
|
||||||
|
BackColor = SystemColors.Control,
|
||||||
|
Padding = new Padding(16, 10, 16, 16),
|
||||||
|
};
|
||||||
|
|
||||||
|
var card = new Panel { Dock = DockStyle.Fill, BackColor = Color.White };
|
||||||
|
card.Paint += (_, e) =>
|
||||||
|
{
|
||||||
|
using var penna = new Pen(ColoreBordoCard);
|
||||||
|
e.Graphics.DrawRectangle(penna, 0, 0, card.Width - 1, card.Height - 1);
|
||||||
|
};
|
||||||
|
Arrotonda(card, 14);
|
||||||
|
|
||||||
|
var bottoni = new FlowLayoutPanel
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Fill,
|
||||||
|
FlowDirection = FlowDirection.RightToLeft,
|
||||||
|
Padding = new Padding(16, 12, 16, 12),
|
||||||
|
};
|
||||||
|
|
||||||
|
card.Controls.Add(bottoni);
|
||||||
|
contenitore.Controls.Add(card);
|
||||||
|
return (contenitore, bottoni);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Forza le stesse dimensioni per due bottoni della stessa barra (es. Avanti/
|
||||||
|
/// Indietro): AutoSize li dimensiona in base al proprio testo, e con testi di lunghezza
|
||||||
|
/// diversa (o solo uno dei due in grassetto, come Primario/Secondario) risulterebbero
|
||||||
|
/// disallineati sia in larghezza che in altezza — un dettaglio che un designer non
|
||||||
|
/// lascerebbe passare. Va chiamato dopo aver impostato Text/Font su entrambi.</summary>
|
||||||
|
public static void LarghezzaUguale(params Button[] bottoni)
|
||||||
|
{
|
||||||
|
int larghezza = bottoni.Max(b => b.PreferredSize.Width);
|
||||||
|
int altezza = bottoni.Max(b => b.PreferredSize.Height);
|
||||||
|
foreach (var b in bottoni)
|
||||||
|
b.MinimumSize = new Size(larghezza, altezza);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
|
||||||
```
|
|
||||||
@@ -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
|
||||||
|
```
|
||||||
+182
-39
@@ -1,10 +1,11 @@
|
|||||||
# Architettura di PrepStep (Cevolani StepPrep)
|
# Architettura di PrepStep
|
||||||
|
|
||||||
Panoramica di tutti i file sorgente e di come collaborano. `PrepStep` è il nome
|
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`; per la parallelizzazione della
|
formato binario `.asm` vedi `docs/formato-asm.md` (euristica testuale) e
|
||||||
|
`docs/formato-asm-record.md` (parser esatto dei record); per la parallelizzazione della
|
||||||
copia in `raw/` vedi `docs/copia-raw-parallela.md`.
|
copia in `raw/` vedi `docs/copia-raw-parallela.md`.
|
||||||
|
|
||||||
## Vista d'insieme
|
## Vista d'insieme
|
||||||
@@ -24,6 +25,57 @@ Distinta (.xls/.xml) → CSV (componente;descrizione) → raw/ (CAD copiati
|
|||||||
drag&drop .asm su Creo → Distributed Batch → step/ (esportazione manuale)
|
drag&drop .asm su Creo → Distributed Batch → step/ (esportazione manuale)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Il processo, dall'inizio alla fine
|
||||||
|
|
||||||
|
Le 5 fasi sono una pipeline: ognuna consuma quello che ha prodotto la precedente, e il
|
||||||
|
bottone "Avanti" si sblocca solo quando il risultato c'è (`AvantiConsentito`). Sotto, cosa
|
||||||
|
entra e cosa esce da ciascuna — il dettaglio dei metodi è nella tabella della sezione
|
||||||
|
successiva.
|
||||||
|
|
||||||
|
**1. Indice della share → `cad_index.db`.** `Indexer.ScanAsync` cammina tutte le cartelle di
|
||||||
|
primo livello sotto `X:\`, riconosce i nomi Pro-E `<codice>.<tipo>.<versione>` e fa un upsert
|
||||||
|
incrementale (confronto su percorso+dimensione+data), così i giri successivi costano solo il
|
||||||
|
delta. La **cartella di lavoro**, che da qui in poi ospita tutto il resto, non si sceglie qui
|
||||||
|
ma da "File → Scegli cartella di lavoro..." (default: cartella di installazione).
|
||||||
|
|
||||||
|
**2. Distinta del gestionale → CSV.** `DistintaBaseImporter.ConvertiInCsv` riduce l'export a
|
||||||
|
due colonne `componente;descrizione`. Se il nome del file segue la convenzione
|
||||||
|
`DBP_<codice>_<timestamp>` aggiunge l'assieme generale come riga in più: da quel momento è un
|
||||||
|
componente come tutti gli altri, nessun caso speciale a valle.
|
||||||
|
|
||||||
|
**3. Componenti del CSV → `raw/`.** Per ogni codice del CSV `TrovaPercorsiCad` cerca
|
||||||
|
nell'indice l'ultima versione (vista `files_ultima_versione`) e `CopiaFileTrovati` la copia in
|
||||||
|
`raw/` in parallelo. Finisce qui il livello "distinta": in `raw/` ci sono i componenti
|
||||||
|
elencati dal gestionale, non ancora quello che serve ad aprirli.
|
||||||
|
|
||||||
|
**4. Chiusura ricorsiva dell'albero → `raw/` completa.** È il cuore del programma, ed è
|
||||||
|
l'unica fase che deve ragionare come Creo invece che come il gestionale.
|
||||||
|
`RisolviAlberoComponenti` tiene una coda di `.asm` da leggere, partendo da quelli già in
|
||||||
|
`raw/`, e per ognuno raccoglie **tre liste diverse**, che non vanno confuse (vedi
|
||||||
|
`docs/formato-asm-record.md` §7.3 e §7.5):
|
||||||
|
|
||||||
|
| Lista | Da dove | Ricorsione | Se non è nell'indice |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Componenti della distinta | sezione `members` (`EstraiCodiciReferenziati`) | sì | va nei "non trovati" |
|
||||||
|
| Componenti **soppressi** | stringhe di piazzamento (`EstraiCodiciSoppressi`) | no — Creo non carica i figli di un soppresso | ignorato: non è un componente, segnalarlo sarebbe un falso allarme |
|
||||||
|
| **Riferimenti esterni** | tabella dei modelli citati (`EstraiCodiciRiferimentiEsterni`) | sì — Creo carica il modello citato con tutto il suo albero | ignorato, stesso motivo |
|
||||||
|
|
||||||
|
Ogni codice trovato viene copiato in `raw/` scegliendo **quale** copia con `ScegliPerCartella`
|
||||||
|
(§7.4: lo stesso codice esiste in più cartelle della share nel 13% dei casi, e vince quella
|
||||||
|
nella stessa cartella dell'assieme che lo referenzia); se è un `.asm` torna in coda e il giro
|
||||||
|
ricomincia, finché restano solo foglie. I rami raggiunti per riferimento esterno viaggiano con
|
||||||
|
un flag `Esterno` che li tiene fuori dall'elenco dei "non trovati": quell'elenco continua a
|
||||||
|
parlare solo della distinta, che è quello che l'utente si aspetta di leggerci.
|
||||||
|
|
||||||
|
Il motivo per cui questa fase è più complicata di come sembra: **la lista dei componenti e la
|
||||||
|
lista dei file necessari al caricamento non coincidono**, e la seconda è più lunga. Quando si
|
||||||
|
sbaglia, Creo Distributed Batch non dice quale file manca — dice solo `[-4] Dati di input non
|
||||||
|
completi`.
|
||||||
|
|
||||||
|
**5. Verifica e consegna a Creo.** `AggiornaAnteprimaAsm` mostra i `.asm` della distinta già
|
||||||
|
in `raw/` come icone trascinabili. L'esportazione STEP vera è manuale e fuori dall'app: si
|
||||||
|
trascinano i `.asm` su Creo Distributed Batch, che scrive in `step/`.
|
||||||
|
|
||||||
## Entry point
|
## Entry point
|
||||||
|
|
||||||
- **Program.cs** — `Main()` standard WinForms: `Application.Run(new Form1())`. Nessuna logica.
|
- **Program.cs** — `Main()` standard WinForms: `Application.Run(new Form1())`. Nessuna logica.
|
||||||
@@ -41,26 +93,42 @@ Distinta (.xls/.xml) → CSV (componente;descrizione) → raw/ (CAD copiati
|
|||||||
|
|
||||||
| # | Fase | Bottone → metodo | Cosa fa |
|
| # | Fase | Bottone → metodo | Cosa fa |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| 1 | Database | "Aggiorna indice" → `Indexer.ScanAsync`; "Cambia cartella..." → `ScegliCartellaLavoro` | Crea/aggiorna `cad_index.db` scansionando la share; sceglie/mostra la cartella di lavoro |
|
| 1 | Database | "Aggiorna indice" → `Indexer.ScanAsync` | Crea/aggiorna `cad_index.db` scansionando la share; un pallino verde/rosso accanto al titolo indica se un DB è caricato. La cartella di lavoro non è mostrata qui (cambiabile in qualunque momento da "File → Scegli cartella di lavoro...") |
|
||||||
| 2 | Importa Distinta Base | selezione file → `ImportaDistinta` → `DistintaBaseImporter.ConvertiInCsv` | Xls/Xml gestionale → CSV componente;descrizione, scritto nella cartella di lavoro |
|
| 2 | Importa Distinta Base | selezione file/drag&drop → `ImportaDistinta` → `DistintaBaseImporter.ConvertiInCsv`, poi "Verifica presenza" → `VerificaCodiciNelDb` | Xls/Xml gestionale → CSV componente;descrizione, scritto nella cartella di lavoro (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/` con `AssemblyComponentScanner`, risolve ricorsivamente sotto-assiemi/parti nell'indice, li copia anch'essi in `raw/` |
|
| 4 | Trova componenti referenziati | "Trova componenti referenziati" → `TrovaPrtReferenziati` → `RisolviAlberoComponenti` | Legge i `.asm` in `raw/`, risolve ricorsivamente sotto-assiemi/parti nell'indice e li copia anch'essi in `raw/`, insieme ai componenti soppressi e ai modelli citati per riferimento esterno che servono a Creo per aprire l'assieme |
|
||||||
| 5 | Verifica completezza | "Verifica completezza assiemi" → `VerificaCompletezzaAssiemi` | Completezza ricorsiva sui percorsi in indice. All'ingresso nella fase, `AggiornaAnteprimaAsm` mostra le icone trascinabili dei `.asm` della distinta già in `raw/`, da trascinare su Creo per l'esportazione STEP (Distributed Batch) |
|
| 5 | Verifica completezza | (ingresso in fase) → `AggiornaAnteprimaAsm` | Mostra le icone trascinabili dei `.asm` della distinta già in `raw/`, da trascinare su Creo per l'esportazione STEP (Distributed Batch) |
|
||||||
|
|
||||||
Stato condiviso tra le fasi (campi privati di `Form1`): `_dbPath`, `_cartellaLavoro`,
|
Stato condiviso tra le fasi (campi privati di `Form1`): `_dbPath`, `_cartellaLavoro`,
|
||||||
`_percorsoInputCorrente`, `_righeCorrenti` (righe del CSV), `_ultimaVerifica` (cache percorsi
|
`_percorsoInputCorrente`, `_righeCorrenti` (righe del CSV), `_ultimaVerifica` (cache percorsi
|
||||||
CAD), `_ultimaCartellaRaw`.
|
CAD), `_ultimaCartellaRaw`.
|
||||||
|
|
||||||
|
Menu (`MenuStrip`, in cima alla finestra): "File → Nuovo" (Ctrl+N) azzera lo
|
||||||
|
stato delle fasi 2-5 (`NuovaEstrazione`) e va direttamente alla fase 2, senza passare dalla
|
||||||
|
fase 1: database e cartella di lavoro restano quelli già caricati, non serve rifare lo scan;
|
||||||
|
"File → Scegli cartella di lavoro..." → `ScegliCartellaLavoro`, cambia in qualunque momento
|
||||||
|
la destinazione di CSV/`raw/`/`step/`; "File → Pulisci raw e step..." svuota le cartelle
|
||||||
|
`raw/`/`step/` nella cartella di lavoro (`PulisciRawEStep`, non tocca l'indice); "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
|
||||||
@@ -81,8 +149,7 @@ Distinta (.xls/.xml) → CSV (componente;descrizione) → raw/ (CAD copiati
|
|||||||
|
|
||||||
## Lettura dei componenti referenziati in un `.asm`
|
## Lettura dei componenti referenziati in un `.asm`
|
||||||
|
|
||||||
`Form1.EstraiCodiciReferenziati` (usata da `RisolviAlberoComponenti`, fase 4, e da
|
`Form1.EstraiCodiciReferenziati` (usata da `RisolviAlberoComponenti`, fase 4) prova prima
|
||||||
`AssiemeCompleto` dentro `VerificaCompletezzaAssiemi`, fase 5) prova prima
|
|
||||||
`AsmRecordReader.EstraiCodiciReferenziati` e ripiega su
|
`AsmRecordReader.EstraiCodiciReferenziati` e ripiega su
|
||||||
`AssemblyComponentScanner.EstraiCodiciReferenziati` solo se il primo lancia
|
`AssemblyComponentScanner.EstraiCodiciReferenziati` solo se il primo lancia
|
||||||
`InvalidDataException` (file che non rispetta il formato validato dal reverse engineering).
|
`InvalidDataException` (file che non rispetta il formato validato dal reverse engineering).
|
||||||
@@ -90,8 +157,12 @@ Distinta (.xls/.xml) → CSV (componente;descrizione) → raw/ (CAD copiati
|
|||||||
- **AsmRecordReader.cs** — legge esattamente la sezione `members` del binario `.asm` (id/tipo/
|
- **AsmRecordReader.cs** — legge esattamente la sezione `members` del binario `.asm` (id/tipo/
|
||||||
nome dei componenti diretti, con risoluzione del nome istanza di family table dal parametro
|
nome dei componenti diretti, con risoluzione del nome istanza di family table dal parametro
|
||||||
`ConfigName`): nessuna euristica, struttura a record. Lancia `InvalidDataException` se il
|
`ConfigName`): nessuna euristica, struttura a record. Lancia `InvalidDataException` se il
|
||||||
file non ha una sezione `members` o se il conteggio dichiarato non torna. Dettagli in
|
file non ha una sezione `members` o se il conteggio dichiarato non torna. Espone anche le
|
||||||
`docs/formato-asm-record.md`. Condivisa con `scripts/CreoBom.cs` via `#:project`.
|
due liste "non di distinta": `EstraiCodiciSoppressi` (che passa da `members` per sapere
|
||||||
|
quali codici sono già noti, quindi può lanciare la stessa eccezione — `Form1.CodiciSoppressi`
|
||||||
|
la intercetta e restituisce vuoto) e `EstraiCodiciRiferimentiEsterni` (scansione a sé, non
|
||||||
|
lancia). Dettagli in `docs/formato-asm-record.md`. Condivisa con `scripts/CreoBom.cs` via
|
||||||
|
`#:project`.
|
||||||
- **AssemblyComponentScanner.cs** — estrae euristicamente i codici componente referenziati
|
- **AssemblyComponentScanner.cs** — estrae euristicamente i codici componente referenziati
|
||||||
contando le occorrenze di stringhe candidate nel testo binario. Due strategie: codici
|
contando le occorrenze di stringhe candidate nel testo binario. Due strategie: codici
|
||||||
numerici Pro-E riconosciuti da soli via regex (come nei nomi file), e nomi alfabetici di
|
numerici Pro-E riconosciuti da soli via regex (come nei nomi file), e nomi alfabetici di
|
||||||
@@ -100,6 +171,29 @@ Distinta (.xls/.xml) → CSV (componente;descrizione) → raw/ (CAD copiati
|
|||||||
positivi noti in `docs/formato-asm.md`. Resta il fallback per i file che il parser esatto
|
positivi noti in `docs/formato-asm.md`. Resta il fallback per i file che il parser esatto
|
||||||
non copre, e la fonte di verità per `ExtractCreoStrings.cs`/`BomViaDb.cs`.
|
non copre, e la fonte di verità per `ExtractCreoStrings.cs`/`BomViaDb.cs`.
|
||||||
|
|
||||||
|
Quella lista è **la distinta**, e non coincide con i file che servono a Creo per aprire
|
||||||
|
l'assieme: ne restano fuori due categorie, che il loro file serve comunque, altrimenti il
|
||||||
|
caricamento fallisce (`docs/formato-asm-record.md` §7.3). `AsmRecordReader` le restituisce a
|
||||||
|
parte, e la fase 4 le copia in `raw/` senza contarle fra i "non trovati":
|
||||||
|
|
||||||
|
- `EstraiCodiciSoppressi` — i componenti **soppressi**: spariti da `members`, ancora citati
|
||||||
|
dalla feature che li posiziona. Non si ricorre dentro (Creo non carica i figli di un
|
||||||
|
componente soppresso).
|
||||||
|
- `EstraiCodiciRiferimentiEsterni` — i modelli legati da **riferimento esterno** (assembly
|
||||||
|
feature, datum copiati), elencati in una tabella a parte del binario (§7.5). Qui si ricorre
|
||||||
|
dentro, perché Creo carica il modello citato con tutto il suo albero.
|
||||||
|
|
||||||
|
## Quale copia di un componente si copia in `raw/`
|
||||||
|
|
||||||
|
Lo stesso codice esiste spesso in più cartelle della share, con contenuto diverso e pari
|
||||||
|
versione (13% dei codici, vedi `docs/formato-asm-record.md` §7.4). Il `.asm` non registra
|
||||||
|
quale copia usa, quindi `RisolviAlberoComponenti` replica la regola di Creo:
|
||||||
|
`Form1.ScegliPerCartella` preferisce la copia nella **stessa cartella dell'assieme che la
|
||||||
|
referenzia** (`Form1.CartellaDiOrigine` ricostruisce quella cartella per gli assiemi di
|
||||||
|
partenza), e solo in mancanza ripiega su versione più alta e data più recente. Sbagliare copia
|
||||||
|
non dà un errore comprensibile: Creo non apre l'assieme e dice solo "dati di input non
|
||||||
|
completi".
|
||||||
|
|
||||||
## Import distinta base
|
## Import distinta base
|
||||||
|
|
||||||
- **DistintaBaseImporter.cs** — converte l'export del gestionale (SpreadsheetML XML con
|
- **DistintaBaseImporter.cs** — converte l'export del gestionale (SpreadsheetML XML con
|
||||||
@@ -107,8 +201,8 @@ Distinta (.xls/.xml) → CSV (componente;descrizione) → raw/ (CAD copiati
|
|||||||
file) nelle sole colonne Componente/Descrizione, foglio `"Distinta Base"`. Se il nome del
|
file) nelle sole colonne Componente/Descrizione, foglio `"Distinta Base"`. Se il nome del
|
||||||
file segue la convenzione `DBP_<codice>_<timestamp>`, aggiunge l'assieme generale come riga
|
file segue la convenzione `DBP_<codice>_<timestamp>`, aggiunge l'assieme generale come riga
|
||||||
in più (il foglio elenca solo i componenti sotto l'assieme). Scrive il CSV nella cartella
|
in più (il foglio elenca solo i componenti sotto l'assieme). Scrive il CSV nella cartella
|
||||||
passata da `Form1` (la cartella di lavoro scelta in fase 1), non necessariamente accanto al
|
passata da `Form1` (la cartella di lavoro, impostabile da "File → Scegli cartella di lavoro..."),
|
||||||
file di input.
|
non necessariamente accanto al file di input.
|
||||||
|
|
||||||
## Finestre di dialogo (modali, mostrate a fine step)
|
## Finestre di dialogo (modali, mostrate a fine step)
|
||||||
|
|
||||||
@@ -125,9 +219,22 @@ Distinta (.xls/.xml) → CSV (componente;descrizione) → raw/ (CAD copiati
|
|||||||
per i secondari, blu pieno per l'azione principale di ogni fase), entrambi con
|
per i secondari, blu pieno per l'azione principale di ogni fase), entrambi con
|
||||||
`AutoSize`+`Padding`+`MinimumSize` invece di una `Size` fissa in pixel indovinata a
|
`AutoSize`+`Padding`+`MinimumSize` invece di una `Size` fissa in pixel indovinata a
|
||||||
mano (altrimenti il testo va a capo e si taglia in verticale se non ci sta nella
|
mano (altrimenti il testo va a capo e si taglia in verticale se non ci sta nella
|
||||||
larghezza scelta). `Arrotonda` applica angoli arrotondati via `Region` calcolata su
|
larghezza scelta). `SecondarioPieno` è la variante senza bordo (sfondo grigio chiaro
|
||||||
`SizeChanged` (non una volta sola: con `AutoSize` la dimensione finale del bottone si
|
pieno) per i bottoni secondari sopra le card bianche di `Card`/`FloatBar`, dove il
|
||||||
conosce solo a layout avvenuto), usata sui bottoni "Avanti"/"Indietro" del wizard.
|
bordo sottile di `Secondario` è troppo poco visibile su sfondo bianco. `Arrotonda`
|
||||||
|
applica angoli arrotondati a un `Control` qualsiasi (bottone o card) via `Region`
|
||||||
|
calcolata su `SizeChanged`, non una volta sola: con `AutoSize` la dimensione finale si
|
||||||
|
conosce solo a layout avvenuto — va chiamata dopo aver fissato le dimensioni finali del
|
||||||
|
controllo (es. dopo `LarghezzaUguale`), altrimenti la `Region` vecchia resta visibile
|
||||||
|
come bordo residuo finché non arriva un repaint completo. `Card` costruisce una card di
|
||||||
|
stato (sfondo/bordo distinti, angoli arrotondati), usata in fase 1 per il riepilogo
|
||||||
|
Database. `FloatBar` costruisce la barra di navigazione "flottante" in fondo alle
|
||||||
|
finestre (card bianca staccata su tutti e 4 i lati, non un footer a tutta larghezza):
|
||||||
|
usata per Avanti/Indietro nel wizard e per Chiudi/Apri cartella raw in
|
||||||
|
`PrtMancantiForm`. `LarghezzaUguale` forza la stessa larghezza e altezza tra due
|
||||||
|
bottoni della stessa barra (es. Avanti/Indietro, che altrimenti avrebbero dimensioni
|
||||||
|
diverse: testi di lunghezza diversa, e il font in grassetto di `Primario` richiede più
|
||||||
|
altezza di quello normale di `Secondario`/`SecondarioPieno`).
|
||||||
- **app.ico** — icona dell'app, referenziata da `<ApplicationIcon>` nel csproj (icona
|
- **app.ico** — icona dell'app, referenziata da `<ApplicationIcon>` nel csproj (icona
|
||||||
dell'exe in Esplora risorse/taskbar) e riletta esplicitamente in `Form1.BuildUi()`
|
dell'exe in Esplora risorse/taskbar) e riletta esplicitamente in `Form1.BuildUi()`
|
||||||
con `Icon.ExtractAssociatedIcon(Application.ExecutablePath)` per la titlebar, senza
|
con `Icon.ExtractAssociatedIcon(Application.ExecutablePath)` per la titlebar, senza
|
||||||
@@ -137,39 +244,70 @@ Distinta (.xls/.xml) → CSV (componente;descrizione) → raw/ (CAD copiati
|
|||||||
sotto-assiemi trovati e copiati in `raw/`, più i codici referenziati che non corrispondono a
|
sotto-assiemi trovati e copiati in `raw/`, più i codici referenziati che non corrispondono a
|
||||||
nessun file indicizzato (possono essere disegni, componenti davvero mancanti, o falsi
|
nessun file indicizzato (possono essere disegni, componenti davvero mancanti, o falsi
|
||||||
positivi della scansione `.asm`).
|
positivi della scansione `.asm`).
|
||||||
- **ReportCompletezzaForm.cs** — per ogni assieme della distinta, se tutti i suoi componenti
|
- **GuidaForm.cs** — guida interattiva ("Aiuto → Guida"/F1): elenco delle sezioni a sinistra
|
||||||
(ricorsivamente fino ai PRT) sono nell'indice (✓/✗/? per "non trovato").
|
(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
|
||||||
@@ -179,8 +317,13 @@ 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`,
|
come API pubblica. File: `IndexerParseFilenameTests.cs`, `IndexerScanTests.cs`,
|
||||||
`AssemblyComponentScannerTests.cs`, `DistintaBaseImporterTests.cs`,
|
`AssemblyComponentScannerTests.cs`, `AsmRecordReaderTests.cs`,
|
||||||
`CaseInsensitiveLookupIntegrationTests.cs`.
|
`DistintaBaseImporterTests.cs`, `CaseInsensitiveLookupIntegrationTests.cs`,
|
||||||
|
`PipelineEndToEndTests.cs`.
|
||||||
|
|
||||||
|
`AsmRecordReaderTests.cs` costruisce i `.asm` byte per byte con dei piccoli helper
|
||||||
|
(`Membro`, `ConfigName`, `ObjName`, `Riferimenti`): ogni caso strano trovato sulla share
|
||||||
|
diventa un test minimale invece di un file binario da versionare.
|
||||||
+42
-26
@@ -29,23 +29,42 @@ componenti e nel filtro per cartella).
|
|||||||
|
|
||||||
Vista `files_ultima_versione`: stessa struttura di `files`, filtrata alla sola riga con
|
Vista `files_ultima_versione`: stessa struttura di `files`, filtrata alla sola riga con
|
||||||
`versione` più alta per ogni `(cartella_radice, codice, tipo_file)`, tramite `ROW_NUMBER()
|
`versione` più alta per ogni `(cartella_radice, codice, tipo_file)`, tramite `ROW_NUMBER()
|
||||||
OVER (PARTITION BY ... ORDER BY versione DESC)`. Tutte le ricerche di componenti (`Form1.
|
OVER (PARTITION BY ... ORDER BY versione DESC, data_modifica DESC, percorso_completo)`. Le
|
||||||
TrovaPercorsiCad`, `Form1.CaricaCodiciNoti`) interrogano questa vista, non `files`
|
ricerche per codice (`Form1.TrovaPercorsiCad` in fase 3, `Form1.CaricaCodiciNoti`)
|
||||||
direttamente, così un componente con più versioni sulla share risolve sempre all'ultima.
|
interrogano questa vista, non `files` direttamente, così un componente con più versioni sulla
|
||||||
Deliberatamente una `ROW_NUMBER()` invece di un self-join con `GROUP BY`: su ~200k righe un
|
share risolve sempre all'ultima. Fa eccezione la fase 4 (`Form1.CaricaCandidati`), che legge
|
||||||
self-join degenera in tempi quasi quadratici, `ROW_NUMBER()` resta lineare.
|
`files` per avere tutte le copie di un codice in cartelle diverse e scegliere quella giusta
|
||||||
|
(`ScegliPerCartella`). Deliberatamente una `ROW_NUMBER()` invece di un self-join con
|
||||||
|
`GROUP BY`: su ~200k righe un self-join degenera in tempi quasi quadratici, `ROW_NUMBER()`
|
||||||
|
resta lineare.
|
||||||
|
|
||||||
|
La vista è ricreata (`DROP` + `CREATE`, non `CREATE IF NOT EXISTS`) a ogni "Aggiorna indice",
|
||||||
|
così un DB già esistente prende la definizione corrente senza dover reindicizzare la share.
|
||||||
|
|
||||||
Nota: `cartella_radice` fa parte della partition, quindi se lo stesso codice+tipo esiste
|
Nota: `cartella_radice` fa parte della partition, quindi se lo stesso codice+tipo esiste
|
||||||
in **due cartelle radice diverse** (duplicati sulla share), la vista restituisce una riga per
|
in **due cartelle radice diverse** (duplicati sulla share), la vista restituisce una riga per
|
||||||
ciascuna — non una sola in assoluto. `TrovaPercorsiCad` gestisce questo caso deduplicando per
|
ciascuna — non una sola in assoluto. `TrovaPercorsiCad` gestisce questo caso deduplicando per
|
||||||
`tipo_file` lato C# (vedi sotto).
|
`tipo_file` lato C# (vedi sotto).
|
||||||
|
|
||||||
|
> **I criteri di pareggio non sono decorativi.** Lo stesso codice può esistere **nella stessa
|
||||||
|
> cartella radice, con la stessa `versione`**, in due sottocartelle diverse (una copia in una
|
||||||
|
> cartella di studio, di lavoro o temporanea). Senza `data_modifica` nell'`ORDER BY` il
|
||||||
|
> pareggio veniva rotto in modo arbitrario, e poteva vincere la copia obsoleta — nascondendo
|
||||||
|
> del tutto quella buona, che non compariva più nella vista. Il file che finiva in `raw/` era
|
||||||
|
> allora un modello diverso da quello che l'assieme si aspetta: Creo lo carica, i vincoli di
|
||||||
|
> accoppiamento non si risolvono e **l'assieme non si apre affatto**.
|
||||||
|
>
|
||||||
|
> Il criterio deterministico però non basta a scegliere *bene*: è il minimo per non sbagliare
|
||||||
|
> a caso. Quale copia sia quella giusta dipende da chi la referenzia, non dalla data — vedi
|
||||||
|
> [`formato-asm-record.md`](formato-asm-record.md) §7.4 e `Form1.ScegliPerCartella`.
|
||||||
|
|
||||||
## Come viene popolato: `Indexer.ScanAsync`
|
## Come viene popolato: `Indexer.ScanAsync`
|
||||||
|
|
||||||
Chiamato da "Aggiorna indice" (fase 1) e riutilizza lo stesso `dbPath` già caricato.
|
Chiamato da "Aggiorna indice" (fase 1) e riutilizza lo stesso `dbPath` già caricato.
|
||||||
|
|
||||||
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
|
||||||
@@ -93,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.
|
||||||
@@ -126,10 +141,11 @@ indicizzati) prima di sospettare un bug nella ricerca.
|
|||||||
riga con quell'esatto `codice`. In presenza di duplicati su più `cartella_radice`, tiene solo
|
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.
|
||||||
@@ -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 |
|
||||||
+278
-54
@@ -8,16 +8,28 @@ 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`).
|
||||||
|
|
||||||
**Stato della validazione.** Durante il reverse engineering, il livello 1 estratto da 3
|
**Stato della validazione.** Due livelli di prova, da non confondere:
|
||||||
assiemi con BOM Creo di riferimento coincideva **esattamente**: stesse voci, stesso nome
|
|
||||||
istanza, stesse quantità, stesso ordine. Altri 5 `.asm`, mai usati per ricavare le regole,
|
1. **Byte-per-byte contro BOM Creo di riferimento** (la prova forte): 3 assiemi del
|
||||||
parsavano con il conteggio dichiarato uguale a quello trovato. Tutto ciò che segue oltre
|
reverse engineering iniziale, più 5 casi reali aggiunti in seguito, ciascuno con la sua BOM
|
||||||
questo perimetro (§7) è segnalato esplicitamente come ipotesi non dimostrata.
|
esportata da Creo messa a confronto riga per riga con l'output di `CreoBom.cs` —
|
||||||
|
`9284218024b.asm` (albero profondo, multi-livello), `2546220101.asm`/`2546220101_c04.asm.1`
|
||||||
|
(istanza di family table isolata), `2546220101.asm` risalvato con un componente soppresso,
|
||||||
|
`9244218080_c01.asm` (sottoassieme montato due volte), `9284208092_D110.asm` (revisione del
|
||||||
|
figlio). Ognuno di questi è citato per nome nella sezione del documento a cui si riferisce,
|
||||||
|
così resta verificabile quale affermazione poggia su quale file.
|
||||||
|
2. **Su tutti i 350 `.asm` reali di `raw/`**, solo conteggi interni e coerenza con l'euristica
|
||||||
|
testuale (`AssemblyComponentScanner`), **non** contro BOM di riferimento: usato per
|
||||||
|
calibrare le soglie dei controlli di sicurezza (§7) su scala, non per dimostrare la
|
||||||
|
correttezza dei valori estratti file per file.
|
||||||
|
|
||||||
|
Un'affermazione verificata solo al livello 2 (conteggi su scala) è più debole di una
|
||||||
|
verificata al livello 1 (BOM reale) - il testo lo distingue sempre esplicitamente.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -79,7 +91,7 @@ conteggio dei membri: con la lettura a 1 byte fisso i campioni grandi si
|
|||||||
disallineavano. Non è confermata da una specifica; regge sui campioni
|
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).
|
||||||
|
|
||||||
@@ -211,6 +223,30 @@ ampio degli 8 campioni originali (`9284218024b.asm`, confrontato con la sua BOM
|
|||||||
riferimento) — non era presente in nessuno degli 8 campioni usati per il reverse engineering
|
riferimento) — non era presente in nessuno degli 8 campioni usati per il reverse engineering
|
||||||
iniziale.
|
iniziale.
|
||||||
|
|
||||||
|
### 4.0.2 Istanza risolta direttamente in `members`, senza passare da `ConfigName`
|
||||||
|
|
||||||
|
Non tutti i membri che sono istanze di family table hanno un record `ConfigName` associato.
|
||||||
|
Per alcune parti commerciali/normalizzate non-UNI, il campo `name` (e allo stesso modo
|
||||||
|
`family_name`) del record `members` (§3) porta già per intero la notazione
|
||||||
|
`<nome istanza><generico>` invece del solo nome generico:
|
||||||
|
|
||||||
|
```
|
||||||
|
\xe3 <id> <type> "<istanza><generico>"\0 "<istanza><generico>"\0
|
||||||
|
```
|
||||||
|
|
||||||
|
È la stessa notazione istanza→generico della §4.1 sotto, ma qui compare **dentro** il record
|
||||||
|
`members` stesso, come valore diretto del componente - non nell'elenco di riferimenti
|
||||||
|
geometrici di cui parla la §4.1 (quello sì da ignorare per la BOM). Il generico fra `<` e `>`
|
||||||
|
è il file realmente indicizzato sul disco; il nome prima di `<` è quello che la BOM
|
||||||
|
mostrerebbe.
|
||||||
|
|
||||||
|
`AsmRecordReader.LeggiMembri` lo riconosce per i membri **senza** un `ConfigName`
|
||||||
|
corrispondente il cui campo `name` rispetta per intero il pattern `<testo><testo>`: il testo
|
||||||
|
dopo l'ultima `<` (fino alla `>` finale) diventa il generico da cercare nell'indice, quello
|
||||||
|
prima diventa il nome mostrato. Senza questo split, il codice cercato nell'indice sarebbe la
|
||||||
|
stringa intera con le parentesi comprese, mai presente nell'indice - il componente
|
||||||
|
risulterebbe erroneamente "non trovato".
|
||||||
|
|
||||||
### 4.1 Una seconda pista, che NON va usata per la BOM
|
### 4.1 Una seconda pista, che NON va usata per la BOM
|
||||||
|
|
||||||
Altrove nel file compaiono stringhe nella forma `istanza<generico>`:
|
Altrove nel file compaiono stringhe nella forma `istanza<generico>`:
|
||||||
@@ -271,9 +307,12 @@ subito dopo, ricorsivamente, quelli dei suoi sottoassiemi, prima di passare al
|
|||||||
fratello successivo. Ogni modello compare **una volta sola** anche se montato
|
fratello successivo. Ogni modello compare **una volta sola** anche se montato
|
||||||
più volte (deduplica per nome, `CreoAsm.Blocchi`).
|
più volte (deduplica per nome, `CreoAsm.Blocchi`).
|
||||||
|
|
||||||
> La deduplica è un'**assunzione**: nessuno dei campioni disponibili durante il
|
> **Verificato.** `9244218080_C01.asm` monta `2605410010_C01` due volte
|
||||||
> reverse engineering montava lo stesso sottoassieme due volte, quindi non è
|
> (`scripts/9244218080_c01.asm.1` + `scripts/9244218080_c01.bom.2` di riferimento): la BOM
|
||||||
> dimostrata su un caso reale.
|
> Creo mostra un solo blocco `Sottoassieme 2605410010_C01 contiene:` con quantità 2 in testa,
|
||||||
|
> non due blocchi separati, e `CreoBom.cs` riproduce l'output esatto senza modifiche. Confermata
|
||||||
|
> anche la moltiplicazione delle quantità nel riepilogo (§6.3): le parti di `2605410010_C01`
|
||||||
|
> compaiono raddoppiate (1→2, 1→2, 16→32).
|
||||||
|
|
||||||
### 6.3 Riepilogo finale
|
### 6.3 Riepilogo finale
|
||||||
|
|
||||||
@@ -291,13 +330,17 @@ dall'esterno (`-p`/`--path`). `CreoAsm.CreaFinder` indicizza `<nome>.asm.<rev>`
|
|||||||
senza distinzione di maiuscole e, a parità di nome, prende la **revisione più
|
senza distinzione di maiuscole e, a parità di nome, prende la **revisione più
|
||||||
alta** presente.
|
alta** presente.
|
||||||
|
|
||||||
> Scelta discutibile: la revisione più alta in cartella non è
|
> **Verificato: è la scelta giusta.** Il `.asm` padre non registra la revisione attesa del
|
||||||
> necessariamente quella con cui l'assieme padre è stato salvato. Il `.asm`
|
> figlio (cercata e non trovata, vedi §7), ma non serve: confermato che **Creo stesso apre
|
||||||
> padre non sembra registrare la revisione attesa del figlio, o non è stata
|
> sempre la revisione più alta disponibile** di un componente, indipendentemente da quale
|
||||||
> trovata. Vedi `formato-asm.md` §"Risoluzione ricorsiva dell'albero" per le
|
> fosse presente quando l'assieme padre fu salvato l'ultima volta. Caso reale controllato:
|
||||||
> trappole già incontrate storicamente su questo punto (con l'euristica
|
> `9284208092_D110.asm` (salvato 2018-08-01) referenzia `9284202481.prt`, che sulla share ha
|
||||||
> testuale, ma il problema della revisione attesa è indipendente dal metodo
|
> revisioni sia del 2014 (v1-v3, presenti all'epoca) sia del 2024 (v4-v6, arrivate anni dopo) -
|
||||||
> di estrazione).
|
> Creo carica v6, non v3. `CreaFinder` che prende sempre la revisione più alta in cartella
|
||||||
|
> replica esattamente questo comportamento, non è un'assunzione azzardata. Vedi `formato-
|
||||||
|
> asm.md` §"Risoluzione ricorsiva dell'albero" per le trappole storiche su questo punto (con
|
||||||
|
> l'euristica testuale - restano valide, sono su un problema diverso: trovare la cartella
|
||||||
|
> giusta, non quale revisione preferire).
|
||||||
|
|
||||||
Se un sottoassieme non si trova, il blocco viene emesso con
|
Se un sottoassieme non si trova, il blocco viene emesso con
|
||||||
`*** NON RISOLTO: file .asm non trovato ***` e le sue parti restano fuori dal
|
`*** NON RISOLTO: file .asm non trovato ***` e le sue parti restano fuori dal
|
||||||
@@ -306,44 +349,225 @@ sbagliato.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Cosa resta sconosciuto
|
## 7. Stato di ogni punto aperto
|
||||||
|
|
||||||
Elencato esplicitamente per non far passare per dimostrato ciò che non lo è:
|
Tabella di riferimento: cosa è provato, come, e cosa resta genuinamente ignoto. Il dettaglio
|
||||||
|
di ciascun punto segue sotto, in due gruppi.
|
||||||
|
|
||||||
- **⚠️ Family table di ASSIEMI (non solo di parti), risultato silenziosamente sbagliato.**
|
| Punto | Stato | Prova |
|
||||||
Non solo le parti normalizzate (§4) possono essere family table: un intero **assieme** può
|
|---|---|---|
|
||||||
esserlo. Osservato su `2546220101.asm` (dentro l'albero di `9284218024b.asm`): il file
|
| Deduplica blocchi + moltiplicazione quantità (§6.2/§6.3) | Risolto | BOM reale (§7.1) |
|
||||||
generico ha una propria sezione `members` che elenca il contenuto di UNA rappresentazione
|
| Revisione del figlio che il padre si aspetta (§6.4) | Risolto | BOM reale (§7.1) |
|
||||||
(nell'esempio, 3 cuscinetti + 1 sotto-assieme), ma la BOM Creo per lo stesso file mostra 4
|
| Stato soppresso/escluso in `members` | Risolto | BOM reale (§7.1) |
|
||||||
**istanze diverse** (`2546220101_C01`.."_C04", tre "Parte" e una "Sottoassieme" - tipi
|
| Family table di **assiemi** (rilevamento) | Risolto: serve la conferma del nome proprio (`objname`), il solo `model_name` dava falsi positivi | BOM reali + albero `raw/` completo (§7.1) |
|
||||||
diversi fra loro!), ciascuna con un proprio contenuto potenzialmente diverso. Il nome
|
| Family table di **assiemi** (contenuto di ogni istanza) | Non risolto, nessun impatto sulla BOM (fallback già corretto) | Scan sull'intera share (§7.2) |
|
||||||
istanza qui **non** passa da un record `ConfigName` (verificato: nessuna occorrenza
|
| Costante `\x82\x7b` in `ConfigName` | Significato ignoto, rischio coperto | Scala 350 file (§7.2) |
|
||||||
letterale di `ConfigName` nel file) ma da un meccanismo diverso, imparentato con la
|
| Occorrenze multiple di `members` (simplified rep) | Mai osservato, rischio coperto | Scala 350 file (§7.2) |
|
||||||
"seconda pista" del §4.1 (`\xe3<id>\xe2\x02<stringa>\0`, senza il bracket
|
| Matrice di posizionamento (`e1`/`e2`/`e3`/`origin`) | Non letto, nessun impatto sulla BOM | — (§7.2) |
|
||||||
`<generico>` in questo caso) - non decodificato. **`AsmRecordReader.LeggiMembri` non lo
|
| Campi numerici dell'header dopo `ASSEMBLY` | Non letto, nessun impatto sulla BOM | — (§7.2) |
|
||||||
rileva e non lancia eccezione**: restituisce silenziosamente il contenuto della
|
| Divergenza `family_name` ≠ `name` | Mai osservata, non letta | — (§7.2) |
|
||||||
rappresentazione di default sotto un solo nome, invece delle 4 istanze corrette - a
|
|
||||||
differenza degli altri limiti di questa sezione, qui il fallimento non è segnalato in
|
"Rischio coperto" vuol dire: il codice non sa cosa significhi il byte, ma verifica un
|
||||||
nessun modo al chiamante (né un conteggio che non torna, né un'eccezione). Scoperto sullo
|
invariante che lo renderebbe visibile (eccezione) se mai smettesse di valere - non è una prova
|
||||||
stesso test end-to-end del punto `#AF` sopra, mai osservato negli 8 campioni originali.
|
che il byte non conti mai, è la garanzia che se contasse il programma lo segnalerebbe invece
|
||||||
**Prima di fidarsi del parser esatto su un albero con assiemi a family table, verificare a
|
di sbagliare in silenzio.
|
||||||
mano contro la BOM Creo.**
|
|
||||||
- **Stato soppresso / escluso.** Nessuna evidenza cercata o trovata. Non si sa
|
### 7.1 Risolti con una BOM Creo di riferimento
|
||||||
se un componente soppresso compaia in `members`, né con quale marcatore. Sui
|
|
||||||
campioni disponibili il totale coincideva con la BOM, il che *suggerisce* che
|
- **Deduplica blocchi e moltiplicazione quantità.** `9244218080_C01.asm` monta
|
||||||
non ce ne fossero, non che siano gestiti.
|
`2605410010_C01` due volte (`scripts/9244218080_c01.asm.1` +
|
||||||
- **La matrice di posizionamento** (`e1`, `e2`, `e3`, `origin`): formato
|
`scripts/9244218080_c01.bom.2`): la BOM Creo mostra un solo blocco
|
||||||
numerico non decodificato.
|
`Sottoassieme 2605410010_C01 contiene:` con quantità 2 in testa (non due blocchi separati),
|
||||||
- **`family_name`**: sempre uguale a `name` sui campioni; non si sa in quale
|
e le sue parti compaiono raddoppiate nel riepilogo (1→2, 1→2, 16→32). `CreoBom.cs` riproduce
|
||||||
situazione divergerebbe.
|
l'output esatto senza modifiche: la deduplica per nome (`CreoAsm.Blocchi`) e la
|
||||||
- **La costante `\x82\x7b`** nei record `ConfigName`.
|
moltiplicazione lungo l'albero (`CreoAsm.Sommario`) sono il comportamento reale di Creo, non
|
||||||
- **I campi numerici dell'header** dopo `ASSEMBLY`.
|
solo un'assunzione.
|
||||||
- **Occorrenze multiple di `members`.** `AsmRecordReader.LeggiMembri` prende la prima.
|
- **Revisione del figlio.** `9284208092_D110.asm` (salvato 2018-08-01) referenzia
|
||||||
Sui campioni disponibili funziona, ma non è stato verificato se le
|
`9284202481.prt`, che sulla share ha revisioni sia del 2014 (v1-v3, presenti quando il padre
|
||||||
rappresentazioni semplificate (simplified rep) generino sezioni `members`
|
fu salvato) sia del 2024 (v4-v6, arrivate anni dopo). Confermato direttamente in Creo: apre
|
||||||
aggiuntive.
|
v6, non v3 - **sempre la revisione più alta disponibile**, indipendentemente da quale fosse
|
||||||
- **La deduplica dei blocchi** e **la revisione attesa dei figli** (§6.2, §6.4).
|
presente all'ultimo salvataggio del padre. `CreoAsm.CreaFinder`, che prende sempre la
|
||||||
- **Quale revisione del figlio** il padre si aspetta: non trovata nel `.asm`.
|
revisione più alta trovata in cartella, replica esattamente questo comportamento.
|
||||||
|
- **Stato soppresso/escluso.** Stesso assieme `2546220101.asm` risalvato da Creo con
|
||||||
|
`2546220101_C02` soppresso (`scripts/2546220101.asm.1` + `scripts/2546220101.bom.1`, con le
|
||||||
|
parti/sottoassiemi correlati). Un componente soppresso **non produce alcun record** in
|
||||||
|
`members`: sparisce e basta, il conteggio dichiarato scende di conseguenza (`\xf8 03` invece
|
||||||
|
di `\xf8 04`). Nessun marcatore di stato da gestire: `CreoBom.cs` sul file risalvato
|
||||||
|
riproduce esattamente la BOM di riferimento senza modifiche al parser.
|
||||||
|
|
||||||
|
> **Sparisce da `members`, non dal file.** La feature che lo posiziona resta, e con essa la
|
||||||
|
> stringa `Componente ID <n> (<codice>.PRT)` (localizzata come in
|
||||||
|
> `AssemblyComponentScanner.HaPlacement`). Per la BOM va ignorata - Creo non elenca i
|
||||||
|
> soppressi - ma **il file di quel componente serve comunque per aprire l'assieme**: se manca,
|
||||||
|
> Creo fallisce il caricamento con "dati di input non completi" (`[-4]`, cioè
|
||||||
|
> `PRO_TK_E_NOT_FOUND`). Verificato su un caso reale: un assieme che non si apriva dalla
|
||||||
|
> cartella `raw/`, e che si è aperto senza errori copiandoci dentro quel solo file.
|
||||||
|
> `AsmRecordReader.EstraiCodiciSoppressi` li restituisce a parte, apposta per questo - vedi
|
||||||
|
> §7.3.
|
||||||
|
- **Family table di assiemi — rilevamento.** Non solo le parti normalizzate (§4) possono
|
||||||
|
essere family table: un intero **assieme** può esserlo. Osservato su `2546220101.asm`
|
||||||
|
(dentro l'albero di `9284218024b.asm`): il file generico ha una propria sezione `members`
|
||||||
|
che elenca il contenuto di UNA rappresentazione (3 cuscinetti + 1 sotto-assieme, dati
|
||||||
|
arbitrari), ma la BOM Creo per lo stesso file mostra 4 **istanze diverse**
|
||||||
|
(`2546220101_C01`.."_C04", tre "Parte" e una "Sottoassieme" - tipi diversi fra loro),
|
||||||
|
ciascuna con un contenuto diverso. Il nome istanza qui **non** passa da `ConfigName`
|
||||||
|
(verificato: nessuna occorrenza letterale nel file), ma da una coppia di record distinta
|
||||||
|
subito dopo l'header del modello:
|
||||||
|
|
||||||
|
```
|
||||||
|
\xe0\x01model_t_type\0<int>\xe0\x0amodel_name\0<codice>_C0n\0\xe0\x0ageneric_name\0\0
|
||||||
|
...stamp\0\xe2\xf2\xf7<idSchema>
|
||||||
|
(\xe3<type>\x02<codice>_C0n\0\0\xe2)* -- un'occorrenza per ogni istanza sorella
|
||||||
|
\xf4
|
||||||
|
```
|
||||||
|
|
||||||
|
Quando questo succede il file generico è già di per sé un'istanza (`model_name` porta il
|
||||||
|
suffisso `_C0n`, non è neutro) e `stamp` elenca le sorelle.
|
||||||
|
|
||||||
|
> **Il record `model_name` da solo non è un'identità.** Su file reali quel record nomina
|
||||||
|
> quasi sempre **un altro modello** - tipicamente uno dei membri del file stesso, a volte un
|
||||||
|
> codice imparentato - e in quei casi `members` è già corretto e va usato. Serve una
|
||||||
|
> conferma indipendente: il **nome proprio del modello** (`objname`, §5). `LeggiMembri`
|
||||||
|
> segnala la family table solo se `objname` esiste **e** coincide con `model_name` privato
|
||||||
|
> del suffisso `_C0n`; negli altri casi legge `members` normalmente.
|
||||||
|
>
|
||||||
|
> Il controllo più largo (senza la conferma di `objname`) era la causa di
|
||||||
|
> **falsi "componenti mancanti"**: il parser esatto rifiutava file perfettamente leggibili,
|
||||||
|
> si ripiegava sull'euristica testuale e quella estraeva codici che non esistono, riportati
|
||||||
|
> poi come componenti non trovati nell'indice. Verificato contro BOM Creo di riferimento:
|
||||||
|
> sui file dove il segnale scattava senza conferma di `objname`, `members` elenca
|
||||||
|
> **esattamente** i componenti della BOM reale (anche nello stesso ordine); uno di questi
|
||||||
|
> file è stato inoltre aperto in Creo, e l'albero del modello mostra gli stessi componenti,
|
||||||
|
> senza alcuna family table. Con la conferma di `objname` richiesta, tutti i `.asm` di una
|
||||||
|
> cartella `raw/` reale (351 file, alberi completi di più macchine) si leggono col parser
|
||||||
|
> esatto, zero ricadute sull'euristica.
|
||||||
|
|
||||||
|
Da tenere presente: il sintomo dipende da **come il file è stato salvato**, non solo dal
|
||||||
|
modello. Rileggendo una revisione più recente dello stesso assieme generico, il record di
|
||||||
|
identità può non esserci più e `members` elencare direttamente tutte le istanze, col tipo
|
||||||
|
giusto per ciascuna - cioè esattamente quello che mostra la BOM. Il blocco quindi non è una
|
||||||
|
proprietà stabile del codice: va valutato file per file, revisione per revisione.
|
||||||
|
|
||||||
|
Verificato anche che il fallback su `AssemblyComponentScanner` **risolve già correttamente**
|
||||||
|
il caso vero: i nomi delle istanze compaiono come testo puro nel campo `stamp`, e
|
||||||
|
l'euristica ("codice che compare almeno due volte") li trova senza bisogno di decodificare
|
||||||
|
`stamp`. E che una singola istanza, se esportata da Creo come file a sé (Family Table →
|
||||||
|
istanza → "Salva una copia"), è un `.asm`/`.prt` normale senza traccia di family table al suo
|
||||||
|
interno - letto correttamente dal parser esatto (`2546220101_c04.asm.1`, verificato contro
|
||||||
|
`2546220101.bom.1`).
|
||||||
|
|
||||||
|
### 7.2 Non risolti
|
||||||
|
|
||||||
|
- **Family table di assiemi — contenuto di ogni istanza.** Come recuperare il contenuto
|
||||||
|
specifico di un'istanza **senza** che esista già un file separato per essa sul disco (il
|
||||||
|
caso comune, se nessuno l'ha mai esportata a parte con "Salva una copia"): il meccanismo
|
||||||
|
dietro `stamp` non è stato decodificato oltre a nome+tipo di ciascuna istanza sorella:
|
||||||
|
`stamp` porta **solo i nomi** delle sorelle, non il loro contenuto, quindi non è la
|
||||||
|
struttura da cui partire per risolvere questo punto.
|
||||||
|
|
||||||
|
Resta aperto solo per completezza: sui file dove il segnale scatta davvero (con conferma
|
||||||
|
del nome proprio, §7.1) il parser si ferma invece di restituire un contenuto sbagliato, e
|
||||||
|
il chiamante ripiega sull'euristica testuale, che i nomi delle istanze li trova comunque
|
||||||
|
come testo puro. Nessun comportamento dell'app dipende da questa decodifica.
|
||||||
|
- **Costante `\x82\x7b`** nei record `ConfigName`: significato ignoto. Rischio coperto -
|
||||||
|
`LeggiMembri` conta le occorrenze grezze di `ConfigName` e le confronta col numero di match
|
||||||
|
del regex; se un campione avesse un valore diverso, il regex non farebbe più match (prima:
|
||||||
|
in silenzio) e ora lancia eccezione invece di restituire il nome generico al posto
|
||||||
|
dell'istanza senza dirlo. Verificato su tutti i 350 `.asm` di `raw/`: 215 usano davvero
|
||||||
|
`ConfigName`, 0 mismatch.
|
||||||
|
- **Occorrenze multiple di `members`** (rappresentazioni semplificate, mai osservate). Rischio
|
||||||
|
coperto - `LeggiMembri` lancia eccezione se ne trova più di una invece di prendere
|
||||||
|
silenziosamente la prima senza sapere se sia quella giusta. Verificato su tutti i 350 `.asm`
|
||||||
|
di `raw/`: sempre esattamente una occorrenza.
|
||||||
|
- **La matrice di posizionamento** (`e1`, `e2`, `e3`, `origin`): formato numerico non
|
||||||
|
decodificato, nessun impatto sulla BOM (non letta da nessuna parte del codice).
|
||||||
|
- **I campi numerici dell'header** dopo `ASSEMBLY`: non letti, nessun impatto sulla BOM.
|
||||||
|
- **`family_name`**: sempre uguale a `name` su tutti i campioni disponibili; non si sa in
|
||||||
|
quale situazione divergerebbe, né se il codice dovrebbe fare qualcosa se succedesse (oggi
|
||||||
|
non la usa in nessun modo, legge solo `name`).
|
||||||
|
|
||||||
|
### 7.3 Due domande diverse: cosa va in distinta, cosa serve per aprire
|
||||||
|
|
||||||
|
È la lezione che costa di più riscoprire, quindi sta qui in chiaro: **la lista dei componenti
|
||||||
|
della BOM e la lista dei file che servono a Creo per aprire l'assieme non coincidono.** La
|
||||||
|
seconda è più lunga.
|
||||||
|
|
||||||
|
| Lista | Serve per | Si ottiene con |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Componenti della distinta | BOM, verifica completezza | `LeggiMembri` / `EstraiCodiciReferenziati` (sezione `members`) |
|
||||||
|
| File necessari al caricamento | popolare `raw/` per l'export STEP | i precedenti **più** `EstraiCodiciSoppressi` e `EstraiCodiciRiferimentiEsterni` (§7.5) |
|
||||||
|
|
||||||
|
La differenza sono i **componenti soppressi** (§7.1) e i **riferimenti esterni** (§7.5): fuori
|
||||||
|
dalla BOM, ma senza il loro file Creo non apre l'assieme. Chi popola `raw/` deve quindi usare
|
||||||
|
l'unione; chi genera la BOM o verifica la completezza deve usare **solo** i membri, altrimenti
|
||||||
|
quelle voci diventano falsi "componenti mancanti".
|
||||||
|
|
||||||
|
Il sintomo, se si sbaglia, non assomiglia per niente alla causa: Creo Distributed Batch
|
||||||
|
fallisce con `Carica modello in Creo Pro | Non riuscito | [-4] Errore: Dati di input non
|
||||||
|
completi` - un messaggio che non nomina il file mancante. Il modo più rapido per trovarlo è
|
||||||
|
aprire l'assieme in Creo **dalla cartella `raw/`** (non dalla share, dove i file ci sono tutti
|
||||||
|
e quindi si apre senza errori) e cercare nell'albero la voce con l'icona rossa.
|
||||||
|
|
||||||
|
### 7.4 Lo stesso codice esiste in più cartelle della share
|
||||||
|
|
||||||
|
Sulla share **13% dei codici** (5.193 su 39.274 fra prt e asm) ha la versione più alta in più
|
||||||
|
cartelle, con contenuto diverso. Non sono revisioni: sono copie parallele, lasciate da studi
|
||||||
|
preliminari, allestimenti fiera, cartelle di lavoro datate, raccolte temporanee.
|
||||||
|
|
||||||
|
Il `.asm` **non dice quale copia usa**: verificato che non registra né il percorso, né
|
||||||
|
l'`UOBJ_ID` del figlio (che pure distingue le copie), né la revisione attesa. Cita solo il
|
||||||
|
nome del modello. Quale copia venga caricata lo decide Creo al momento dell'apertura, e la
|
||||||
|
regola pratica è: **prima la cartella del modello che la referenzia**.
|
||||||
|
|
||||||
|
Non esiste quindi un criterio globale corretto - e i numeri lo confermano: nel 79% dei
|
||||||
|
conflitti nessuna delle copie sta in una cartella "ufficiale" (`…\M_B`, `…\A`), perché il
|
||||||
|
conflitto è fra due istantanee parallele, ognuna autoconsistente con i propri componenti. Per
|
||||||
|
data, per versione e per posizione sono equivalenti: solo il contesto di chi referenzia
|
||||||
|
discrimina.
|
||||||
|
|
||||||
|
Conseguenze per il codice:
|
||||||
|
|
||||||
|
- `Form1.ScegliPerCartella` risolve ogni componente preferendo la copia nella **stessa cartella
|
||||||
|
dell'assieme che lo referenzia**, e solo in mancanza ripiega su versione più alta e data più
|
||||||
|
recente. `Form1.CartellaDiOrigine` ricostruisce quella cartella per gli assiemi di partenza,
|
||||||
|
che arrivano dalla fase precedente senza portarsela dietro.
|
||||||
|
- La vista `files_ultima_versione` (vedi [`db-cad-index.md`](db-cad-index.md)) rompe il
|
||||||
|
pareggio fra copie di pari versione in modo deterministico invece che arbitrario.
|
||||||
|
- Resta un rischio noto: quando il componente **non** sta nella cartella del suo assieme, la
|
||||||
|
scelta torna a essere una scommessa. Se un assieme non si apre da `raw/`, è la prima cosa da
|
||||||
|
verificare - `9244206098` è il caso reale che ha portato a questa sezione: due copie di pari
|
||||||
|
versione, una del 2011 in una cartella di file sciolti e una del 2026 accanto all'assieme,
|
||||||
|
e in `raw/` finiva quella del 2011.
|
||||||
|
|
||||||
|
### 7.5 Riferimenti esterni: la tabella dei modelli citati
|
||||||
|
|
||||||
|
Terzo modo in cui un `.asm` dipende da un file che non è fra i suoi membri, dopo i soppressi
|
||||||
|
(§7.1) e le copie ambigue (§7.4). Un assieme che porta **assembly feature** o **datum copiati**
|
||||||
|
da un altro modello lo cita in una tabella a parte, che non è `members` e non lascia una stringa
|
||||||
|
`Componente ID …`: i due lettori esistenti non la vedevano.
|
||||||
|
|
||||||
|
Struttura: un array aperto da `\xf2\xf7\x05` e chiuso da `\xf3`, con elementi
|
||||||
|
`\xe3\xe2<tipo><nome>\x00`. Il nome può portare il suffisso `#AF<n>` di §4.0.1 e/o la notazione
|
||||||
|
`istanza<generico>` di §4.0.2 - il file su disco è il generico:
|
||||||
|
|
||||||
|
```
|
||||||
|
f2 f7 05 e3 e2 01 "9255708017#AF2<9255708017>" 00 ... -> 9255708017
|
||||||
|
e3 e2 02 "9255700181" 00 ... -> 9255700181
|
||||||
|
f3
|
||||||
|
```
|
||||||
|
|
||||||
|
Va letta con `EstraiCodiciRiferimentiEsterni`, e - a differenza dei soppressi - **si ricorre
|
||||||
|
dentro**: Creo carica il modello citato con tutti i suoi componenti, che quindi servono anch'essi
|
||||||
|
in `raw/`. La tabella contiene anche nomi di *feature* (`MIRROR3`, `CHAMFER1`,
|
||||||
|
`M10_TAPPED_HOLE1`), indistinguibili dai modelli guardando il record: non è un problema perché
|
||||||
|
chi la usa cerca ogni nome nell'indice e ignora quelli che non ci sono - per lo stesso motivo
|
||||||
|
questi codici non devono finire fra i "non trovati".
|
||||||
|
|
||||||
|
Il caso reale: `9255728001` falliva in Distributed Batch con lo stesso `[-4] Dati di input non
|
||||||
|
completi` di §7.3 perché il suo sottoassieme `9255728001_200` referenzia per assembly feature
|
||||||
|
`9255708017` e `9255708018` (e con loro `9255700181/182/183`), e monta una vite
|
||||||
|
`V6-12--_-_U5933_C<UNI5933>` il cui generico `uni5933.prt` non era in `raw/`. Nessuno dei sei
|
||||||
|
file compariva in `members` né fra i piazzamenti.
|
||||||
|
|
||||||
## 8. Uso
|
## 8. Uso
|
||||||
|
|
||||||
|
|||||||
+7
-6
@@ -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.
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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`) |
|
||||||
@@ -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
@@ -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` |
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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...**.
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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])
|
||||||
@@ -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)
|
||||||
@@ -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))
|
||||||
@@ -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))
|
||||||
@@ -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}")
|
||||||
@@ -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))
|
||||||
@@ -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]])
|
||||||
@@ -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)
|
||||||
Reference in new issue
Block a user