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

Co-Authored-By: Claude <noreply@anthropic.com>
2026-11-10 14:34:29 +01:00
davideandClaude Sonnet 5.5 1eac3ccaa7 Documenta posizioni, fori e facce dei .prt (9239902104)
Matrici 4x3 di fori e feature (regola degli zeri corretta), foro a punti
di schizzo con facce piane di Estrusione 1, profilo a L, quote per feature
e tipi 928/1104/1052. Corregge l'ipotesi errata sul codice 12266.

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

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

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

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

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 08:05:10 +02:00
36 changed files with 2552 additions and 98 deletions

No files matched your search

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