33 Commits
Author SHA1 Message Date
davideandClaude Opus 5.5 206db85e12 Porta la versione a 0.9.0 e aggiorna il CHANGELOG
Voce 0.9.0 in CHANGELOG.md con le modifiche dal tag 0.1.0 (menu File,
card di stato in fase 1, verifica presenza in fase 2, floatbar, guida
utente e finestra Guida ridisegnata). Versione aggiornata anche in
CLAUDE.md; .vscode/ aggiunta al .gitignore.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 08:05:21 +02:00
davideandClaude Sonnet 5 d81fede4cc Ridisegna la finestra Guida con gerarchia visiva e callout colorati
La guida in-app era testo semplice senza gerarchia (stesso peso per
titoli, corpo, passi, avvisi) e una sidebar con la selezione blu di
sistema, fuori stile rispetto al resto dell'app.

Sezioni ristrutturate come dati (Titolo/Intro/Passi/Nota) invece di un
unico blob di testo libero, e renderizzate con gerarchia esplicita nel
RichTextBox: titolo blu primario grassetto, passi numerati con il
numero in blu, callout "Nota"/"Attenzione" con sfondo a tinta piatta
(blu tenue per le info, ambra tenue per gli avvisi che fanno perdere
tempo se ignorati, come l'errore Creo "[-4]"). Sidebar con selezione
disegnata a mano nel blu dell'app invece del blu di sistema. Bottone
Chiudi spostato sulla stessa floatbar già usata nelle altre finestre.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 16:29:42 +02:00
davideandClaude Sonnet 5 948937ba75 Aggiungi la guida utente e allinea i testi rimasti indietro sulla fase 5
Nuova docs/guida-utente.md: tutti i passi in ordine che deve fare
l'utente, dalla distinta esportata da Fata (gestionale) fino
all'esportazione STEP con Creo Distributed Batch (passi generali presi
da documentazione/community PTC, con avviso che i nomi dei menu
variano per versione/configurazione aziendale). Include anche i
consigli pratici di impostare la cartella di lavoro e pulire raw/step
prima di ripartire con una distinta diversa.

GuidaForm.cs e README.md citavano ancora una "Verifica completezza
assiemi" e un bottone "Cambia cartella..." rimossi da tempo dal
codice, mai allineati nei testi: corretti nello stesso commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 16:22:39 +02:00
davideandClaude Sonnet 5 ff4e7a71e0 Trasforma la barra di navigazione in una floatbar e uniforma i bottoni Avanti/Indietro
La barra Avanti/Indietro era un footer bianco a tutta larghezza con una
linea di separazione da 1px. Ora è una card bianca staccata su tutti e
4 i lati dalla finestra (UiStile.FloatBar, angoli arrotondati, bordo
sottile), riusata anche in PrtMancantiForm per Chiudi/Apri cartella raw.

Avanti e Indietro avevano dimensioni diverse (testi di lunghezza
diversa, più il grassetto di Primario che richiede più altezza del
testo normale) ed erano visivamente disomogenei (bordo sottile quasi
invisibile su sfondo bianco per Indietro contro il blu pieno di
Avanti). Aggiunti UiStile.SecondarioPieno (variante piena, senza
bordo, stessa costruzione di Primario ma grigio chiaro) e
UiStile.LarghezzaUguale (stessa larghezza e altezza tra due bottoni,
chiamata PRIMA di arrotondare gli angoli: arrotondare e poi
ridimensionare lasciava un bordo residuo della Region vecchia).

Doc aggiornati nello stesso commit: docs/architettura.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 16:13:34 +02:00
davideandClaude Sonnet 5 be4989fc8d Sistema il testo delle fasi 3-4 e completa il dialogo "Componenti referenziati trovati"
Fase 3: spostata la spiegazione da paragrafo sopra il bottone a etichetta
affiancata ("Copia i file dal server nella cartella 'raw'."), stesso
pattern già usato in fase 2. Fase 4: tolta la frase finale sul passo
opzionale, ridondante con la frase precedente.

PrtMancantiForm (l'esito di "Trova componenti referenziati"): mancava un
modo esplicito per chiudere la finestra (solo la X) e usava l'icona
default di WinForms invece di quella dell'app. Aggiunto "Chiudi" accanto
ad "Apri cartella raw" (Esc la chiude) e impostata l'icona dall'eseguibile,
stesso pattern già usato in Form1.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 16:00:25 +02:00
davideandClaude Sonnet 5 e94331b69f Ridisegna la fase 1 in card di stato e aggiunge un passo esplicito di verifica in fase 2
Fase 1: le info su DB e cartella di lavoro erano testo grigio uniforme
disallineato con bottoni squadrati senza gerarchia. Ora vivono in una
card di stato ("Database") con un indicatore verde/rosso per lo stato
di caricamento; la card "Cartella di lavoro" è stata rimossa (resta
gestibile da File → Scegli cartella di lavoro..., già presente nel
menu). Le icone sui bottoni (prima Segoe MDL2 Assets, poi disegnate a
mano via GDI+) sono state provate e infine tolte su richiesta: restano
solo testo, più semplici da mantenere per due bottoni.

Fase 2: "Verifica presenza" ora compare solo dopo un import riuscito
(dialogo o drag&drop) invece di essere sempre visibile, ed è lei —non
il solo import— a sbloccare "Avanti" (mancava la constatazione che
l'utente può ancora avanzare senza aver verificato che i file esistano
sul server). Aggiunte etichette numerate "1. Carica la distinta" /
"2. Verifica la presenza dei file sul server" accanto ai bottoni, e
rimosso il messaggio verde ridondante dopo l'import.

Doc aggiornati nello stesso commit: CLAUDE.md, docs/architettura.md,
GuidaForm.cs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 15:50:29 +02:00
davideandClaude Sonnet 5 3e9c78074c Sposta 'Pulisci raw e step' e 'Cambia cartella' nel menu File, rinomina 'Nuovo'
Fase 1 (Database) aveva 4 bottoni tutti sullo stesso piano visivo: due
configurano il DB, gli altri due (cambio cartella di lavoro, pulizia
raw/step) sono azioni occasionali di manutenzione, non passi del wizard.
Spostate in File → "Scegli cartella di lavoro..." e "Pulisci raw e
step...", raggiungibili in qualunque momento invece che solo dalla fase 1.

"File → Nuova estrazione" diventa "Nuovo"; corretto anche un bug per cui
InizializzaCartellaLavoro non richiamava AggiornaStatoAvanti, lasciando
"Avanti" disabilitato in fase 1 finché non si toccava un altro controllo
anche a DB e cartella già caricati all'avvio.

Doc aggiornati nello stesso commit: CLAUDE.md, docs/architettura.md,
GuidaForm.cs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 14:44:27 +02:00
davideandClaude Sonnet 5 e29a1bf90a Aggiungi CHANGELOG.md con la voce 0.1.0
Formato Keep a Changelog; elenca le funzionalità attuali del programma come
prima voce di storico versioni.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 14:25:30 +02:00
davideandClaude Sonnet 5 28aa16837b Aggiungi menu Nuovo estrazione/Guida, rimuovi verifica completezza assiemi
"File -> Nuova estrazione" (Ctrl+N) azzera distinta/raw/verifica per ripartire
senza ricaricare DB e cartella di lavoro; "Aiuto -> Guida" (F1) apre una sintesi
interattiva delle 5 fasi. Rimossa "Verifica completezza assiemi": bottone,
ReportCompletezzaForm e VerificaCompletezzaAssiemi non più necessari.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 14:24:57 +02:00
davide d9f16f2844 Regola in CLAUDE.md: i doc si aggiornano nello stesso commit del codice
docs/ descrive il programma com'e' adesso, non com'e' stato: e' la prima
cosa che si legge per capirlo, quindi un doc che racconta un
comportamento che non esiste piu' fa prendere decisioni sbagliate - e'
peggio di un doc mancante.

Aggiunta la regola, la tabella di quale doc ospita cosa, e la richiesta
di annotare anche il sintomo quando il cambiamento nasce da un fallimento
di Creo: i suoi messaggi non nominano ne' il file ne' la causa, e senza
quel ponte la volta dopo si riparte da zero.
2026-09-22 14:13:07 +02:00
davide db4c444473 Documenta il processo delle 5 fasi in architettura.md
Il doc elencava i file sorgente e una tabella delle fasi, ma non il
processo: cosa entra e cosa esce da ogni fase, e perche' la 4 e' piu'
complicata di "leggi i componenti e copiali". Aggiunta una sezione che lo
descrive come pipeline, con in chiaro le tre liste che la fase 4 raccoglie
da ogni .asm - distinta, soppressi, riferimenti esterni - e per ognuna se
si ricorre dentro e se un codice mancante va segnalato.

Allineati alla stessa modifica i punti rimasti indietro: la riga della
fase 4 nella tabella, la sezione sulla lettura dei .asm (che citava solo i
soppressi), e l'elenco dei file di test, fermo a quattro su sette.
2026-09-22 14:12:27 +02:00
davideandClaude Opus 5 13f6de6402 Copia in raw anche i modelli citati per riferimento esterno
Terzo caso di "[-4] Dati di input non completi" in Creo Distributed
Batch, dopo i soppressi e le copie ambigue del commit precedente: falliva
9255728001, e solo lui fra i sei assiemi del batch.

Il file in raw/ era identico bit per bit a quello sulla share, quindi non
era il modello: mancava il contorno. Il suo sottoassieme _200 e' legato
ad altri due (9255708017, 9255708018) da assembly feature e datum
copiati, e monta una vite V6-12--_-_U5933_C il cui generico uni5933.prt
non era in raw/. Nessuno dei sei file compariva in "members" ne' fra i
piazzamenti: i due lettori esistenti non potevano vederli.

Quei modelli stanno in una tabella a parte del .asm - array \xf2\xf7\x05
… \xf3 con elementi \xe3\xe2<tipo><nome>\0 - che ora si legge con
EstraiCodiciRiferimentiEsterni, riusando la normalizzazione di #AF<n> e
istanza<generico> gia' fatta per i membri. A differenza dei soppressi si
ricorre dentro: Creo carica il modello citato con tutto il suo albero, e
infatti 9255700182 arriva solo per quella via.

La tabella contiene anche nomi di feature (MIRROR3, CHAMFER1) che dal
record non si distinguono dai modelli. Non e' un problema: come per i
soppressi si cerca ogni nome nell'indice e si ignora quello che non c'e'.
Per lo stesso motivo questi codici restano fuori dai "non trovati", e la
coda di scansione porta ora un flag che tiene fuori anche i rami
raggiunti per riferimento esterno - la lista dei componenti mancanti
continua a parlare solo della distinta.

Documentato in docs/formato-asm-record.md §7.5.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 14:09:48 +02:00
davideandClaude Opus 5 54f267b508 Copia in raw i file che servono a Creo, non solo quelli della distinta
Creo Distributed Batch falliva su 8 dei 22 assiemi con "[-4] Dati di
input non completi", un messaggio che non nomina il file mancante. Erano
due cause distinte, entrambe emerse aprendo gli assiemi da raw/ e
cercando nell'albero il componente con l'icona rossa.

Copia sbagliata del componente. Lo stesso codice esiste spesso in piu'
cartelle della share con pari versione e contenuto diverso (13% dei
codici): non sono revisioni, sono istantanee parallele lasciate da studi,
allestimenti fiera e cartelle temporanee. La vista dell'indice rompeva il
pareggio in modo arbitrario, e poteva vincere una copia obsoleta -
nascondendo del tutto quella buona. Creo poi caricava un modello diverso
da quello atteso, non risolveva i vincoli e non apriva l'assieme. Il
.asm non dice quale copia usa (ne' percorso, ne' UOBJ_ID del figlio, ne'
revisione attesa), quindi si replica la regola di Creo: vince la copia
nella stessa cartella dell'assieme che la referenzia. La vista ora rompe
il pareggio in modo deterministico e viene ricreata a ogni scan, cosi' un
DB esistente prende la definizione nuova senza reindicizzare.

Componenti soppressi. Spariscono da "members" ma restano citati dalla
feature che li posiziona, e il loro file serve comunque per aprire
l'assieme. Da quando la lettura usa il parser esatto - che legge solo
"members", ed e' la scelta giusta per la BOM - non venivano piu' copiati.
Ora si copiano a parte, senza entrare nel conteggio dei "non trovati":
non sono componenti della distinta, segnalarli come mancanti sarebbe un
falso allarme.

Documentate entrambe in docs/formato-asm-record.md §7.3 e §7.4, insieme
alla distinzione che le accomuna: la lista della distinta e la lista dei
file necessari a Creo non coincidono, e la seconda e' piu' lunga.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 11:52:16 +02:00
davideandClaude Opus 5 0ae2a5e7cf Elimina i falsi "componenti mancanti" dal rilevamento delle family table
Il record "model_name" veniva preso per l'identita' del file, ma sui file
reali nomina quasi sempre un altro modello - tipicamente uno dei membri del
file stesso. Il parser esatto rifiutava percio' file perfettamente
leggibili, il chiamante ripiegava sull'euristica testuale e quella
estraeva codici inesistenti, riportati poi come componenti non trovati
nell'indice.

Il segnale ora richiede la conferma del nome proprio del modello
(objname): scatta solo se il file dichiara davvero se stesso come istanza.

Verificato contro le BOM esportate da Creo per 7 assiemi reali: 6 vengono
riprodotte byte per byte (3 di queste prima non si generavano affatto), e
la settima differisce per una sola parte, presente nella revisione di un
sotto-assieme che Creo ha caricato ma assente in quella piu' recente. La
verifica di completezza passa su tutti e 7 (prima 3 risultavano
incompleti), e tutti i 351 .asm di una cartella raw/ reale si leggono col
parser esatto, senza mai ricadere sull'euristica.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 10:47:08 +02:00
davide b136e7f49b Semplifica la finestra di progresso di "Aggiorna indice"
Sostituita la barra disegnata a mano (colori custom, footer con
separatore, pulsante Annulla arrotondato) con la ProgressBar nativa di
Windows e un layout ad assoluti minimale, in stile con la barra di
progresso di "Trova componenti referenziati" (ProgressoComponentiForm).
Mantenuti conteggio file, dettaglio nuovi/aggiornati/invariati/rimossi,
stima del tempo rimanente e il pulsante Annulla, che l'altra finestra non
ha ma qui servono per uno scan lungo su tutta la share.

Aggiornati due commenti in Form1.cs/UiStile.cs che facevano riferimento
allo stile precedente di ProgressForm, ora rimosso.
2026-09-22 10:02:14 +02:00
davide cf4c2e8e29 Risolve i membri family table senza ConfigName e la scritta troncata in "Componenti referenziati"
Alcuni membri di family table non-UNI portano la notazione "<istanza><generico>"
direttamente nel campo name/family_name di members, senza un ConfigName
associato: AsmRecordReader cercava quella stringa intera (parentesi comprese)
nell'indice e non la trovava mai. Ora separa istanza e generico.

Corretta anche l'etichetta di PrtMancantiForm, che usciva dal bordo della
finestra invece di andare a capo.

Documentato il nuovo meccanismo in docs/formato-asm-record.md, e aggiunta una
nota sul segnale di rilevamento family table di assiemi: validato su un caso
reale ma non affidabile al 100% su scala (trovato un falso positivo).
2026-09-22 09:57:48 +02:00
davideandClaude Sonnet 5 9af426ca1b Rileva la family table di assiemi e chiude i punti oscuri residui del parser .asm
AsmRecordReader.LeggiMembri ora lancia InvalidDataException quando un assieme e'
esso stesso un'istanza di family table (members descriverebbe solo una
rappresentazione, non le istanze sorelle) invece di restituire un risultato
sbagliato in silenzio - il chiamante ripiega sull'euristica testuale, che risolve
gia' correttamente il caso. Aggiunti due controlli di coerenza analoghi (conteggio
grezzo vs interpretato dei record ConfigName; occorrenze multiple di "members"),
verificati senza falsi positivi su tutti i 350 .asm reali della share.

Documentazione riorganizzata per distinguere cio' che e' verificato contro una BOM
Creo di riferimento da cio' che e' verificato solo su scala (conteggi), e cio' che
resta genuinamente non risolto.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:20:56 +02:00
davideandClaude Sonnet 5 28a72c80b2 Normalizza il suffisso #AF di ConfigName; documenta il limite sulle family table di assiemi
Test end-to-end su un albero reale (9284218024b.asm, confrontato con la sua
BOM Creo di riferimento tramite scripts/CreoBom.cs) oltre gli 8 campioni usati
per il reverse engineering iniziale.

Trovato: i componenti creati da un pattern/array feature hanno un ConfigName
con suffisso "<codice>#AF<n>" (n = indice nel pattern), non un'istanza di
family table vera - la BOM di Creo lo ignora e unisce le occorrenze duplicate.
AsmRecordReader ora rimuove questo suffisso prima di usare il nome istanza.

