# 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 `#\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 \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 ` | 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 ` | chiudi `n` contenitori | | `f5` | prefisso di un intero: bit 31 acceso | | `f6` | intero −1 | | `f7 ` | il prossimo valore è del campo numero `id` (§2.2) | | `f8 ` | array a una dimensione di `n` elementi | | `f9 ` | array a due dimensioni (`a`×`b` numeri; per gli array di strutture `a` è il totale) | | `fa ..` | array a `k` dimensioni | | `fb` | segue l'intestazione di un array di strutture, prima del primo elemento | | `fc ` | 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 ` 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 `. - 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 ` (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 ` 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 ` 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 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/.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_`), 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.