Trovato ma non risolto: un intero ASSIEME (non solo le parti normalizzate)
puo' avere una family table, risolta da Creo con un meccanismo diverso da
ConfigName (non decodificato). AsmRecordReader non lo rileva e non lancia
eccezione in questo caso: il fallback automatico di Form1 non scatta, e il
risultato puo' essere silenziosamente incompleto. Documentato come limite
noto in formato-asm-record.md e in CLAUDE.md - nessun fix applicato finche'
il meccanismo non e' meglio compreso.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 16:27:15 +02:00
davideandClaude Sonnet 5 cce377a091 Integra il parser esatto del .asm nella GUI (fase 4 e 5)
AsmRecordReader (spostato da scripts/CreoBom.cs nel progetto principale,
unica fonte di verita' condivisa con lo script via #:project) sostituisce
AssemblyComponentScanner in Form1.RisolviAlberoComponenti (fase "Trova
componenti referenziati") e AssiemeCompleto (fase "Verifica completezza"):
legge esattamente la sezione "members" del binario invece di contare
occorrenze testuali, eliminando i falsi positivi/negativi noti dell'euristica
(riferimenti fantasma, placement senza suffisso, ecc.).

Se un file non rispetta il formato validato (InvalidDataException, coperto
solo dagli 8 campioni usati per il reverse engineering) Form1 ripiega
sull'euristica di AssemblyComponentScanner, che resta quindi come fallback
e come fonte di verita' per gli script BomViaDb.cs/ExtractCreoStrings.cs.

Aggiunti test per AsmRecordReader (fixture binarie costruite a mano: lettura
membri, risoluzione ConfigName, conteggio disallineato, sezione assente).
Aggiornata la documentazione (CLAUDE.md, docs/architettura.md,
docs/formato-asm.md, docs/formato-asm-record.md) di conseguenza.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 16:14:00 +02:00
davideandClaude Sonnet 5 60dfd26e8b Snellisci CLAUDE.md: rimuovi duplicazione con docs/architettura.md e db-cad-index.md
La sezione Architettura ripeteva quasi verbatim il dettaglio file-per-file
gia' in docs/architettura.md e docs/db-cad-index.md, gonfiando il contesto
caricato in ogni sessione senza aggiungere informazione. Sostituita con un
riepilogo delle 5 fasi del wizard e rimandi puntuali; nessun contenuto perso,
solo spostato dove viveva gia' in dettaglio.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 15:41:22 +02:00
davideandClaude Sonnet 5 35009b5cd3 Aggiungi CreoBom.cs: parser esatto del formato .asm via record binari
Porting C# del reverse engineering della struttura a record del .asm Creo
(sezione "members" + parametro "ConfigName" per le family table): legge
esattamente i componenti diretti e genera la stessa BOM esportata da Creo,
senza le euristiche testuali (conteggio occorrenze) di AssemblyComponentScanner.
Resta uno script di analisi indipendente, non integrato nella GUI.

Aggiorna anche formato-asm.md (non e' piu' vero che "non esiste un parser
strutturale") e architettura.md (script mancante nell'elenco).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 15:41:14 +02:00
davideandClaude Sonnet 5 3cfc040fb0 Aggiungi documentazione tecnica su cad_index.db
Schema, logica di scansione/upsert incrementale di Indexer.ScanAsync,
come l'app individua il DB all'avvio (incluso il caveat sulla cartella
di build in dev) e le query piu' usate.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 14:37:08 +02:00
davideandClaude Sonnet 5 3ea582e645 Usa la cartella del DB come fallback per la cartella di lavoro
Se non c'e' una cartella di lavoro salvata (o punta a una cartella
cancellata), AppContext.BaseDirectory finiva spesso nella cartella di
build (bin/Release/...) invece che dove l'utente tiene effettivamente
il proprio lavoro. Ora il fallback usa la cartella del DB caricato.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 14:33:18 +02:00
davideandClaude Sonnet 5 9ecdccb6bb Migliora UI: finestra aggiornamento indice, barra nav wizard, fase Verifica
- ProgressForm: gerarchia visiva, contrasto testo, barra piatta blu al posto
  della ProgressBar nativa verde, footer separato dal bottone Annulla.
- Barra Avanti/Indietro: footer separato con linea, padding allineato ai
  pannelli di ogni fase, riordinata sopra la barra di stato come convenzione
  standard invece che sotto.
- Fase 5 (Verifica completezza): bottone principale reso primario, lista
  file .asm passata da griglia di icone troncate a lista a riga singola
  leggibile.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 14:16:32 +02:00
davideandClaude Sonnet 5 5c9661eb12 Fix euristica BOM, unifica script/GUI su un'unica fonte, aggiungi test integrazione/E2E
Due bug reali trovati confrontando l'estrazione BOM con la BOM ufficiale Creo:
un componente scartato per placement fallito in un file veniva bloccato per
sempre anche quando il file giusto aveva un vero placement (perdendo interi
sotto-alberi come 9256510047), e i codici con suffisso (_C01 ecc.) richiedevano
sempre placement esplicito invece dell'eccezione per occorrenze ripetute già
presente nella GUI. Entrambi corretti.

AssemblyComponentScanner reso pubblico e scripts/BomViaDb.cs, ExtractCreoStrings.cs
riscritti per referenziarlo via "#:project" invece di duplicare l'euristica:
un'unica fonte di verità condivisa da GUI e script standalone.

Aggiunti test di integrazione per Indexer.ScanAsync (mai coperto oltre al
parsing del nome file) e test end-to-end che ricostruiscono l'intera pipeline
disco -> indice -> risoluzione albero componenti senza toccare la UI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 13:43:54 +02:00
davideandClaude Sonnet 5 353a1028d9 Fix icon: bigger logo, uniform white background, better source quality
Regenerated from the last committed (already-transparent) icon instead
of continuing to reprocess an already-degraded local copy, avoiding
further quality loss from repeated extract/recomposite passes.

Two real defects fixed along the way:
- The white background had a faint ghost outline: the transparent
  source still carried a very-low-alpha (~5-9%) remnant of the original
  card's border, invisible on its own but visible once composited onto
  a new solid white background. Added a hard low-alpha cutoff before
  cropping/compositing instead of keeping every non-zero alpha pixel.
- Margin around the logo was too generous (14%), making it look small;
  reduced to 8%, checked against the 16%-radius rounded corners so nothing
  gets clipped.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 11:41:12 +02:00
davideandClaude Sonnet 5 4d5717f827 Bundle native libraries into the single-file publish
PublishSingleFile alone still left e_sqlite3.dll as a loose file next
to PrepStep.exe; moving just the exe (e.g. copying it elsewhere) broke
SQLite with an opaque SqliteConnection type initializer exception.
IncludeNativeLibrariesForSelfExtract embeds it in the exe instead,
self-extracted to a temp folder at first run, so the published exe is
a genuinely single, movable file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 11:30:29 +02:00
davideandClaude Sonnet 5 379deec986 Remove the step_finali copy/report feature
Not needed anymore: the app no longer copies matched .stp files from
step/ into a separate step_finali/ folder, or reports which distinta
codes are still missing a STEP. Removed the "Crea cartella step_finali"
button, GeneraReportStep, and ReportStepForm/EsitoStep. Phase 5 keeps
"Verifica completezza assiemi" and the drag&drop .asm list (still how
files get sent to Creo Distributed Batch); renamed from "Verifica
completezza e STEP" to "Verifica completezza" since it's no longer about
STEP output. PulisciRawEStep now only clears raw/ and step/.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 11:23:46 +02:00
davideandClaude Sonnet 5 13798ad47c Add explicit working folder; rename project from CadSearchGui to PrepStep
Working folder (phase 1): CSV/raw/step/step_finali all used to live split
across two different implicit locations (CSV in the "repo root" found by
walking up for CLAUDE.md, raw/step next to the input distinta file) with
neither shown to the user. That repo-root lookup also silently breaks once
the app runs as a standalone published exe on a machine with no repo/
CLAUDE.md at all. Replaced both with one explicit "cartella di lavoro",
auto-suggested (last used, remembered the same way as the DB path) and
changeable from phase 1, with no dependency on any repo marker file.

Rename: CadSearchGui was the working name from before the app had a
product identity; renamed folders, csproj files, namespace and the
AppData settings folder to PrepStep throughout, matching the exe name
already in use. Product-facing name stays "Cevolani StepPrep".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 11:18:43 +02:00
davideandClaude Sonnet 5 6726dd2216 Add app icon, product name/version, and document them
- New app.ico (from the Cevolani favicon, background made transparent and
  cropped tighter to the logo so it stays legible at 16/24px), wired up via
  ApplicationIcon in the csproj and re-read at runtime for the window icon.
- Named the product "Cevolani StepPrep" and set Version=0.1.0 in the csproj;
  shown in the window title. The CadSearchGui folder/namespace/csproj keep
  their technical name, only the product-facing name changed.
- Updated CLAUDE.md, README.md and docs/architettura.md to reflect the
  product name/version, UiStile.cs, app.ico, and that this app is a
  pre-process step ahead of Creo Distributed Batch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 11:00:01 +02:00
davideandClaude Sonnet 5 ba18d682f3 Fix overflowing/clipped UI text and give buttons a consistent flat style
Instruction and status labels used AutoSize/fixed widths with no wrap
constraint, so long Italian paragraphs and network paths ran past the
window edge or got clipped instead of wrapping. Report dialogs had
fixed-height labels that clipped multi-line text. ProgressForm's folder
label could overflow its non-resizable dialog.

- Labels now wrap responsively (TableLayoutPanel column at 100% + Dock=Top)
  instead of a fixed pixel cap, so they reflow when the window is resized.
- Report dialog labels switched from fixed Height to AutoSize.
- ProgressForm's folder label truncates with ellipsis + tooltip, matching
  the existing pattern used by the import status label.
- Added ApplicationHighDpiMode=PerMonitorV2 to fix control proportions on
  scaled displays.
- Added UiStile.cs: shared flat button style (primary/secondary), rounded
  corners for the wizard nav buttons, and AutoSize+padding on all buttons
  so button size always matches its text (fixes text getting clipped when
  a hardcoded Size was too narrow for the label).
- Fase 5's drag&drop .asm list now fills available vertical space instead
  of a fixed 680x160 box.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 10:40:32 +02:00
davideandClaude Sonnet 5 6511d58b01 Parallelize raw/ copy and document overall architecture
CopiaFileTrovati now copies with Parallel.ForEach (degree 6) instead of
sequentially: the bottleneck is network latency to the share, not CPU,
so overlapping copies gives a large speedup (~5x in a synthetic
benchmark with simulated SMB latency, documented in
docs/copia-raw-parallela.md).

Also adds docs/architettura.md, a full architecture overview of every
source file, the wizard's 5 phases, dialogs, persistence and standalone
scripts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 10:07:46 +02:00
davideandClaude Sonnet 5 0966e66232 Add drag&drop .asm preview and force CSV output to repo root
Fase 5 now shows the distinta's .asm files already copied to raw/ as
draggable icons, so they can be dropped straight onto Creo or Explorer
without re-hitting the DB/share. The CSV from fase 2 is now always
written to the repo root (found by walking up to CLAUDE.md) instead of
next to the input file, wherever that happens to be.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 09:54:06 +02:00
37 changed files with 3820 additions and 801 deletions

No files matched your search

+3
View File
@@ -37,3 +37,6 @@ publish/
# Output di scripts/ExtractCreoStrings.cs # Output di scripts/ExtractCreoStrings.cs
/BOM.txt /BOM.txt
# Impostazioni locali dell'editor
.vscode/
+64
View File
@@ -0,0 +1,64 @@
# Changelog
Tutte le modifiche rilevanti di questo progetto sono documentate in questo file.
Il formato segue [Keep a Changelog](https://keepachangelog.com/it/1.1.0/), e il progetto
aderisce a [Semantic Versioning](https://semver.org/lang/it/).
## [0.9.0] - 2026-09-23
### Aggiunto
- Guida utente `docs/guida-utente.md`: tutti i passi, dalla distinta esportata dal gestionale
fino all'esportazione STEP con Creo Distributed Batch.
- Fase 2: bottone "Verifica presenza", visibile solo dopo un import riuscito; è lui (non il
solo import) a sbloccare "Avanti". Etichette numerate accanto ai bottoni.
- Dialogo "Componenti referenziati trovati": bottone "Chiudi" (anche con Esc) e icona dell'app.
### Modificato
- "Scegli cartella di lavoro..." e "Pulisci raw e step..." spostati dalla fase 1 al menu File,
raggiungibili in qualunque momento; "File → Nuova estrazione" rinominato in "Nuovo".
- Fase 1 ridisegnata in una card di stato "Database" con indicatore verde/rosso; rimossa la card
"Cartella di lavoro".
- Barra Avanti/Indietro trasformata in floatbar staccata dai bordi, con bottoni di dimensioni e
stile uniformi (riusata anche nel dialogo dei componenti referenziati).
- Finestra Guida ridisegnata: sezioni con titolo, passi numerati e callout "Nota"/"Attenzione"
colorati; sidebar con selezione nel blu dell'app.
- Testi delle fasi 3-4 semplificati.
### Corretto
- "Avanti" restava disabilitato in fase 1 all'avvio anche con DB e cartella di lavoro già
caricati.
- Guida in-app e README citavano ancora "Verifica completezza assiemi" e "Cambia cartella...",
rimossi da tempo.
## [0.1.0] - 2026-09-22
### Aggiunto
- Wizard a 5 fasi (Database → Importa Distinta Base → Crea cartella raw → Trova componenti
referenziati → Verifica completezza) per preparare i file `raw/` da passare a Creo
Distributed Batch per l'esportazione STEP.
- Indicizzazione dei file CAD Pro-ENGINEER/Creo di un disco/share di rete (radice di default
`X:\`) in un DB SQLite locale (`cad_index.db`), con aggiornamento incrementale e barra di
progresso.
- Import della distinta base dall'export del gestionale (`.xls`/`.xml`) in CSV
componente;descrizione, incluso il riconoscimento della convenzione `DBP_<codice>_<timestamp>`
per aggiungere l'assieme generale come riga.
- Creazione della cartella `raw/` con copia parallela dei file CAD dei componenti trovati
nell'indice.
- Risoluzione ricorsiva dell'albero componenti dai file `.asm` (sotto-assiemi, parti,
componenti soppressi, riferimenti esterni), con parser esatto dei record binari
(`AsmRecordReader`) e fallback euristico testuale (`AssemblyComponentScanner`) per i file
fuori formato.
- Scelta automatica della copia corretta quando lo stesso codice esiste in più cartelle della
share, preferendo quella della cartella dell'assieme che la referenzia.
- Anteprima trascinabile (drag&drop) dei file `.asm` della distinta già in `raw/`, pronti per
Creo Distributed Batch.
- Menu "File → Nuova estrazione" (Ctrl+N) per ripartire con una nuova distinta senza ricaricare
database e cartella di lavoro, e "Aiuto → Guida" (F1) con una sintesi interattiva delle 5 fasi.
- Script standalone C# file-based (`scripts/`): `ExtractCreoStrings.cs`, `BomViaDb.cs`,
`CreoBom.cs`, per estrarre la distinta componenti da riga di comando.
- Pubblicazione come eseguibile standalone single-file self-contained (Windows x64).
+98 -81
View File
@@ -4,14 +4,19 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Cos'è questo progetto ## Cos'è questo progetto
`CadSearchGui` è un'app Windows Forms (.NET 10, WinForms) che indicizza e permette di `PrepStep` è il nome tecnico del progetto (cartella, namespace, csproj, eseguibile
`PrepStep.exe`); il nome mostrato all'utente finale è **Cevolani StepPrep** (`<Product>`/
`<AssemblyTitle>` in `PrepStep/PrepStep.csproj`, usato nel titolo della finestra insieme
alla versione, `<Version>`, attualmente `0.9.0`). È un'app Windows Forms (.NET 10, WinForms) che indicizza e permette di
cercare i file CAD Pro-ENGINEER/Creo contenuti in tutte le cartelle di primo livello cercare i file CAD Pro-ENGINEER/Creo contenuti in tutte le cartelle di primo livello
di un disco/share di rete (radice di default `X:\`), salvando i metadati in un DB di un disco/share di rete (radice di default `X:\`), salvando i metadati in un DB
SQLite locale (`cad_index.db`). SQLite locale (`cad_index.db`). Il flusso a valle (fasi 2-5, vedi sotto) prepara i file
`raw/` da passare a Creo Distributed Batch per l'esportazione STEP: la app è quindi un
pre-processo per quel passaggio, non lo sostituisce.
## Comandi ## Comandi
Tutti da eseguire dentro `CadSearchGui/`. Tutti da eseguire dentro `PrepStep/`.
```powershell ```powershell
dotnet build -c Release dotnet build -c Release
@@ -24,95 +29,107 @@ Pubblicazione come eseguibile standalone (single-file, self-contained, Windows x
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -o publish dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -o publish
``` ```
`CadSearchGui.Tests/` (xUnit) contiene unit e integration test — vedi `PrepStep.Tests/` (xUnit) contiene unit e integration test — vedi
`dotnet test` da dentro quella cartella. Riferimento al progetto principale `dotnet test` da dentro quella cartella. Riferimento al progetto principale
via `ProjectReference` + `InternalsVisibleTo` (in `CadSearchGui.csproj`), così via `ProjectReference` + `InternalsVisibleTo` (in `PrepStep.csproj`), così
i test possono chiamare direttamente `Indexer.ParseFilename` e i test possono chiamare direttamente `Indexer.ParseFilename` e
`AssemblyComponentScanner.EstraiCodiciReferenziati` senza esporli come API `AssemblyComponentScanner.EstraiCodiciReferenziati` senza esporli come API
pubblica. pubblica.
## Documentazione: va tenuta allineata
`docs/` non è un archivio di note storiche: descrive **il programma com'è adesso**, ed è la
prima cosa che si legge per capirlo. Ogni modifica strutturale — un passo in più o in meno nel
flusso delle fasi, un nuovo modo di leggere il `.asm`, un cambio nello schema del DB o nel
criterio con cui si sceglie un file — va riflessa nei doc **nello stesso commit** del codice,
non "dopo". Un doc che descrive un comportamento che non esiste più è peggio di un doc
mancante: fa prendere decisioni sbagliate.
Dove scrivere cosa:
| Doc | Contenuto |
|---|---|
| `CLAUDE.md` (questo file) | il flusso essenziale e i punti che si sbaglierebbero senza saperli |
| [`docs/architettura.md`](docs/architettura.md) | il processo delle 5 fasi e la mappa file-per-file |
| [`docs/guida-utente.md`](docs/guida-utente.md) | cosa clicca l'utente, passo per passo, fino a Creo Distributed Batch |
| [`docs/formato-asm-record.md`](docs/formato-asm-record.md) | parser esatto dei record `.asm`, con §7 per i casi limite |
| [`docs/formato-asm.md`](docs/formato-asm.md) | euristica testuale di fallback |
| [`docs/db-cad-index.md`](docs/db-cad-index.md) | schema e vista di `cad_index.db` |
| [`docs/copia-raw-parallela.md`](docs/copia-raw-parallela.md) | parallelizzazione della copia in `raw/` |
| [`CHANGELOG.md`](CHANGELOG.md) | storico delle versioni (formato Keep a Changelog); ogni release che cambia `<Version>` nel csproj aggiunge qui la sua voce |
Quando il cambiamento nasce da un fallimento reale di Creo, scrivere anche **il sintomo**: i
messaggi di Creo non nominano il file o la causa, e senza quel ponte la prossima volta si
riparte da zero (i §7.3/§7.4/§7.5 di `formato-asm-record.md` sono l'esempio del formato
atteso).
## Architettura ## Architettura
Il programma è tutto in `CadSearchGui/` (nessun altro progetto nella solution — in Il programma è tutto in `PrepStep/` (nessun altro progetto nella solution). Dettaglio
passato esisteva anche una CLI separata e uno script Python di indicizzazione, completo file-per-file in [`docs/architettura.md`](docs/architettura.md); qui solo il flusso
entrambi rimossi: ora l'unico programma è questo, con GUI e indicizzazione integrate). che conta per capire il prodotto.
- **Program.cs** — entry point WinForms standard, avvia `Form1`. `Form1.cs` è un wizard a 5 fasi (una sola visibile alla volta, `_panelFasi[]`, si avanza con
- **Form1.cs** / **Form1.Designer.cs** — finestra principale, costruita come wizard: una "Avanti" solo se `AvantiConsentito`), UI costruita interamente in codice (`BuildUi`, nessun
sola fase alla volta occupa tutta la finestra (`_panelFasi[]`/`NomiFasi`), si avanza designer visuale):
con "Avanti" solo quando la fase corrente è pronta (`AvantiConsentito`). La UI è
costruita interamente in codice in `Form1.BuildUi()` (nessun designer visuale usato).
`Form1.Designer.cs` contiene solo lo scheletro minimo (`InitializeComponent`/`Dispose`)
generato dal template. Le fasi, in ordine:
1. **Database** — carica/aggiorna l'indice, mostra data ultimo aggiornamento; qui vive
anche "Pulisci raw e step" (manutenzione, non fa parte del flusso a valle).
2. **Importa Distinta Base** — `DistintaBaseImporter.ConvertiInCsv` genera il CSV dei
componenti; se il nome del file segue la convenzione del gestionale
(`DBP_<codice>_<timestamp>`), l'assieme generale viene aggiunto come riga in più nel
CSV stesso (il foglio elenca solo i componenti sotto l'assieme, non l'assieme in sé).
Da qui in avanti l'assieme generale è un componente come tutti gli altri, nessun
caso speciale nel resto del codice.
3. **Crea cartella raw** — cerca nell'indice il percorso CAD di ogni componente del CSV
(`TrovaPercorsiCad`) e lo copia in `raw/` (`CopiaFileTrovati`).
4. **Trova componenti referenziati** — legge euristicamente i metadati binari dei `.asm`
già in `raw/` (`AssemblyComponentScanner`) per risolvere ricorsivamente i
sotto-assiemi/parti che contengono (`RisolviAlberoComponenti`), copiandoli anch'essi
in `raw/`.
5. **Verifica completezza e STEP** — "Verifica completezza assiemi" (ricorsivo sui
percorsi originali in indice, non su `raw/`) e "Crea cartella step_finali": solo per i
codici della distinta importata dal CSV (non i sotto-componenti di fase 4, che servono
solo a completare `raw/`) cerca in `step/` il file `<codice>.stp` e lo copia in
`step_finali/` se lo trova. Per i mancanti distingue, incrociando l'indice CAD,
`EsitoStep.StepMancante` (il CAD c'è, manca solo l'esportazione STEP) da
`ComponenteNonTrovato` (il componente non è nemmeno nell'indice) — vedi `ReportStepForm`.
- **Indexer.cs** — logica di scansione del disco e scrittura/aggiornamento del DB
SQLite (`Indexer.ScanAsync`). Cammina tutte le cartelle di primo livello trovate sotto
la radice (scoperte dinamicamente con `Directory.GetDirectories`, non un elenco fisso)
con un walk manuale a stack (`Walk`, più veloce di `EnumerateFiles(..., AllDirectories)`
su share di rete con centinaia di migliaia di file), estrae da ogni nome file il
pattern Pro-E `<codice>.<tipo>.<versione>` via regex, e fa un upsert incrementale
riga per riga (confronto per `percorso_completo` + dimensione + data modifica).
Riporta il progresso tramite `IProgress<ScanProgress>`, marshalato automaticamente
sul thread UI. Contiene anche lo schema SQL del DB (tabella `files` + vista
`files_ultima_versione`, che seleziona solo l'ultima versione per
`(cartella_radice, codice, tipo_file)` usando `ROW_NUMBER() OVER (...)` — non un
self-join con `GROUP BY`, che su ~200k righe degenera in tempi quasi quadratici).
- **ProgressForm.cs** — dialog modale mostrato durante l'aggiornamento indice: barra
di progresso, conteggi nuovi/aggiornati/invariati/rimossi, tempo trascorso e stima
del tempo rimanente (calcolata dal ritmo file/secondo rispetto al totale atteso,
preso dal conteggio della scansione precedente salvato nel DB).
### Schema del DB (`cad_index.db`) 1. **Database** — carica/aggiorna `cad_index.db` (`Indexer.ScanAsync`, radice disco di
default `X:\` hardcoded in `RadiceDiscoDefault`); un pallino verde/rosso accanto al titolo
indica se un DB è caricato. La **cartella di lavoro** (destinazione di CSV/`raw/`/`step/`)
non è più mostrata qui — proposta dall'ultima usata o dalla cartella del DB se non c'è
ancora nulla di salvato, cambiabile in qualunque momento da "File → Scegli cartella di
lavoro..." — vedi `docs/db-cad-index.md` per il dettaglio di schema e risoluzione del
percorso DB.
2. **Importa Distinta Base** — `DistintaBaseImporter.ConvertiInCsv` genera il CSV
componente;descrizione nella cartella di lavoro (da file scelto o trascinato); se il file
segue la convenzione del gestionale (`DBP_<codice>_<timestamp>`), aggiunge l'assieme
generale come riga in più (da quel momento è un componente come tutti gli altri, nessun
caso speciale nel resto del codice). Il bottone "Verifica presenza" compare solo dopo un
import riuscito e cerca i componenti nell'indice (`VerificaCodiciNelDb`, righe colorate in
verde/rosso in griglia): è quello — non il solo import — a sbloccare "Avanti"
(`AvantiConsentito`).
3. **Crea cartella raw** — `TrovaPercorsiCad` + `CopiaFileTrovati` (parallela, vedi
`docs/copia-raw-parallela.md`) copiano in `raw/` i file CAD dei componenti del CSV trovati
nell'indice.
4. **Trova componenti referenziati** — legge i `.asm` in `raw/` per risolvere ricorsivamente
sotto-assiemi/parti (`RisolviAlberoComponenti`), copiandoli anch'essi in `raw/`. Usa
`AsmRecordReader` (parser esatto dei record binari, `docs/formato-asm-record.md`) e ripiega
su `AssemblyComponentScanner` (euristica testuale, `docs/formato-asm.md`) solo se il file
non rispetta il formato validato (`InvalidDataException`). Tre punti da non confondere,
tutti imparati facendo fallire Creo (`docs/formato-asm-record.md` §7.3, §7.4 e §7.5):
i **componenti soppressi** non sono nella distinta ma il loro file serve per aprire
l'assieme (`EstraiCodiciSoppressi`, copiati senza finire fra i "non trovati"); i
**riferimenti esterni** (assembly feature, datum copiati) citano modelli che non sono né
membri né piazzamenti, e che Creo carica con tutto il loro albero
(`EstraiCodiciRiferimentiEsterni`, anche questi fuori dai "non trovati", ma con ricorsione);
e quando lo stesso codice esiste in più cartelle della share — succede per il 13% dei
codici — va copiata la copia nella **stessa cartella dell'assieme che la referenzia**
(`ScegliPerCartella`), perché il `.asm` non registra quale usa e sbagliarla impedisce a
Creo di aprire l'assieme.
5. **Verifica completezza** — mostra i `.asm` della distinta già in `raw/`, trascinabili su
Creo per l'esportazione STEP (Distributed Batch, manuale, fuori dall'app).
Tabella `files`: un record per ogni file trovato, con `percorso_completo` (chiave Menu in cima alla finestra: "File → Nuovo" (Ctrl+N) azzera lo stato delle fasi 2-5
univoca), `cartella_radice`, `famiglia` (primo livello di sottocartella), per ripartire con una nuova distinta, senza toccare DB/cartella di lavoro della fase 1 (va
`codice`/`tipo_file`/`versione` estratti dal nome file, dimensione, data modifica e volutamente alla fase 2, non alla 1: quei dati restano quelli già caricati); "File → Scegli
timestamp dell'ultima scansione (`scansionato_il`, usato per rilevare ed eliminare i cartella di lavoro..." cambia la cartella di CSV/`raw/`/`step/` in qualunque momento; "File →
file non più presenti su disco a fine scansione). Pulisci raw e step..." svuota le cartelle `raw/`/`step/` nella cartella di lavoro (non tocca
l'indice); "Aiuto → Guida" (F1) apre una sintesi interattiva delle 5 fasi (`GuidaForm.cs`).
Vista `files_ultima_versione`: stessa struttura, filtrata alla sola versione più
recente per codice+tipo.
### Percorso del DB
All'avvio la app cerca `cad_index.db` accanto all'eseguibile o nella working
directory corrente; se non lo trova, l'utente può selezionarlo con "Apri DB...".
Il bottone "Aggiorna indice" rilancia `Indexer.ScanAsync` sullo stesso path (radice
disco di default `X:\`, hardcoded in `Form1.RadiceDiscoDefault`).
## Script standalone (`scripts/`) ## Script standalone (`scripts/`)
`scripts/` contiene C# file-based apps (.NET 10, `dotnet run scripts/Nome.cs -- arg1 arg2`), `scripts/` contiene C# file-based apps (.NET 10, `dotnet run scripts/Nome.cs -- arg1 arg2`,
senza csproj — dipendenze NuGet extra si aggiungono con `#:package Nome@versione` in cima lanciati dalla root del repo — **non** da dentro `PrepStep/`, che ha un `.csproj` `WinExe`:
al file. `ExtractCreoStrings.cs` estrae la BOM di un `.asm` risolvendo ricorsivamente i `dotnet run` lì avvia la GUI vera invece dello script). Elenco e dettaglio di ciascuno in
sotto-assiemi contro `cad_index.db` (stessa logica di `Form1.AssiemeCompleto`, solo codici `docs/architettura.md`.
numerici Pro-E); `BomViaDb.cs` fa lo stesso ma con la tokenizzazione generica validata contro
il DB, usata per intercettare anche i nomi alfabetici delle parti commerciali (vedi
`docs/formato-asm.md`); `Analyze.cs` dumpa il contesto testo/hex attorno a un pattern in un
file binario, per investigare a mano nuovi falsi positivi prima di toccare la regex; `Query.cs`
esegue una query SQL ad-hoc contro `cad_index.db` da riga di comando.
Il formato binario `.asm`, l'euristica di estrazione codici e i falsi positivi noti (ID vista Il pattern da preservare: gli script referenziano `PrepStep/PrepStep.csproj` via `#:project`
`ModelView#`, self-reference, riferimenti fantasma a codici superati/rinominati — sintatticamente invece di duplicare la logica di lettura dei `.asm`, che vive **solo** nel progetto
identici a un componente vero, non filtrabili in modo affidabile) sono documentati in principale — un'unica fonte di verità condivisa da GUI e script. Due famiglie distinte, non
`docs/formato-asm.md`. intercambiabili:
- `ExtractCreoStrings.cs`/`BomViaDb.cs` usano `AssemblyComponentScanner` (euristica testuale,
`docs/formato-asm.md`) e risolvono i sottoassiemi contro `cad_index.db`.
- `CreoBom.cs` usa `AsmRecordReader` (parser esatto dei record binari,
`docs/formato-asm-record.md`, condiviso con la GUI in fase 4/5) e risolve i sottoassiemi
cercandoli in cartelle locali (`-p`), non contro `cad_index.db`.
-62
View File
@@ -1,62 +0,0 @@
namespace CadSearchGui;
/// <summary>Mostra, per ogni assieme della distinta base, se tutti i suoi componenti
/// (ricorsivamente fino ai PRT) sono presenti nell'indice (v) o mancano (x).</summary>
public class ReportCompletezzaForm : Form
{
public ReportCompletezzaForm(List<(string Componente, string Descrizione, bool? Completo)> righe)
{
int completi = righe.Count(r => r.Completo == true);
int incompleti = righe.Count(r => r.Completo == false);
int nonTrovati = righe.Count(r => r.Completo is null);
Text = "Completezza assiemi";
ClientSize = new Size(720, 440);
StartPosition = FormStartPosition.CenterParent;
Font = new Font("Segoe UI", 9.5f);
var lblInfo = new Label
{
Dock = DockStyle.Top,
AutoSize = false,
Height = 48,
Padding = new Padding(12, 8, 12, 8),
Text = $"{completi} completi, {incompleti} incompleti, {nonTrovati} non trovati nell'indice "
+ $"su {righe.Count} componenti. Nota: la scansione dei .asm è euristica, qualche "
+ "falso 'incompleto' è possibile.",
};
var grid = new DataGridView
{
Dock = DockStyle.Fill,
ReadOnly = true,
AllowUserToAddRows = false,
AllowUserToDeleteRows = false,
RowHeadersVisible = false,
SelectionMode = DataGridViewSelectionMode.FullRowSelect,
AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill,
};
grid.Columns.Add("componente", "Componente");
grid.Columns.Add("descrizione", "Descrizione");
grid.Columns.Add("completo", "Completo");
grid.Columns["componente"]!.FillWeight = 20;
grid.Columns["descrizione"]!.FillWeight = 55;
grid.Columns["completo"]!.FillWeight = 25;
foreach (var (componente, descrizione, completo) in righe.OrderBy(r => r.Completo).ThenBy(r => r.Componente))
{
string testo = completo switch { true => "✓", false => "✗", null => "?" };
int i = grid.Rows.Add(componente, descrizione, testo);
grid.Rows[i].DefaultCellStyle.ForeColor = completo switch
{
true => Color.DarkGreen,
false => Color.DarkRed,
null => Color.Gray,
};
grid.Rows[i].Cells["completo"].Style.Alignment = DataGridViewContentAlignment.MiddleCenter;
}
Controls.Add(grid);
Controls.Add(lblInfo);
}
}
-85
View File
@@ -1,85 +0,0 @@
using System.Diagnostics;
namespace CadSearchGui;
public enum EsitoStep
{
/// <summary>File .stp trovato in "step" e copiato in "step_finali".</summary>
Creato,
/// <summary>Il componente è nell'indice CAD (asm/prt trovato), ma nessuno STEP esportato.</summary>
StepMancante,
/// <summary>Il componente non è nemmeno nell'indice CAD.</summary>
ComponenteNonTrovato,
}
/// <summary>Mostra, per ogni componente della distinta base, se lo STEP corrispondente è stato
/// trovato in "step" e copiato in "step_finali", se manca solo lo STEP (il CAD sorgente c'è,
/// ma non è ancora stato esportato) o se manca anche il CAD sorgente nell'indice.</summary>
public class ReportStepForm : Form
{
public ReportStepForm(List<(string Componente, string Descrizione, EsitoStep Esito)> righe, string cartellaStepFinali)
{
int creati = righe.Count(r => r.Esito == EsitoStep.Creato);
int stepMancanti = righe.Count(r => r.Esito == EsitoStep.StepMancante);
int nonTrovati = righe.Count(r => r.Esito == EsitoStep.ComponenteNonTrovato);
Text = "Report STEP finali";
ClientSize = new Size(720, 440);
StartPosition = FormStartPosition.CenterParent;
Font = new Font("Segoe UI", 9.5f);
var lblInfo = new Label
{
Dock = DockStyle.Top,
AutoSize = false,
Height = 32,
Padding = new Padding(12, 8, 12, 8),
Text = $"{creati} copiati, {stepMancanti} da esportare (CAD presente), "
+ $"{nonTrovati} non nell'indice, su {righe.Count} totali. In \"{cartellaStepFinali}\".",
};
var grid = new DataGridView
{
Dock = DockStyle.Fill,
ReadOnly = true,
AllowUserToAddRows = false,
AllowUserToDeleteRows = false,
RowHeadersVisible = false,
SelectionMode = DataGridViewSelectionMode.FullRowSelect,
AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill,
};
grid.Columns.Add("componente", "Componente");
grid.Columns.Add("descrizione", "Descrizione");
grid.Columns.Add("step", "STEP");
grid.Columns["componente"]!.FillWeight = 20;
grid.Columns["descrizione"]!.FillWeight = 55;
grid.Columns["step"]!.FillWeight = 25;
foreach (var (componente, descrizione, esito) in righe.OrderBy(r => r.Esito).ThenBy(r => r.Componente))
{
var (testo, colore) = esito switch
{
EsitoStep.Creato => ("Creato", Color.DarkGreen),
EsitoStep.StepMancante => ("Da esportare (CAD presente)", Color.DarkOrange),
_ => ("Componente non trovato nel server", Color.DarkRed),
};
int i = grid.Rows.Add(componente, descrizione, testo);
grid.Rows[i].DefaultCellStyle.ForeColor = colore;
}
var btnApriCartella = new Button { Text = "Apri cartella step_finali", Dock = DockStyle.Bottom, Height = 34 };
btnApriCartella.Click += (_, _) =>
{
try { Process.Start("explorer.exe", cartellaStepFinali); }
catch (Exception ex)
{
MessageBox.Show(this, $"Impossibile aprire Esplora risorse:\n{ex.Message}", "Errore",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
};
Controls.Add(grid);
Controls.Add(lblInfo);
Controls.Add(btnApriCartella);
}
}
+293
View File
@@ -0,0 +1,293 @@
using System.Text;
using PrepStep;
namespace PrepStep.Tests;
public class AsmRecordReaderTests
{
private static string ScriviFile(byte[] dati)
{
string percorso = Path.GetTempFileName();
File.WriteAllBytes(percorso, dati);
return percorso;
}
private static byte[] Bytes(params object[] parti)
{
using var ms = new MemoryStream();
foreach (var p in parti)
{
switch (p)
{
case byte b: ms.WriteByte(b); break;
case int i: ms.WriteByte((byte)i); break;
case string s: var buf = Encoding.Latin1.GetBytes(s); ms.Write(buf); break;
case byte[] arr: ms.Write(arr); break;
default: throw new ArgumentException($"tipo non gestito: {p.GetType()}");
}
}
return ms.ToArray();
}
// \xe3 id type name\0 family_name\0 - id/type come singolo byte (0x00-0x7f, un byte)
private static byte[] Membro(int id, int tipo, string nome, string? nomeFamiglia = null) =>
Bytes(0xe3, id, tipo, nome, 0, nomeFamiglia ?? nome, 0);
// \xe3 ConfigName\0 \xe23 <istanza>\0 <idMembro> \x82\x7b
private static byte[] ConfigName(string istanza, int idMembro) =>
Bytes(0xe3, "ConfigName", 0, 0xe2, (byte)'3', istanza, 0, idMembro, 0x82, 0x7b);
// \0objname\0 \xe2 \xe0\x01 object_type\0 <int> \xe0\x0a name\0 <nome>\0 - il nome proprio
// del modello (quello che la BOM intesta), vedi docs/formato-asm-record.md §5
private static byte[] ObjName(string nome) =>
Bytes(0, "objname", 0, 0xe2, 0xe0, 0x01, "object_type", 0, 2, 0xe0, 0x0a, "name", 0, nome, 0);
// identita' dichiarata dal file: \xe0\x01 model_t_type\0 <int> \xe0\x0a model_name\0 <nome>\0
// \xe0\x0a generic_name\0 \0 (vuoto)
private static byte[] ModelName(string nome) =>
Bytes(0xe0, 0x01, "model_t_type", 0, 2, 0xe0, 0x0a, "model_name", 0, nome, 0,
0xe0, 0x0a, "generic_name", 0, 0);
// array dei modelli referenziati: \xf2\xf7\x05 <elementi> \xf3, ogni elemento
// \xe3\xe2<tipo><nome>\0 - vedi docs/formato-asm-record.md §7.5
private static byte[] Riferimenti(params string[] nomi)
{
var parti = new List<object> { 0xf2, 0xf7, 0x05 };
foreach (var nome in nomi)
parti.AddRange(new object[] { 0xe3, 0xe2, 0x02, nome, 0 });
parti.Add(0xf3);
return Bytes(parti.ToArray());
}
private static byte[] Asm(int conteggioDichiarato, byte[] membri, byte[]? extra = null) =>
Bytes("header ", extra ?? Array.Empty<byte>(), "\0members\0", 0xf1, 0xf8, conteggioDichiarato, membri, "#MdlStatus");
[Fact]
public void Legge_membri_semplici_senza_family_table()
{
byte[] dati = Asm(2, Bytes(
Membro(1, 2, "9244206098"),
Membro(2, 1, "9244228558_C01")));
var membri = AsmRecordReader.LeggiMembri(dati);
Assert.Equal(2, membri.Count);
Assert.Equal((1, 2, "9244206098", "9244206098"), (membri[0].Id, membri[0].Tipo, membri[0].Nome, membri[0].Generico));
Assert.Equal((2, 1, "9244228558_C01", "9244228558_C01"), (membri[1].Id, membri[1].Tipo, membri[1].Nome, membri[1].Generico));
}
[Fact]
public void Risolve_il_nome_istanza_di_una_family_table_da_ConfigName()
{
// Il membro cita solo il generico (UNI6592); il nome che la BOM mostra
// (R8--_-_U6592) sta nel parametro ConfigName, collegato per id membro.
byte[] dati = Asm(1, Membro(1, 2, "UNI6592"), extra: ConfigName("R8--_-_U6592", idMembro: 1));
var membri = AsmRecordReader.LeggiMembri(dati);
Assert.Single(membri);
Assert.Equal("R8--_-_U6592", membri[0].Nome);
Assert.Equal("UNI6592", membri[0].Generico);
}
[Fact]
public void Rimuove_il_suffisso_array_feature_dal_nome_istanza()
{
// Regressione: caso reale 9244250136.asm -> ConfigName con suffisso "#AF<n>" per
// componenti creati da un pattern/array feature (id 45 e id 78, entrambi variante
// "_C03" con suffisso diverso "#AF0"/"#AF1"): non un'istanza di family table vera,
// la BOM di Creo mostra il codice pulito e unisce le occorrenze duplicate.
byte[] dati = Asm(2, Bytes(
Membro(45, 2, "9244250136_C03"),
Membro(78, 2, "9244250136_C03")),
extra: Bytes(
ConfigName("9244250136_C03#AF0", idMembro: 45),
ConfigName("9244250136_C03#AF1", idMembro: 78)));
var membri = AsmRecordReader.LeggiMembri(dati);
Assert.All(membri, m => Assert.Equal("9244250136_C03", m.Nome));
}
[Fact]
public void Membro_con_notazione_istanza_generico_diretta_nel_nome_usa_il_generico()
{
// Regressione: 9244248577a_c01.asm - alcuni membri di family table non-UNI non
// passano da ConfigName, il campo "name"/"family_name" del record members porta gia'
// la forma "<istanza><generico>" (stessa notazione di §4.1). Senza lo split, il codice
// cercato nell'indice sarebbe "V6-12--_-_U5931_GC<UNI5931>" per intero, mai trovato -
// va cercato invece il generico "UNI5931".
byte[] dati = Asm(1, Membro(1, 2, "V6-12--_-_U5931_GC<UNI5931>"));
var membri = AsmRecordReader.LeggiMembri(dati);
Assert.Single(membri);
Assert.Equal("V6-12--_-_U5931_GC", membri[0].Nome);
Assert.Equal("UNI5931", membri[0].Generico);
}
[Fact]
public void Componente_senza_ConfigName_non_e_toccato_da_istanze_di_altri_membri()
{
byte[] dati = Asm(2, Bytes(
Membro(1, 2, "UNI6592"),
Membro(2, 2, "9244206098")),
extra: ConfigName("R8--_-_U6592", idMembro: 1));
var membri = AsmRecordReader.LeggiMembri(dati);
Assert.Equal("R8--_-_U6592", membri.Single(m => m.Id == 1).Nome);
Assert.Equal("9244206098", membri.Single(m => m.Id == 2).Nome);
}
[Fact]
public void Conteggio_dichiarato_diverso_da_quello_letto_lancia_eccezione()
{
// Dichiara 2 membri ma ne fornisce solo 1: lettura disallineata, va segnalata invece
// di restituire un risultato parziale silenzioso.
byte[] dati = Asm(2, Membro(1, 2, "9244206098"));
Assert.Throws<InvalidDataException>(() => AsmRecordReader.LeggiMembri(dati));
}
[Fact]
public void Assieme_family_table_con_generic_name_lancia_eccezione()
{
// Regressione: un file che e' esso stesso un'istanza di family table (il suo nome
// proprio "2546220101" piu' il suffisso "_C01" in model_name) descrive in "members"
// solo quella rappresentazione, non le istanze sorelle. Va segnalato invece di
// restituire silenziosamente il contenuto sbagliato (docs/formato-asm-record.md §7).
byte[] dati = Asm(1, Membro(1, 2, "9244206098"),
extra: Bytes(ObjName("2546220101"), ModelName("2546220101_C01")));
Assert.Throws<InvalidDataException>(() => AsmRecordReader.LeggiMembri(dati));
}
[Fact]
public void Model_name_di_un_altro_modello_non_lancia_eccezione()
{
// Regressione: sui file reali della share il record "model_name" nomina spesso un
// modello diverso dal file (tipicamente un suo membro), non l'identita' del file. Il
// nome proprio (objname) non conferma, quindi non e' il sintomo della family table:
// "members" e' corretto e va usato. Verificato contro BOM Creo reali - senza questo
// controllo il file cadeva sull'euristica testuale, che produceva codici inesistenti
// segnalati poi come "componenti mancanti".
byte[] dati = Asm(1, Membro(1, 2, "9044207999A_C01"),
extra: Bytes(ObjName("9244207999A"), ModelName("9044207999A_C01")));
var membri = AsmRecordReader.LeggiMembri(dati);
Assert.Equal("9044207999A_C01", membri.Single().Nome);
}
[Fact]
public void Model_name_senza_nome_proprio_non_lancia_eccezione()
{
// Stesso caso del test sopra ma senza record "objname" (formato osservato su tutti i
// file reali che facevano scattare il falso positivo): senza conferma del nome proprio
// il record non e' un'identita', quindi "members" va letto normalmente.
byte[] dati = Asm(1, Membro(1, 2, "9244206098"), extra: ModelName("2546220101_C01"));
Assert.Single(AsmRecordReader.LeggiMembri(dati));
}
[Fact]
public void Generic_name_senza_suffisso_istanza_non_lancia_eccezione()
{
// Regressione: su ~7% dei file reali della share compare un record "model_name" +
// "generic_name" (vuoto) che NON e' il sintomo della family table di assiemi - il
// valore non ha il suffisso "_C0n" delle istanze (es. e' un codice qualunque non
// correlato al nome del file). In quei casi "members" e' letto correttamente e va
// usato normalmente, non bloccato per un falso positivo.
byte[] dati = Asm(1, Membro(1, 2, "9244206098"),
extra: Bytes(ObjName("9244230117"), ModelName("9244230117")));
var membri = AsmRecordReader.LeggiMembri(dati);
Assert.Single(membri);
}
[Fact]
public void Componente_soppresso_non_e_fra_i_membri_ma_e_segnalato_a_parte()
{
// Un componente soppresso sparisce da "members" (il conteggio scende) ma resta citato
// dalla feature che lo posiziona. Non va nella BOM, però senza il suo file Creo non
// apre l'assieme, quindi va copiato lo stesso.
byte[] dati = Asm(1, Membro(1, 2, "9244206098"),
extra: Bytes("Componente ID 52 (9230100043.PRT)", 0));
Assert.Equal(new[] { "9244206098" }, AsmRecordReader.LeggiMembri(dati).Select(m => m.Nome));
string percorso = ScriviFile(dati);
try
{
Assert.Equal(new[] { "9230100043" }, AsmRecordReader.EstraiCodiciSoppressi(percorso));
Assert.DoesNotContain("9230100043", AsmRecordReader.EstraiCodiciReferenziati(percorso));
}
finally { File.Delete(percorso); }
}
[Fact]
public void Componente_gia_fra_i_membri_non_e_segnalato_come_soppresso()
{
byte[] dati = Asm(1, Membro(1, 2, "9244206098"),
extra: Bytes("Componente ID 7 (9244206098.PRT)", 0));
string percorso = ScriviFile(dati);
try
{
Assert.Empty(AsmRecordReader.EstraiCodiciSoppressi(percorso));
}
finally { File.Delete(percorso); }
}
[Fact]
public void Riferimenti_esterni_non_sono_fra_i_membri_e_si_risolvono_sul_generico()
{
// Un modello legato da riferimento esterno (assembly feature "#AFn", datum copiati) non
// compare ne' in "members" ne' fra i piazzamenti, ma senza il suo file Creo non apre
// l'assieme: e' il caso che faceva fallire 9255728001_200 in Distributed Batch. Per le
// istanze di family table il file su disco e' il generico fra parentesi angolari.
byte[] dati = Asm(1, Membro(1, 2, "9244206098"),
extra: Bytes(Riferimenti("9255708017#AF2<9255708017>", "9255708018"),
Riferimenti("V6-12--_-_U5933_C<UNI5933>")));
Assert.Equal(new[] { "9244206098" }, AsmRecordReader.LeggiMembri(dati).Select(m => m.Nome));
string percorso = ScriviFile(dati);
try
{
Assert.Equal(new[] { "9255708017", "9255708018", "UNI5933" },
AsmRecordReader.EstraiCodiciRiferimentiEsterni(percorso).OrderBy(c => c, StringComparer.Ordinal));
}
finally { File.Delete(percorso); }
}
[Fact]
public void Sezione_members_assente_lancia_eccezione()
{
byte[] dati = Encoding.Latin1.GetBytes("un file qualunque senza la sezione attesa");
Assert.Throws<InvalidDataException>(() => AsmRecordReader.LeggiMembri(dati));
}
[Fact]
public void EstraiCodiciReferenziati_restituisce_i_generici_distinti()
{
byte[] dati = Asm(3, Bytes(
Membro(1, 2, "9144210327"),
Membro(2, 2, "9144210327"), // stesso codice montato due volte: un solo codice distinto
Membro(3, 1, "9244238563_D100")));
string percorso = ScriviFile(dati);
try
{
var codici = AsmRecordReader.EstraiCodiciReferenziati(percorso);
Assert.Equal(new[] { "9144210327", "9244238563_D100" }, codici.OrderBy(c => c));
}
finally { File.Delete(percorso); }
}
[Fact]
public void File_inesistente_lancia_eccezione_IO() =>
Assert.Throws<DirectoryNotFoundException>(() =>
AsmRecordReader.EstraiCodiciReferenziati(@"C:\non\esiste\pippo.asm.1"));
}
@@ -1,6 +1,6 @@
using CadSearchGui; using PrepStep;
namespace CadSearchGui.Tests; namespace PrepStep.Tests;
public class AssemblyComponentScannerTests public class AssemblyComponentScannerTests
{ {
@@ -148,4 +148,54 @@ public class AssemblyComponentScannerTests
} }
finally { File.Delete(percorso); } finally { File.Delete(percorso); }
} }
[Fact]
public void Riconosce_codice_con_suffisso_anche_senza_dettaglio_nel_placement()
{
// Regressione: caso reale 9284218024b.asm -> 2546220101_C01.._C04. Il placement
// riporta solo il codice padre senza suffisso ("Componente ID N (2546220101.PRT)"),
// il dettaglio "_C01" compare solo altrove come stringa ripetuta, mai dentro un
// placement esplicito. Un codice nudo nella stessa situazione andrebbe scartato
// (vedi Scarta_codice_ripetuto_senza_un_vero_placement), ma un codice con suffisso
// di variante è considerato "forma nota": bastano le occorrenze ripetute. Prima
// che questa eccezione fosse portata anche in scripts/BomViaDb.cs, lo script
// perdeva l'intero sotto-assieme (vedi docs/formato-asm.md).
string percorso = ScriviAsmFinto(
"Componente ID 1 (2546220101.PRT) x 2546220101_C01 y 2546220101_C01 z");
try
{
var codici = AssemblyComponentScanner.EstraiCodiciReferenziati(percorso);
Assert.Contains("2546220101_C01", codici);
}
finally { File.Delete(percorso); }
}
[Fact]
public void Il_placement_e_indipendente_per_ogni_file_scansionato()
{
// Regressione: caso reale 9244228558.asm -> 9244236104 dentro 9244228558_C01.asm.
// Lo stesso codice compare come riferimento fantasma (nudo, senza placement) nel
// file dell'assieme padre, e con un vero placement nel file del sotto-assieme che
// lo contiene realmente. La proprietà che rende sicura la BFS del chiamante
// (Form1.RisolviAlberoComponenti, scripts/BomViaDb.cs, scripts/ExtractCreoStrings.cs)
// è che EstraiCodiciReferenziati valuta il placement da zero per ogni file, senza
// stato condiviso tra le chiamate: un fallimento nel file padre non deve
// condizionare il risultato quando lo stesso codice viene ripassato per il file
// figlio (vedi docs/formato-asm.md, "Trappola nella BFS").
string filePadre = ScriviAsmFinto("assieme 11112222 padre 11112222 fine");
string fileFiglio = ScriviAsmFinto("Componente ID 1 (11112222.PRT) x 11112222 y");
try
{
Assert.DoesNotContain("11112222", AssemblyComponentScanner.EstraiCodiciReferenziati(filePadre));
Assert.Contains("11112222", AssemblyComponentScanner.EstraiCodiciReferenziati(fileFiglio));
}
finally { File.Delete(filePadre); File.Delete(fileFiglio); }
}
[Theory]
[InlineData("9284208188", "9284208188")]
[InlineData("9284208188_D600", "9284208188")]
[InlineData("9244228558_C03-01", "9244228558")]
public void CodiceRadice_estrae_la_parte_prima_del_primo_underscore(string codice, string radiceAttesa) =>
Assert.Equal(radiceAttesa, AssemblyComponentScanner.CodiceRadice(codice));
} }
@@ -1,7 +1,7 @@
using CadSearchGui; using PrepStep;
using Microsoft.Data.Sqlite; using Microsoft.Data.Sqlite;
namespace CadSearchGui.Tests; namespace PrepStep.Tests;
/// <summary>Ricrea lo schema reale del DB (stessa costante SQL usata da Indexer.ScanAsync) e /// <summary>Ricrea lo schema reale del DB (stessa costante SQL usata da Indexer.ScanAsync) e
/// verifica, con dati inseriti a mano, i due comportamenti che in passato hanno causato bug /// verifica, con dati inseriti a mano, i due comportamenti che in passato hanno causato bug
@@ -1,6 +1,6 @@
using CadSearchGui; using PrepStep;
namespace CadSearchGui.Tests; namespace PrepStep.Tests;
public class DistintaBaseImporterTests public class DistintaBaseImporterTests
{ {
@@ -1,6 +1,6 @@
using CadSearchGui; using PrepStep;
namespace CadSearchGui.Tests; namespace PrepStep.Tests;
public class IndexerParseFilenameTests public class IndexerParseFilenameTests
{ {
+169
View File
@@ -0,0 +1,169 @@
using Microsoft.Data.Sqlite;
using PrepStep;
namespace PrepStep.Tests;
/// <summary>Integration test di Indexer.ScanAsync: disco temporaneo reale + DB SQLite reale,
/// nessun dato inserito a mano (a differenza di CaseInsensitiveLookupIntegrationTests, che
/// verifica solo le query a valle su righe già presenti). Copre la scansione vera - upsert
/// incrementale, rilevamento nuovi/aggiornati/invariati/rimossi, esclusione cartelle "temp_" -
/// che prima di questi test non aveva nessuna copertura (solo Indexer.ParseFilename era
/// testato).</summary>
public class IndexerScanTests : IDisposable
{
private readonly string _root = Path.Combine(Path.GetTempPath(), $"cadsearch_scan_{Guid.NewGuid():N}");
private readonly string _dbPath;
public IndexerScanTests()
{
Directory.CreateDirectory(_root);
_dbPath = Path.Combine(_root, "indice.db");
}
public void Dispose()
{
SqliteConnection.ClearAllPools();
Directory.Delete(_root, recursive: true);
}
private static void ScriviFile(string percorso, string contenuto = "dati")
{
Directory.CreateDirectory(Path.GetDirectoryName(percorso)!);
File.WriteAllText(percorso, contenuto);
}
/// <summary>IProgress&lt;T&gt; sincrono: Progress&lt;T&gt; marshala il report tramite
/// SynchronizationContext.Post (o il thread pool, se assente, come nei test) - l'ultimo
/// valore non è garantito visibile subito dopo l'await. Qui Report gira sullo stesso
/// thread di background di Indexer.Scan, prima che Task.Run completi: l'ultimo valore è
/// quindi affidabile non appena l'await ritorna.</summary>
private sealed class ProgressoSincrono : IProgress<ScanProgress>
{
public ScanProgress? Ultimo { get; private set; }
public void Report(ScanProgress value) => Ultimo = value;
}
private async Task ScanAsync()
{
await Indexer.ScanAsync(_root, _dbPath, new ProgressoSincrono(), CancellationToken.None);
}
private List<(string Codice, string Tipo, string? Famiglia, string CartellaRadice)> LeggiFiles()
{
using var connection = new SqliteConnection($"Data Source={_dbPath};Mode=ReadOnly");
connection.Open();
using var cmd = connection.CreateCommand();
cmd.CommandText = "SELECT codice, tipo_file, famiglia, cartella_radice FROM files";
using var reader = cmd.ExecuteReader();
var righe = new List<(string, string, string?, string)>();
while (reader.Read())
righe.Add((reader.GetString(0), reader.GetString(1),
reader.IsDBNull(2) ? null : reader.GetString(2), reader.GetString(3)));
return righe;
}
[Fact]
public async Task Indicizza_un_file_estraendo_codice_tipo_versione_e_famiglia()
{
ScriviFile(Path.Combine(_root, "__Corpi", "442", "9244228573.asm.16"));
await ScanAsync();
var righe = LeggiFiles();
Assert.Single(righe);
Assert.Equal(("9244228573", "asm", "442", "__Corpi"), righe[0]);
}
[Fact]
public async Task Esclude_le_cartelle_radice_che_iniziano_con_temp_()
{
ScriviFile(Path.Combine(_root, "temp_paris", "vecchio.asm.1"));
ScriviFile(Path.Combine(_root, "__Corpi", "9244228573.asm.16"));
await ScanAsync();
var righe = LeggiFiles();
Assert.Single(righe);
Assert.Equal("__Corpi", righe[0].CartellaRadice);
}
[Fact]
public async Task File_non_modificato_resta_invariato_tra_due_scansioni()
{
string percorso = Path.Combine(_root, "__Corpi", "file.prt.1");
ScriviFile(percorso);
await ScanAsync();
var progress = new ProgressoSincrono();
await Indexer.ScanAsync(_root, _dbPath, progress, CancellationToken.None);
Assert.NotNull(progress.Ultimo);
Assert.Equal(0, progress.Ultimo!.Nuovi);
Assert.Equal(0, progress.Ultimo.Aggiornati);
Assert.Equal(1, progress.Ultimo.Invariati);
Assert.Equal(0, progress.Ultimo.Rimossi);
}
[Fact]
public async Task File_modificato_viene_rilevato_come_aggiornato()
{
string percorso = Path.Combine(_root, "__Corpi", "file.prt.1");
ScriviFile(percorso, "versione 1");
await ScanAsync();
// Contenuto più lungo -> dimensione diversa, quindi rilevato come modifica anche se
// il file system non aggiornasse data_modifica entro la risoluzione del clock.
ScriviFile(percorso, "versione 2 con più testo dentro");
var progress = new ProgressoSincrono();
await Indexer.ScanAsync(_root, _dbPath, progress, CancellationToken.None);
Assert.NotNull(progress.Ultimo);
Assert.Equal(1, progress.Ultimo!.Aggiornati);
Assert.Equal(0, progress.Ultimo.Nuovi);
}
[Fact]
public async Task File_cancellato_dal_disco_viene_rimosso_dal_db_alla_scansione_successiva()
{
string percorso = Path.Combine(_root, "__Corpi", "file.prt.1");
ScriviFile(percorso);
await ScanAsync();
Assert.Single(LeggiFiles());
File.Delete(percorso);
await ScanAsync();
Assert.Empty(LeggiFiles());
}
[Fact]
public async Task Files_ultima_versione_tiene_solo_la_versione_piu_recente_per_codice_e_tipo()
{
ScriviFile(Path.Combine(_root, "__Corpi", "9244228573.asm.40"));
ScriviFile(Path.Combine(_root, "__Corpi", "9244228573.asm.41"));
await ScanAsync();
using var connection = new SqliteConnection($"Data Source={_dbPath};Mode=ReadOnly");
connection.Open();
using var cmd = connection.CreateCommand();
cmd.CommandText = "SELECT versione FROM files_ultima_versione WHERE codice = '9244228573'";
using var reader = cmd.ExecuteReader();
Assert.True(reader.Read());
Assert.Equal(41L, reader.GetInt64(0));
Assert.False(reader.Read());
}
[Fact]
public async Task File_senza_pattern_codice_tipo_versione_va_in_tipo_altro()
{
ScriviFile(Path.Combine(_root, "__Corpi", "leggimi.txt"));
await ScanAsync();
var righe = LeggiFiles();
Assert.Single(righe);
Assert.Equal("altro", righe[0].Tipo);
}
}
+156
View File
@@ -0,0 +1,156 @@
using Microsoft.Data.Sqlite;
using PrepStep;
namespace PrepStep.Tests;
/// <summary>End-to-end: disco CAD finto -> Indexer.ScanAsync -> cad_index.db reale -> BFS di
/// risoluzione albero componenti (stesso algoritmo di Form1.RisolviAlberoComponenti e
/// scripts/BomViaDb.cs, che ora delegano entrambi ad AssemblyComponentScanner - vedi
/// docs/formato-asm.md). Nessuna UI coinvolta: verifica che le tre fasi si compongano
/// correttamente, non solo che ciascuna funzioni isolata. I contenuti .asm/.prt sono testo
/// sintetico con gli stessi marcatori documentati per un vero file Creo ("Componente ID N
/// (...)"), non file binari reali - sufficiente perché AssemblyComponentScanner legge il file
/// come testo Latin1 indipendentemente dal resto del formato.</summary>
public class PipelineEndToEndTests : IDisposable
{
private readonly string _root = Path.Combine(Path.GetTempPath(), $"cadsearch_e2e_{Guid.NewGuid():N}");
private readonly string _dbPath;
public PipelineEndToEndTests()
{
Directory.CreateDirectory(_root);
_dbPath = Path.Combine(_root, "indice.db");
}
public void Dispose()
{
SqliteConnection.ClearAllPools();
Directory.Delete(_root, recursive: true);
}
private static void ScriviFile(string percorso, string contenuto)
{
Directory.CreateDirectory(Path.GetDirectoryName(percorso)!);
File.WriteAllText(percorso, contenuto);
}
/// <summary>Stesso algoritmo BFS di scripts/BomViaDb.cs: qui non è duplicazione a rischio
/// come lo era prima della riscrittura (vedi docs/formato-asm.md) - è la prova, in un
/// test, che comporre AssemblyComponentScanner con un semplice ciclo di accodamento dà il
/// risultato atteso sopra un indice reale, non un mock.</summary>
private static (SortedSet<string> Trovati, List<string> NonTrovati) RisolviAlbero(
string asmIniziale, Dictionary<string, List<(string Tipo, string Percorso)>> codiciNoti)
{
string codiceProprio = Path.GetFileName(asmIniziale).Split('.')[0];
string codiceRadice = AssemblyComponentScanner.CodiceRadice(codiceProprio);
var trovati = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
var nonTrovati = new List<string>();
var visti = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { codiceProprio, codiceRadice };
var coda = new Queue<string>(new[] { asmIniziale });
while (coda.Count > 0)
{
var asm = coda.Dequeue();
foreach (var codice in AssemblyComponentScanner.EstraiCodiciReferenziati(asm, codiciNoti.Keys))
{
if (!visti.Add(codice))
continue;
if (!codiciNoti.TryGetValue(codice, out var componenti))
{
nonTrovati.Add(codice);
continue;
}
trovati.Add(codice);
foreach (var (tipo, percorso) in componenti)
if (tipo == "asm")
coda.Enqueue(percorso);
}
}
return (trovati, nonTrovati);
}
private Dictionary<string, List<(string Tipo, string Percorso)>> CaricaCodiciNoti()
{
using var connection = new SqliteConnection($"Data Source={_dbPath};Mode=ReadOnly");
connection.Open();
using var cmd = connection.CreateCommand();
cmd.CommandText = "SELECT codice, tipo_file, percorso_completo FROM files_ultima_versione "
+ "WHERE LOWER(tipo_file) IN ('prt', 'asm')";
using var reader = cmd.ExecuteReader();
var codiciNoti = new Dictionary<string, List<(string, string)>>(StringComparer.OrdinalIgnoreCase);
while (reader.Read())
{
string codice = reader.GetString(0);
var lista = codiciNoti.TryGetValue(codice, out var l) ? l : codiciNoti[codice] = new();
lista.Add((reader.GetString(1), reader.GetString(2)));
}
return codiciNoti;
}
[Fact]
public async Task Risolve_lalbero_completo_a_partire_dal_disco_indicizzato()
{
// TOP -> CHILD (vero placement, 2 occorrenze) -> LEAF (vero placement, 2 occorrenze).
// "99999999" compare una sola volta in TOP: rumore binario, va scartato anche se
// fosse indicizzato (vedi AssemblyComponentScannerTests.Scarta_codici_con_una_sola_occorrenza).
ScriviFile(Path.Combine(_root, "MACCHINE", "GRUPPO1", "12345678.asm.1"),
"inizio Componente ID 1 (23456789.ASM) testo Componente ID 2 (23456789.ASM) rumore 99999999 fine");
ScriviFile(Path.Combine(_root, "MACCHINE", "GRUPPO1", "23456789.asm.1"),
"Componente ID 1 (34567890.PRT) blah Componente ID 2 (34567890.PRT) fine");
ScriviFile(Path.Combine(_root, "MACCHINE", "GRUPPO1", "34567890.prt.1"), "dati foglia");
ScriviFile(Path.Combine(_root, "MACCHINE", "GRUPPO1", "99999999.prt.1"), "mai referenziato davvero");
var progress = new Progress<ScanProgress>();
await Indexer.ScanAsync(_root, _dbPath, progress, CancellationToken.None);
var codiciNoti = CaricaCodiciNoti();
var asmIniziale = Path.Combine(_root, "MACCHINE", "GRUPPO1", "12345678.asm.1");
var (trovati, nonTrovati) = RisolviAlbero(asmIniziale, codiciNoti);
Assert.Equal(new[] { "23456789", "34567890" }, trovati);
Assert.DoesNotContain("99999999", trovati);
Assert.Empty(nonTrovati);
}
[Fact]
public async Task Segnala_come_non_trovato_un_componente_con_placement_vero_ma_mai_indicizzato()
{
// "23456789" ha un vero placement in TOP ma il suo file non esiste sul disco finto
// (mai scansionato -> non in cad_index.db): deve finire tra i "non trovati", non far
// fallire la risoluzione dell'albero.
ScriviFile(Path.Combine(_root, "MACCHINE", "GRUPPO1", "12345678.asm.1"),
"Componente ID 1 (23456789.ASM) testo Componente ID 2 (23456789.ASM) fine");
var progress = new Progress<ScanProgress>();
await Indexer.ScanAsync(_root, _dbPath, progress, CancellationToken.None);
var codiciNoti = CaricaCodiciNoti();
var asmIniziale = Path.Combine(_root, "MACCHINE", "GRUPPO1", "12345678.asm.1");
var (trovati, nonTrovati) = RisolviAlbero(asmIniziale, codiciNoti);
Assert.Empty(trovati);
Assert.Contains("23456789", nonTrovati);
}
[Fact]
public async Task File_in_cartella_temp_non_entra_nellindice_e_quindi_non_viene_risolto()
{
// Stesso codice "23456789" esiste per davvero, ma solo dentro una cartella radice
// "temp_..." - esclusa dalla scansione (vedi Indexer.cs e docs/formato-asm.md, falso
// positivo su copie scollegate dentro temp_*). Il componente deve restare "non
// trovato", non risolversi per sbaglio contro la copia in temp_.
ScriviFile(Path.Combine(_root, "MACCHINE", "GRUPPO1", "12345678.asm.1"),
"Componente ID 1 (23456789.ASM) testo Componente ID 2 (23456789.ASM) fine");
ScriviFile(Path.Combine(_root, "temp_davide", "23456789.asm.1"), "copia scollegata");
var progress = new Progress<ScanProgress>();
await Indexer.ScanAsync(_root, _dbPath, progress, CancellationToken.None);
var codiciNoti = CaricaCodiciNoti();
var asmIniziale = Path.Combine(_root, "MACCHINE", "GRUPPO1", "12345678.asm.1");
var (trovati, nonTrovati) = RisolviAlbero(asmIniziale, codiciNoti);
Assert.Empty(trovati);
Assert.Contains("23456789", nonTrovati);
}
}
@@ -16,7 +16,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\CadSearchGui\CadSearchGui.csproj" /> <ProjectReference Include="..\PrepStep\PrepStep.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
+273
View File
@@ -0,0 +1,273 @@
using System.Text;
using System.Text.RegularExpressions;
namespace PrepStep;
/// <summary>Lettura esatta della struttura a record del binario .asm Creo (sezione
/// "members" + parametro "ConfigName" per le family table), ricavata per reverse
/// engineering e verificata confrontando l'output con le BOM esportate da Creo - vedi
/// docs/formato-asm-record.md per il dettaglio byte-per-byte.
///
/// Alternativa esatta all'euristica testuale di <see cref="AssemblyComponentScanner"/>
/// (docs/formato-asm.md): quella conta le occorrenze dei codici nel testo, questa legge
/// la struttura dei record. Copre solo gli 8 campioni usati per il reverse engineering: su
/// un file che non rispetta il formato atteso lancia <see cref="InvalidDataException"/>
/// invece di restituire un risultato parziale silenzioso - il chiamante (<see
/// cref="Form1"/>) ripiega sull'euristica in quel caso, non essendoci garanzia di
/// copertura completa su tutte le versioni Creo mai incontrate sulla share.
///
/// Unica fonte di verità condivisa da GUI e script (stesso pattern di
/// AssemblyComponentScanner): scripts/CreoBom.cs referenzia questa classe via
/// "#:project" per la parte di lettura, aggiungendo solo la resa in formato BOM.</summary>
public static class AsmRecordReader
{
public sealed record Membro(int Id, int Tipo, string Nome, string Generico);
// interi big-endian, lunghezza nei bit alti del primo byte (0xe0+ e' riservato ai
// marcatori di struttura): 0x00-0x7f 1 byte, 0x80-0xbf 2 byte, 0xc0-0xdf 3.
private const string Int = @"([\x00-\x7f]|[\x80-\xbf][\x00-\xff]|[\xc0-\xdf][\x00-\xff][\x00-\xff])";
// \xe3 id type name\0 family_name\0
private static readonly Regex MemberRegex = new(
@"\xe3" + Int + Int + @"([\x20-\x7e]+)\x00([\x20-\x7e]+)\x00", RegexOptions.Compiled);
// parametro ConfigName: nome istanza family table + id del membro a cui si applica.
private static readonly Regex ConfigNameRegex = new(
@"\xe3ConfigName\x00\xe23(?:\xf7[\x00-\xff])?([\x20-\x7e]+)\x00" + Int + @"\x82\x7b",
RegexOptions.Compiled);
// nome proprio del modello (quello che la BOM stampa in intestazione).
private static readonly Regex ObjNameRegex = new(
@"\x00objname\x00\xe2\xe0\x01object_type\x00[\x00-\x7f]\xe0\x0aname\x00([^\x00]+)\x00",
RegexOptions.Compiled);
// Un componente creato da un pattern/array feature ha un ConfigName con un tag interno
// "<codice>#AF<n>" (Array Feature, n = indice nel pattern), non un'istanza di family
// table vera: la BOM reale di Creo lo ignora e mostra il codice pulito - osservato su
// 9244250136.asm (4 dei suoi 8 membri, due dei quali stesso codice "_C03" con "#AF0" e
// "#AF1" diversi, che la BOM unisce in una sola riga con quantita' 2). Senza questa
// normalizzazione ogni istanza del pattern risulterebbe un componente diverso.
private static readonly Regex SuffissoArrayFeatureRegex = new(@"#AF\d+$", RegexOptions.Compiled);
// record di identita' del modello, seguito da "generic_name" (vuoto): compare su una
// minoranza di assiemi (~7% dei campioni sulla share) per motivi diversi e nella
// maggioranza dei casi "members" resta corretto - la sola presenza di "generic_name" NON
// e' un segnale affidabile (verificato: su 144 file che lo contengono, solo 25 hanno
// anche il sintomo sotto; per gli altri 119 "members" letto normalmente coincide con
// l'euristica testuale, quindi e' un contesto diverso, non lo stesso problema).
private static readonly Regex OwnModelIdentityRegex = new(
@"\xe0\x01model_t_type\x00" + Int + @"\xe0\x0amodel_name\x00([\x20-\x7e]+)\x00\xe0\x0ageneric_name\x00\x00",
RegexOptions.Compiled);
// Alcuni membri di family table non-UNI (es. viti/dadi commerciali) non passano da
// ConfigName: il campo "name" del record members porta gia' la forma "<istanza><generico>"
// (stessa notazione di §4.1, li' sconsigliata per la BOM perche' e' un elenco di
// riferimenti geometrici - qui invece e' il valore vero del componente, va solo separato).
// Osservato su 9244248577a_c01.asm: "V6-12--_-_U5931_GC<UNI5931>" - senza questo split il
// codice cercato nell'indice sarebbe la stringa intera con le parentesi, mai trovata.
private static readonly Regex IstanzaConGenericoRegex = new(@"^([\x20-\x7e]+?)<([\x20-\x7e]+)>$", RegexOptions.Compiled);
// Suffisso "_C0n" delle istanze di family table (stessa convenzione di
// SuffissoArrayFeatureRegex/ConfigName sopra). Usato insieme a OwnModelIdentityRegex per
// riconoscere un file che e' esso stesso un'istanza, il cui "members" descriverebbe solo
// quella rappresentazione invece delle istanze sorelle (docs/formato-asm-record.md §7).
// Da solo NON basta: su file reali quel record nomina spesso un altro modello (un membro
// del file, o un codice imparentato), non il file stesso - per quello serve la conferma
// del nome proprio (objname).
private static readonly Regex SuffissoIstanzaFamilyTableRegex = new(@"_C\d{2,}$", RegexOptions.Compiled);
// primo elemento dell'array "members": porta le chiavi per esteso.
private static readonly Regex FirstMemberRegex = new(
@"\x01id\x00" + Int + @"\xe0\x01type\x00" + Int
+ @"\xe0\x0aname\x00([^\x00]+)\x00\xe0\x0afamily_name\x00([^\x00]+)\x00",
RegexOptions.Compiled);
private static int ContaOccorrenze(string testo, string marcatore)
{
int conteggio = 0;
for (int i = testo.IndexOf(marcatore, StringComparison.Ordinal); i >= 0; i = testo.IndexOf(marcatore, i + 1, StringComparison.Ordinal))
conteggio++;
return conteggio;
}
private static int AsInt(string bytes)
{
int mask = bytes.Length switch { 1 => 0x7F, 2 => 0x3F, 3 => 0x1F, _ => throw new ArgumentException("lunghezza intero non valida") };
int valore = bytes[0] & mask;
for (int i = 1; i < bytes.Length; i++)
valore = (valore << 8) | bytes[i];
return valore;
}
/// <summary>Nome proprio del modello (quello che Creo stampa in intestazione BOM),
/// dalla prima occorrenza del record "objname".</summary>
public static string NomeProprio(byte[] dati)
{
var m = ObjNameRegex.Match(Encoding.Latin1.GetString(dati));
if (!m.Success)
throw new InvalidDataException("record 'objname' non trovato");
return m.Groups[1].Value;
}
/// <summary>Componenti diretti di un assieme, in ordine di montaggio. "Nome" e' il nome
/// che mostrerebbe la BOM (istanza di family table se presente), "Generico" e' il file
/// realmente su disco (coincide con "Nome" per i componenti che non sono istanze).
/// Lancia <see cref="InvalidDataException"/> se il file non ha una sezione "members" o
/// se il conteggio dichiarato non coincide con i record letti (lettura disallineata:
/// il formato del file non e' quello coperto dal reverse engineering).</summary>
public static List<Membro> LeggiMembri(byte[] dati)
{
string testo = Encoding.Latin1.GetString(dati);
var configNameMatches = ConfigNameRegex.Matches(testo);
int configNameGrezzi = ContaOccorrenze(testo, (char)0xe3 + "ConfigName\0");
if (configNameMatches.Count != configNameGrezzi)
throw new InvalidDataException(
$"trovati {configNameGrezzi} record 'ConfigName' ma solo {configNameMatches.Count} " +
"interpretati: formato del parametro diverso da quello atteso (vedi §4, costante " +
"\\x82\\x7b), lettura non affidabile");
var istanze = new Dictionary<int, string>();
foreach (Match m in configNameMatches)
istanze[AsInt(m.Groups[2].Value)] = SuffissoArrayFeatureRegex.Replace(m.Groups[1].Value, "");
const string chiaveMembers = "\0members\0";
int start = testo.IndexOf(chiaveMembers, StringComparison.Ordinal);
if (start < 0)
throw new InvalidDataException("sezione 'members' assente");
if (testo.IndexOf(chiaveMembers, start + chiaveMembers.Length, StringComparison.Ordinal) >= 0)
throw new InvalidDataException(
"trovate piu' occorrenze di 'members': probabile rappresentazione semplificata, " +
"mai verificata (vedi §7) - non e' chiaro quale sezione descriva la BOM");
start += chiaveMembers.Length;
int conteggio = dati[start + 2]; // \xf1 \xf8 <conteggio>
int end = testo.IndexOf("#MdlStatus", start, StringComparison.Ordinal);
string region = testo.Substring(start, (end > 0 ? end : testo.Length) - start);
var grezzi = new List<(int Id, int Tipo, string Nome)>();
// il primo record e' esteso: ricava i campi dalle chiavi, i successivi sono posizionali
var primo = FirstMemberRegex.Match(region);
if (primo.Success)
{
grezzi.Add((AsInt(primo.Groups[1].Value), AsInt(primo.Groups[2].Value), primo.Groups[3].Value));
region = region.Substring(primo.Index + primo.Length);
}
foreach (Match m in MemberRegex.Matches(region))
{
grezzi.Add((AsInt(m.Groups[1].Value), AsInt(m.Groups[2].Value), m.Groups[3].Value));
if (grezzi.Count == conteggio)
break;
}
if (grezzi.Count != conteggio)
throw new InvalidDataException($"attesi {conteggio} membri, trovati {grezzi.Count}: lettura disallineata");
// Il record "model_name" e' l'identita' del file solo se il nome proprio del modello
// (objname, §5) lo conferma: da solo nomina spesso un altro modello - un membro del
// file, o un modello imparentato - e in quei casi "members" e' gia' corretto. Vedi
// docs/formato-asm-record.md §7.
var identita = OwnModelIdentityRegex.Match(testo);
var nomeProprio = ObjNameRegex.Match(testo);
if (identita.Success && nomeProprio.Success
&& SuffissoIstanzaFamilyTableRegex.IsMatch(identita.Groups[2].Value)
&& string.Equals(SuffissoIstanzaFamilyTableRegex.Replace(identita.Groups[2].Value, ""),
nomeProprio.Groups[1].Value, StringComparison.OrdinalIgnoreCase))
throw new InvalidDataException(
$"assieme con family table: il file e' esso stesso l'istanza '{identita.Groups[2].Value}', " +
"'members' descriverebbe solo questa rappresentazione, non le istanze sorelle - vedi " +
"docs/formato-asm-record.md §7");
return grezzi.Select(g =>
{
if (istanze.TryGetValue(g.Id, out var nomeIstanza))
return new Membro(g.Id, g.Tipo, nomeIstanza, g.Nome);
var bracket = IstanzaConGenericoRegex.Match(g.Nome);
return bracket.Success
? new Membro(g.Id, g.Tipo, bracket.Groups[1].Value, bracket.Groups[2].Value)
: new Membro(g.Id, g.Tipo, g.Nome, g.Nome);
}).ToList();
}
/// <summary>Codici dei componenti diretti (il generico su disco, non il nome istanza),
/// distinti, nell'ordine in cui compaiono. Equivalente esatto di
/// <see cref="AssemblyComponentScanner.EstraiCodiciReferenziati"/> per i file coperti dal
/// formato: stesso contratto di ritorno (un insieme di codici da cercare nell'indice),
/// lettura strutturale invece di euristica testuale.</summary>
public static HashSet<string> EstraiCodiciReferenziati(string percorsoAsm)
{
var membri = LeggiMembri(File.ReadAllBytes(percorsoAsm));
return new HashSet<string>(membri.Select(m => m.Generico), StringComparer.OrdinalIgnoreCase);
}
/// <summary>Codici che compaiono come piazzamento ma non fra i membri: sono i componenti
/// **soppressi**, che spariscono da "members" senza lasciare traccia (§7.1) ma restano
/// citati dalla feature che li posiziona.
///
/// Non fanno parte della BOM - Creo non li elenca - ma il loro file serve comunque per
/// **aprire** l'assieme: senza, il caricamento fallisce con "dati di input non completi".
/// Sono quindi due domande diverse: cosa compare nella distinta (<see
/// cref="EstraiCodiciReferenziati"/>) e cosa serve avere su disco (questo metodo in
/// aggiunta), e la seconda lista e' piu' lunga della prima.</summary>
public static HashSet<string> EstraiCodiciSoppressi(string percorsoAsm)
{
byte[] dati = File.ReadAllBytes(percorsoAsm);
var membri = LeggiMembri(dati);
var noti = new HashSet<string>(
membri.Select(m => m.Generico).Concat(membri.Select(m => m.Nome)), StringComparer.OrdinalIgnoreCase);
var soppressi = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (Match m in PlacementRegex.Matches(Encoding.Latin1.GetString(dati)))
if (!noti.Contains(m.Groups[1].Value))
soppressi.Add(m.Groups[1].Value);
return soppressi;
}
/// <summary>Modelli che l'assieme cita nella tabella dei riferimenti (§7.5): oltre ai propri
/// membri comprende quelli legati da **riferimenti esterni** - assembly feature, datum
/// copiati - che non compaiono né in "members" né come piazzamento, ma che Creo carica
/// comunque per aprire l'assieme.
///
/// Terza lista dopo <see cref="EstraiCodiciReferenziati"/> ("cosa c'è in distinta") e <see
/// cref="EstraiCodiciSoppressi"/>: come quest'ultima risponde a "cosa serve su disco", non a
/// "cosa va nella BOM. L'array contiene anche nomi di feature (MIRROR3, CHAMFER1...) e non
/// solo di modelli: non si riesce a distinguerli dal record, ma non danno fastidio perché il
/// chiamante li cerca nell'indice e ignora quelli che non ci sono.</summary>
public static HashSet<string> EstraiCodiciRiferimentiEsterni(string percorsoAsm)
{
string testo = Encoding.Latin1.GetString(File.ReadAllBytes(percorsoAsm));
var codici = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
for (int i = testo.IndexOf(InizioRiferimenti, StringComparison.Ordinal); i >= 0;
i = testo.IndexOf(InizioRiferimenti, i + 1, StringComparison.Ordinal))
{
int max = Math.Min(LunghezzaMassimaRiferimenti, testo.Length - i);
int fine = testo.IndexOf(FineRiferimenti, i, max);
foreach (Match m in RiferimentoRegex.Matches(testo.Substring(i, fine >= 0 ? fine - i : max)))
{
var bracket = IstanzaConGenericoRegex.Match(m.Groups[1].Value);
codici.Add(bracket.Success
? bracket.Groups[2].Value
: SuffissoArrayFeatureRegex.Replace(m.Groups[1].Value, ""));
}
}
return codici;
}
// Array dei modelli referenziati: "\xf2\xf7\x05" lo apre, "\xf3" lo chiude, ogni elemento e'
// "\xe3\xe2<tipo><nome>\x00" (§7.5). Il tetto di lunghezza limita il danno se il terminatore
// non si trova dove atteso: si perde qualche riferimento, non si raccoglie mezzo file.
private const string InizioRiferimenti = "ò÷\u0005ãâ";
private const char FineRiferimenti = 'ó';
private const int LunghezzaMassimaRiferimenti = 4096;
private static readonly Regex RiferimentoRegex = new(
@"\xe3\xe2[\x01\x02]([\x20-\x7e]{3,})\x00", RegexOptions.Compiled);
// "Componente ID 52 (9230100043.PRT)" - la stringa cambia con la localizzazione Creo del
// file, come in AssemblyComponentScanner.HaPlacement. Minimo 3 caratteri per lo stesso
// motivo per cui li' i token generici partono da 3: sotto quella soglia si raccoglie
// rumore (sulla share esistono davvero modelli chiamati "1.prt", "2.prt"...).
private static readonly Regex PlacementRegex = new(
@"Component(?:e)? [Ii][Dd] \d+ \(([A-Za-z0-9_\-]{3,})\.(?:PRT|ASM)\)", RegexOptions.Compiled);
}
@@ -1,7 +1,7 @@
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace CadSearchGui; namespace PrepStep;
/// <summary> /// <summary>
/// Estrae i codici componente referenziati dentro un file .asm Pro-E/Creo. /// Estrae i codici componente referenziati dentro un file .asm Pro-E/Creo.
@@ -70,7 +70,11 @@ namespace CadSearchGui;
/// silenzio come "nudo senza placement" - lì verrà comunque scartato come "non in indice", /// silenzio come "nudo senza placement" - lì verrà comunque scartato come "non in indice",
/// ma in modo tracciabile. /// ma in modo tracciabile.
/// </summary> /// </summary>
static class AssemblyComponentScanner // Pubblica (non solo internal) perché è anche l'unica fonte di verità per l'euristica di
// estrazione, referenziata via "#:project" dagli script standalone (scripts/BomViaDb.cs,
// scripts/ExtractCreoStrings.cs) invece che duplicata al loro interno - vedi
// docs/formato-asm.md, sezione sulla trappola del "codice duplicato in tre punti".
public static class AssemblyComponentScanner
{ {
private static readonly Regex CodiceNumericoRegex = new( private static readonly Regex CodiceNumericoRegex = new(
@"(?<!ModelView#)\b\d{8,12}[A-Za-z]{0,2}(?:_[A-Za-z]?\d{2,4})?\b", @"(?<!ModelView#)\b\d{8,12}[A-Za-z]{0,2}(?:_[A-Za-z]?\d{2,4})?\b",
@@ -2,7 +2,7 @@ using System.Text;
using System.Xml.Linq; using System.Xml.Linq;
using ExcelDataReader; using ExcelDataReader;
namespace CadSearchGui; namespace PrepStep;
/// <summary> /// <summary>
/// Converte il file "Distinta Base" esportato dal gestionale (SpreadsheetML /// Converte il file "Distinta Base" esportato dal gestionale (SpreadsheetML
@@ -25,7 +25,8 @@ static class DistintaBaseImporter
Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance); Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
} }
public static (string PercorsoCsv, List<(string Componente, string Descrizione)> Righe) ConvertiInCsv(string percorsoInput) public static (string PercorsoCsv, List<(string Componente, string Descrizione)> Righe) ConvertiInCsv(
string percorsoInput, string? cartellaOutput = null)
{ {
var righeGrezze = EImmagineSpreadsheetMl(percorsoInput) var righeGrezze = EImmagineSpreadsheetMl(percorsoInput)
? LeggiRigheSpreadsheetMl(percorsoInput) ? LeggiRigheSpreadsheetMl(percorsoInput)
@@ -44,7 +45,7 @@ static class DistintaBaseImporter
} }
var percorsoCsv = Path.Combine( var percorsoCsv = Path.Combine(
Path.GetDirectoryName(Path.GetFullPath(percorsoInput))!, cartellaOutput ?? Path.GetDirectoryName(Path.GetFullPath(percorsoInput))!,
Path.GetFileNameWithoutExtension(percorsoInput) + ".csv"); Path.GetFileNameWithoutExtension(percorsoInput) + ".csv");
using var writer = new StreamWriter(percorsoCsv, false, Encoding.UTF8); using var writer = new StreamWriter(percorsoCsv, false, Encoding.UTF8);
@@ -1,6 +1,6 @@
#nullable enable #nullable enable
namespace CadSearchGui; namespace PrepStep;
partial class Form1 partial class Form1
{ {
@@ -22,9 +22,7 @@ partial class Form1
AutoScaleDimensions = new SizeF(7F, 15F); AutoScaleDimensions = new SizeF(7F, 15F);
AutoScaleMode = AutoScaleMode.Font; AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(1000, 650); ClientSize = new Size(1000, 650);
MinimumSize = new Size(700, 400);
Name = "Form1"; Name = "Form1";
Text = "CadSearch — ricerca archivio CAD";
ResumeLayout(false); ResumeLayout(false);
} }
} }
File diff suppressed because it is too large. Load diff
+281
View File
@@ -0,0 +1,281 @@
namespace PrepStep;
/// <summary>Guida interattiva richiamabile da "Aiuto ▸ Guida" (F1): stessa guida completa di
/// docs/guida-utente.md (passo-passo fino a Creo Distributed Batch), pensata per chi apre
/// l'app senza aver letto i docs e senza esperienza pregressa. Il dettaglio tecnico (formato
/// .asm, schema DB, ecc.) resta solo in docs/architettura.md e affini, per non duplicarlo qui.
///
/// Gerarchia tipografica nel pannello di lettura (vedi ScriviSezione): titolo sezione in blu
/// primario/grassetto/14pt, corpo in grigio quasi-nero/10pt, passi numerati con il numero in
/// blu grassetto per farli scorrere a colpo d'occhio, callout con sfondo tinta piatta (nessun
/// bordo/ombra: RichTextBox non li supporta) — blu tenue per le note informative, ambra tenue
/// per gli avvisi importanti, con etichetta ("Nota"/"Attenzione") in grassetto a inizio blocco
/// invece di un colore isolato, così restano leggibili anche senza vedere i colori.</summary>
public class GuidaForm : Form
{
private sealed record Sezione(
string Titolo,
string Intro,
string[]? Passi = null,
string? NotaTesto = null,
bool NotaAttenzione = false);
// Palette: blu primario dell'app per titoli/passi, ambra tenue solo per gli avvisi che
// possono far perdere tempo se ignorati (es. l'errore Creo [-4]) — il rosso resta riservato
// ai messaggi di errore veri e propri altrove nell'app.
private static readonly Color ColoreTesto = Color.FromArgb(40, 40, 40);
private static readonly Color ColoreNotaInfoSfondo = Color.FromArgb(232, 240, 250);
private static readonly Color ColoreNotaInfoTesto = Color.FromArgb(15, 65, 120);
private static readonly Color ColoreNotaAttenzioneSfondo = Color.FromArgb(253, 240, 219);
private static readonly Color ColoreNotaAttenzioneTesto = Color.FromArgb(140, 90, 10);
private static readonly Sezione[] Sezioni =
{
new("Panoramica",
"Cevolani StepPrep prepara i file CAD Pro-ENGINEER/Creo da passare a Creo " +
"Distributed Batch per l'esportazione STEP: indicizza i file di una share di rete " +
"e guida attraverso 5 fasi, una alla volta, con \"Avanti\"/\"Indietro\".",
NotaTesto: "\"File ▸ Nuovo\" (Ctrl+N) azzera distinta/raw/verifica per ripartire " +
"con una nuova distinta base, senza dover ricaricare il database."),
new("Prima di iniziare",
"Prima di importare la prima distinta:",
Passi:
[
"Serve accesso alla share di rete con i file CAD (di default X:\\).",
"Serve l'estrazione \"Distinta Base\" da Fata (il gestionale): il file .xls " +
"così come Fata lo esporta, senza risalvarlo o convertirlo in altri " +
"formati — l'app riconosce da sola se sotto l'estensione .xls c'è XML o " +
"Excel binario vero.",
"Se il file segue la convenzione di nome DBP_<codice>_<timestamp>, l'app " +
"aggiunge da sola l'assieme generale come componente in più: non serve " +
"fare nulla di diverso.",
"Conviene impostare la cartella di lavoro giusta (fase 1, o \"File ▸ Scegli " +
"cartella di lavoro...\") prima di importare la distinta: è lì che " +
"finiscono CSV, \"raw\" e, dopo l'esportazione con Creo, \"step\".",
"Se la cartella di lavoro è già stata usata per un'altra distinta, conviene " +
"pulire \"raw\" e \"step\" prima di ripartire (\"File ▸ Pulisci raw e " +
"step...\"): \"File ▸ Nuovo\" azzera solo lo stato dell'app, non i file " +
"già copiati su disco.",
]),
new("1. Database",
"Carica o aggiorna l'indice \"cad_index.db\" a partire dalle cartelle di primo " +
"livello del disco/share (radice di default X:\\). Un pallino verde/rosso indica " +
"se un database è caricato. La cartella di lavoro, dove finiscono CSV, \"raw\" e " +
"\"step\", si cambia da \"File ▸ Scegli cartella di lavoro...\"."),
new("2. Importa Distinta Base",
"Due passi in sequenza:",
Passi:
[
"Carica la distinta: trascina il file .xls nella finestra, oppure premi " +
"\"Scegli file distinta...\". Le righe importate compaiono nella griglia " +
"(Componente/Descrizione) e viene scritto un CSV nella cartella di lavoro.",
"Verifica la presenza dei file sul server: compare solo dopo un import " +
"riuscito. Cerca ogni componente nel database — le righe della griglia si " +
"colorano di verde (trovato) o rosso (non trovato nell'indice).",
],
NotaTesto: "\"Avanti\" si sblocca solo dopo il passo 2, non basta il solo import: " +
"è il modo per accorgersi subito se il database è disallineato rispetto alla " +
"share prima di andare avanti.",
NotaAttenzione: true),
new("3. Crea cartella raw",
"Premi \"Crea cartella raw\": per ogni componente della distinta trovato " +
"nell'indice, l'app ne copia il file CAD dalla share nella cartella \"raw\" " +
"(dentro la cartella di lavoro). La griglia mostra il percorso CAD copiato per " +
"ogni componente."),
new("4. Trova componenti referenziati",
"Gli assiemi (.asm) referenziano altri file (sotto-assiemi, parti, componenti " +
"soppressi, riferimenti esterni) che Creo deve poter caricare insieme " +
"all'assieme principale, anche se non compaiono nella distinta del gestionale. " +
"Premi \"Trova componenti referenziati\": l'app legge i file .asm già in \"raw\", " +
"risolve ricorsivamente tutto l'albero e copia anche quei file in \"raw\".\n\n" +
"Si apre una finestra con l'elenco di quanto trovato e copiato, con in fondo " +
"eventuali codici non corrispondenti a nessun file indicizzato (possono essere " +
"disegni, componenti davvero mancanti, o falsi positivi — vale la pena " +
"controllarli). Da lì si può anche aprire direttamente la cartella \"raw\".",
NotaTesto: "Passo opzionale se la distinta contiene solo parti singole senza " +
"sotto-assiemi: in quel caso si può passare direttamente alla fase successiva."),
new("5. Verifica completezza",
"Mostra i file .asm della distinta già in \"raw\", trascinabili. Da qui il " +
"passaggio a Creo è manuale, fuori da quest'app:",
Passi:
[
"Apri Creo Distributed Batch (dal menu Start di Windows, o dal file " +
"ptcdbatch.bat nella cartella bin dell'installazione Creo).",
"Crea (o riusa, se già presente in azienda) un gruppo di attività per " +
"l'esportazione STEP: menu Actions ▸ New Task Group.",
"Aggiungi i file: trascina i .asm dalla finestra dell'app (o dalla cartella " +
"\"raw\" in Esplora risorse) sul gruppo di attività, oppure sfoglia e " +
"selezionali a mano.",
"Verifica che il gruppo di attività punti al config.pro/profilo di export " +
"STEP giusto (sezione Configuration Files) — di norma è già impostato " +
"una volta sola per tutti.",
"Avvia l'elaborazione (Start/Run): Distributed Batch legge ogni .asm, con " +
"tutto l'albero di sotto-componenti già raccolto nelle fasi 3-4, ed " +
"esporta i file STEP nella cartella \"step\" (dentro la cartella di " +
"lavoro, accanto a \"raw\").",
],
NotaTesto: "Se Distributed Batch fallisce con \"[-4] Dati di input non " +
"completi\", quasi sempre manca un file nell'albero dei componenti — Creo " +
"non dice quale. Torna alla fase 4 e controlla l'elenco dei \"non trovati\": " +
"è il punto di partenza per capire cosa recuperare prima di riprovare.",
NotaAttenzione: true),
new("Ripartire da capo",
"Quando devi processare un'altra distinta, usa \"File ▸ Nuovo\" (Ctrl+N): azzera " +
"CSV, raw e verifica delle fasi 2-5 e riporta al passo 2, senza dover ricaricare " +
"il database o la cartella di lavoro (restano quelli già impostati in fase 1).",
NotaTesto: "Per ripulire le cartelle \"raw\" e \"step\" della cartella di lavoro " +
"prima di un nuovo giro (senza toccare l'indice), usa \"File ▸ Pulisci raw e " +
"step...\"."),
};
public GuidaForm()
{
Text = "Guida";
ClientSize = new Size(780, 520);
MinimumSize = new Size(580, 400);
StartPosition = FormStartPosition.CenterParent;
Font = new Font("Segoe UI", 9.5f);
try
{
Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath);
}
catch (Exception ex) when (ex is IOException or ArgumentException)
{
// ponytail: nessuna icona nell'exe (es. lanciato da dotnet run) -> resta quella di default
}
var headerPanel = new Panel { Dock = DockStyle.Top, Height = 40, BackColor = SystemColors.ControlLight };
var lblHeader = new Label
{
Dock = DockStyle.Fill,
Text = "Guida",
TextAlign = ContentAlignment.MiddleLeft,
Padding = new Padding(16, 0, 0, 0),
Font = new Font(Font, FontStyle.Bold),
};
headerPanel.Controls.Add(lblHeader);
// Sidebar disegnata a mano (OwnerDrawFixed) invece della selezione blu di sistema:
// stessa identità visiva del resto dell'app (UiStile.ColorePrimario) invece del colore
// di selezione standard di Windows, che varia con il tema dell'utente.
var lista = new ListBox
{
Dock = DockStyle.Left,
Width = 230,
IntegralHeight = false,
BorderStyle = BorderStyle.None,
DrawMode = DrawMode.OwnerDrawFixed,
ItemHeight = 34,
BackColor = UiStile.ColoreSfondoCard,
};
foreach (var sezione in Sezioni)
lista.Items.Add(sezione.Titolo);
lista.DrawItem += (_, e) =>
{
if (e.Index < 0)
return;
bool selezionato = (e.State & DrawItemState.Selected) != 0;
using var sfondo = new SolidBrush(selezionato ? UiStile.ColorePrimario : UiStile.ColoreSfondoCard);
e.Graphics.FillRectangle(sfondo, e.Bounds);
var colore = selezionato ? Color.White : ColoreTesto;
using var testoFont = new Font(lista.Font, selezionato ? FontStyle.Bold : FontStyle.Regular);
var rettangoloTesto = new Rectangle(e.Bounds.X + 16, e.Bounds.Y, e.Bounds.Width - 20, e.Bounds.Height);
TextRenderer.DrawText(e.Graphics, lista.Items[e.Index].ToString(), testoFont, rettangoloTesto,
colore, TextFormatFlags.VerticalCenter | TextFormatFlags.Left | TextFormatFlags.EndEllipsis);
};
var testo = new RichTextBox
{
Dock = DockStyle.Fill,
ReadOnly = true,
BorderStyle = BorderStyle.None,
BackColor = Color.White,
Font = new Font(Font.FontFamily, 10f),
};
lista.SelectedIndexChanged += (_, _) =>
{
if (lista.SelectedIndex >= 0)
ScriviSezione(testo, Sezioni[lista.SelectedIndex]);
};
var (barraChiudi, azioniChiudi) = UiStile.FloatBar();
var btnChiudi = new Button { Text = "Chiudi" };
UiStile.Primario(btnChiudi);
UiStile.Arrotonda(btnChiudi, 10);
btnChiudi.Click += (_, _) => Close();
azioniChiudi.Controls.Add(btnChiudi);
CancelButton = btnChiudi;
var contenuto = new Panel { Dock = DockStyle.Fill, Padding = new Padding(24, 20, 24, 20), BackColor = Color.White };
contenuto.Controls.Add(testo);
Controls.Add(contenuto);
Controls.Add(lista);
Controls.Add(barraChiudi);
Controls.Add(headerPanel);
lista.SelectedIndex = 0;
}
/// <summary>Costruisce il pannello di lettura con gerarchia tipografica esplicita: titolo
/// (blu primario, grassetto, 14pt), corpo (grigio quasi-nero, 10pt), passi numerati (numero
/// in blu grassetto, indentati) e callout finale con sfondo a tinta piatta ed etichetta in
/// grassetto — informativo (blu tenue) o di attenzione (ambra tenue). Applica la
/// formattazione DOPO aver scritto il testo, selezionando il range appena aggiunto: è il
/// solo modo affidabile di formattare i paragrafi in un RichTextBox (impostare
/// SelectionFont/Color su una selezione vuota prima di AppendText non è garantito che
/// "si attacchi" al testo che segue).</summary>
private static void ScriviSezione(RichTextBox box, Sezione sezione)
{
box.Clear();
var titoloFont = new Font(box.Font.FontFamily, 14f, FontStyle.Bold);
var corpoFont = new Font(box.Font.FontFamily, 10f, FontStyle.Regular);
var corpoGrassetto = new Font(box.Font.FontFamily, 10f, FontStyle.Bold);
void Scrivi(string testo, Font font, Color colore, int indent = 0, Color? sfondo = null)
{
int inizio = box.TextLength;
box.AppendText(testo);
box.Select(inizio, box.TextLength - inizio);
box.SelectionFont = font;
box.SelectionColor = colore;
box.SelectionBackColor = sfondo ?? box.BackColor;
box.SelectionIndent = indent;
}
Scrivi(sezione.Titolo + "\n\n", titoloFont, UiStile.ColorePrimario);
Scrivi(sezione.Intro + "\n", corpoFont, ColoreTesto);
if (sezione.Passi is { Length: > 0 })
{
Scrivi("\n", corpoFont, ColoreTesto);
for (int i = 0; i < sezione.Passi.Length; i++)
{
Scrivi($"{i + 1}. ", corpoGrassetto, UiStile.ColorePrimario, indent: 8);
Scrivi(sezione.Passi[i] + "\n\n", corpoFont, ColoreTesto, indent: 8);
}
}
if (sezione.NotaTesto is not null)
{
var sfondo = sezione.NotaAttenzione ? ColoreNotaAttenzioneSfondo : ColoreNotaInfoSfondo;
var coloreEtichetta = sezione.NotaAttenzione ? ColoreNotaAttenzioneTesto : ColoreNotaInfoTesto;
string etichetta = sezione.NotaAttenzione ? "Attenzione" : "Nota";
Scrivi("\n", corpoFont, ColoreTesto);
Scrivi(" " + etichetta + "\n", corpoGrassetto, coloreEtichetta, indent: 12, sfondo: sfondo);
Scrivi(" " + sezione.NotaTesto + " \n", corpoFont, ColoreTesto, indent: 12, sfondo: sfondo);
}
box.SelectionStart = 0;
box.SelectionLength = 0;
}
}
@@ -2,7 +2,7 @@ using System.Diagnostics;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Microsoft.Data.Sqlite; using Microsoft.Data.Sqlite;
namespace CadSearchGui; namespace PrepStep;
public sealed record ScanProgress( public sealed record ScanProgress(
string Cartella, string Cartella,
@@ -40,13 +40,22 @@ public static class Indexer
CREATE INDEX IF NOT EXISTS idx_files_tipo ON files(tipo_file); CREATE INDEX IF NOT EXISTS idx_files_tipo ON files(tipo_file);
CREATE INDEX IF NOT EXISTS idx_files_cartella ON files(cartella_radice); CREATE INDEX IF NOT EXISTS idx_files_cartella ON files(cartella_radice);
CREATE VIEW IF NOT EXISTS files_ultima_versione AS -- DROP+CREATE invece di "IF NOT EXISTS": la vista e' ricalcolata a ogni avvio di scan,
-- cosi' un DB gia' esistente prende la definizione corrente senza reindicizzare.
DROP VIEW IF EXISTS files_ultima_versione;
CREATE VIEW files_ultima_versione AS
SELECT percorso_completo, percorso_relativo, nome_file, cartella_radice, famiglia, SELECT percorso_completo, percorso_relativo, nome_file, cartella_radice, famiglia,
codice, tipo_file, versione, dimensione_byte, data_modifica, scansionato_il codice, tipo_file, versione, dimensione_byte, data_modifica, scansionato_il
FROM ( FROM (
SELECT f.*, ROW_NUMBER() OVER ( SELECT f.*, ROW_NUMBER() OVER (
PARTITION BY cartella_radice, codice, tipo_file PARTITION BY cartella_radice, codice, tipo_file
ORDER BY versione DESC -- A parita' di versione (stesso codice in due sottocartelle della stessa
-- radice, es. una copia vecchia in una cartella di lavoro) vince il file
-- modificato piu' di recente: senza questo criterio il pareggio e' rotto in
-- modo arbitrario e puo' vincere la copia obsoleta, nascondendo del tutto
-- quella buona. Il file copiato in "raw" sarebbe allora un modello diverso da
-- quello che l'assieme si aspetta, e Creo non riesce ad aprirlo.
ORDER BY versione DESC, data_modifica DESC, percorso_completo
) AS rn ) AS rn
FROM files f FROM files f
) WHERE rn = 1; ) WHERE rn = 1;
@@ -6,6 +6,13 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms> <UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode>
<ApplicationIcon>app.ico</ApplicationIcon>
<AssemblyTitle>Cevolani StepPrep</AssemblyTitle>
<Product>Cevolani StepPrep</Product>
<Company>Cevolani</Company>
<Version>0.9.0</Version>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -16,7 +23,7 @@
<ItemGroup> <ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute"> <AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>CadSearchGui.Tests</_Parameter1> <_Parameter1>PrepStep.Tests</_Parameter1>
</AssemblyAttribute> </AssemblyAttribute>
</ItemGroup> </ItemGroup>
@@ -1,4 +1,4 @@
namespace CadSearchGui; namespace PrepStep;
static class Program static class Program
{ {
@@ -1,73 +1,73 @@
namespace CadSearchGui; namespace PrepStep;
public sealed class ProgressForm : Form public sealed class ProgressForm : Form
{ {
private readonly Label _lblCartella = new(); private readonly Label _lblCartella = new();
private readonly ProgressBar _progressBar = new(); private readonly ProgressBar _bar = new();
private readonly Label _lblConteggio = new(); private readonly Label _lblConteggio = new();
private readonly Label _lblDettaglio = new(); private readonly Label _lblDettaglio = new();
private readonly Label _lblEta = new(); private readonly Label _lblEta = new();
private readonly Button _btnAnnulla = new(); private readonly Button _btnAnnulla = new();
private readonly ToolTip _tipCartella = new();
private const int Margine = 16;
public event EventHandler? Annullato; public event EventHandler? Annullato;
public ProgressForm() public ProgressForm()
{ {
Text = "Aggiornamento indice in corso..."; Text = "Aggiornamento indice";
FormBorderStyle = FormBorderStyle.FixedDialog; FormBorderStyle = FormBorderStyle.FixedDialog;
StartPosition = FormStartPosition.CenterParent; StartPosition = FormStartPosition.CenterParent;
MinimizeBox = false; MinimizeBox = false;
MaximizeBox = false; MaximizeBox = false;
ControlBox = false; ControlBox = false;
ClientSize = new Size(460, 190); ClientSize = new Size(420, 190);
_lblCartella.Text = "Preparazione..."; _lblCartella.Text = "Preparazione...";
_lblCartella.Location = new Point(16, 16); _lblCartella.Location = new Point(Margine, Margine);
_lblCartella.AutoSize = true; // AutoSize=false + AutoEllipsis: un percorso lungo (share di rete) va troncato con "..."
_lblCartella.Font = new Font(Font, FontStyle.Bold); // invece di sforare il bordo del dialog, che è FixedDialog e non si può allargare. Il
// testo completo resta disponibile nel tooltip.
_lblCartella.AutoSize = false;
_lblCartella.AutoEllipsis = true;
_lblCartella.Size = new Size(ClientSize.Width - Margine * 2, 20);
_progressBar.Location = new Point(16, 44); _bar.Location = new Point(Margine, 40);
_progressBar.Size = new Size(428, 24); _bar.Size = new Size(ClientSize.Width - Margine * 2, 24);
_progressBar.Minimum = 0; _bar.Minimum = 0;
_progressBar.Maximum = 1000; _bar.Maximum = 100;
_progressBar.Style = ProgressBarStyle.Marquee;
_lblConteggio.Location = new Point(16, 80); _lblConteggio.Location = new Point(Margine, 72);
_lblConteggio.AutoSize = true; _lblConteggio.AutoSize = true;
_lblDettaglio.Location = new Point(16, 104); _lblDettaglio.Location = new Point(Margine, 96);
_lblDettaglio.AutoSize = true; _lblDettaglio.AutoSize = true;
_lblDettaglio.ForeColor = SystemColors.GrayText;
_lblEta.Location = new Point(16, 128); _lblEta.Location = new Point(Margine, 120);
_lblEta.AutoSize = true; _lblEta.AutoSize = true;
_lblEta.Font = new Font(Font, FontStyle.Bold);
_btnAnnulla.Text = "Annulla"; _btnAnnulla.Text = "Annulla";
_btnAnnulla.Location = new Point(354, 152);
_btnAnnulla.Size = new Size(90, 28); _btnAnnulla.Size = new Size(90, 28);
_btnAnnulla.Click += (_, e) => Annullato?.Invoke(this, e); _btnAnnulla.Click += (_, e) => Annullato?.Invoke(this, e);
Controls.AddRange(new Control[] Controls.AddRange(new Control[]
{ {
_lblCartella, _progressBar, _lblConteggio, _lblDettaglio, _lblEta, _btnAnnulla _lblCartella, _bar, _lblConteggio, _lblDettaglio, _lblEta, _btnAnnulla
}); });
_btnAnnulla.Location = new Point(
ClientSize.Width - Margine - _btnAnnulla.Width, ClientSize.Height - Margine - _btnAnnulla.Height);
} }
public void Aggiorna(ScanProgress p) public void Aggiorna(ScanProgress p)
{ {
_lblCartella.Text = $"Scansione {p.Cartella}..."; _lblCartella.Text = $"Scansione: {p.Cartella}";
_tipCartella.SetToolTip(_lblCartella, _lblCartella.Text);
if (p.PrevistiTotali > 0) _bar.Value = p.PrevistiTotali > 0
{ ? (int)Math.Clamp(100.0 * p.FattiTotali / p.PrevistiTotali, 0, 100)
_progressBar.Style = ProgressBarStyle.Continuous; : 0;
double frazione = Math.Min(1.0, (double)p.FattiTotali / p.PrevistiTotali);
_progressBar.Value = (int)(frazione * _progressBar.Maximum);
}
else
{
_progressBar.Style = ProgressBarStyle.Marquee;
}
_lblConteggio.Text = p.PrevistiTotali > 0 _lblConteggio.Text = p.PrevistiTotali > 0
? $"{p.FattiTotali:N0} / ~{p.PrevistiTotali:N0} file" ? $"{p.FattiTotali:N0} / ~{p.PrevistiTotali:N0} file"
@@ -1,4 +1,4 @@
namespace CadSearchGui; namespace PrepStep;
public sealed class ProgressoComponentiForm : Form public sealed class ProgressoComponentiForm : Form
{ {
@@ -1,6 +1,6 @@
using System.Diagnostics; using System.Diagnostics;
namespace CadSearchGui; namespace PrepStep;
/// <summary>Mostra l'esito della risoluzione ricorsiva dell'albero componenti: i PRT e i /// <summary>Mostra l'esito della risoluzione ricorsiva dell'albero componenti: i PRT e i
/// sotto-assiemi trovati nell'indice e già copiati in "raw" a partire dagli assiemi iniziali.</summary> /// sotto-assiemi trovati nell'indice e già copiati in "raw" a partire dagli assiemi iniziali.</summary>
@@ -15,12 +15,20 @@ public class PrtMancantiForm : Form
ClientSize = new Size(720, 440); ClientSize = new Size(720, 440);
StartPosition = FormStartPosition.CenterParent; StartPosition = FormStartPosition.CenterParent;
Font = new Font("Segoe UI", 9.5f); Font = new Font("Segoe UI", 9.5f);
try
{
Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath);
}
catch (Exception ex) when (ex is IOException or ArgumentException)
{
// ponytail: nessuna icona nell'exe (es. lanciato da dotnet run) -> resta quella di default
}
var lblInfo = new Label var lblInfo = new Label
{ {
Dock = DockStyle.Top, Dock = DockStyle.Top,
AutoSize = false, AutoSize = true,
Height = 44, MaximumSize = new Size(ClientSize.Width - 24, 0),
Padding = new Padding(12, 8, 12, 8), Padding = new Padding(12, 8, 12, 8),
Text = $"Copiati in \"{cartellaRaw}\": {numPrt} PRT e {numAsm} sotto-assiemi trovati nell'indice." Text = $"Copiati in \"{cartellaRaw}\": {numPrt} PRT e {numAsm} sotto-assiemi trovati nell'indice."
+ (codiciNonTrovati.Count > 0 + (codiciNonTrovati.Count > 0
@@ -56,7 +64,17 @@ public class PrtMancantiForm : Form
grid.Rows[i].DefaultCellStyle.ForeColor = Color.DarkRed; grid.Rows[i].DefaultCellStyle.ForeColor = Color.DarkRed;
} }
var btnApriCartella = new Button { Text = "Apri cartella raw", Dock = DockStyle.Bottom, Height = 34 }; var (pannelloAzioni, flowAzioni) = UiStile.FloatBar();
var btnChiudi = new Button { Text = "Chiudi" };
UiStile.Primario(btnChiudi);
UiStile.Arrotonda(btnChiudi, 10);
btnChiudi.Click += (_, _) => Close();
flowAzioni.Controls.Add(btnChiudi);
var btnApriCartella = new Button { Text = "Apri cartella raw", Margin = new Padding(8, 0, 0, 0) };
UiStile.SecondarioPieno(btnApriCartella);
UiStile.Arrotonda(btnApriCartella, 10);
btnApriCartella.Click += (_, _) => btnApriCartella.Click += (_, _) =>
{ {
try { Process.Start("explorer.exe", cartellaRaw); } try { Process.Start("explorer.exe", cartellaRaw); }
@@ -66,9 +84,13 @@ public class PrtMancantiForm : Form
MessageBoxButtons.OK, MessageBoxIcon.Error); MessageBoxButtons.OK, MessageBoxIcon.Error);
} }
}; };
flowAzioni.Controls.Add(btnApriCartella);
Controls.Add(grid); Controls.Add(grid);
Controls.Add(lblInfo); Controls.Add(lblInfo);
Controls.Add(btnApriCartella); Controls.Add(pannelloAzioni);
CancelButton = btnChiudi;
Resize += (_, _) => lblInfo.MaximumSize = new Size(ClientSize.Width - 24, 0);
} }
} }
+175
View File
@@ -0,0 +1,175 @@
using System.Drawing.Drawing2D;
namespace PrepStep;
/// <summary>Stile bottoni condiviso tra la form principale e i dialog secondari: piatto con
/// bordo sottile e hover, per un aspetto coerente sia per l'azione primaria di ogni fase
/// (blu pieno) sia per le azioni secondarie, al posto del bottone grigio di default.</summary>
internal static class UiStile
{
/// <summary>Blu del bottone primario, esposto qui per essere riusato anche da elementi
/// non-bottone (es. il colore di selezione delle griglie in Form1) invece di duplicare
/// l'RGB.</summary>
public static readonly Color ColorePrimario = Color.FromArgb(0, 99, 177);
/// <summary>Sfondo/bordo delle card di stato (es. riepilogo Database/cartella di lavoro in
/// fase 1): un grigio-azzurro appena percettibile rispetto al bianco del resto della
/// pagina, sufficiente a separare "stato attuale" da "istruzioni" senza ombre/gradienti.</summary>
public static readonly Color ColoreSfondoCard = Color.FromArgb(247, 249, 251);
public static readonly Color ColoreBordoCard = Color.FromArgb(226, 231, 236);
// AutoSize+Padding invece di una Size fissa in pixel: il bottone si dimensiona sempre
// al proprio testo (a qualunque lunghezza/font), niente più testo che va a capo e viene
// tagliato in verticale quando non ci sta nella larghezza indovinata a mano.
private static void DimensionamentoAutomatico(Button b)
{
b.AutoSize = true;
b.AutoSizeMode = AutoSizeMode.GrowAndShrink;
b.Padding = new Padding(18, 4, 18, 4);
b.MinimumSize = new Size(0, 32);
}
public static void Primario(Button b)
{
DimensionamentoAutomatico(b);
b.FlatStyle = FlatStyle.Flat;
b.FlatAppearance.BorderSize = 0;
b.FlatAppearance.MouseOverBackColor = Color.FromArgb(0, 84, 152);
b.FlatAppearance.MouseDownBackColor = Color.FromArgb(0, 70, 130);
b.BackColor = ColorePrimario;
b.ForeColor = Color.White;
b.Font = new Font(b.Font, FontStyle.Bold);
b.Cursor = Cursors.Hand;
}
public static void Secondario(Button b)
{
DimensionamentoAutomatico(b);
b.FlatStyle = FlatStyle.Flat;
b.FlatAppearance.BorderColor = Color.FromArgb(190, 195, 200);
b.FlatAppearance.BorderSize = 1;
b.FlatAppearance.MouseOverBackColor = Color.FromArgb(230, 238, 245);
b.FlatAppearance.MouseDownBackColor = Color.FromArgb(213, 226, 238);
b.BackColor = Color.White;
b.Cursor = Cursors.Hand;
}
/// <summary>Come Secondario, ma pieno invece che a bordo: sulle card bianche delle floatbar
/// (vedi FloatBar) il bordo sottile di Secondario è quasi invisibile su sfondo bianco, e il
/// bottone risulterebbe visivamente diverso dal Primario pieno accanto — stessa costruzione
/// (nessun bordo, riempimento pieno), solo grigio chiaro invece che blu.</summary>
public static void SecondarioPieno(Button b)
{
DimensionamentoAutomatico(b);
b.FlatStyle = FlatStyle.Flat;
b.FlatAppearance.BorderSize = 0;
b.FlatAppearance.MouseOverBackColor = Color.FromArgb(230, 233, 237);
b.FlatAppearance.MouseDownBackColor = Color.FromArgb(213, 217, 222);
b.BackColor = Color.FromArgb(241, 243, 245);
b.Cursor = Cursors.Hand;
}
/// <summary>Arrotonda gli angoli del controllo (bottone o card). Si aggancia a SizeChanged
/// invece di calcolare la Region una volta sola: con AutoSize la dimensione finale si
/// conosce solo a layout avvenuto (dopo Text/Padding), non subito dopo la creazione del
/// controllo.</summary>
public static void Arrotonda(Control c, int raggio = 6)
{
c.SizeChanged += (_, _) => AggiornaRegione(c, raggio);
AggiornaRegione(c, raggio);
}
private static void AggiornaRegione(Control c, int raggio)
{
if (c.Width <= 0 || c.Height <= 0)
return;
int d = raggio * 2;
var rect = new Rectangle(Point.Empty, c.Size);
using var percorso = new GraphicsPath();
percorso.AddArc(rect.X, rect.Y, d, d, 180, 90);
percorso.AddArc(rect.Right - d, rect.Y, d, d, 270, 90);
percorso.AddArc(rect.Right - d, rect.Bottom - d, d, d, 0, 90);
percorso.AddArc(rect.X, rect.Bottom - d, d, d, 90, 90);
percorso.CloseFigure();
c.Region?.Dispose();
c.Region = new Region(percorso);
}
/// <summary>Card di stato: sfondo/bordo leggermente distinti dalla pagina, angoli arrotondati,
/// padding interno. Il bordo va disegnato a mano (Paint) perché Region ritaglia il
/// controllo ma non lascia spazio a un Border WinForms nativo sugli angoli arrotondati.</summary>
public static Panel Card()
{
var card = new Panel
{
Dock = DockStyle.Top,
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
BackColor = ColoreSfondoCard,
Padding = new Padding(18, 16, 18, 16),
Margin = new Padding(0, 0, 0, 16),
};
card.Paint += (_, e) =>
{
using var penna = new Pen(ColoreBordoCard);
e.Graphics.DrawRectangle(penna, 0, 0, card.Width - 1, card.Height - 1);
};
Arrotonda(card, 8);
return card;
}
/// <summary>Barra di bottoni "flottante" (Avanti/Indietro, Apri cartella raw/Chiudi, ecc.):
/// una card bianca staccata su tutti e 4 i lati dal bordo della finestra — non un footer a
/// tutta larghezza — con angoli arrotondati e bordo sottile, sospesa sopra lo sfondo grigio
/// della pagina invece che ancorata al bordo. Lo sfondo del contenitore esterno deve
/// combaciare con quello di default della Form (nessun BackColor impostato altrove:
/// SystemColors.Control) perché il "distacco" si veda solo nel margine attorno alla card,
/// non in una fascia di colore diverso.</summary>
public static (Panel Contenitore, FlowLayoutPanel Bottoni) FloatBar()
{
// Altezza minima per non tagliare il contenuto: bottone (MinimumSize.Height = 32) +
// padding verticale della riga bottoni (12+12) + padding verticale del contenitore
// esterno (10+16) = 82, con un po' di margine in più.
var contenitore = new Panel
{
Dock = DockStyle.Bottom,
Height = 88,
BackColor = SystemColors.Control,
Padding = new Padding(16, 10, 16, 16),
};
var card = new Panel { Dock = DockStyle.Fill, BackColor = Color.White };
card.Paint += (_, e) =>
{
using var penna = new Pen(ColoreBordoCard);
e.Graphics.DrawRectangle(penna, 0, 0, card.Width - 1, card.Height - 1);
};
Arrotonda(card, 14);
var bottoni = new FlowLayoutPanel
{
Dock = DockStyle.Fill,
FlowDirection = FlowDirection.RightToLeft,
Padding = new Padding(16, 12, 16, 12),
};
card.Controls.Add(bottoni);
contenitore.Controls.Add(card);
return (contenitore, bottoni);
}
/// <summary>Forza le stesse dimensioni per due bottoni della stessa barra (es. Avanti/
/// Indietro): AutoSize li dimensiona in base al proprio testo, e con testi di lunghezza
/// diversa (o solo uno dei due in grassetto, come Primario/Secondario) risulterebbero
/// disallineati sia in larghezza che in altezza — un dettaglio che un designer non
/// lascerebbe passare. Va chiamato dopo aver impostato Text/Font su entrambi.</summary>
public static void LarghezzaUguale(params Button[] bottoni)
{
int larghezza = bottoni.Max(b => b.PreferredSize.Width);
int altezza = bottoni.Max(b => b.PreferredSize.Height);
foreach (var b in bottoni)
b.MinimumSize = new Size(larghezza, altezza);
}
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

+42 -28
View File
@@ -1,34 +1,46 @@
# CadSearchGui # Cevolani StepPrep
App Windows Forms (.NET 10) per indicizzare, cercare e recuperare i file CAD App Windows Forms (.NET 10, progetto `PrepStep`) per indicizzare, cercare e
Pro-ENGINEER/Creo di un disco/share di rete, e per trasformare l'estrazione recuperare i file CAD Pro-ENGINEER/Creo di un disco/share di rete, e per trasformare
"Distinta Base" del gestionale (file `.xls`/`.xml`) in un CSV con il percorso l'estrazione "Distinta Base" del gestionale (file `.xls`/`.xml`) in un CSV con il
CAD di ogni componente. percorso CAD di ogni componente, fino a raccogliere in `raw/` tutto il necessario per
l'esportazione STEP con **Creo Distributed Batch**: è un pre-processo per quel
passaggio, non lo sostituisce.
## Funzionalità ## Funzionalità
1. **Indice CAD** — all'avvio carica automaticamente `cad_index.db` (se 1. **Indice CAD** — all'avvio carica automaticamente l'ultimo `cad_index.db`
presente accanto all'eseguibile). Se manca, si può selezionare un DB usato (ricordato tra un avvio e l'altro). Se manca, si può selezionare un DB
esistente con **Apri DB...** o generarne uno nuovo con **Aggiorna esistente con **Apri DB...** o generarne uno nuovo con **Aggiorna
indice**, che scansiona il disco (di default `X:\`, tutte le cartelle di primo indice**, che scansiona il disco (di default `X:\`, tutte le cartelle di primo
livello che trova) e indicizza tutti i file. livello che trova) e indicizza tutti i file. La **cartella di lavoro** (dove
finiscono il CSV e la cartella `raw/`; gli STEP esportati con Creo vanno in
`step/`, dentro la stessa cartella) è proposta automaticamente e ricordata tra
un avvio e l'altro, modificabile in qualunque momento da **File → Scegli
cartella di lavoro...**.
2. **Importa Distinta Base** — trascina (o scegli da file) l'estrazione 2. **Importa Distinta Base** — trascina (o scegli da file) l'estrazione
`.xls`/`.xml` del gestionale (foglio "Distinta Base", colonne `.xls`/`.xml` del gestionale (foglio "Distinta Base", colonne
Componente/Descrizione): viene generato un CSV con anteprima in griglia. Componente/Descrizione): viene generato un CSV nella cartella di lavoro, con
Se il file segue la convenzione di nome `DBP_<codice>_<timestamp>` del anteprima in griglia. Se il file segue la convenzione di nome
gestionale, l'assieme generale dell'estrazione viene aggiunto come riga `DBP_<codice>_<timestamp>` del gestionale, l'assieme generale dell'estrazione
in più nel CSV (il foglio elenca solo i componenti sotto l'assieme, non viene aggiunto come riga in più nel CSV (il foglio elenca solo i componenti
l'assieme stesso). sotto l'assieme, non l'assieme stesso). Il bottone **Verifica presenza**
(compare solo dopo un import riuscito) cerca i componenti nel database — è
questo passo, non il solo import, a sbloccare la fase successiva.
3. **Crea cartella raw** — cerca nell'indice il percorso CAD di ogni 3. **Crea cartella raw** — cerca nell'indice il percorso CAD di ogni
componente importato (assieme generale incluso) e copia i file trovati componente importato (assieme generale incluso) e copia i file trovati
in una cartella `raw/` accanto al file di distinta originale. in una cartella `raw/` dentro la cartella di lavoro.
4. **Trova componenti referenziati** — legge i metadati dei file `.asm` in 4. **Trova componenti referenziati** — legge i metadati dei file `.asm` in
`raw/` per trovare ricorsivamente i sotto-assiemi/parti che li `raw/` per trovare ricorsivamente i sotto-assiemi/parti che li
compongono, copiandoli a loro volta in `raw/`. compongono, copiandoli a loro volta in `raw/` (passo opzionale se la
5. **Verifica completezza assiemi** / **Report STEP realizzati** — confronta distinta contiene solo parti singole).
la distinta con l'indice (per capire se manca qualche sotto-componente) 5. **Trascina su Creo** — mostra i file `.asm` della distinta già in `raw/`,
e con la cartella `step/` (per capire quali STEP sono già stati trascinabili su Creo Distributed Batch per l'esportazione STEP (passo
realizzati). 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)
(stessa sintesi anche in-app da **Aiuto → Guida**, F1).
## Requisiti ## Requisiti
@@ -40,7 +52,7 @@ CAD di ogni componente.
## Build ## Build
```powershell ```powershell
cd CadSearchGui cd PrepStep
dotnet build -c Release dotnet build -c Release
``` ```
@@ -49,15 +61,15 @@ dotnet build -c Release
Da sorgente: Da sorgente:
```powershell ```powershell
cd CadSearchGui cd PrepStep
dotnet run -c Release dotnet run -c Release
``` ```
Oppure l'eseguibile già compilato: Oppure l'eseguibile già compilato:
```powershell ```powershell
cd CadSearchGui\bin\Release\net10.0-windows cd PrepStep\bin\Release\net10.0-windows
.\CadSearchGui.exe .\PrepStep.exe
``` ```
## Pubblicazione (eseguibile standalone) ## Pubblicazione (eseguibile standalone)
@@ -66,15 +78,17 @@ Genera un `.exe` self-contained per Windows x64, senza bisogno del runtime
.NET installato sulla macchina di destinazione: .NET installato sulla macchina di destinazione:
```powershell ```powershell
cd CadSearchGui cd PrepStep
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -o publish dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -o publish
``` ```
L'eseguibile risultante è in `CadSearchGui\publish\CadSearchGui.exe`. L'eseguibile risultante è in `PrepStep\publish\PrepStep.exe` — un unico
file, si copia e si lancia su un'altra macchina Windows senza bisogno di .NET
installato.
## Struttura del progetto ## Struttura del progetto
Tutto il codice è in `CadSearchGui/` (unico progetto nella solution, GUI e Tutto il codice è in `PrepStep/` (unico progetto nella solution, GUI e
indicizzazione integrate): indicizzazione integrate):
- `Program.cs` — entry point. - `Program.cs` — entry point.
@@ -91,12 +105,12 @@ sono tracciati in git (vedi `.gitignore`).
## Test ## Test
`CadSearchGui.Tests/` contiene unit test (regex di estrazione dei componenti `PrepStep.Tests/` contiene unit test (regex di estrazione dei componenti
dai `.asm`, parsing del nome file Pro-E) e integration test (schema/vista SQL dai `.asm`, parsing del nome file Pro-E) e integration test (schema/vista SQL
reale, con focus sui bug di case-sensitivity/duplicati già risolti). Da reale, con focus sui bug di case-sensitivity/duplicati già risolti). Da
eseguire: eseguire:
```powershell ```powershell
cd CadSearchGui.Tests cd PrepStep.Tests
dotnet test dotnet test
``` ```
+295
View File
@@ -0,0 +1,295 @@
# Architettura di PrepStep (Cevolani StepPrep)
Panoramica di tutti i file sorgente e di come collaborano. `PrepStep` è il nome
tecnico del progetto (cartella, namespace, csproj, eseguibile `PrepStep.exe`);
**Cevolani StepPrep** è 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
formato binario `.asm` vedi `docs/formato-asm.md` (euristica testuale) e
`docs/formato-asm-record.md` (parser esatto dei record); per la parallelizzazione della
copia in `raw/` vedi `docs/copia-raw-parallela.md`.
## Vista d'insieme
Un solo eseguibile WinForms (nessuna CLI, nessun processo esterno): indicizza i file CAD di
una share di rete in un DB SQLite locale, poi guida l'utente in un wizard a 5 fasi che, a
partire da una distinta base esportata dal gestionale, raccoglie in locale (`raw/`) tutti i
file CAD dei componenti e dei loro sotto-assiemi e verifica che l'indice li copra, come
pre-process prima dell'esportazione STEP con Creo Distributed Batch (manuale, fuori
dall'app: i file esportati finiscono in `step/`, dentro la cartella di lavoro).
```
Distinta (.xls/.xml) → CSV (componente;descrizione) → raw/ (CAD copiati dall'indice)
│
scansione .asm ─────┘ (sotto-assiemi/parti mancanti)
│
drag&drop .asm su Creo → Distributed Batch → step/ (esportazione manuale)
```
## Il processo, dall'inizio alla fine
Le 5 fasi sono una pipeline: ognuna consuma quello che ha prodotto la precedente, e il
bottone "Avanti" si sblocca solo quando il risultato c'è (`AvantiConsentito`). Sotto, cosa
entra e cosa esce da ciascuna — il dettaglio dei metodi è nella tabella della sezione
successiva.
**1. Indice della share → `cad_index.db`.** `Indexer.ScanAsync` cammina tutte le cartelle di
primo livello sotto `X:\`, riconosce i nomi Pro-E `<codice>.<tipo>.<versione>` e fa un upsert
incrementale (confronto su percorso+dimensione+data), così i giri successivi costano solo il
delta. Qui si sceglie anche la **cartella di lavoro**, che da qui in poi ospita tutto il resto.
**2. Distinta del gestionale → CSV.** `DistintaBaseImporter.ConvertiInCsv` riduce l'export a
due colonne `componente;descrizione`. Se il nome del file segue la convenzione
`DBP_<codice>_<timestamp>` aggiunge l'assieme generale come riga in più: da quel momento è un
componente come tutti gli altri, nessun caso speciale a valle.
**3. Componenti del CSV → `raw/`.** Per ogni codice del CSV `TrovaPercorsiCad` cerca
nell'indice l'ultima versione (vista `files_ultima_versione`) e `CopiaFileTrovati` la copia in
`raw/` in parallelo. Finisce qui il livello "distinta": in `raw/` ci sono i componenti
elencati dal gestionale, non ancora quello che serve ad aprirli.
**4. Chiusura ricorsiva dell'albero → `raw/` completa.** È il cuore del programma, ed è
l'unica fase che deve ragionare come Creo invece che come il gestionale.
`RisolviAlberoComponenti` tiene una coda di `.asm` da leggere, partendo da quelli già in
`raw/`, e per ognuno raccoglie **tre liste diverse**, che non vanno confuse (vedi
`docs/formato-asm-record.md` §7.3 e §7.5):
| Lista | Da dove | Ricorsione | Se non è nell'indice |
|---|---|---|---|
| Componenti della distinta | sezione `members` (`EstraiCodiciReferenziati`) | sì | va nei "non trovati" |
| Componenti **soppressi** | stringhe di piazzamento (`EstraiCodiciSoppressi`) | no — Creo non carica i figli di un soppresso | ignorato: non è un componente, segnalarlo sarebbe un falso allarme |
| **Riferimenti esterni** | tabella dei modelli citati (`EstraiCodiciRiferimentiEsterni`) | sì — Creo carica il modello citato con tutto il suo albero | ignorato, stesso motivo |
Ogni codice trovato viene copiato in `raw/` scegliendo **quale** copia con `ScegliPerCartella`
(§7.4: lo stesso codice esiste in più cartelle della share nel 13% dei casi, e vince quella
nella stessa cartella dell'assieme che lo referenzia); se è un `.asm` torna in coda e il giro
ricomincia, finché restano solo foglie. I rami raggiunti per riferimento esterno viaggiano con
un flag `Esterno` che li tiene fuori dall'elenco dei "non trovati": quell'elenco continua a
parlare solo della distinta, che è quello che l'utente si aspetta di leggerci.
Il motivo per cui questa fase è più complicata di come sembra: **la lista dei componenti e la
lista dei file necessari al caricamento non coincidono**, e la seconda è più lunga. Quando si
sbaglia, Creo Distributed Batch non dice quale file manca — dice solo `[-4] Dati di input non
completi`.
**5. Verifica e consegna a Creo.** `AggiornaAnteprimaAsm` mostra i `.asm` della distinta già
in `raw/` come icone trascinabili. L'esportazione STEP vera è manuale e fuori dall'app: si
trascinano i `.asm` su Creo Distributed Batch, che scrive in `step/`.
## Entry point
- **Program.cs** — `Main()` standard WinForms: `Application.Run(new Form1())`. Nessuna logica.
## Finestra principale (wizard)
- **Form1.cs** — tutta la UI è costruita in codice (`BuildUi`/`BuildFase*`), non con il
designer visuale. È un wizard: `_panelFasi[]` contiene un `Panel` per fase, uno solo
visibile alla volta (`VaiAllaFase`); si avanza con "Avanti" solo se `AvantiConsentito`
ritorna vero per la fase corrente. `Form1.Designer.cs` contiene solo lo scheletro minimo
generato dal template (`InitializeComponent`/`Dispose`), non è usato per costruire i
controlli.
Le 5 fasi e i metodi principali dietro ciascuna:
| # | Fase | Bottone → metodo | Cosa fa |
|---|---|---|---|
| 1 | Database | "Aggiorna indice" → `Indexer.ScanAsync` | Crea/aggiorna `cad_index.db` scansionando la share; un pallino verde/rosso accanto al titolo indica se un DB è caricato. La cartella di lavoro non è mostrata qui (cambiabile in qualunque momento da "File → Scegli cartella di lavoro...") |
| 2 | Importa Distinta Base | selezione file/drag&drop → `ImportaDistinta` → `DistintaBaseImporter.ConvertiInCsv`, poi "Verifica presenza" → `VerificaCodiciNelDb` | Xls/Xml gestionale → CSV componente;descrizione, scritto nella cartella di lavoro; "Verifica presenza" (visibile solo dopo un import riuscito) cerca i componenti nell'indice e colora le righe della griglia — è quella che sblocca "Avanti", non il solo import |
| 3 | Crea cartella raw | "Crea cartella raw" → `CreaCartellaRaw` | `TrovaPercorsiCad` + `CopiaFileTrovati` (parallela) → `raw/` dentro la cartella di lavoro |
| 4 | Trova componenti referenziati | "Trova componenti referenziati" → `TrovaPrtReferenziati` → `RisolviAlberoComponenti` | Legge i `.asm` in `raw/`, risolve ricorsivamente sotto-assiemi/parti nell'indice e li copia anch'essi in `raw/`, insieme ai componenti soppressi e ai modelli citati per riferimento esterno che servono a Creo per aprire l'assieme |
| 5 | Verifica completezza | (ingresso in fase) → `AggiornaAnteprimaAsm` | Mostra le icone trascinabili dei `.asm` della distinta già in `raw/`, da trascinare su Creo per l'esportazione STEP (Distributed Batch) |
Stato condiviso tra le fasi (campi privati di `Form1`): `_dbPath`, `_cartellaLavoro`,
`_percorsoInputCorrente`, `_righeCorrenti` (righe del CSV), `_ultimaVerifica` (cache percorsi
CAD), `_ultimaCartellaRaw`.
Menu (`MenuStrip`, in cima alla finestra): "File → Nuovo" (Ctrl+N) azzera lo
stato delle fasi 2-5 (`NuovaEstrazione`) e va direttamente alla fase 2, senza passare dalla
fase 1: database e cartella di lavoro restano quelli già caricati, non serve rifare lo scan;
"File → Scegli cartella di lavoro..." → `ScegliCartellaLavoro`, cambia in qualunque momento
la destinazione di CSV/`raw/`/`step/`; "File → Pulisci raw e step..." svuota le cartelle
`raw/`/`step/` nella cartella di lavoro (`PulisciRawEStep`, non tocca l'indice); "Aiuto →
Guida" (F1) apre `GuidaForm`, sintesi interattiva delle 5 fasi per chi non ha letto questi
docs.
Altre responsabilità in Form1.cs:
- `TrovaPercorsiCad` / `CaricaCodiciNoti` — query sul DB (`files_ultima_versione`), la
seconda carica tutti i codici prt/asm noti in un colpo solo (usata sia per risolvere i
componenti sia da `AssemblyComponentScanner` per validare i token generici).
- `CopiaFileTrovati` — copia (in parallelo, grado 6) i file trovati in `raw/`, saltando
silenziosamente quelli irraggiungibili (share offline/permessi).
- `PulisciRawEStep` — svuota `raw/` e `step/` nella cartella di lavoro (manutenzione,
richiede conferma).
- `InizializzaDatabase` / `ScegliDatabase` — localizzazione del DB (ultimo usato, salvato in
`%AppData%/PrepStep/ultimo_db.txt`, o `cad_index.db` accanto all'eseguibile).
- `InizializzaCartellaLavoro` / `ScegliCartellaLavoro` — stesso pattern per la cartella di
lavoro (`ultima_cartella_lavoro.txt`), fallback `AppContext.BaseDirectory` se non c'è
ancora nulla di salvato: nessuna dipendenza da un marker di repo, funziona identica
sull'exe pubblicato standalone spostato su un'altra macchina.
## Indicizzazione
- **Indexer.cs** — `ScanAsync` cammina tutte le cartelle di primo livello sotto la radice
(scoperte dinamicamente, non un elenco fisso) con un walk manuale a stack (`Walk`, più
veloce di `EnumerateFiles(..., AllDirectories)` su share con centinaia di migliaia di
file), esclude le cartelle `temp_*`, estrae da ogni nome file il pattern Pro-E
`<codice>.<tipo>.<versione>` (`ParseFilename`, regex), e fa un upsert incrementale riga per
riga confrontando `percorso_completo` + dimensione + data modifica. Progresso riportato via
`IProgress<ScanProgress>` (marshalato sul thread UI). Contiene anche lo schema SQL: tabella
`files` e vista `files_ultima_versione` (ultima versione per `codice`+`tipo_file` via
`ROW_NUMBER() OVER (...)`, non self-join+`GROUP BY` per evitare tempi quasi quadratici su
~200k righe).
## Lettura dei componenti referenziati in un `.asm`
`Form1.EstraiCodiciReferenziati` (usata da `RisolviAlberoComponenti`, fase 4) prova prima
`AsmRecordReader.EstraiCodiciReferenziati` e ripiega su
`AssemblyComponentScanner.EstraiCodiciReferenziati` solo se il primo lancia
`InvalidDataException` (file che non rispetta il formato validato dal reverse engineering).
- **AsmRecordReader.cs** — legge esattamente la sezione `members` del binario `.asm` (id/tipo/
nome dei componenti diretti, con risoluzione del nome istanza di family table dal parametro
`ConfigName`): nessuna euristica, struttura a record. Lancia `InvalidDataException` se il
file non ha una sezione `members` o se il conteggio dichiarato non torna. Espone anche le
due liste "non di distinta": `EstraiCodiciSoppressi` (che passa da `members` per sapere
quali codici sono già noti, quindi può lanciare la stessa eccezione — `Form1.CodiciSoppressi`
la intercetta e restituisce vuoto) e `EstraiCodiciRiferimentiEsterni` (scansione a sé, non
lancia). Dettagli in `docs/formato-asm-record.md`. Condivisa con `scripts/CreoBom.cs` via
`#:project`.
- **AssemblyComponentScanner.cs** — estrae euristicamente i codici componente referenziati
contando le occorrenze di stringhe candidate nel testo binario. Due strategie: codici
numerici Pro-E riconosciuti da soli via regex (come nei nomi file), e nomi alfabetici di
parti commerciali tokenizzati genericamente e validati contro l'elenco di codici noti del DB
(altrimenti il rumore binario sarebbe ingestibile). Dettagli sull'euristica e i falsi
positivi noti in `docs/formato-asm.md`. Resta il fallback per i file che il parser esatto
non copre, e la fonte di verità per `ExtractCreoStrings.cs`/`BomViaDb.cs`.
Quella lista è **la distinta**, e non coincide con i file che servono a Creo per aprire
l'assieme: ne restano fuori due categorie, che il loro file serve comunque, altrimenti il
caricamento fallisce (`docs/formato-asm-record.md` §7.3). `AsmRecordReader` le restituisce a
parte, e la fase 4 le copia in `raw/` senza contarle fra i "non trovati":
- `EstraiCodiciSoppressi` — i componenti **soppressi**: spariti da `members`, ancora citati
dalla feature che li posiziona. Non si ricorre dentro (Creo non carica i figli di un
componente soppresso).
- `EstraiCodiciRiferimentiEsterni` — i modelli legati da **riferimento esterno** (assembly
feature, datum copiati), elencati in una tabella a parte del binario (§7.5). Qui si ricorre
dentro, perché Creo carica il modello citato con tutto il suo albero.
## Quale copia di un componente si copia in `raw/`
Lo stesso codice esiste spesso in più cartelle della share, con contenuto diverso e pari
versione (13% dei codici, vedi `docs/formato-asm-record.md` §7.4). Il `.asm` non registra
quale copia usa, quindi `RisolviAlberoComponenti` replica la regola di Creo:
`Form1.ScegliPerCartella` preferisce la copia nella **stessa cartella dell'assieme che la
referenzia** (`Form1.CartellaDiOrigine` ricostruisce quella cartella per gli assiemi di
partenza), e solo in mancanza ripiega su versione più alta e data più recente. Sbagliare copia
non dà un errore comprensibile: Creo non apre l'assieme e dice solo "dati di input non
completi".
## Import distinta base
- **DistintaBaseImporter.cs** — converte l'export del gestionale (SpreadsheetML XML con
estensione `.xls`/`.xml`, oppure `.xls`/`.xlsx` binario vero, distinti dai primi byte del
file) nelle sole colonne Componente/Descrizione, foglio `"Distinta Base"`. Se il nome del
file segue la convenzione `DBP_<codice>_<timestamp>`, aggiunge l'assieme generale come riga
in più (il foglio elenca solo i componenti sotto l'assieme). Scrive il CSV nella cartella
passata da `Form1` (la cartella di lavoro, impostabile da "File → Scegli cartella di lavoro..."),
non necessariamente accanto al file di input.
## Finestre di dialogo (modali, mostrate a fine step)
- **ProgressForm.cs** — durante l'aggiornamento indice: barra di progresso, conteggi
nuovi/aggiornati/invariati/rimossi, tempo trascorso e stima del tempo rimanente (dal ritmo
file/secondo rispetto al totale atteso, preso dal conteggio della scansione precedente
salvato nel DB).
- **ProgressoComponentiForm.cs** — dialog minimale (etichetta + barra) durante
`RisolviAlberoComponenti` in fase 4.
## Stile UI condiviso
- **UiStile.cs** — `Primario`/`Secondario`: stile piatto per i bottoni (bordo sottile
per i secondari, blu pieno per l'azione principale di ogni fase), entrambi con
`AutoSize`+`Padding`+`MinimumSize` invece di una `Size` fissa in pixel indovinata a
mano (altrimenti il testo va a capo e si taglia in verticale se non ci sta nella
larghezza scelta). `SecondarioPieno` è la variante senza bordo (sfondo grigio chiaro
pieno) per i bottoni secondari sopra le card bianche di `Card`/`FloatBar`, dove il
bordo sottile di `Secondario` è troppo poco visibile su sfondo bianco. `Arrotonda`
applica angoli arrotondati a un `Control` qualsiasi (bottone o card) via `Region`
calcolata su `SizeChanged`, non una volta sola: con `AutoSize` la dimensione finale si
conosce solo a layout avvenuto — va chiamata dopo aver fissato le dimensioni finali del
controllo (es. dopo `LarghezzaUguale`), altrimenti la `Region` vecchia resta visibile
come bordo residuo finché non arriva un repaint completo. `Card` costruisce una card di
stato (sfondo/bordo distinti, angoli arrotondati), usata in fase 1 per il riepilogo
Database. `FloatBar` costruisce la barra di navigazione "flottante" in fondo alle
finestre (card bianca staccata su tutti e 4 i lati, non un footer a tutta larghezza):
usata per Avanti/Indietro nel wizard e per Chiudi/Apri cartella raw in
`PrtMancantiForm`. `LarghezzaUguale` forza la stessa larghezza e altezza tra due
bottoni della stessa barra (es. Avanti/Indietro, che altrimenti avrebbero dimensioni
diverse: testi di lunghezza diversa, e il font in grassetto di `Primario` richiede più
altezza di quello normale di `Secondario`/`SecondarioPieno`).
- **app.ico** — icona dell'app, referenziata da `<ApplicationIcon>` nel csproj (icona
dell'exe in Esplora risorse/taskbar) e riletta esplicitamente in `Form1.BuildUi()`
con `Icon.ExtractAssociatedIcon(Application.ExecutablePath)` per la titlebar, senza
copiare il file `.ico` nell'output. Sfondo trasparente (nessuna card bianca), 7
risoluzioni da 16 a 256px.
- **PrtMancantiForm.cs** — esito della risoluzione ricorsiva dell'albero componenti: PRT e
sotto-assiemi trovati e copiati in `raw/`, più i codici referenziati che non corrispondono a
nessun file indicizzato (possono essere disegni, componenti davvero mancanti, o falsi
positivi della scansione `.asm`).
- **GuidaForm.cs** — guida interattiva ("Aiuto → Guida"/F1): lista delle 5 fasi a sinistra,
testo a destra; sintesi, il dettaglio resta solo in questi docs.
## Persistenza
- **DB** (`cad_index.db`, SQLite) — vedi tabella `files`/vista `files_ultima_versione` sopra.
Percorso ricordato tra un avvio e l'altro in `%AppData%/PrepStep/ultimo_db.txt`
(`SalvaUltimoDbUsato`/`LeggiUltimoDbUsato`), scritto ogni volta che si carica un DB
(all'avvio se già noto, oppure da "Apri DB..." o al termine di "Aggiorna indice"); se il
file non c'è o punta a un percorso cancellato, `InizializzaDatabase` ripiega su
`cad_index.db` accanto all'eseguibile o nella working directory. Radice disco di scansione
di default `X:\` (`Form1.RadiceDiscoDefault`, hardcoded).
- **Cartella di lavoro** — stesso meccanismo del DB sopra, salvata in
`%AppData%/PrepStep/ultima_cartella_lavoro.txt`; se non c'è una preferenza salvata (o punta
a una cartella cancellata) si propone la cartella del DB caricato, o in ultima istanza
`AppContext.BaseDirectory`; cambiabile in qualunque momento da "File → Scegli cartella di
lavoro...". Tutti i file generati a valle ci finiscono dentro: il CSV della distinta, `raw/`
(CAD dei componenti e sotto-componenti); `step/` non è scritta dall'app ma è dove l'utente
esporta gli STEP a mano con Creo Distributed Batch, a partire dai file trascinati da `raw/`
in fase 5.
## Script standalone (`scripts/`)
C# file-based apps (.NET 10, `dotnet run scripts/Nome.cs -- arg1 arg2`, nessun `.csproj`,
dipendenze extra con `#:package Nome@versione` in testa al file):
- **ExtractCreoStrings.cs** — BOM di un `.asm` risolvendo ricorsivamente i sotto-assiemi
contro `cad_index.db` (stessa logica di `AssiemeCompleto` in Form1, solo codici numerici
Pro-E).
- **BomViaDb.cs** — come sopra ma con la tokenizzazione generica validata contro il DB, per
intercettare anche i nomi alfabetici delle parti commerciali.
- **CreoBom.cs** — usa `AsmRecordReader` (stesso lettore esatto della GUI, fase 4/5, via
`#:project`) invece dell'euristica testuale. Risolve i sottoassiemi cercandoli in cartelle
locali (`-p`), non contro `cad_index.db` come `ExtractCreoStrings.cs`/`BomViaDb.cs`.
- **Analyze.cs** — dump del contesto testo/hex attorno a un pattern in un file binario, per
investigare a mano nuovi falsi positivi prima di toccare la regex di
`AssemblyComponentScanner`.
- **Query.cs** — query SQL ad-hoc contro `cad_index.db` da riga di comando.
> ⚠️ Nota importante di sicurezza: se `dotnet run` viene lanciato dentro `PrepStep/`
> (che contiene un `.csproj` di tipo `WinExe`), esegue l'app GUI vera invece dello script
> standalone e resta bloccato in attesa che una finestra si chiuda. Lanciare gli script da
> una cartella senza `.csproj` (es. la root del repo o `scripts/` stessa).
## Test
- **PrepStep.Tests/** (xUnit) — riferimento al progetto principale via `ProjectReference`
+ `InternalsVisibleTo` (in `PrepStep.csproj`), così i test chiamano direttamente
`Indexer.ParseFilename` e `AssemblyComponentScanner.EstraiCodiciReferenziati` senza esporli
come API pubblica. File: `IndexerParseFilenameTests.cs`, `IndexerScanTests.cs`,
`AssemblyComponentScannerTests.cs`, `AsmRecordReaderTests.cs`,
`DistintaBaseImporterTests.cs`, `CaseInsensitiveLookupIntegrationTests.cs`,
`PipelineEndToEndTests.cs`.
`AsmRecordReaderTests.cs` costruisce i `.asm` byte per byte con dei piccoli helper
(`Membro`, `ConfigName`, `ObjName`, `Riferimenti`): ogni caso strano trovato sulla share
diventa un test minimale invece di un file binario da versionare.
+46
View File
@@ -0,0 +1,46 @@
# Copia parallela dei file in `raw/`
`Form1.CopiaFileTrovati` (usata in fase 3 "Crea cartella raw" e in fase 4 "Trova componenti
referenziati" per copiare i file trovati nell'indice dentro `raw/`) copia i file in parallelo
invece che uno alla volta.
## Perché
I file vengono letti da una share di rete (`X:\`, vedi CLAUDE.md). Il tempo per ogni copia è
dominato dalla latenza di rete (round-trip SMB per aprire/leggere/chiudere il file), non dalla
CPU: un thread passa la maggior parte del tempo in attesa della risposta del server. Copiare
più file in parallelo sovrappone queste attese invece di sommarle, con un guadagno quasi
proporzionale al grado di parallelismo.
## Implementazione
`Parallel.ForEach` con `MaxDegreeOfParallelism = 6` (non un thread per file: troppa
concorrenza sulla stessa share rischia di saturare le connessioni SMB o subire throttling dal
server, risultando più lento invece che più veloce). Il contatore dei file copiati usa
`Interlocked.Increment` per essere thread-safe. La logica di skip per file irraggiungibili
(share offline, permessi — `catch (IOException or UnauthorizedAccessException)`) resta
invariata: essendo dentro il corpo della lambda, ogni iterazione ha il proprio try/catch
indipendente.
Il valore 6 è una stima di partenza, non misurata sulla share reale: se in pratica risulta
troppo aggressivo o troppo conservativo per la vostra rete, va aggiustato empiricamente.
## Benchmark (sintetico)
Non essendoci accesso diretto alla share `X:\` da questo ambiente, il confronto prima/dopo è
stato fatto con uno script standalone (`dotnet run <file>.cs`, pattern già usato in `scripts/`)
che copia file locali fittizi aggiungendo un ritardo artificiale di 80 ms per file, per simulare
la latenza di un round-trip SMB (il disco locale da solo è troppo veloce per mostrare qualunque
differenza).
50 file, ritardo simulato 80 ms/file:
| Versione | Tempo totale |
|---|---|
| Prima (sequenziale) | 4751 ms |
| Ora (parallela, grado 6) | 961 ms |
~5x più veloce, coerente con l'aspettativa teorica (limite di ~6x dato il grado di
parallelismo, scontato l'overhead di scheduling). Essendo una simulazione, il guadagno reale
sulla share va verificato sul campo: la latenza effettiva, la banda disponibile e il
comportamento del server sotto più connessioni concorrenti possono cambiare il risultato.
+160
View File
@@ -0,0 +1,160 @@
# Il database `cad_index.db`
Documentazione tecnica del DB SQLite che indicizza i file CAD: schema, come viene
popolato/aggiornato (`Indexer.ScanAsync`), come l'app lo trova all'avvio e i problemi più
comuni quando i risultati sembrano "mancanti".
## Schema
Tabella `files` (schema SQL in `Indexer.Schema`, `PrepStep/Indexer.cs`): un record per ogni
file trovato sotto la radice del disco (default `X:\`, `Form1.RadiceDiscoDefault`).
| Colonna | Contenuto |
|---|---|
| `id` | chiave primaria autoincrementale |
| `percorso_completo` | percorso assoluto del file — **chiave univoca**, usata per upsert |
| `percorso_relativo` | percorso relativo alla cartella di primo livello (`cartella_radice`) |
| `nome_file` | solo il nome del file |
| `cartella_radice` | prima cartella sotto la radice del disco (es. `__B.end`, `Commerciali`, ...) |
| `famiglia` | primo livello di sottocartella dentro `cartella_radice` (null se il file è direttamente dentro `cartella_radice`) |
| `codice` | estratto dal nome file col pattern Pro-E `<codice>.<tipo>.<versione>` |
| `tipo_file` | es. `prt`, `asm`, o `altro` se il nome non rispetta il pattern |
| `versione` | numero di versione Pro-E (intero), null se non rispetta il pattern |
| `dimensione_byte` | dimensione del file |
| `data_modifica` | `LastWriteTimeUtc` del file, formato ISO 8601 |
| `scansionato_il` | timestamp UTC dell'ultima scansione in cui il file è stato visto |
Indici su `codice`, `tipo_file`, `cartella_radice` (le colonne usate nelle query di ricerca
componenti e nel filtro per cartella).
Vista `files_ultima_versione`: stessa struttura di `files`, filtrata alla sola riga con
`versione` più alta per ogni `(cartella_radice, codice, tipo_file)`, tramite `ROW_NUMBER()
OVER (PARTITION BY ... ORDER BY versione DESC, data_modifica DESC, percorso_completo)`. Tutte
le ricerche di componenti (`Form1.TrovaPercorsiCad`, `Form1.CaricaCodiciNoti`) interrogano
questa vista, non `files` direttamente, così un componente con più versioni sulla share
risolve sempre all'ultima. Deliberatamente una `ROW_NUMBER()` invece di un self-join con
`GROUP BY`: su ~200k righe un self-join degenera in tempi quasi quadratici, `ROW_NUMBER()`
resta lineare.
La vista è ricreata (`DROP` + `CREATE`, non `CREATE IF NOT EXISTS`) a ogni "Aggiorna indice",
così un DB già esistente prende la definizione corrente senza dover reindicizzare la share.
Nota: `cartella_radice` fa parte della partition, quindi se lo stesso codice+tipo esiste
in **due cartelle radice diverse** (duplicati sulla share), la vista restituisce una riga per
ciascuna — non una sola in assoluto. `TrovaPercorsiCad` gestisce questo caso deduplicando per
`tipo_file` lato C# (vedi sotto).
> **I criteri di pareggio non sono decorativi.** Lo stesso codice può esistere **nella stessa
> cartella radice, con la stessa `versione`**, in due sottocartelle diverse (una copia in una
> cartella di studio, di lavoro o temporanea). Senza `data_modifica` nell'`ORDER BY` il
> pareggio veniva rotto in modo arbitrario, e poteva vincere la copia obsoleta — nascondendo
> del tutto quella buona, che non compariva più nella vista. Il file che finiva in `raw/` era
> allora un modello diverso da quello che l'assieme si aspetta: Creo lo carica, i vincoli di
> accoppiamento non si risolvono e **l'assieme non si apre affatto**.
>
> Il criterio deterministico però non basta a scegliere *bene*: è il minimo per non sbagliare
> a caso. Quale copia sia quella giusta dipende da chi la referenzia, non dalla data — vedi
> [`formato-asm-record.md`](formato-asm-record.md) §7.4 e `Form1.ScegliPerCartella`.
## Come viene popolato: `Indexer.ScanAsync`
Chiamato da "Aggiorna indice" (fase 1) e riutilizza lo stesso `dbPath` già caricato.
1. Crea lo schema se non esiste (`CREATE TABLE/VIEW IF NOT EXISTS`) — quindi funziona sia su
un DB vuoto (prima esecuzione) sia su uno esistente (aggiornamento incrementale).
2. Cancella subito tutte le righe con `cartella_radice LIKE 'temp\_%'`: le cartelle
`temp_paris`, `temp_davide`, `temp_studi esterni`, ecc. sono lavori personali/temporanei che
possono contenere copie datate con lo stesso codice numerico di file "ufficiali" altrove
sulla share, causando falsi positivi nella risoluzione dei sotto-componenti (vedi
`docs/formato-asm.md`). Vengono escluse a monte, non solo filtrate nelle singole query.
3. Elenca le cartelle di primo livello sotto la radice con `Directory.GetDirectories` (non un
elenco fisso in codice: una nuova cartella sulla share entra nell'indice al prossimo
aggiornamento senza bisogno di modificare `PrepStep`), escludendo quelle che iniziano per
`temp_`.
4. Per ogni cartella di primo livello, cammina l'albero con uno stack manuale (`Walk`, in
`Indexer.cs`) invece di `Directory.EnumerateFiles(..., SearchOption.AllDirectories)`: più
veloce su una share di rete con centinaia di migliaia di file, e ogni sottocartella
irraggiungibile (permessi, link rotti) viene saltata con un `try/catch` invece di far fallire
l'intera scansione.
5. Per ogni file trovato, estrae `codice`/`tipo`/`versione` dal nome (`ParseFilename`, vedi
sotto) e fa un **upsert riga per riga** dentro una transazione per cartella: confronta
`dimensione_byte` + `data_modifica` con quanto già salvato per lo stesso
`percorso_completo`—
- non esiste ancora → `INSERT` (contatore "nuovi")
- esiste ma dimensione/data sono cambiate → `UPDATE` (contatore "aggiornati")
- esiste e non è cambiato nulla → solo `scansionato_il` viene aggiornato (contatore
"invariati"), per non riscrivere l'intera riga inutilmente
6. Al termine di ogni cartella, cancella dal DB le righe di quella `cartella_radice` il cui
`scansionato_il` non è quello di questa scansione (`DELETE ... WHERE scansionato_il !=
@now`): sono file rimossi dal disco dall'ultima scansione. Il conteggio finisce nel
contatore "rimossi". Questo avviene **per cartella**, non a scansione intera completata: se
annulli a metà, le cartelle già scansionate hanno comunque gli eliminati ripuliti.
7. Il progresso (`ScanProgress`, con conteggi nuovi/aggiornati/invariati/rimossi, file
fatti/previsti e tempo trascorso) viene riportato ogni 200 file tramite `IProgress<T>`,
marshalato automaticamente sul thread UI, e mostrato in `ProgressForm` (che stima anche il
tempo rimanente dal ritmo file/secondo rispetto al totale atteso, preso dal conteggio della
scansione precedente salvato nel DB).
### Estrazione codice/tipo/versione: `ParseFilename`
Regex: `^(?<codice>.+)\.(?<tipo>[A-Za-z0-9]+)\.(?<versione>\d+)$` applicata al nome file
(non al percorso). Esempio: `9251208030c.asm.5` → `codice="9251208030c"`, `tipo="asm"`,
`versione=5`. Il gruppo `codice` è **greedy**, quindi cattura tutto quello che precede l'ultimo
`.tipo.versione` — se un componente ha una lettera di revisione attaccata nel nome del file
(es. `9248908002a.asm.3`), quella lettera fa parte del `codice`, non è un gruppo separato.
Se il nome file non rispetta il pattern (niente `.tipo.numero` finale), l'intero nome finisce
in `codice` e `tipo_file` diventa `"altro"` (usato per capire quanti file "strani" ci sono
sulla share, non tipicamente cercato dalla UI).
## Come l'app trova il DB all'avvio
`Form1.InizializzaDatabase` prova, in ordine, i seguenti candidati e usa il primo che esiste
su disco:
1. l'ultimo DB caricato con successo, salvato in
`%AppData%/PrepStep/ultimo_db.txt` (`Form1.LeggiUltimoDbUsato`/`SalvaUltimoDbUsato`)
2. `cad_index.db` accanto all'eseguibile (`AppContext.BaseDirectory`)
3. `cad_index.db` nella working directory corrente
Se nessuno dei tre esiste, la fase 1 resta bloccata su "Database non trovato" finché l'utente
non preme "Apri DB..." (`Form1.ScegliDatabase`, file picker libero) o "Aggiorna indice"
(`Form1.AggiornaIndice`, che crea `cad_index.db` da zero accanto all'eseguibile se non c'è
ancora un `_dbPath`).
**Attenzione in sviluppo (dotnet run / IDE):** `AppContext.BaseDirectory` è la cartella di
build (`PrepStep/bin/Release/net10.0-windows` o `Debug/...`), non la radice del repo. Se in
quella cartella finisce per errore una copia di `cad_index.db` (es. creata lì da una vecchia
sessione), l'app la userà invece di quella nella radice del repo, e le due copie divergeranno
a ogni "Aggiorna indice" fatto nell'uno o nell'altro ambiente. Se i risultati di una ricerca
sembrano incoerenti con quello che ti aspetti di vedere sul DB "giusto", controlla con "Apri
DB..." quale file è effettivamente caricato (mostrato in fase 1 accanto al conteggio dei file
indicizzati) prima di sospettare un bug nella ricerca.
## Query più usate
- **Verifica completezza / crea raw** (`Form1.TrovaPercorsiCad`): per ogni codice del CSV,
`SELECT tipo_file, percorso_completo FROM files_ultima_versione WHERE LOWER(codice) =
LOWER(@codice) AND LOWER(tipo_file) IN ('prt','asm') ORDER BY tipo_file, data_modifica DESC`.
Il match sul `codice` è **esatto** (case-insensitive, non un LIKE): se il codice del CSV ha
una lettera di revisione in più o in meno rispetto al nome del file su disco, non c'è match e
la cella "Percorso CAD" resta vuota — non è un errore della query, è che il DB non ha nessuna
riga con quell'esatto `codice`. In presenza di duplicati su più `cartella_radice`, tiene solo
il primo per `tipo_file` (il più recente, dato l'`ORDER BY data_modifica DESC`) e scarta gli
altri.
- **Risoluzione codici noti per lo scanner assiemi** (`Form1.CaricaCodiciNoti`, usata sia dalla
GUI in fase 4 sia da `scripts/BomViaDb.cs`): carica in un'unica query tutti i codici
prt/asm noti con i relativi percorsi, invece di una query per codice — usata per validare i
token generici trovati nei binari `.asm` (vedi `docs/formato-asm.md`).
## Debug ad-hoc
Per interrogare il DB direttamente da riga di comando senza aprire un tool SQLite esterno:
```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"
```
Utile per capire in fretta se un codice mancante nella UI non è nell'indice affatto (nessuna
riga, nemmeno con un `LIKE` più permissivo) oppure c'è ma con un `codice` leggermente diverso
da quello atteso (es. lettera di revisione, maiuscole/minuscole, spazi).
+583
View File
@@ -0,0 +1,583 @@
# Decodifica strutturale del .asm Creo: record, membri, family table
Questo documento descrive il formato **record per record** del `.asm` Creo, ricavato per
reverse engineering su un campione di assiemi reali e verificato confrontando l'output con
le BOM esportate da Creo per gli stessi file. Sostituisce, per l'estrazione dei componenti,
l'euristica testuale ("codice che compare almeno due volte") descritta in
[`formato-asm.md`](formato-asm.md): quella era una statistica sui nomi, questa legge la
struttura effettiva dei record.
Implementazione: lettura in [`PrepStep/AsmRecordReader.cs`](../PrepStep/AsmRecordReader.cs)
(condivisa con la GUI, `Form1.EstraiCodiciReferenziati` in fase "Trova componenti
referenziati"/"Verifica completezza" - con fallback su `AssemblyComponentScanner` per i file
che non rispettano il formato validato), resa in formato BOM e ricorsione sui sottoassiemi
in [`scripts/CreoBom.cs`](../scripts/CreoBom.cs) (via `#:project`).
**Stato della validazione.** Due livelli di prova, da non confondere:
1. **Byte-per-byte contro BOM Creo di riferimento** (la prova forte): 3 assiemi del
reverse engineering iniziale, più 5 casi reali aggiunti in seguito, ciascuno con la sua BOM
esportata da Creo messa a confronto riga per riga con l'output di `CreoBom.cs` —
`9284218024b.asm` (albero profondo, multi-livello), `2546220101.asm`/`2546220101_c04.asm.1`
(istanza di family table isolata), `2546220101.asm` risalvato con un componente soppresso,
`9244218080_c01.asm` (sottoassieme montato due volte), `9284208092_D110.asm` (revisione del
figlio). Ognuno di questi è citato per nome nella sezione del documento a cui si riferisce,
così resta verificabile quale affermazione poggia su quale file.
2. **Su tutti i 350 `.asm` reali di `raw/`**, solo conteggi interni e coerenza con l'euristica
testuale (`AssemblyComponentScanner`), **non** contro BOM di riferimento: usato per
calibrare le soglie dei controlli di sicurezza (§7) su scala, non per dimostrare la
correttezza dei valori estratti file per file.
Un'affermazione verificata solo al livello 2 (conteggi su scala) è più debole di una
verificata al livello 1 (BOM reale) - il testo lo distingue sempre esplicitamente.
---
## 1. Intestazione
Il file si apre con un header ASCII a righe terminate da `\` + newline:
```
#UGC:2 ASSEMBLY 2038 1760 805 1 1 15 4000 2022414 000002d0 \
#- VERS 0 0 \
#- HOST \
#- LINK \
#- DBID \
#- REVS 0, \
```
`ASSEMBLY` distingue il tipo di modello. Gli altri campi numerici non sono
stati decodificati e non servono per la BOM. Dopo l'header inizia il corpo
binario. **Il corpo non è compresso**: i nomi dei modelli sono in chiaro,
1 byte per carattere (Latin1 — `Encoding.Latin1.GetString`, stesso metodo già
usato in `AssemblyComponentScanner.cs` per leggere il file come testo senza
alterarne i byte).
## 2. Il formato dei record
Il corpo è una serializzazione a marcatori. I byte `\xe0`-`\xff` sono
marcatori di struttura, tutti gli altri sono dati. I marcatori osservati:
| byte | significato dedotto |
|------|---------------------|
| `\xe0` | inizio campo: `\xe0 <tipo> <nome campo>\0 <valore>` |
| `\xe1` | valore nullo / vuoto |
| `\xe2` | inizio struttura o unione (per i parametri: il byte dopo è il discriminante) |
| `\xe3` | inizio struttura, usato per gli elementi di un array |
| `\xf1` | apre un contenitore (visto prima di un array) |
| `\xf7` | id di schema: `\xf7 <n>` identifica il layout dei record che seguono |
| `\xf8` | array: `\xf8 <conteggio> ... <elementi>` |
| `\xfb` | lo schema è definito in linea nel primo elemento dell'array |
| `\xf4` | chiude una sezione (seguito da `#\n#<NomeSezione>\n`) |
Il byte `<tipo>` dopo `\xe0` codifica il tipo del valore. Osservati:
`\x00` struttura/nullable, `\x01` intero, `\x02` array numerico,
`\x0a` stringa terminata da `\0`.
### 2.1 Interi
Gli interi sono big-endian con la **lunghezza codificata nei bit alti del primo
byte**, per non collidere con i marcatori `\xe0`+:
| primo byte | byte totali | valore |
|------------|-------------|--------|
| `0x00`-`0x7f` | 1 | il byte stesso |
| `0x80`-`0xbf` | 2 | `(b0 & 0x3f) << 8 \| b1` |
| `0xc0`-`0xdf` | 3 | `(b0 & 0x1f) << 16 \| b1 << 8 \| b2` |
Esempi verificati: `\x2e` = 46; `\x80\x81` = 129; `\xc0\x4d\x51` = 19793.
Questa regola è stata **ricavata** dalla necessità di far quadrare il
conteggio dei membri: con la lettura a 1 byte fisso i campioni grandi si
disallineavano. Non è confermata da una specifica; regge sui campioni
disponibili al momento del reverse engineering.
In `CreoBom.cs` è `AsmRecordReader.AsInt`, applicata al testo decodificato Latin1 (ogni `char`
della stringa corrisponde 1:1 a un byte, quindi indicizzare la stringa o l'array di byte
originale dà lo stesso offset).
### 2.2 Compressione per schema
Un array `\xf8 <n> \xf7 <idSchema> \xfb` scrive il **primo** elemento con tutti
i nomi di campo per esteso; gli elementi successivi sono **posizionali**, senza
nomi. È il motivo per cui nel file si vedono i nomi dei componenti "nudi", uno
dopo l'altro, senza etichetta. Per leggere l'elemento N bisogna conoscere lo
schema definito dall'elemento 1.
---
## 3. L'elenco dei componenti: la chiave `members`
La chiave `members` è l'elenco dei **componenti diretti** dell'assieme
(livello 1), nell'ordine di montaggio.
```
\xe0 \x00 "members" \0
\xf1 \xf8 <N> \xf7 <idSchema> \xfb
\xe3 \xe0\x01 "id"\0 <int>
\xe0\x01 "type"\0 <int>
\xe0\x0a "name"\0 <stringa>\0
\xe0\x0a "family_name"\0 <stringa>\0
\xe0\x00 "tha_ref"\0 \xe1
\xe0\x02 "e1"\0 ... "e2" ... "e3" ... "origin" ...
\xe3 <id> <type> <name>\0 <family_name>\0 <resto posizionale>
\xe3 <id> <type> <name>\0 <family_name>\0 <resto posizionale>
...
```
Campi utili:
- **`id`** — identificatore del componente **dentro questo assieme**. È lo
stesso numero che Creo mostra come "Componente ID N". Serve da chiave per
collegare il membro ad altre sezioni (vedi family table, §4).
- **`type`** — `1` = sottoassieme (`.asm`), `2` = parte (`.prt`). Sui campioni
osservati compaiono solo questi due valori.
- **`name`** — il **file su disco**, senza estensione. Per le family table è
il **generico** (`UNI6592`), non l'istanza.
- **`family_name`** — sui campioni disponibili è **sempre uguale a `name`**.
Non porta quindi l'informazione sull'istanza; il nome è fuorviante.
- `e1`, `e2`, `e3`, `origin` — la matrice di posizionamento (assi + origine).
Non decodificati: non servono per la BOM, ma occupano il resto del record e
hanno lunghezza variabile.
`N` (il conteggio dichiarato) è il numero totale di **occorrenze**, non di
codici distinti: una parte montata 4 volte conta 4 e compare in 4 record con id
diversi. Il conteggio va usato come controllo di correttezza: se i record
trovati non sono esattamente `N`, la lettura si è disallineata e va segnalata
un'eccezione invece di restituire un risultato parziale silenzioso — vedi
`AsmRecordReader.LeggiMembri`, che lancia `InvalidDataException` in questo caso.
La sezione termina a `\xf4\x04#\n#MdlStatus\n`.
### 3.1 Come lo legge `AsmRecordReader.LeggiMembri`
Il parser non ricostruisce lo schema completo (servirebbe decodificare le
matrici). Si ancora invece al pattern `\xe3 <int> <int> <stringa>\0 <stringa>\0`
(`AsmRecordReader.MemberRegex`, una regex .NET sul testo Latin1 del file) e salta il
resto del record fino al `\xe3` successivo che soddisfa il pattern.
La doppia stringa terminata da `\0` rende i falsi positivi improbabili, e il
conteggio dichiarato `N` fa da rete di sicurezza.
> `ponytail:` scansione a pattern invece del parser di schema — il punto debole
> è che si appoggia al conteggio per accorgersi di un errore. Da sostituire con
> la lettura dello schema se dovessero comparire campioni che non quadrano.
---
## 4. Family table: dove sta il nome dell'istanza
Le parti normalizzate (viti, rondelle, spine, boccole) sono **istanze di family
table**. In Creo esiste un solo file, il **generico** (`UNI6592.prt`), che
contiene una tabella di varianti; ogni variante è un'**istanza**
(`R8--_-_U6592_AISI304`). La BOM elenca le istanze, il disco contiene il
generico. La sezione `members` cita **solo il generico**: senza risolvere
`ConfigName` (sotto), il livello 1 produrrebbe `UNI6592` dove la BOM dice
`R8--_-_U6592_AISI304`.
Il collegamento sta in un record di **parametro** applicato al membro:
```
\xe3 "ConfigName"\0 \xe2 \x33 [\xf7 <idSchema>] <nome istanza>\0 <id membro> \x82\x7b \xe1\xe1
```
- `\xe2\x33` — parametro di tipo stringa (`\x33` = `'3'`). Confronto: `\xe2\x32`
è un parametro numerico, es. `DENSITA`.
- `<nome istanza>` — il nome esatto che la BOM stampa.
- `<id membro>` — l'`id` del record `members` a cui il parametro si applica,
con la codifica intera di §2.1. **È questo il collegamento.**
- `\xf7 <idSchema>` compare solo sulla prima occorrenza (schema definito in
linea), poi è omesso.
- `\x82\x7b` — costante (= 635) identica su tutte le occorrenze di tutti i
campioni. Significato ignoto, presumibilmente attributi/flag del parametro.
`AsmRecordReader.ConfigNameRegex` la usa come ancora di fine record; se un campione
futuro avesse un valore diverso, il match salterebbe silenziosamente (nessun
contatore di sicurezza analogo a quello di `members` copre questo caso).
`ConfigName` compare **esattamente** per i membri che sono istanze, e per
nessun altro. L'algoritmo è quindi: leggere `members`, costruire la mappa
`id membro → nome istanza` dai record `ConfigName`
(`AsmRecordReader.LeggiMembri`, dizionario `istanze`), e sostituire il nome dove la
mappa ha una voce. Il generico resta disponibile come `Membro.Generico`.
> **Avvertenza.** `ConfigName` sta in mezzo a parametri palesemente aziendali
> (`TIPO_PARTE`, `CLASSE_RICAR`, `GR_PRODOTTO`, `TECN_RIORDINO`), quindi
> potrebbe non essere un meccanismo standard di Creo ma una convenzione
> interna. Regge sui campioni disponibili, ma su file provenienti da
> un'altra installazione va riverificato prima di fidarsene.
### 4.0.1 Eccezione: componenti da pattern/array feature (`#AF<n>`)
Un componente creato da un **pattern/array feature** (stessa geometria ripetuta N volte da
una feature di pattern, non N placement manuali distinti) ha un `ConfigName` con un nome
nella forma `<codice>#AF<n>` (`n` = indice dell'istanza nel pattern), es.
`9244250136_C01#AF0`. **Non è un'istanza di family table**: è un tag interno di
disambiguazione fra le ripetizioni del pattern. La BOM reale di Creo lo **ignora** e mostra
il codice pulito, unendo le occorrenze duplicate in una sola riga con la quantità totale.
Osservato su `9244250136.asm`: 4 dei suoi 8 membri hanno `ConfigName` con suffisso `#AF`,
due dei quali (`_C03#AF0` e `_C03#AF1`) sono lo **stesso codice** con `n` diverso — la BOM li
mostra come un'unica riga `9244250136_C03` con quantità 2. Senza normalizzare il suffisso,
ogni istanza del pattern risulterebbe un componente distinto (e con nome sbagliato).
`AsmRecordReader` rimuove `#AF\d+$` da ogni valore di `ConfigName` prima di usarlo
(`SuffissoArrayFeatureRegex`). Scoperto durante un test end-to-end su un albero reale più
ampio degli 8 campioni originali (`9284218024b.asm`, confrontato con la sua BOM Creo di
riferimento) — non era presente in nessuno degli 8 campioni usati per il reverse engineering
iniziale.
### 4.0.2 Istanza risolta direttamente in `members`, senza passare da `ConfigName`
Non tutti i membri che sono istanze di family table hanno un record `ConfigName` associato.
Per alcune parti commerciali/normalizzate non-UNI, il campo `name` (e allo stesso modo
`family_name`) del record `members` (§3) porta già per intero la notazione
`<nome istanza><generico>` invece del solo nome generico:
```
\xe3 <id> <type> "<istanza><generico>"\0 "<istanza><generico>"\0
```
È la stessa notazione istanza→generico della §4.1 sotto, ma qui compare **dentro** il record
`members` stesso, come valore diretto del componente - non nell'elenco di riferimenti
geometrici di cui parla la §4.1 (quello sì da ignorare per la BOM). Il generico fra `<` e `>`
è il file realmente indicizzato sul disco; il nome prima di `<` è quello che la BOM
mostrerebbe.
`AsmRecordReader.LeggiMembri` lo riconosce per i membri **senza** un `ConfigName`
corrispondente il cui campo `name` rispetta per intero il pattern `<testo><testo>`: il testo
dopo l'ultima `<` (fino alla `>` finale) diventa il generico da cercare nell'indice, quello
prima diventa il nome mostrato. Senza questo split, il codice cercato nell'indice sarebbe la
stringa intera con le parentesi comprese, mai presente nell'indice - il componente
risulterebbe erroneamente "non trovato".
### 4.1 Una seconda pista, che NON va usata per la BOM
Altrove nel file compaiono stringhe nella forma `istanza<generico>`:
```
\xe2\x02 "R8--_-_U6592_AISI304<UNI6592>"\0
```
Documenta la stessa relazione istanza→generico in modo autonomo ed è utile come
riscontro incrociato (è lo stesso pattern `<codice><GENERICO>` già sfruttato
dall'euristica testuale in `formato-asm.md` per le family table non-UNI).
**Non è però una lista dei componenti**: su un campione produceva 26 voci
contro 17 membri reali, includeva alcuni nipoti (figli dei sottoassiemi) ma
non tutti, e ripeteva le stesse voci più volte. È la lista dei riferimenti
geometrici dei vincoli di accoppiamento, non una distinta. Usarla per la BOM
dà risultati sbagliati sia in eccesso sia in difetto.
---
## 5. Il nome proprio del modello
La BOM intesta i blocchi col nome **del modello**, non del file: il file
`9244248550b.asm.11` produce `Assieme 9244248550B contiene:`. Il nome sta nel
primo record `objname` del file:
```
\x00 "objname"\0 \xe2 \xe0\x01 "object_type"\0 <int> \xe0\x0a "name"\0 <nome>\0
```
La **prima** occorrenza è il modello stesso; quelle successive sono altri
oggetti referenziati (disegni, modelli citati). `AsmRecordReader.NomeProprio` cerca
solo la prima occorrenza (`Regex.Match`, non `Matches`), verificato su tutti i
campioni disponibili durante il reverse engineering.
---
## 6. Generare la BOM: ricorsione e formato di output
`members` dà un solo livello. La BOM di Creo è ricorsiva: apre anche i `.asm`
dei sottoassiemi. Implementato in `CreoAsm.GeneraBom`/`Render` in
[`scripts/CreoBom.cs`](../scripts/CreoBom.cs).
### 6.1 Struttura del file BOM
- Codifica **UTF-8 con BOM** (`EF BB BF`), fine riga **CRLF**.
- Due righe vuote in testa, poi il primo blocco.
- **Tre** righe vuote fra un blocco e il successivo, **due** prima del
riepilogo. Il file termina con un solo CRLF.
- Riga voce: `{qta:6} {tipo:>12} {nome}` — cioè quantità allineata a destra su
6 caratteri, spazio, tipo allineato a destra su 12, due spazi, nome.
- Intestazioni: `Assieme <nome> contiene:` per la radice,
`Sottoassieme <nome> contiene:` per gli altri.
### 6.2 Ordine dei blocchi
Visita **depth-first pre-order**: si emette il blocco di un sottoassieme e
subito dopo, ricorsivamente, quelli dei suoi sottoassiemi, prima di passare al
fratello successivo. Ogni modello compare **una volta sola** anche se montato
più volte (deduplica per nome, `CreoAsm.Blocchi`).
> **Verificato.** `9244218080_C01.asm` monta `2605410010_C01` due volte
> (`scripts/9244218080_c01.asm.1` + `scripts/9244218080_c01.bom.2` di riferimento): la BOM
> Creo mostra un solo blocco `Sottoassieme 2605410010_C01 contiene:` con quantità 2 in testa,
> non due blocchi separati, e `CreoBom.cs` riproduce l'output esatto senza modifiche. Confermata
> anche la moltiplicazione delle quantità nel riepilogo (§6.3): le parti di `2605410010_C01`
> compaiono raddoppiate (1→2, 1→2, 16→32).
### 6.3 Riepilogo finale
`Summary of items for <radice>:` elenca **solo le parti**, con la quantità
totale moltiplicata lungo l'albero (una parte in 4 esemplari dentro un
sottoassieme montato 2 volte conta 8), nell'ordine di **prima comparsa** nella
visita depth-first (`CreoAsm.Sommario`). Una parte che compare a più livelli
occupa la posizione della sua prima comparsa e somma le quantità.
### 6.4 Risoluzione dei file dei sottoassiemi
È il punto che il formato **non** risolve: il `.asm` registra il nome del
modello, non il percorso. Servono quindi cartelle di ricerca fornite
dall'esterno (`-p`/`--path`). `CreoAsm.CreaFinder` indicizza `<nome>.asm.<rev>`
senza distinzione di maiuscole e, a parità di nome, prende la **revisione più
alta** presente.
> **Verificato: è la scelta giusta.** Il `.asm` padre non registra la revisione attesa del
> figlio (cercata e non trovata, vedi §7), ma non serve: confermato che **Creo stesso apre
> sempre la revisione più alta disponibile** di un componente, indipendentemente da quale
> fosse presente quando l'assieme padre fu salvato l'ultima volta. Caso reale controllato:
> `9284208092_D110.asm` (salvato 2018-08-01) referenzia `9284202481.prt`, che sulla share ha
> revisioni sia del 2014 (v1-v3, presenti all'epoca) sia del 2024 (v4-v6, arrivate anni dopo) -
> Creo carica v6, non v3. `CreaFinder` che prende sempre la revisione più alta in cartella
> replica esattamente questo comportamento, non è un'assunzione azzardata. Vedi `formato-
> asm.md` §"Risoluzione ricorsiva dell'albero" per le trappole storiche su questo punto (con
> l'euristica testuale - restano valide, sono su un problema diverso: trovare la cartella
> giusta, non quale revisione preferire).
Se un sottoassieme non si trova, il blocco viene emesso con
`*** NON RISOLTO: file .asm non trovato ***` e le sue parti restano fuori dal
riepilogo. Il risultato è dichiaratamente incompleto invece che silenziosamente
sbagliato.
---
## 7. Stato di ogni punto aperto
Tabella di riferimento: cosa è provato, come, e cosa resta genuinamente ignoto. Il dettaglio
di ciascun punto segue sotto, in due gruppi.
| Punto | Stato | Prova |
|---|---|---|
| Deduplica blocchi + moltiplicazione quantità (§6.2/§6.3) | Risolto | BOM reale (§7.1) |
| Revisione del figlio che il padre si aspetta (§6.4) | Risolto | BOM reale (§7.1) |
| Stato soppresso/escluso in `members` | Risolto | BOM reale (§7.1) |
| Family table di **assiemi** (rilevamento) | Risolto: serve la conferma del nome proprio (`objname`), il solo `model_name` dava falsi positivi | BOM reali + albero `raw/` completo (§7.1) |
| Family table di **assiemi** (contenuto di ogni istanza) | Non risolto, nessun impatto sulla BOM (fallback già corretto) | Scan sull'intera share (§7.2) |
| Costante `\x82\x7b` in `ConfigName` | Significato ignoto, rischio coperto | Scala 350 file (§7.2) |
| Occorrenze multiple di `members` (simplified rep) | Mai osservato, rischio coperto | Scala 350 file (§7.2) |
| Matrice di posizionamento (`e1`/`e2`/`e3`/`origin`) | Non letto, nessun impatto sulla BOM | — (§7.2) |
| Campi numerici dell'header dopo `ASSEMBLY` | Non letto, nessun impatto sulla BOM | — (§7.2) |
| Divergenza `family_name` ≠ `name` | Mai osservata, non letta | — (§7.2) |
"Rischio coperto" vuol dire: il codice non sa cosa significhi il byte, ma verifica un
invariante che lo renderebbe visibile (eccezione) se mai smettesse di valere - non è una prova
che il byte non conti mai, è la garanzia che se contasse il programma lo segnalerebbe invece
di sbagliare in silenzio.
### 7.1 Risolti con una BOM Creo di riferimento
- **Deduplica blocchi e moltiplicazione quantità.** `9244218080_C01.asm` monta
`2605410010_C01` due volte (`scripts/9244218080_c01.asm.1` +
`scripts/9244218080_c01.bom.2`): la BOM Creo mostra un solo blocco
`Sottoassieme 2605410010_C01 contiene:` con quantità 2 in testa (non due blocchi separati),
e le sue parti compaiono raddoppiate nel riepilogo (1→2, 1→2, 16→32). `CreoBom.cs` riproduce
l'output esatto senza modifiche: la deduplica per nome (`CreoAsm.Blocchi`) e la
moltiplicazione lungo l'albero (`CreoAsm.Sommario`) sono il comportamento reale di Creo, non
solo un'assunzione.
- **Revisione del figlio.** `9284208092_D110.asm` (salvato 2018-08-01) referenzia
`9284202481.prt`, che sulla share ha revisioni sia del 2014 (v1-v3, presenti quando il padre
fu salvato) sia del 2024 (v4-v6, arrivate anni dopo). Confermato direttamente in Creo: apre
v6, non v3 - **sempre la revisione più alta disponibile**, indipendentemente da quale fosse
presente all'ultimo salvataggio del padre. `CreoAsm.CreaFinder`, che prende sempre la
revisione più alta trovata in cartella, replica esattamente questo comportamento.
- **Stato soppresso/escluso.** Stesso assieme `2546220101.asm` risalvato da Creo con
`2546220101_C02` soppresso (`scripts/2546220101.asm.1` + `scripts/2546220101.bom.1`, con le
parti/sottoassiemi correlati). Un componente soppresso **non produce alcun record** in
`members`: sparisce e basta, il conteggio dichiarato scende di conseguenza (`\xf8 03` invece
di `\xf8 04`). Nessun marcatore di stato da gestire: `CreoBom.cs` sul file risalvato
riproduce esattamente la BOM di riferimento senza modifiche al parser.
> **Sparisce da `members`, non dal file.** La feature che lo posiziona resta, e con essa la
> stringa `Componente ID <n> (<codice>.PRT)` (localizzata come in
> `AssemblyComponentScanner.HaPlacement`). Per la BOM va ignorata - Creo non elenca i
> soppressi - ma **il file di quel componente serve comunque per aprire l'assieme**: se manca,
> Creo fallisce il caricamento con "dati di input non completi" (`[-4]`, cioè
> `PRO_TK_E_NOT_FOUND`). Verificato su un caso reale: un assieme che non si apriva dalla
> cartella `raw/`, e che si è aperto senza errori copiandoci dentro quel solo file.
> `AsmRecordReader.EstraiCodiciSoppressi` li restituisce a parte, apposta per questo - vedi
> §7.3.
- **Family table di assiemi — rilevamento.** Non solo le parti normalizzate (§4) possono
essere family table: un intero **assieme** può esserlo. Osservato su `2546220101.asm`
(dentro l'albero di `9284218024b.asm`): il file generico ha una propria sezione `members`
che elenca il contenuto di UNA rappresentazione (3 cuscinetti + 1 sotto-assieme, dati
arbitrari), ma la BOM Creo per lo stesso file mostra 4 **istanze diverse**
(`2546220101_C01`.."_C04", tre "Parte" e una "Sottoassieme" - tipi diversi fra loro),
ciascuna con un contenuto diverso. Il nome istanza qui **non** passa da `ConfigName`
(verificato: nessuna occorrenza letterale nel file), ma da una coppia di record distinta
subito dopo l'header del modello:
```
\xe0\x01model_t_type\0<int>\xe0\x0amodel_name\0<codice>_C0n\0\xe0\x0ageneric_name\0\0
...stamp\0\xe2\xf2\xf7<idSchema>
(\xe3<type>\x02<codice>_C0n\0\0\xe2)* -- un'occorrenza per ogni istanza sorella
\xf4
```
Quando questo succede il file generico è già di per sé un'istanza (`model_name` porta il
suffisso `_C0n`, non è neutro) e `stamp` elenca le sorelle.
> **Il record `model_name` da solo non è un'identità.** Su file reali quel record nomina
> quasi sempre **un altro modello** - tipicamente uno dei membri del file stesso, a volte un
> codice imparentato - e in quei casi `members` è già corretto e va usato. Serve una
> conferma indipendente: il **nome proprio del modello** (`objname`, §5). `LeggiMembri`
> segnala la family table solo se `objname` esiste **e** coincide con `model_name` privato
> del suffisso `_C0n`; negli altri casi legge `members` normalmente.
>
> Il controllo più largo (senza la conferma di `objname`) era la causa di
> **falsi "componenti mancanti"**: il parser esatto rifiutava file perfettamente leggibili,
> si ripiegava sull'euristica testuale e quella estraeva codici che non esistono, riportati
> poi come componenti non trovati nell'indice. Verificato contro BOM Creo di riferimento:
> sui file dove il segnale scattava senza conferma di `objname`, `members` elenca
> **esattamente** i componenti della BOM reale (anche nello stesso ordine); uno di questi
> file è stato inoltre aperto in Creo, e l'albero del modello mostra gli stessi componenti,
> senza alcuna family table. Con la conferma di `objname` richiesta, tutti i `.asm` di una
> cartella `raw/` reale (351 file, alberi completi di più macchine) si leggono col parser
> esatto, zero ricadute sull'euristica.
Da tenere presente: il sintomo dipende da **come il file è stato salvato**, non solo dal
modello. Rileggendo una revisione più recente dello stesso assieme generico, il record di
identità può non esserci più e `members` elencare direttamente tutte le istanze, col tipo
giusto per ciascuna - cioè esattamente quello che mostra la BOM. Il blocco quindi non è una
proprietà stabile del codice: va valutato file per file, revisione per revisione.
Verificato anche che il fallback su `AssemblyComponentScanner` **risolve già correttamente**
il caso vero: i nomi delle istanze compaiono come testo puro nel campo `stamp`, e
l'euristica ("codice che compare almeno due volte") li trova senza bisogno di decodificare
`stamp`. E che una singola istanza, se esportata da Creo come file a sé (Family Table →
istanza → "Salva una copia"), è un `.asm`/`.prt` normale senza traccia di family table al suo
interno - letto correttamente dal parser esatto (`2546220101_c04.asm.1`, verificato contro
`2546220101.bom.1`).
### 7.2 Non risolti
- **Family table di assiemi — contenuto di ogni istanza.** Come recuperare il contenuto
specifico di un'istanza **senza** che esista già un file separato per essa sul disco (il
caso comune, se nessuno l'ha mai esportata a parte con "Salva una copia"): il meccanismo
dietro `stamp` non è stato decodificato oltre a nome+tipo di ciascuna istanza sorella:
`stamp` porta **solo i nomi** delle sorelle, non il loro contenuto, quindi non è la
struttura da cui partire per risolvere questo punto.
Resta aperto solo per completezza: sui file dove il segnale scatta davvero (con conferma
del nome proprio, §7.1) il parser si ferma invece di restituire un contenuto sbagliato, e
il chiamante ripiega sull'euristica testuale, che i nomi delle istanze li trova comunque
come testo puro. Nessun comportamento dell'app dipende da questa decodifica.
- **Costante `\x82\x7b`** nei record `ConfigName`: significato ignoto. Rischio coperto -
`LeggiMembri` conta le occorrenze grezze di `ConfigName` e le confronta col numero di match
del regex; se un campione avesse un valore diverso, il regex non farebbe più match (prima:
in silenzio) e ora lancia eccezione invece di restituire il nome generico al posto
dell'istanza senza dirlo. Verificato su tutti i 350 `.asm` di `raw/`: 215 usano davvero
`ConfigName`, 0 mismatch.
- **Occorrenze multiple di `members`** (rappresentazioni semplificate, mai osservate). Rischio
coperto - `LeggiMembri` lancia eccezione se ne trova più di una invece di prendere
silenziosamente la prima senza sapere se sia quella giusta. Verificato su tutti i 350 `.asm`
di `raw/`: sempre esattamente una occorrenza.
- **La matrice di posizionamento** (`e1`, `e2`, `e3`, `origin`): formato numerico non
decodificato, nessun impatto sulla BOM (non letta da nessuna parte del codice).
- **I campi numerici dell'header** dopo `ASSEMBLY`: non letti, nessun impatto sulla BOM.
- **`family_name`**: sempre uguale a `name` su tutti i campioni disponibili; non si sa in
quale situazione divergerebbe, né se il codice dovrebbe fare qualcosa se succedesse (oggi
non la usa in nessun modo, legge solo `name`).
### 7.3 Due domande diverse: cosa va in distinta, cosa serve per aprire
È la lezione che costa di più riscoprire, quindi sta qui in chiaro: **la lista dei componenti
della BOM e la lista dei file che servono a Creo per aprire l'assieme non coincidono.** La
seconda è più lunga.
| Lista | Serve per | Si ottiene con |
| --- | --- | --- |
| Componenti della distinta | BOM, verifica completezza | `LeggiMembri` / `EstraiCodiciReferenziati` (sezione `members`) |
| File necessari al caricamento | popolare `raw/` per l'export STEP | i precedenti **più** `EstraiCodiciSoppressi` e `EstraiCodiciRiferimentiEsterni` (§7.5) |
La differenza sono i **componenti soppressi** (§7.1) e i **riferimenti esterni** (§7.5): fuori
dalla BOM, ma senza il loro file Creo non apre l'assieme. Chi popola `raw/` deve quindi usare
l'unione; chi genera la BOM o verifica la completezza deve usare **solo** i membri, altrimenti
quelle voci diventano falsi "componenti mancanti".
Il sintomo, se si sbaglia, non assomiglia per niente alla causa: Creo Distributed Batch
fallisce con `Carica modello in Creo Pro | Non riuscito | [-4] Errore: Dati di input non
completi` - un messaggio che non nomina il file mancante. Il modo più rapido per trovarlo è
aprire l'assieme in Creo **dalla cartella `raw/`** (non dalla share, dove i file ci sono tutti
e quindi si apre senza errori) e cercare nell'albero la voce con l'icona rossa.
### 7.4 Lo stesso codice esiste in più cartelle della share
Sulla share **13% dei codici** (5.193 su 39.274 fra prt e asm) ha la versione più alta in più
cartelle, con contenuto diverso. Non sono revisioni: sono copie parallele, lasciate da studi
preliminari, allestimenti fiera, cartelle di lavoro datate, raccolte temporanee.
Il `.asm` **non dice quale copia usa**: verificato che non registra né il percorso, né
l'`UOBJ_ID` del figlio (che pure distingue le copie), né la revisione attesa. Cita solo il
nome del modello. Quale copia venga caricata lo decide Creo al momento dell'apertura, e la
regola pratica è: **prima la cartella del modello che la referenzia**.
Non esiste quindi un criterio globale corretto - e i numeri lo confermano: nel 79% dei
conflitti nessuna delle copie sta in una cartella "ufficiale" (`…\M_B`, `…\A`), perché il
conflitto è fra due istantanee parallele, ognuna autoconsistente con i propri componenti. Per
data, per versione e per posizione sono equivalenti: solo il contesto di chi referenzia
discrimina.
Conseguenze per il codice:
- `Form1.ScegliPerCartella` risolve ogni componente preferendo la copia nella **stessa cartella
dell'assieme che lo referenzia**, e solo in mancanza ripiega su versione più alta e data più
recente. `Form1.CartellaDiOrigine` ricostruisce quella cartella per gli assiemi di partenza,
che arrivano dalla fase precedente senza portarsela dietro.
- La vista `files_ultima_versione` (vedi [`db-cad-index.md`](db-cad-index.md)) rompe il
pareggio fra copie di pari versione in modo deterministico invece che arbitrario.
- Resta un rischio noto: quando il componente **non** sta nella cartella del suo assieme, la
scelta torna a essere una scommessa. Se un assieme non si apre da `raw/`, è la prima cosa da
verificare - `9244206098` è il caso reale che ha portato a questa sezione: due copie di pari
versione, una del 2011 in una cartella di file sciolti e una del 2026 accanto all'assieme,
e in `raw/` finiva quella del 2011.
### 7.5 Riferimenti esterni: la tabella dei modelli citati
Terzo modo in cui un `.asm` dipende da un file che non è fra i suoi membri, dopo i soppressi
(§7.1) e le copie ambigue (§7.4). Un assieme che porta **assembly feature** o **datum copiati**
da un altro modello lo cita in una tabella a parte, che non è `members` e non lascia una stringa
`Componente ID …`: i due lettori esistenti non la vedevano.
Struttura: un array aperto da `\xf2\xf7\x05` e chiuso da `\xf3`, con elementi
`\xe3\xe2<tipo><nome>\x00`. Il nome può portare il suffisso `#AF<n>` di §4.0.1 e/o la notazione
`istanza<generico>` di §4.0.2 - il file su disco è il generico:
```
f2 f7 05 e3 e2 01 "9255708017#AF2<9255708017>" 00 ... -> 9255708017
e3 e2 02 "9255700181" 00 ... -> 9255700181
f3
```
Va letta con `EstraiCodiciRiferimentiEsterni`, e - a differenza dei soppressi - **si ricorre
dentro**: Creo carica il modello citato con tutti i suoi componenti, che quindi servono anch'essi
in `raw/`. La tabella contiene anche nomi di *feature* (`MIRROR3`, `CHAMFER1`,
`M10_TAPPED_HOLE1`), indistinguibili dai modelli guardando il record: non è un problema perché
chi la usa cerca ogni nome nell'indice e ignora quelli che non ci sono - per lo stesso motivo
questi codici non devono finire fra i "non trovati".
Il caso reale: `9255728001` falliva in Distributed Batch con lo stesso `[-4] Dati di input non
completi` di §7.3 perché il suo sottoassieme `9255728001_200` referenzia per assembly feature
`9255708017` e `9255708018` (e con loro `9255700181/182/183`), e monta una vite
`V6-12--_-_U5933_C<UNI5933>` il cui generico `uni5933.prt` non era in `raw/`. Nessuno dei sei
file compariva in `members` né fra i piazzamenti.
## 8. Uso
```powershell
dotnet run scripts/CreoBom.cs -- raw/9244228558.asm.20 -p M:\modelli -o out\9244228558.bom.1
dotnet run scripts/CreoBom.cs -- raw/9244228558.asm.20 -p M:\modelli # stampa a video
dotnet run scripts/Analyze.cs -- raw/9244228558.asm.20 "ConfigName" # dump dei byte per investigare a mano
```
Senza `-p`/`--path`, la ricerca dei sottoassiemi usa la cartella del file di partenza.
`-p` è ripetibile per più cartelle. `-o` scrive il file col BOM UTF-8 e CRLF descritti
al §6.1; senza `-o` il testo va su stdout (con `\n` al posto di `\r\n`, per leggibilità
in console).
+97 -7
View File
@@ -1,10 +1,24 @@
# Come leggere un .asm Creo per dedurne i componenti # Come leggere un .asm Creo per dedurne i componenti
Il file `.asm` di Pro/ENGINEER-Creo è un formato binario proprietario, non Il file `.asm` di Pro/ENGINEER-Creo è un formato binario proprietario, non
documentato pubblicamente. Non esiste un parser strutturale completo in questo documentato pubblicamente. Questo documento descrive l'euristica testuale di
progetto: l'estrazione dei componenti si basa su un'euristica testuale, `AssemblyComponentScanner.cs`: conta le occorrenze di stringhe candidate nel testo binario,
validata a mano confrontando l'output con l'albero reale mostrato da Creo. validata a mano confrontando l'output con l'albero reale mostrato da Creo. Riassume cosa si
Questo documento riassume cosa si è osservato e i suoi limiti. è osservato e i suoi limiti, in particolare i falsi positivi che un conteggio testuale non
può escludere con certezza (riferimenti fantasma, componenti soppressi con placement
residuo — vedi sotto).
Un reverse engineering successivo ha decodificato la **struttura a record** del file (non
solo pattern testuali): legge esattamente la sezione `members` di un assieme, senza contare
occorrenze né rischiare i falsi positivi descritti in questo documento. Implementato in
`PrepStep/AsmRecordReader.cs`, documentato in [`formato-asm-record.md`](formato-asm-record.md).
È il metodo **preferito** in GUI (fase "Trova componenti referenziati"/"Verifica
completezza", `Form1.EstraiCodiciReferenziati`) e nello script `CreoBom.cs`:
`AssemblyComponentScanner` (l'euristica di questo documento) resta come **fallback**, usato
solo quando `AsmRecordReader` lancia `InvalidDataException` (file che non rispetta il
formato validato dal reverse engineering — copertura limitata agli 8 campioni disponibili al
momento), e come fonte di verità per gli script `ExtractCreoStrings.cs`/`BomViaDb.cs`, che
risolvono l'albero contro `cad_index.db` invece che tramite un percorso di ricerca locale.
## Struttura osservata ## Struttura osservata
@@ -33,7 +47,7 @@ ASCII leggibili in diversi campi ricorrenti:
Un codice reale compare **più volte** nel file (una per feature/placement Un codice reale compare **più volte** nel file (una per feature/placement
diverso); rumore binario che assomiglia casualmente a un codice compare quasi diverso); rumore binario che assomiglia casualmente a un codice compare quasi
sempre una sola volta. Questa è la base del filtro in sempre una sola volta. Questa è la base del filtro in
[`AssemblyComponentScanner.cs`](../CadSearchGui/AssemblyComponentScanner.cs) [`AssemblyComponentScanner.cs`](../PrepStep/AssemblyComponentScanner.cs)
e in [`scripts/ExtractCreoStrings.cs`](../scripts/ExtractCreoStrings.cs): e in [`scripts/ExtractCreoStrings.cs`](../scripts/ExtractCreoStrings.cs):
``` ```
@@ -249,7 +263,7 @@ regex aggiuntiva: (?<=<)(?:UNI\d+|CUSCI_[A-Za-z0-9]+)(?=>)
``` ```
Implementata sia in [`scripts/ExtractCreoStrings.cs`](../scripts/ExtractCreoStrings.cs) sia in Implementata sia in [`scripts/ExtractCreoStrings.cs`](../scripts/ExtractCreoStrings.cs) sia in
[`AssemblyComponentScanner.cs`](../CadSearchGui/AssemblyComponentScanner.cs) (fase "Trova componenti [`AssemblyComponentScanner.cs`](../PrepStep/AssemblyComponentScanner.cs) (fase "Trova componenti
referenziati" dell'app). Il prefisso family table non è garantito essere solo `UNI`/`CUSCI`: referenziati" dell'app). Il prefisso family table non è garantito essere solo `UNI`/`CUSCI`:
se ricompaiono famiglie diverse (altri componenti normalizzati), vanno aggiunte allo stesso modo, se ricompaiono famiglie diverse (altri componenti normalizzati), vanno aggiunte allo stesso modo,
verificando prima con `scripts/Analyze.cs` che il contenuto tra `<` e `>` corrisponda a un codice verificando prima con `scripts/Analyze.cs` che il contenuto tra `<` e `>` corrisponda a un codice
@@ -271,7 +285,7 @@ livello) serve una BFS:
Stessa logica di estrazione candidati ed euristica del codice radice (sopra), Stessa logica di estrazione candidati ed euristica del codice radice (sopra),
duplicata in due punti del codice per scopi diversi: duplicata in due punti del codice per scopi diversi:
- [`AssemblyComponentScanner.EstraiCodiciReferenziati`](../CadSearchGui/AssemblyComponentScanner.cs) + `CodiceRadice` — estrazione dei candidati da un file, usata dalla BFS in [`Form1.RisolviAlberoComponenti`](../CadSearchGui/Form1.cs) (fase "Trova componenti referenziati") e [`Form1.AssiemeCompleto`](../CadSearchGui/Form1.cs) (fase "Verifica completezza"). Entrambe seminano l'insieme dei codici visitati con codice + radice di ogni file di partenza, e caricano una volta sola tutti i codici noti all'indice (`Form1.CaricaCodiciNoti`) sia per validare i token generici delle parti commerciali sia per risolvere i percorsi dei componenti trovati, senza una query SQL per candidato. - [`AssemblyComponentScanner.EstraiCodiciReferenziati`](../PrepStep/AssemblyComponentScanner.cs) + `CodiceRadice` — estrazione dei candidati da un file, usata dalla BFS in [`Form1.RisolviAlberoComponenti`](../PrepStep/Form1.cs) (fase "Trova componenti referenziati") e [`Form1.AssiemeCompleto`](../PrepStep/Form1.cs) (fase "Verifica completezza"). Entrambe seminano l'insieme dei codici visitati con codice + radice di ogni file di partenza, e caricano una volta sola tutti i codici noti all'indice (`Form1.CaricaCodiciNoti`) sia per validare i token generici delle parti commerciali sia per risolvere i percorsi dei componenti trovati, senza una query SQL per candidato.
- [`scripts/ExtractCreoStrings.cs`](../scripts/ExtractCreoStrings.cs) e [`scripts/BomViaDb.cs`](../scripts/BomViaDb.cs) — stessa BFS ricorsiva standalone da riga di comando (copia indipendente della stessa euristica, nessun `ProjectReference` verso la GUI). Usati per confrontare l'euristica contro l'albero Creo di un singolo file durante il reverse engineering, senza dover passare dalla UI/dal DB `raw`. - [`scripts/ExtractCreoStrings.cs`](../scripts/ExtractCreoStrings.cs) e [`scripts/BomViaDb.cs`](../scripts/BomViaDb.cs) — stessa BFS ricorsiva standalone da riga di comando (copia indipendente della stessa euristica, nessun `ProjectReference` verso la GUI). Usati per confrontare l'euristica contro l'albero Creo di un singolo file durante il reverse engineering, senza dover passare dalla UI/dal DB `raw`.
Nota: le query contro `files_ultima_versione` filtrano solo per Nota: le query contro `files_ultima_versione` filtrano solo per
@@ -281,6 +295,82 @@ per `data_modifica DESC` e tenuta solo la riga più recente per tipo, altrimenti
si rischia di risolvere un codice contro una copia diversa (e potenzialmente si rischia di risolvere un codice contro una copia diversa (e potenzialmente
diversa nel contenuto) da quella realmente referenziata. diversa nel contenuto) da quella realmente referenziata.
### Trappola nella BFS: un fallimento di placement non è definitivo
Un codice può comparire come riferimento fantasma (senza vero placement, vedi
sopra) in un file scansionato **prima** di quello in cui invece ha un vero
placement — tipicamente perché lo stesso codice è mostrato anche nel file
dell'assieme padre come promemoria/metadato, oltre che nel file del proprio
assieme dove è effettivamente posizionato. Osservato su un caso reale
(assieme `9244228558.asm`): il componente `9244236104` compare nel file
dell'assieme generale come riferimento nudo senza placement (viene scartato
lì, correttamente), ma ha un vero `Component id 45 (9244236104.PRT)` dentro
`9244228558_c01.asm`, il sotto-assieme che lo contiene realmente — scansionato
solo _dopo_ il file padre nella BFS.
Una BFS che marca un candidato come "già visto" al primo incontro, anche
quando quell'incontro è un fallimento di placement, blocca per sempre ogni
ri-verifica successiva: il componente resta perso anche quando il file giusto
viene scansionato più avanti. Nei casi più gravi il codice bloccato è esso
stesso un **sotto-assieme** (osservato con `9256510047` dentro
`9256518042.asm`): se il suo codice viene scartato prematuramente, il suo file
non viene nemmeno accodato, e si perde l'intero sotto-albero sottostante
(compresi `9056510047`/`9056510047_C01`/`9056510047_C02` in questo caso).
`scripts/BomViaDb.cs` (versione iniziale) aveva esattamente questo bug: un set
`codiciVisti` unico bloccava sia i candidati confermati sia quelli scartati
per mancato placement. Corretto separando le due cose: un fallimento di
placement non aggiunge il codice a nessun set di blocco, quindi resta
ritestabile ogni volta che lo stesso candidato ricompare in un file successivo
già in coda. L'accodamento di un nuovo file da scansionare resta invece
condizionato al placement confermato (non al solo essere un codice noto nel
DB) — senza questo secondo vincolo, un codice generico che per puro caso
coincide con un codice noto (es. `"915"`, `"932"`) farebbe esplodere la
scansione su file estranei in tutto l'indice (verificato: passa da ~100 a
quasi 19000 "componenti trovati" e la scansione si blocca).
**Il codice della GUI (`Form1.RisolviAlberoComponenti` +
`AssemblyComponentScanner.EstraiCodiciReferenziati`) non ha questo bug**,
verificato riproducendo lo stesso caso reale con un test dedicato: la
classificazione placement-sì/no è già interamente delegata a
`EstraiCodiciReferenziati`, chiamata da zero per ogni file scansionato — un
fallimento in un file non lascia traccia nel set `codiciVisti` del chiamante,
che accumula solo codici già confermati. Il file successivo che referenzia
davvero il componente lo ritrova quindi senza ostacoli, senza bisogno di
nessuna modifica. Vale comunque come nota di design da preservare: il giorno
in cui la logica di `RisolviAlberoComponenti` venisse riscritta per fare la
verifica di placement inline nella BFS (come `BomViaDb.cs`), andrebbe
applicata la stessa correzione.
### Seconda discrepanza trovata: l'eccezione dei codici con suffisso
Confrontando `scripts/BomViaDb.cs` (corretto come sopra) con la GUI sullo
stesso assieme di un caso reale più grande (`9284218024b.asm`, ~530
componenti), le due liste **non coincidevano**: lo script perdeva l'intero
sotto-assieme `2546220101` (`_C01`..`_C04`, inclusi i cuscinetti
`INAFAG_NA6901-XL_...` dentro `_C04`), presente invece nella BOM ufficiale
Creo e trovato correttamente dalla GUI.
Causa: `2546220101_C01`..`_C04` hanno un vero placement nel proprio file
(`2546220101.asm`), ma **senza il suffisso** - il testo riporta solo
`Componente ID N (2546220101.PRT)`, il dettaglio `_C01` compare solo altrove
nei metadati, non vicino al placement (stesso fenomeno già documentato sopra
in "Placement senza il dettaglio del suffisso/bracket", osservato lì solo per
famiglie UNI/dimensionali). `AssemblyComponentScanner.EstraiCodiciReferenziati`
già gestisce questo caso: un codice con suffisso di revisione/variante
(`SuffissoRegex`, es. `_C01`, `_D100`) o prefisso di family table nota (`UNI`,
`CUSCI_`) è considerato "forma nota" ed esentato dal placement esplicito -
basta che occorra almeno 2 volte nel file. `BomViaDb.cs` richiedeva invece
sempre il placement letterale per ogni codice, senza questa eccezione.
Corretto portando la stessa regola (`FormaNota` + conteggio occorrenze) in
`BomViaDb.cs`. Riverificato su entrambi i casi reali (`9244228558.asm`,
`9284218024b.asm`): script e GUI producono ora **esattamente lo stesso
insieme** di componenti, e in entrambi coincide con la BOM ufficiale Creo (a
meno delle sole istanze di family table risolte al nome del file realmente
indicizzato, es. `2502020105<CUSCI_DGBB7>` → `CUSCI_DGBB7` - comportamento
corretto, non una discrepanza).
## Strumento di analisi manuale ## Strumento di analisi manuale
[`scripts/Analyze.cs`](../scripts/Analyze.cs) stampa il contesto testuale ed [`scripts/Analyze.cs`](../scripts/Analyze.cs) stampa il contesto testuale ed
+125
View File
@@ -0,0 +1,125 @@
# Guida utente
Questa guida descrive, in ordine, tutti i passi da fare in **Cevolani StepPrep** per
arrivare dalla distinta base esportata dal gestionale ai file STEP pronti, passando per
**Creo Distributed Batch**. Per il dettaglio tecnico di come funziona internamente ogni
fase vedi [`architettura.md`](architettura.md); questa guida si ferma a cosa clicca e
cosa vede l'utente. La stessa sintesi è disponibile anche dentro l'app da **Aiuto → Guida**
(F1).
## Prima di iniziare
- Serve accesso alla share di rete con i file CAD (di default `X:\`).
- Serve l'estrazione "Distinta Base" dal gestionale (Fata): il file `.xls` così come
Fata lo salva, con le colonne Componente/Descrizione (foglio `"Distinta Base"`) — va
lasciato nel formato/estensione in cui viene esportato, senza risalvarlo o convertirlo
in altri formati prima di usarlo nell'app. Sotto l'estensione `.xls` il contenuto può
essere XML (SpreadsheetML) o Excel binario vero: l'app riconosce da sola quale dei due
è, non serve fare nulla di diverso a seconda del caso.
- Se il file segue la convenzione di nome del gestionale (`DBP_<codice>_<timestamp>`),
l'app aggiunge da sola l'assieme generale come componente in più — non serve fare
nulla di diverso.
- Conviene definire la **cartella di lavoro** giusta (fase 1, o "File → Scegli cartella
di lavoro...") prima di importare la distinta: è lì che finiscono CSV, `raw/` e, dopo
l'esportazione con Creo, `step/`.
- Se la cartella di lavoro è già stata usata per un'altra distinta, conviene **pulire
`raw/` e `step/`** prima di ripartire ("File → Pulisci raw e step..."): evita di
confondere i file CAD/STEP della copia precedente con quelli della nuova distinta (il
"File → Nuovo" descritto più sotto azzera solo lo stato di CSV/griglie dell'app, non
tocca i file già copiati su disco).
## 1. Database
Alla prima apertura, se un database (`cad_index.db`) è già stato creato in precedenza,
l'app lo ricarica da sola: si vede il pallino verde accanto a "Database", il percorso del
file e la data dell'ultimo aggiornamento.
- **Se il pallino è rosso** (nessun database trovato): premi **Aggiorna indice** per
scansionare la share (`X:\`) e crearne uno nuovo — l'operazione può richiedere qualche
minuto la prima volta, i giri successivi sono più veloci perché aggiornano solo quello
che è cambiato.
- **Se hai già un database da un'altra postazione o cartella**: premi **Apri DB...** e
selezionalo a mano.
- La **cartella di lavoro** (dove l'app scrive il CSV della distinta e la cartella
`raw/`) è scelta automaticamente e ricordata tra un avvio e l'altro; per cambiarla usa
**File → Scegli cartella di lavoro...**.
Premi **Avanti** quando il database è caricato.
## 2. Importa Distinta Base
- **1. Carica la distinta**: trascina il file `.xls` esportato da Fata nella finestra,
oppure premi **Scegli file distinta...** e selezionalo da Esplora risorse. Le righe importate
compaiono nella griglia (Componente/Descrizione) e viene scritto un CSV nella cartella
di lavoro.
- **2. Verifica la presenza dei file sul server**: compare solo dopo un import riuscito.
Premila per cercare ogni componente nel database — le righe della griglia si colorano
di verde (trovato) o rosso (non trovato nell'indice).
**Avanti** si sblocca solo dopo aver premuto "Verifica la presenza dei file sul server",
non basta il solo import: è il modo per accorgersi subito se il database è disallineato
rispetto alla share (es. indice non aggiornato) prima di andare avanti.
## 3. Crea cartella raw
Premi **Crea cartella raw**: per ogni componente della distinta trovato nell'indice,
l'app ne copia il file CAD dalla share nella cartella `raw/` (dentro la cartella di
lavoro). La griglia mostra il percorso CAD copiato per ogni componente.
## 4. Trova componenti referenziati
Gli assiemi (`.asm`) referenziano altri file (sotto-assiemi, parti, componenti soppressi,
riferimenti esterni) che Creo deve poter caricare insieme all'assieme principale, anche
se non compaiono nella distinta del gestionale. Premi **Trova componenti referenziati**:
l'app legge i file `.asm` già in `raw/`, risolve ricorsivamente tutto l'albero e copia
anche quei file in `raw/`.
Si apre una finestra con l'elenco di quanto trovato e copiato (PRT e sotto-assiemi), con
in fondo eventuali codici referenziati che non corrispondono a nessun file indicizzato
(possono essere disegni, componenti davvero mancanti, o falsi positivi della scansione —
non bloccano il lavoro, ma vale la pena controllarli). Da qui puoi anche aprire
direttamente la cartella `raw/` con **Apri cartella raw**.
Questo passo è opzionale se la distinta contiene solo parti singole senza sotto-assiemi:
in quel caso si può passare direttamente alla fase successiva.
## 5. Trascina su Creo Distributed Batch
L'ultima fase mostra l'elenco dei file `.asm` della distinta già presenti in `raw/`,
trascinabili. Da qui il passaggio a Creo è manuale, fuori dall'app:
1. Apri **Creo Distributed Batch** (dal menu Start di Windows, o dal file
`ptcdbatch.bat` nella cartella `bin` dell'installazione Creo).
2. Crea un nuovo gruppo di attività per l'esportazione STEP (menu **Actions → New Task
Group**), scegliendo il tipo di attività per l'export STEP — se in azienda è già
presente un profilo/gruppo predefinito per l'export STEP, riusa quello invece di
crearne uno nuovo.
3. Aggiungi i file: trascina i `.asm` dalla finestra dell'app (o direttamente da Esplora
risorse, dalla cartella `raw/`) sul gruppo di attività, oppure sfoglia e selezionali a
mano da Distributed Batch.
4. Verifica che il gruppo di attività punti al `config.pro`/profilo di export STEP giusto
(sezione **Configuration Files** del gruppo) — di norma è già impostato una volta sola
per tutti, non richiede modifiche a ogni giro.
5. Avvia l'elaborazione (**Start**/**Run**): Distributed Batch legge ogni `.asm`, con
tutto l'albero di sotto-componenti già raccolto in `raw/` nelle fasi 3-4, ed esporta i
file STEP nella cartella `step/` (dentro la cartella di lavoro, accanto a `raw/`).
I nomi esatti dei menu possono variare leggermente a seconda della versione di Creo e di
come Distributed Batch è già configurato in azienda — questi passi descrivono il flusso
generale (fonte: [documentazione e community PTC](https://www.ptc.com/en/support/article/CS28241));
in caso di dubbio su profili/config già pronti, chiedi a chi ha impostato Distributed
Batch la prima volta.
**Se Distributed Batch fallisce con `[-4] Dati di input non completi`**, quasi sempre
manca un file nell'albero dei componenti — Creo non dice quale. Torna alla fase 4 e
controlla l'elenco dei "non trovati": è il punto di partenza per capire cosa recuperare
prima di riprovare.
## Ripartire con una nuova distinta
Quando devi processare un'altra distinta, usa **File → Nuovo** (Ctrl+N): azzera CSV, raw
e verifica delle fasi 2-5 e riporta al passo 2, senza dover ricaricare il database o
la cartella di lavoro (restano quelli già impostati in fase 1).
Per ripulire le cartelle `raw/` e `step/` della cartella di lavoro prima di un nuovo giro
(senza toccare l'indice), usa **File → Pulisci raw e step...**.
+24 -54
View File
@@ -1,17 +1,16 @@
#:project ../PrepStep/PrepStep.csproj
#:property TargetFramework=net10.0-windows
#:package Microsoft.Data.Sqlite@10.0.12 #:package Microsoft.Data.Sqlite@10.0.12
// dotnet run scripts/BomViaDb.cs -- <file.asm> [cad_index.db] [output.txt] // dotnet run scripts/BomViaDb.cs -- <file.asm> [cad_index.db] [output.txt]
// //
// PROVA: a differenza di ExtractCreoStrings.cs (che restringe i candidati a un solo // Stessa risoluzione ricorsiva dell'albero componenti di Form1.RisolviAlberoComponenti
// pattern di codice, tarato sui codici numerici Pro-E), qui l'estrazione è generica - // nella GUI, da riga di comando: usa direttamente PrepStep.AssemblyComponentScanner (via
// qualunque run di lettere/cifre/underscore/trattino - e il DB decide se è un codice // "#:project", niente ProjectReference separato) invece di duplicare l'euristica di
// vero, così si intercettano anche i nomi alfabetici delle parti commerciali scaricate // estrazione/placement. Un'unica fonte di verità - niente rischio che script e GUI
// come STEP (es. "ANELLO_ESTERNO-1-SOLID1"). L'essere nel DB da solo non basta però: // divergano silenziosamente, come accaduto prima di questa riscrittura (vedi
// codici corti o generici (es. "907", "NONE") comparirebbero per puro rumore binario, // docs/formato-asm.md).
// quindi si richiede comunque il placement esplicito nel file, come in
// ExtractCreoStrings.cs. Stessa espansione ricorsiva sui sotto-assiemi.
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Data.Sqlite; using Microsoft.Data.Sqlite;
using PrepStep;
if (args.Length < 1) if (args.Length < 1)
{ {
@@ -23,20 +22,12 @@ string asmIniziale = args[0];
string dbPath = args.Length > 1 ? args[1] : "cad_index.db"; string dbPath = args.Length > 1 ? args[1] : "cad_index.db";
string outPath = args.Length > 2 ? args[2] : "BOM.txt"; string outPath = args.Length > 2 ? args[2] : "BOM.txt";
// Oltre il 90% dei codici in indice sono numerici Pro-E, ma una minoranza (parti
// commerciali scaricate come STEP, es. "ANELLO_ESTERNO-1-SOLID1") ha nomi alfabetici
// con trattini/underscore. Niente forma di codice ipotizzata a mano: si tokenizza in
// modo generico (run di lettere/cifre/underscore/trattino) e si lascia al DB decidere
// cos'è un codice vero - stesso principio già usato per il filtro, esteso anche
// all'estrazione dei candidati. Cattura anche "UNI6592"/"CUSCI_ACBB7" dentro
// "<UNI6592>" gratis: '<' e '>' non fanno parte del charset e delimitano il token.
var codiceRegex = new Regex(@"[A-Za-z0-9_-]{3,}", RegexOptions.Compiled);
using var connection = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly"); using var connection = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly");
connection.Open(); connection.Open();
// Tutti i codici prt/asm noti all'indice, caricati una volta sola: il lookup per ogni // Tutti i codici prt/asm noti all'indice, caricati una volta sola: il lookup per ogni
// candidato estratto dal binario diventa O(1) invece di una query per file. // candidato estratto dal binario diventa O(1) invece di una query per file, e serve anche
// a EstraiCodiciReferenziati per validare i token generici delle parti commerciali.
var codiciNoti = new Dictionary<string, List<(string Tipo, string Percorso)>>(StringComparer.OrdinalIgnoreCase); var codiciNoti = new Dictionary<string, List<(string Tipo, string Percorso)>>(StringComparer.OrdinalIgnoreCase);
using (var command = connection.CreateCommand()) using (var command = connection.CreateCommand())
{ {
@@ -54,51 +45,31 @@ using (var command = connection.CreateCommand())
} }
Console.WriteLine($"Codici prt/asm noti nell'indice: {codiciNoti.Count}"); Console.WriteLine($"Codici prt/asm noti nell'indice: {codiciNoti.Count}");
// Stesso controllo di placement di ExtractCreoStrings.cs/AssemblyComponentScanner.cs:
// la sola presenza del codice nel DB non basta, perché codici corti o generici
// (es. "907", "NONE", "start_part") possono comparire per puro rumore binario. Un
// componente vero è sempre referenziato in una di queste forme (vedi
// docs/formato-asm.md): "Componente ID N (<codice>...)"/"Component id N (<codice>...)",
// il codice ripetuto due volte separato da un byte NUL, il campo
// "family_name\0<codice>\0", o il codice seguito da un bracket "<codice><GENERICO>".
bool HaPlacement(string testo, string codice) =>
Regex.IsMatch(testo, $@"Component(?:e)? id \d+ \([^()]*{Regex.Escape(codice)}[^()]*\)", RegexOptions.IgnoreCase)
|| testo.Contains(codice + "\0" + codice + "\0")
|| testo.Contains("family_name\0" + codice + "\0")
|| testo.Contains(codice + "<");
var codiceProprio = Path.GetFileName(asmIniziale).Split('.')[0]; var codiceProprio = Path.GetFileName(asmIniziale).Split('.')[0];
// Stessa esclusione di ExtractCreoStrings.cs: un assieme "_D..." referenzia sempre al var codiceRadice = AssemblyComponentScanner.CodiceRadice(codiceProprio);
// suo interno anche l'assieme generale a cui appartiene (riferimento fantasma verso
// l'alto), da escludere subito o si risale tutto l'albero del macchinario.
var codiceRadice = codiceProprio.Split('_')[0];
var componentiTrovati = new SortedSet<string>(StringComparer.OrdinalIgnoreCase); var componentiTrovati = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
var scartatiNonInIndice = new SortedSet<string>(StringComparer.OrdinalIgnoreCase); var nonTrovati = new List<string>();
var scartatiSenzaPlacement = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
var codiciVisti = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { codiceProprio, codiceRadice }; var codiciVisti = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { codiceProprio, codiceRadice };
var daScansionare = new Queue<string>(new[] { asmIniziale }); var daScansionare = new Queue<string>(new[] { asmIniziale });
while (daScansionare.Count > 0) while (daScansionare.Count > 0)
{ {
var asm = daScansionare.Dequeue(); var asm = daScansionare.Dequeue();
var testo = Encoding.Latin1.GetString(File.ReadAllBytes(asm)); foreach (var codice in AssemblyComponentScanner.EstraiCodiciReferenziati(asm, codiciNoti.Keys))
var candidati = codiceRegex.Matches(testo).Select(m => m.Value).ToHashSet(StringComparer.OrdinalIgnoreCase);
foreach (var candidato in candidati)
{ {
if (!codiciVisti.Add(candidato)) // Solo i codici già confermati da EstraiCodiciReferenziati arrivano qui: nessun
// rischio del bug "primo fallimento blocca per sempre" (vedi docs/formato-asm.md),
// perché il placement è già stato verificato per questo file specifico al suo
// interno, da zero, indipendentemente da cosa è successo negli altri file.
if (!codiciVisti.Add(codice))
continue; continue;
if (!codiciNoti.TryGetValue(candidato, out var componenti)) if (!codiciNoti.TryGetValue(codice, out var componenti))
{ {
scartatiNonInIndice.Add(candidato); nonTrovati.Add(codice);
continue; continue;
} }
if (!HaPlacement(testo, candidato)) componentiTrovati.Add(codice);
{
scartatiSenzaPlacement.Add(candidato);
continue;
}
componentiTrovati.Add(candidato);
foreach (var (tipo, percorso) in componenti) foreach (var (tipo, percorso) in componenti)
if (tipo == "asm") if (tipo == "asm")
daScansionare.Enqueue(percorso); daScansionare.Enqueue(percorso);
@@ -107,7 +78,6 @@ while (daScansionare.Count > 0)
File.WriteAllLines(outPath, componentiTrovati); File.WriteAllLines(outPath, componentiTrovati);
Console.WriteLine($"{componentiTrovati.Count} componenti scritti in {outPath}"); Console.WriteLine($"{componentiTrovati.Count} componenti scritti in {outPath}");
Console.WriteLine($"Candidati nell'indice ma senza placement (scartati, {scartatiSenzaPlacement.Count}): {string.Join(", ", scartatiSenzaPlacement.Take(30))}" Console.WriteLine($"Candidati non nell'indice (scartati, {nonTrovati.Count}): {string.Join(", ", nonTrovati.Take(30))}"
+ (scartatiSenzaPlacement.Count > 30 ? ", ..." : "")); + (nonTrovati.Count > 30 ? ", ..." : ""));
Console.WriteLine($"Candidati non nell'indice (rumore binario): {scartatiNonInIndice.Count}");
return 0; return 0;
+201
View File
@@ -0,0 +1,201 @@
#:project ../PrepStep/PrepStep.csproj
#:property TargetFramework=net10.0-windows
// dotnet run scripts/CreoBom.cs -- <file.asm.rev> [-p cartella]... [-o out.bom.1]
//
// Genera la distinta base (BOM) di un assieme Creo, dato un elenco di cartelle dove
// cercare i sottoassiemi (-p). La lettura del formato binario (sezione "members" +
// parametro "ConfigName", vedi docs/formato-asm-record.md) vive in
// PrepStep.AsmRecordReader, via "#:project" - stessa fonte usata da Form1 nella GUI (fase
// 4/5, con fallback automatico all'euristica di AssemblyComponentScanner.cs per i file che
// non rispettano il formato validato): qui solo la ricorsione sui sottoassiemi, il
// riepilogo delle quantita' e la resa nel formato esatto della BOM Creo.
using System.Text;
using System.Text.RegularExpressions;
using PrepStep;
if (args.Length == 0)
{
Console.WriteLine("Uso: dotnet run scripts/CreoBom.cs -- <file.asm.rev> [-p cartella]... [-o out.bom.1]");
return 1;
}
string fileIniziale = args[0];
var cartelle = new List<string>();
string? outPath = null;
for (int i = 1; i < args.Length; i++)
{
if ((args[i] == "-p" || args[i] == "--path") && i + 1 < args.Length)
cartelle.Add(args[++i]);
else if (args[i] == "-o" && i + 1 < args.Length)
outPath = args[++i];
}
if (cartelle.Count == 0)
cartelle.Add(Path.GetDirectoryName(Path.GetFullPath(fileIniziale)) ?? ".");
string testoBom = CreoAsm.GeneraBom(fileIniziale, cartelle);
if (outPath is not null)
{
File.WriteAllBytes(outPath, new byte[] { 0xEF, 0xBB, 0xBF }.Concat(Encoding.UTF8.GetBytes(testoBom)).ToArray());
Console.WriteLine($"scritto {outPath} ({testoBom.Split("\r\n").Length} righe)");
}
else
{
Console.Write(testoBom.Replace("\r\n", "\n"));
}
return 0;
/// <summary>Ricorsione sui sottoassiemi e resa nel formato della BOM esportata da Creo. La
/// lettura del formato binario vive in <see cref="AsmRecordReader"/> (PrepStep/, condivisa
/// con la GUI). Vedi docs/formato-asm-record.md per il dettaglio dei record e i limiti noti
/// (stato soppresso non gestito, matrice di posizionamento non decodificata, deduplica dei
/// blocchi non dimostrata su assiemi che montano lo stesso sottoassieme due volte).</summary>
public static class CreoAsm
{
private static readonly Dictionary<int, string> TipoNome = new() { [1] = "Sottoassieme", [2] = "Parte" };
/// <summary>Voci distinte con quantita', nell'ordine di prima comparsa (come la BOM).</summary>
public static List<((int Tipo, string Nome) Chiave, int Qta)> Rollup(IEnumerable<AsmRecordReader.Membro> membri)
{
var ordine = new List<(int, string)>();
var qty = new Dictionary<(int, string), int>();
foreach (var m in membri)
{
var chiave = (m.Tipo, m.Nome);
if (!qty.ContainsKey(chiave))
ordine.Add(chiave);
qty[chiave] = qty.GetValueOrDefault(chiave) + 1;
}
return ordine.Select(c => (c, qty[c])).ToList();
}
// --- risoluzione dei file -------------------------------------------
/// <summary>nome modello -> membri, cercando &lt;nome&gt;.asm.&lt;rev&gt; nelle cartelle
/// date. A parita' di nome vince la revisione piu' alta presente. Nome null se il file
/// non c'e' in nessuna delle cartelle.</summary>
private static Func<string, List<AsmRecordReader.Membro>?> CreaFinder(IEnumerable<string> cartelle)
{
var indice = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var revisioni = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
foreach (var dir in cartelle)
{
if (!Directory.Exists(dir))
continue;
foreach (var f in Directory.GetFiles(dir, "*.asm.*"))
{
if (!int.TryParse(Path.GetExtension(f).TrimStart('.'), out int rev))
continue;
string chiave = Path.GetFileName(f).Split('.')[0];
if (!revisioni.TryGetValue(chiave, out int attuale) || rev > attuale)
{
indice[chiave] = f;
revisioni[chiave] = rev;
}
}
}
var cache = new Dictionary<string, List<AsmRecordReader.Membro>?>();
return nome =>
{
if (!cache.TryGetValue(nome, out var mem))
{
mem = indice.TryGetValue(nome, out var percorso) ? AsmRecordReader.LeggiMembri(File.ReadAllBytes(percorso)) : null;
cache[nome] = mem;
}
return mem;
};
}
// --- costruzione dell'albero -----------------------------------------
private sealed record Blocco(string Tipo, string Nome, List<((int Tipo, string Nome) Chiave, int Qta)>? Righe);
/// <summary>Blocchi in ordine depth-first pre-order. Ogni modello compare una volta sola
/// (deduplica per nome) anche se montato piu' volte.</summary>
private static List<Blocco> Blocchi(string root, Func<string, List<AsmRecordReader.Membro>?> get)
{
var risultato = new List<Blocco>();
var visti = new HashSet<string>();
void Visita(string nome, string tipo)
{
if (!visti.Add(nome))
return; // Creo stampa ogni modello una volta sola
var mem = get(nome);
if (mem is null)
{
risultato.Add(new Blocco(tipo, nome, null));
return;
}
var righe = Rollup(mem);
risultato.Add(new Blocco(tipo, nome, righe));
foreach (var (chiave, _) in righe)
if (chiave.Tipo == 1)
Visita(chiave.Nome, "Sottoassieme");
}
Visita(root, "Assieme");
return risultato;
}
/// <summary>Totale per parte, moltiplicato lungo l'albero, in ordine di prima comparsa: una
/// parte che compare a piu' livelli occupa la posizione della sua prima comparsa e somma
/// le quantita'.</summary>
private static void Sommario(string root, Func<string, List<AsmRecordReader.Membro>?> get, int mult,
List<string> ordine, Dictionary<string, int> acc)
{
var mem = get(root);
if (mem is null)
return;
foreach (var (chiave, qta) in Rollup(mem))
{
if (chiave.Tipo == 2)
{
if (!acc.ContainsKey(chiave.Nome))
ordine.Add(chiave.Nome);
acc[chiave.Nome] = acc.GetValueOrDefault(chiave.Nome) + qta * mult;
}
else
{
Sommario(chiave.Nome, get, mult * qta, ordine, acc);
}
}
}
// --- resa nel formato di Creo ------------------------------------------
private static string RigaVoce(int qta, string tipo, string nome) => $"{qta,6} {tipo,12} {nome}";
/// <summary>Testo della BOM, identico byte per byte a quello esportato da Creo (UTF-8 con
/// BOM, CRLF - vedi GeneraBom per la scrittura su file).</summary>
private static string Render(string root, Func<string, List<AsmRecordReader.Membro>?> get)
{
var pezzi = new List<string>();
foreach (var blocco in Blocchi(root, get))
{
string testa = $"{blocco.Tipo} {blocco.Nome} contiene:";
if (blocco.Righe is null)
{
pezzi.Add(testa + "\r\n" + new string(' ', 5) + "*** NON RISOLTO: file .asm non trovato ***");
continue;
}
var righe = blocco.Righe.Select(r => RigaVoce(r.Qta, TipoNome[r.Chiave.Tipo], r.Chiave.Nome));
pezzi.Add(string.Join("\r\n", new[] { testa }.Concat(righe)));
}
var ordine = new List<string>();
var acc = new Dictionary<string, int>();
Sommario(root, get, 1, ordine, acc);
var totale = ordine.Select(n => RigaVoce(acc[n], "Parte", n));
pezzi.Add(string.Join("\r\n", new[] { $"Summary of items for {root}:" }.Concat(totale)));
// due righe vuote iniziali, tre fra i blocchi, due prima del riepilogo
return "\r\n\r\n" + string.Join("\r\n\r\n\r\n\r\n", pezzi.Take(pezzi.Count - 1)) + "\r\n\r\n\r\n" + pezzi[^1] + "\r\n";
}
public static string GeneraBom(string percorsoAsm, IEnumerable<string> cartelle)
{
byte[] dati = File.ReadAllBytes(percorsoAsm);
return Render(AsmRecordReader.NomeProprio(dati), CreaFinder(cartelle));
}
}
+22 -110
View File
@@ -1,15 +1,15 @@
#:project ../PrepStep/PrepStep.csproj
#:property TargetFramework=net10.0-windows
#:package Microsoft.Data.Sqlite@10.0.12 #:package Microsoft.Data.Sqlite@10.0.12
// dotnet run scripts/ExtractCreoStrings.cs -- <file.asm> [cad_index.db] [output.txt] // dotnet run scripts/ExtractCreoStrings.cs -- <file.asm> [cad_index.db] [output.txt]
// //
// Estrae dal file .asm i codici dei componenti referenziati, espandendo ricorsivamente // Come scripts/BomViaDb.cs, ma senza passare "codiciNoti" a EstraiCodiciReferenziati: si
// ogni sotto-assieme (.asm) trovato nell'indice, finché non restano solo PRT (foglie) o // riconoscono solo i codici numerici Pro-E (che si validano dalla forma, senza bisogno del
// codici non presenti nell'indice. Stessa euristica di CadSearchGui/AssemblyComponentScanner.cs: // DB - vedi PrepStep/AssemblyComponentScanner.cs), non le parti commerciali con nome
// i codici veri compaiono più volte nel file, il rumore binario quasi sempre una sola volta; // alfabetico. Prova più semplice/mirata rispetto a BomViaDb.cs, utile quando serve solo
// gli ID interni "ModelView#<numero>" sono esclusi via lookbehind - vedi scripts/Analyze.cs. // individuare/segnalare codici numerici mancanti dall'indice.
// Ogni codice trovato viene validato contro l'indice cad_index.db.
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Data.Sqlite; using Microsoft.Data.Sqlite;
using PrepStep;
if (args.Length < 1) if (args.Length < 1)
{ {
@@ -21,114 +21,29 @@ string asmIniziale = args[0];
string dbPath = args.Length > 1 ? args[1] : "cad_index.db"; string dbPath = args.Length > 1 ? args[1] : "cad_index.db";
string outPath = args.Length > 2 ? args[2] : "BOM.txt"; string outPath = args.Length > 2 ? args[2] : "BOM.txt";
// Seconda alternativa: componenti normalizzati definiti come family table in Creo (viti/
// dadi/rosette UNI, ma anche cuscinetti CUSCI_...). Compaiono come "Componente ID N
// (R12--_-_U6592<UNI6592>.PRT)": "R12--_-_U6592" è il nome della singola istanza (riga
// della family table, non un file separato su disco), il file reale indicizzato è il
// generico "uni6592.prt.<versione>"/"cusci_acbb7.prt.<versione>" - va quindi estratto solo
// il contenuto tra "<" e ">".
// Terza alternativa: varianti dimensionali con suffisso puramente numerico (es. cinghie
// "2702220182_8188", dove "8188" è una misura, non una lettera di revisione/sotto-assieme
// come "_C01"/"_D100") - la lettera dopo "_" è quindi opzionale, non obbligatoria. La
// lettera di revisione e il suffisso "_" possono anche comparire insieme sullo stesso
// codice (es. "9284200366A_C01", componente di un sotto-assieme già di per sé revisionato).
var codiceRegex = new Regex(
@"(?<!ModelView#)\b\d{8,12}[A-Za-z]{0,2}(?:_[A-Za-z]?\d{2,4})?\b|(?<=<)(?:UNI\d+|CUSCI_[A-Za-z0-9]+)(?=>)",
RegexOptions.Compiled);
const int occorrenzeMinime = 2;
HashSet<string> EstraiCodiciReferenziati(string percorsoAsm)
{
var testo = Encoding.Latin1.GetString(File.ReadAllBytes(percorsoAsm));
var occorrenze = new Dictionary<string, int>();
foreach (Match m in codiceRegex.Matches(testo))
occorrenze[m.Value] = occorrenze.GetValueOrDefault(m.Value) + 1;
var candidati = occorrenze.Where(kv => kv.Value >= occorrenzeMinime).Select(kv => kv.Key).ToHashSet();
// Ripetersi >=2 volte nel file non basta per un codice "nudo" (senza suffisso "_..."):
// riferimenti al proprio assieme padre (nome sessione/file) o a codici superati/
// rinominati (caso "fantasma", docs/formato-asm.md) ripetono comunque il codice più
// volte pur non essendo mai un componente vero. Per questi si richiede un vero
// placement, in una delle quattro forme osservate nel binario: "Componente ID N
// (<codice>...)"/"Component id N (<codice>...)" (la stringa cambia con la
// localizzazione Creo del file); il codice ripetuto due volte di seguito separato da
// un singolo byte NUL ("<codice>\0<codice>\0", osservato per componenti di family
// table non-UNI, es. cuscinetti "CUSCI_..."); il campo "family_name\0<codice>\0"
// (osservato una sola volta per file, su un sotto-assieme referenziato come family
// table generica invece che con un placement testuale esplicito); oppure il codice
// seguito immediatamente da un bracket "<codice><GENERICO>" - l'istanza di una family
// table non-UNI vista nel nome (es. "2510060303<CUSCI_ACBB7>", un cuscinetto): il
// codice dell'istanza non è mai indicizzato a sé (solo il generico tra "<" e ">" lo è,
// vedi sotto), ma questo segnale lo fa comunque sopravvivere fino alla ricerca nel DB
// invece di scomparire in silenzio come "nudo senza placement" - lì verrà comunque
// scartato come "non in indice", ma in modo tracciabile. Non si applica alle varianti
// con suffisso "_..." né alle istanze family table UNI: in alcuni file il placement
// riporta solo il nome istanza/generico senza il dettaglio del suffisso o del bracket
// "<...>", che vive altrove nei metadati - lì l'occorrenza ripetuta resta l'unico
// segnale disponibile.
bool HaPlacement(string codice) =>
Regex.IsMatch(testo, $@"Component(?:e)? id \d+ \([^()]*{Regex.Escape(codice)}[^()]*\)", RegexOptions.IgnoreCase)
|| testo.Contains(codice + "\0" + codice + "\0")
|| testo.Contains("family_name\0" + codice + "\0")
|| testo.Contains(codice + "<");
candidati.RemoveWhere(c => Regex.IsMatch(c, @"^\d{8,12}[A-Za-z]{0,2}$") && !HaPlacement(c));
return candidati;
}
using var connection = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly"); using var connection = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly");
connection.Open(); connection.Open();
// "files_ultima_versione" è una VIEW con ROW_NUMBER() OVER (...), che SQLite non var codiciNoti = new Dictionary<string, List<(string Tipo, string Percorso)>>(StringComparer.OrdinalIgnoreCase);
// materializza: ogni query la ricalcola sull'intera tabella "files" (~230ms anche per un using (var command = connection.CreateCommand())
// solo codice, su ~300k righe). Batchare tutti i codici trovati in un file in un'unica
// query paga quel costo una volta per file invece che una volta per codice - decisivo
// sugli assiemi con centinaia di sotto-componenti referenziati.
Dictionary<string, List<(string Tipo, string Percorso)>> CercaComponenti(IReadOnlyCollection<string> codici)
{ {
var risultato = new Dictionary<string, List<(string, string)>>(StringComparer.OrdinalIgnoreCase);
foreach (var codice in codici)
risultato[codice] = new List<(string, string)>();
if (codici.Count == 0)
return risultato;
using var command = connection.CreateCommand();
var nomiParametri = codici.Select((_, i) => $"@c{i}").ToList();
// Lo stesso codice+tipo può esistere in più cartelle radice (duplicati/copie storiche
// sulla share, es. un generico family table ripetuto in 5 progetti scollegati): ordinato
// per data_modifica DESC, si tiene solo il più recente per codice+tipo (stessa regola di
// Form1.RisolviAlberoComponenti, vedi docs/formato-asm.md).
command.CommandText = "SELECT codice, tipo_file, percorso_completo FROM files_ultima_versione " command.CommandText = "SELECT codice, tipo_file, percorso_completo FROM files_ultima_versione "
+ $"WHERE LOWER(codice) IN ({string.Join(",", nomiParametri)}) AND LOWER(tipo_file) IN ('prt', 'asm') " + "WHERE LOWER(tipo_file) IN ('prt', 'asm') ORDER BY codice, tipo_file, data_modifica DESC";
+ "ORDER BY codice, tipo_file, data_modifica DESC";
int i = 0;
foreach (var codice in codici)
command.Parameters.AddWithValue(nomiParametri[i++], codice.ToLowerInvariant());
var tipiVisti = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
using var reader = command.ExecuteReader(); using var reader = command.ExecuteReader();
while (reader.Read()) while (reader.Read())
{ {
string codiceDb = reader.GetString(0); string codice = reader.GetString(0);
string tipo = reader.GetString(1).ToLowerInvariant(); string tipo = reader.GetString(1).ToLowerInvariant();
var visti = tipiVisti.TryGetValue(codiceDb, out var v) ? v : tipiVisti[codiceDb] = new HashSet<string>(); var lista = codiciNoti.TryGetValue(codice, out var l) ? l : codiciNoti[codice] = new();
if (visti.Add(tipo)) if (!lista.Any(x => x.Tipo == tipo))
risultato[codiceDb].Add((tipo, reader.GetString(2))); lista.Add((tipo, reader.GetString(2)));
} }
return risultato;
} }
Console.WriteLine($"Codici prt/asm noti nell'indice: {codiciNoti.Count}");
// Espansione ricorsiva: ogni sotto-assieme (.asm) trovato viene a sua volta letto e i suoi
// componenti aggiunti alla coda, finché non restano solo PRT (foglie) o codici non presenti
// nell'indice - stessa logica di Form1.RisolviAlberoComponenti.
var codiceProprio = Path.GetFileName(asmIniziale).Split('.')[0]; var codiceProprio = Path.GetFileName(asmIniziale).Split('.')[0];
// Un assieme con suffisso "_D..." referenzia sempre al suo interno anche l'assieme var codiceRadice = AssemblyComponentScanner.CodiceRadice(codiceProprio);
// generale (codice nudo, senza suffisso) a cui appartiene - riferimento fantasma verso
// l'alto, non un componente vero (vedi docs/formato-asm.md). Va escluso subito: se
// seguito, riporta dentro l'intero albero del macchinario invece del solo sotto-assieme
// richiesto (visto su 9284208188_D600 -> 9284208188, che da solo genera 598 falsi
// positivi invece di ~90 componenti reali).
var codiceRadice = codiceProprio.Split('_')[0];
var componentiTrovati = new SortedSet<string>(StringComparer.OrdinalIgnoreCase); var componentiTrovati = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
var nonTrovati = new List<string>(); var nonTrovati = new List<string>();
var codiciVisti = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { codiceProprio, codiceRadice }; var codiciVisti = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { codiceProprio, codiceRadice };
@@ -137,14 +52,11 @@ var daScansionare = new Queue<string>(new[] { asmIniziale });
while (daScansionare.Count > 0) while (daScansionare.Count > 0)
{ {
var asm = daScansionare.Dequeue(); var asm = daScansionare.Dequeue();
var candidati = EstraiCodiciReferenziati(asm) foreach (var codice in AssemblyComponentScanner.EstraiCodiciReferenziati(asm))
.Where(codiciVisti.Add)
.ToList();
var trovatiPerCodice = CercaComponenti(candidati);
foreach (var codice in candidati)
{ {
var componenti = trovatiPerCodice[codice]; if (!codiciVisti.Add(codice))
if (componenti.Count == 0) continue;
if (!codiciNoti.TryGetValue(codice, out var componenti))
{ {
nonTrovati.Add(codice); nonTrovati.Add(codice);
continue; continue;