Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a8a7d0417 | ||
|
|
c75dd21551 | ||
|
|
e7b5d85f6b | ||
|
|
a7e0df0247 | ||
|
|
9660c4883d | ||
|
|
e6de563f60 | ||
|
|
1f6cb2da00 | ||
|
|
338a7327a0 | ||
|
|
52d126372d | ||
|
|
ee408219bd |
No files matched your search
@@ -28,6 +28,7 @@ publish/
|
||||
/*.csv
|
||||
/raw/
|
||||
/step/
|
||||
/step_finali/
|
||||
*.dxc
|
||||
|
||||
# Report BOM generati manualmente da Creo per debug/validazione: mai una dipendenza
|
||||
|
||||
@@ -59,8 +59,12 @@ entrambi rimossi: ora l'unico programma è questo, con GUI e indicizzazione inte
|
||||
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 "Report STEP realizzati" (confronto
|
||||
con `step/`).
|
||||
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)
|
||||
@@ -101,9 +105,12 @@ disco di default `X:\`, hardcoded in `Form1.RadiceDiscoDefault`).
|
||||
`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
|
||||
al file. `ExtractCreoStrings.cs` estrae la BOM di un `.asm` risolvendo ricorsivamente i
|
||||
sotto-assiemi contro `cad_index.db` (stessa logica di `Form1.AssiemeCompleto`); `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.
|
||||
sotto-assiemi contro `cad_index.db` (stessa logica di `Form1.AssiemeCompleto`, solo codici
|
||||
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
|
||||
`ModelView#`, self-reference, riferimenti fantasma a codici superati/rinominati — sintatticamente
|
||||
|
||||
@@ -14,8 +14,10 @@ public class AssemblyComponentScannerTests
|
||||
[Fact]
|
||||
public void Scarta_codici_con_una_sola_occorrenza()
|
||||
{
|
||||
// "11112222" compare 2 volte (riferimento vero), "99998888" una sola (rumore).
|
||||
string percorso = ScriviAsmFinto("blah 11112222 blah 99998888 blah 11112222 fine");
|
||||
// "11112222" compare 2 volte con un vero placement (riferimento vero), "99998888"
|
||||
// una sola volta senza placement (rumore).
|
||||
string percorso = ScriviAsmFinto(
|
||||
"blah Componente ID 1 (11112222.PRT) blah 99998888 blah 11112222 fine");
|
||||
try
|
||||
{
|
||||
var codici = AssemblyComponentScanner.EstraiCodiciReferenziati(percorso);
|
||||
@@ -25,6 +27,20 @@ public class AssemblyComponentScannerTests
|
||||
finally { File.Delete(percorso); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Scarta_codice_ripetuto_senza_un_vero_placement()
|
||||
{
|
||||
// Riferimento al nome di un altro assieme (es. il padre che lo contiene): ripetuto
|
||||
// più volte ma mai dentro un placement "Componente ID N (...)" - va scartato.
|
||||
string percorso = ScriviAsmFinto("padre 33334444 assieme 33334444_D200 33334444");
|
||||
try
|
||||
{
|
||||
var codici = AssemblyComponentScanner.EstraiCodiciReferenziati(percorso);
|
||||
Assert.DoesNotContain("33334444", codici);
|
||||
}
|
||||
finally { File.Delete(percorso); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Riconosce_sotto_assiemi_con_suffisso_stile_D100()
|
||||
{
|
||||
@@ -67,8 +83,69 @@ public class AssemblyComponentScannerTests
|
||||
finally { File.Delete(percorso); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Estrae_il_codice_generico_da_istanze_di_family_table_UNI()
|
||||
{
|
||||
// Visto su 9244228576.asm: le viti normalizzate compaiono come istanza di family
|
||||
// table "R12--_-_U6592<UNI6592>", ma il file reale indicizzato è "uni6592.prt".
|
||||
string percorso = ScriviAsmFinto(
|
||||
"Componente ID 1 (R12--_-_U6592<UNI6592>.PRT) x Componente ID 2 (R12--_-_U6592<UNI6592>.PRT)");
|
||||
try
|
||||
{
|
||||
var codici = AssemblyComponentScanner.EstraiCodiciReferenziati(percorso, codiciNoti: new[] { "UNI6592" });
|
||||
Assert.Contains("UNI6592", codici);
|
||||
Assert.DoesNotContain("R12--_-_U6592<UNI6592>", codici);
|
||||
}
|
||||
finally { File.Delete(percorso); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void File_inesistente_lancia_eccezione_IO() =>
|
||||
Assert.Throws<DirectoryNotFoundException>(() =>
|
||||
AssemblyComponentScanner.EstraiCodiciReferenziati(@"C:\non\esiste\pippo.asm.1"));
|
||||
|
||||
[Fact]
|
||||
public void Riconosce_parte_commerciale_con_nome_alfabetico_se_nel_db()
|
||||
{
|
||||
// Visto su un caso reale: le parti commerciali scaricate come STEP (es. anelli,
|
||||
// boccole) hanno nomi Creo alfabetici, non un codice numerico Pro-E - si
|
||||
// riconoscono solo se il nome esiste davvero come codice indicizzato.
|
||||
string percorso = ScriviAsmFinto(
|
||||
"blah Componente ID 11 (ANELLO_ESTERNO-1-SOLID1.PRT) blah RUMORE_CASUALE blah");
|
||||
try
|
||||
{
|
||||
var codiciNoti = new[] { "ANELLO_ESTERNO-1-SOLID1" };
|
||||
var codici = AssemblyComponentScanner.EstraiCodiciReferenziati(percorso, codiciNoti);
|
||||
Assert.Contains("ANELLO_ESTERNO-1-SOLID1", codici);
|
||||
}
|
||||
finally { File.Delete(percorso); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Scarta_token_alfabetico_nel_db_ma_senza_placement()
|
||||
{
|
||||
// Un token che coincide per puro caso con un codice DB corto/generico (es. una
|
||||
// keyword interna di Creo) non basta: senza un vero placement resta rumore.
|
||||
string percorso = ScriviAsmFinto("start_part blah blah start_part");
|
||||
try
|
||||
{
|
||||
var codiciNoti = new[] { "start_part" };
|
||||
var codici = AssemblyComponentScanner.EstraiCodiciReferenziati(percorso, codiciNoti);
|
||||
Assert.DoesNotContain("start_part", codici);
|
||||
}
|
||||
finally { File.Delete(percorso); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ignora_token_alfabetici_non_presenti_nel_db()
|
||||
{
|
||||
string percorso = ScriviAsmFinto(
|
||||
"blah Componente ID 1 (QUALCOSA_DI_INESISTENTE-1.PRT) blah");
|
||||
try
|
||||
{
|
||||
var codici = AssemblyComponentScanner.EstraiCodiciReferenziati(percorso, codiciNoti: new[] { "ALTRO_CODICE" });
|
||||
Assert.DoesNotContain("QUALCOSA_DI_INESISTENTE-1", codici);
|
||||
}
|
||||
finally { File.Delete(percorso); }
|
||||
}
|
||||
}
|
||||
@@ -6,35 +6,145 @@ namespace CadSearchGui;
|
||||
/// <summary>
|
||||
/// Estrae i codici componente referenziati dentro un file .asm Pro-E/Creo.
|
||||
/// Il formato è per lo più binario, ma i codici dei sotto-componenti compaiono
|
||||
/// come stringhe ASCII leggibili al suo interno: si scansiona il file come testo
|
||||
/// cercando pattern che assomigliano a un codice (stesso stile usato nei nomi file,
|
||||
/// vedi Indexer.FilenameRegex) e si lascia poi al chiamante il compito di verificarli
|
||||
/// contro l'indice DB, che scarta automaticamente i falsi positivi (numeri che non
|
||||
/// corrispondono a nessun codice reale).
|
||||
/// come stringhe ASCII leggibili al suo interno.
|
||||
///
|
||||
/// Verificato su un caso reale confrontando con un report BOM generato da Creo: i
|
||||
/// Due famiglie di codici, due strategie:
|
||||
///
|
||||
/// 1) La maggioranza (>90%) sono codici numerici Pro-E del gestionale, con una forma
|
||||
/// nota (stesso stile usato nei nomi file, vedi Indexer.FilenameRegex): si riconoscono
|
||||
/// da soli, senza bisogno del DB, e possono quindi essere segnalati come "referenziati
|
||||
/// ma non trovati nell'indice" quando un codice risulta rinominato/spostato - il
|
||||
/// chiamante fa la ricerca nel DB e riporta i mancanti.
|
||||
///
|
||||
/// 2) Una minoranza sono parti commerciali scaricate come STEP da cataloghi online
|
||||
/// (viti, anelli, boccole...), che in Creo hanno nomi alfabetici senza una forma
|
||||
/// riconoscibile a priori (es. "ANELLO_ESTERNO-1-SOLID1", non un numero). Per questi
|
||||
/// non esiste un pattern da indovinare: si tokenizza in modo generico (qualunque run
|
||||
/// di lettere/cifre/underscore/trattino) e si lascia decidere al DB, già caricato dal
|
||||
/// chiamante, se il token è un codice vero - altrimenti il rumore binario sarebbe
|
||||
/// ingestibile. Un token così "convalidato" è per costruzione già nell'indice, quindi
|
||||
/// non contribuisce mai ai "non trovati": per le parti commerciali mancanti dall'indice
|
||||
/// non c'è modo di distinguerle dal rumore, restano semplicemente invisibili come lo
|
||||
/// erano prima.
|
||||
///
|
||||
/// Verificato su un caso reale confrontando con l'albero componenti di Creo: i
|
||||
/// riferimenti veri compaiono più volte nel file (una volta per feature/placement),
|
||||
/// mentre il rumore binario casuale che assomiglia a un codice compare quasi sempre
|
||||
/// una sola volta. Si scartano quindi i candidati con una sola occorrenza.
|
||||
/// una sola volta. Si scartano quindi, per entrambe le famiglie, i candidati "nudi" (senza
|
||||
/// un suffisso di revisione/variante o una forma family-table nota) che non abbiano anche
|
||||
/// un vero placement testuale nel binario.
|
||||
///
|
||||
/// Falso positivo noto ed escluso: gli ID interni di viste salvate, nel formato
|
||||
/// "ModelView#<numero>", hanno la stessa forma di un codice (8-12 cifre) e
|
||||
/// Falso positivo noto ed escluso (famiglia 1): gli ID interni di viste salvate, nel
|
||||
/// formato "ModelView#<numero>", hanno la stessa forma di un codice (8-12 cifre) e
|
||||
/// compaiono più volte come un riferimento vero. Vedi docs/formato-asm.md.
|
||||
///
|
||||
/// Viti/dadi/rosette normalizzati UNI (e cuscinetti CUSCI_...) sono family table Creo: il
|
||||
/// nome che compare nel binario (es. "R6--_-_U6592<UNI6592>",
|
||||
/// "2510060303<CUSCI_ACBB7>") è la singola istanza, non un file separato su disco - il
|
||||
/// file reale indicizzato è il generico "uni6592.prt.<versione>"/
|
||||
/// "cusci_acbb7.prt.<versione>". Con la tokenizzazione generica "UNI6592"/
|
||||
/// "CUSCI_ACBB7" vengono catturati da soli: "<" e ">" non fanno parte del charset del
|
||||
/// token e ne delimitano naturalmente i confini. Vedi docs/formato-asm.md.
|
||||
///
|
||||
/// Lettera di revisione e suffisso "_..." possono comparire insieme sullo stesso codice
|
||||
/// (es. "9284200366A_C01"), e il suffisso dopo "_" può essere puramente numerico (es.
|
||||
/// "2702220182_8188", una variante dimensionale, non lettera+cifre come "_C01"/"_D100").
|
||||
///
|
||||
/// Un codice "nudo" (senza suffisso "_..." e senza forma family-table nota) ripetuto più
|
||||
/// volte non basta: può trattarsi del riferimento al nome di un altro assieme che lo
|
||||
/// contiene (non noto a priori, quindi non escludibile per confronto diretto come il
|
||||
/// proprio codice) o di un residuo di feature soppresse/superate, oppure - per la
|
||||
/// tokenizzazione generica - di puro rumore binario che coincide con un codice DB corto o
|
||||
/// generico (vedi docs/formato-asm.md). Per questi si richiede anche 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 è), 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.
|
||||
/// </summary>
|
||||
static class AssemblyComponentScanner
|
||||
{
|
||||
private static readonly Regex CodiceRegex = new(
|
||||
@"(?<!ModelView#)\b\d{8,12}(?:_[A-Za-z]\d{2,4}|[A-Za-z]{0,2})\b", RegexOptions.Compiled);
|
||||
private static readonly Regex CodiceNumericoRegex = new(
|
||||
@"(?<!ModelView#)\b\d{8,12}[A-Za-z]{0,2}(?:_[A-Za-z]?\d{2,4})?\b",
|
||||
RegexOptions.Compiled);
|
||||
private static readonly Regex CodiceNudoRegex = new(@"^\d{8,12}[A-Za-z]{0,2}$", RegexOptions.Compiled);
|
||||
|
||||
// Nessuna forma ipotizzata a mano per le parti commerciali: qualunque run di
|
||||
// lettere/cifre/underscore/trattino è un candidato, validato poi contro il DB.
|
||||
private static readonly Regex TokenGenericoRegex = new(@"[A-Za-z0-9_-]{3,}", RegexOptions.Compiled);
|
||||
|
||||
// Suffisso di revisione/variante (es. "_C01", "_D100", "_8188") o prefisso di family
|
||||
// table nota (UNI/CUSCI_): segnale forte abbastanza da non richiedere un placement
|
||||
// esplicito, come per i codici numerici nudi con lo stesso suffisso.
|
||||
private static readonly Regex SuffissoRegex = new(@"_[A-Za-z]?\d{2,4}$", RegexOptions.Compiled);
|
||||
|
||||
private const int OccorrenzeMinime = 2;
|
||||
|
||||
public static HashSet<string> EstraiCodiciReferenziati(string percorsoAsm)
|
||||
/// <summary>Codice radice (parte prima del primo "_") di un codice componente. Un
|
||||
/// assieme con suffisso "_D.../_C..." referenzia sempre al suo interno anche l'assieme
|
||||
/// generale a cui appartiene (codice radice, senza suffisso) - riferimento fantasma
|
||||
/// verso l'alto, non un componente vero (vedi docs/formato-asm.md). Chi fa una
|
||||
/// risoluzione ricorsiva dell'albero deve seminare l'insieme dei codici già visitati
|
||||
/// anche con la radice del codice di partenza, non solo col codice stesso, altrimenti
|
||||
/// risale fino all'assieme generale e riporta dentro l'intero albero del macchinario.</summary>
|
||||
public static string CodiceRadice(string codice) => codice.Split('_')[0];
|
||||
|
||||
/// <param name="codiciNoti">Tutti i codici prt/asm noti all'indice (caricati una sola
|
||||
/// volta dal chiamante, non per-file): usati per validare i token generici della
|
||||
/// famiglia 2 (parti commerciali). Non serve per la famiglia 1 (codici numerici), che
|
||||
/// si riconosce dalla forma senza bisogno del DB.</param>
|
||||
public static HashSet<string> EstraiCodiciReferenziati(string percorsoAsm, ICollection<string>? codiciNoti = null)
|
||||
{
|
||||
var testo = Encoding.Latin1.GetString(File.ReadAllBytes(percorsoAsm));
|
||||
var occorrenze = new Dictionary<string, int>();
|
||||
foreach (Match m in CodiceRegex.Matches(testo))
|
||||
|
||||
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 + "<");
|
||||
|
||||
var risultato = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var occorrenzeNumeriche = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (Match m in CodiceNumericoRegex.Matches(testo))
|
||||
occorrenzeNumeriche[m.Value] = occorrenzeNumeriche.GetValueOrDefault(m.Value) + 1;
|
||||
foreach (var (codice, occorrenze) in occorrenzeNumeriche)
|
||||
{
|
||||
occorrenze[m.Value] = occorrenze.GetValueOrDefault(m.Value) + 1;
|
||||
if (occorrenze < OccorrenzeMinime)
|
||||
continue;
|
||||
if (CodiceNudoRegex.IsMatch(codice) && !HaPlacement(codice))
|
||||
continue;
|
||||
risultato.Add(codice);
|
||||
}
|
||||
return occorrenze.Where(kv => kv.Value >= OccorrenzeMinime).Select(kv => kv.Key).ToHashSet();
|
||||
|
||||
if (codiciNoti is { Count: > 0 })
|
||||
{
|
||||
var occorrenzeGeneriche = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (Match m in TokenGenericoRegex.Matches(testo))
|
||||
{
|
||||
if (codiciNoti.Contains(m.Value))
|
||||
occorrenzeGeneriche[m.Value] = occorrenzeGeneriche.GetValueOrDefault(m.Value) + 1;
|
||||
}
|
||||
foreach (var (codice, occorrenze) in occorrenzeGeneriche)
|
||||
{
|
||||
if (risultato.Contains(codice))
|
||||
continue;
|
||||
bool formaNota = SuffissoRegex.IsMatch(codice)
|
||||
|| codice.StartsWith("UNI", StringComparison.OrdinalIgnoreCase)
|
||||
|| codice.StartsWith("CUSCI_", StringComparison.OrdinalIgnoreCase);
|
||||
if (formaNota ? occorrenze >= OccorrenzeMinime : HaPlacement(codice))
|
||||
risultato.Add(codice);
|
||||
}
|
||||
}
|
||||
|
||||
return risultato;
|
||||
}
|
||||
}
|
||||
+109
-82
@@ -432,13 +432,13 @@ public partial class Form1 : Form
|
||||
AutoSize = true,
|
||||
ForeColor = SystemColors.GrayText,
|
||||
Margin = new Padding(0, 0, 0, 8),
|
||||
Text = "Dopo aver esportato gli STEP con Creo, premi qui per confrontare i "
|
||||
+ "componenti della distinta con i file .stp presenti in 'step' e vedere quali "
|
||||
+ "sono già stati realizzati e quali mancano ancora.",
|
||||
Text = "Dopo aver esportato gli STEP con Creo, premi qui per confrontare i componenti "
|
||||
+ "della distinta (e i sotto-componenti trovati in fase 4) con i file .stp presenti "
|
||||
+ "in 'step', copiare quelli trovati in 'step_finali' e vedere quali mancano ancora.",
|
||||
};
|
||||
contenuto.Controls.Add(lblIstruzioniStep);
|
||||
|
||||
_btnReport.Text = "Report STEP realizzati";
|
||||
_btnReport.Text = "Crea cartella step_finali";
|
||||
_btnReport.Size = new Size(220, 32);
|
||||
_btnReport.Click += (_, _) => GeneraReportStep();
|
||||
contenuto.Controls.Add(_btnReport);
|
||||
@@ -551,33 +551,65 @@ public partial class Form1 : Form
|
||||
if (_percorsoInputCorrente is null || _righeCorrenti is null)
|
||||
return;
|
||||
|
||||
var cartellaStep = Path.Combine(Path.GetDirectoryName(Path.GetFullPath(_percorsoInputCorrente))!, "step");
|
||||
string radice = Path.GetDirectoryName(Path.GetFullPath(_percorsoInputCorrente))!;
|
||||
var cartellaStep = Path.Combine(radice, "step");
|
||||
if (!Directory.Exists(cartellaStep))
|
||||
{
|
||||
ImpostaStatoImport($"Cartella 'step' non trovata in {cartellaStep}.", Color.DarkRed);
|
||||
return;
|
||||
}
|
||||
|
||||
var codiciConStep = new HashSet<string>(
|
||||
Directory.GetFiles(cartellaStep, "*.stp").Select(Path.GetFileNameWithoutExtension)!);
|
||||
var descrizioni = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var r in _righeCorrenti)
|
||||
descrizioni[r.Componente] = r.Descrizione;
|
||||
|
||||
var righeReport = _righeCorrenti
|
||||
.Select(r => (r.Componente, r.Descrizione, Realizzato: codiciConStep.Contains(r.Componente)))
|
||||
// Solo i componenti della distinta importata dal CSV: i sotto-componenti trovati in
|
||||
// fase 4 (_componentiRisolti) servono a completare "raw", non entrano nello STEP finale.
|
||||
var tuttiICodici = _righeCorrenti.Select(r => r.Componente)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
int realizzati = righeReport.Count(r => r.Realizzato);
|
||||
|
||||
// Per i codici senza STEP, distingue "il CAD c'è ma lo STEP non è stato esportato" da
|
||||
// "il componente non è nemmeno nell'indice CAD" (riusa la verifica già fatta in fase 2/3
|
||||
// se disponibile, altrimenti la rifà).
|
||||
var cadTrovati = _ultimaVerifica ?? TrovaPercorsiCad(tuttiICodici);
|
||||
|
||||
var cartellaStepFinali = Path.Combine(radice, "step_finali");
|
||||
Directory.CreateDirectory(cartellaStepFinali);
|
||||
|
||||
var righeReport = new List<(string Componente, string Descrizione, EsitoStep Esito)>();
|
||||
foreach (var codice in tuttiICodici)
|
||||
{
|
||||
var percorsoStp = Path.Combine(cartellaStep, codice + ".stp");
|
||||
EsitoStep esito;
|
||||
if (File.Exists(percorsoStp))
|
||||
{
|
||||
File.Copy(percorsoStp, Path.Combine(cartellaStepFinali, codice + ".stp"), overwrite: true);
|
||||
esito = EsitoStep.Creato;
|
||||
}
|
||||
else
|
||||
{
|
||||
esito = cadTrovati.ContainsKey(codice) ? EsitoStep.StepMancante : EsitoStep.ComponenteNonTrovato;
|
||||
}
|
||||
righeReport.Add((codice, descrizioni.GetValueOrDefault(codice, ""), esito));
|
||||
}
|
||||
int creati = righeReport.Count(r => r.Esito == EsitoStep.Creato);
|
||||
int stepMancanti = righeReport.Count(r => r.Esito == EsitoStep.StepMancante);
|
||||
int nonTrovati = righeReport.Count(r => r.Esito == EsitoStep.ComponenteNonTrovato);
|
||||
|
||||
ImpostaStatoImport(
|
||||
$"Report STEP: {realizzati} componenti realizzati su {righeReport.Count} "
|
||||
+ $"({righeReport.Count - realizzati} mancanti).",
|
||||
$"STEP finali: {creati} copiati, {stepMancanti} da esportare (CAD presente), "
|
||||
+ $"{nonTrovati} componenti non nell'indice, su {righeReport.Count} totali. "
|
||||
+ $"Cartella: {cartellaStepFinali}.",
|
||||
Color.DarkGreen);
|
||||
|
||||
using var dialog = new ReportStepForm(righeReport, cartellaStep);
|
||||
using var dialog = new ReportStepForm(righeReport, cartellaStepFinali);
|
||||
dialog.ShowDialog(this);
|
||||
}
|
||||
|
||||
/// <summary>Svuota in un colpo solo le cartelle "raw" e "step" nella root (stessa cartella
|
||||
/// base del file distinta importato), previa un'unica conferma. Non tocca l'indice CAD: solo
|
||||
/// le copie locali generate dalla pipeline.</summary>
|
||||
/// <summary>Svuota in un colpo solo le cartelle "raw", "step" e "step_finali" nella root
|
||||
/// (stessa cartella base del file distinta importato), previa un'unica conferma. Non tocca
|
||||
/// l'indice CAD: solo le copie locali generate dalla pipeline.</summary>
|
||||
private void PulisciRawEStep()
|
||||
{
|
||||
string radice;
|
||||
@@ -595,12 +627,12 @@ public partial class Form1 : Form
|
||||
radice = dialog.SelectedPath;
|
||||
}
|
||||
|
||||
var cartelle = new[] { Path.Combine(radice, "raw"), Path.Combine(radice, "step") }
|
||||
var cartelle = new[] { Path.Combine(radice, "raw"), Path.Combine(radice, "step"), Path.Combine(radice, "step_finali") }
|
||||
.Where(Directory.Exists)
|
||||
.ToList();
|
||||
if (cartelle.Count == 0)
|
||||
{
|
||||
ImpostaStatoImport($"Nessuna cartella 'raw' o 'step' trovata in {radice}.", Color.DarkRed);
|
||||
ImpostaStatoImport($"Nessuna cartella 'raw', 'step' o 'step_finali' trovata in {radice}.", Color.DarkRed);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -630,57 +662,63 @@ public partial class Form1 : Form
|
||||
ImpostaStatoImport($"{eliminati} file eliminati da {string.Join(" e ", cartelle)}.", Color.DarkGreen);
|
||||
}
|
||||
|
||||
/// <summary>Tutti i codici prt/asm noti all'indice, con i relativi percorsi (ultima
|
||||
/// versione, un solo file per tipo - stesso criterio di dedup per data_modifica DESC
|
||||
/// usato ovunque in questa classe). Caricato in un'unica query invece che una per
|
||||
/// codice: serve sia a risolvere i percorsi dei componenti trovati, sia a
|
||||
/// AssemblyComponentScanner per validare i token generici (parti commerciali).</summary>
|
||||
private static Dictionary<string, List<(string Tipo, string Percorso)>> CaricaCodiciNoti(string dbPath)
|
||||
{
|
||||
var codiciNoti = new Dictionary<string, List<(string Tipo, string Percorso)>>(StringComparer.OrdinalIgnoreCase);
|
||||
using var connection = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly");
|
||||
connection.Open();
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = "SELECT codice, tipo_file, percorso_completo FROM files_ultima_versione "
|
||||
+ "WHERE LOWER(tipo_file) IN ('prt', 'asm') ORDER BY codice, tipo_file, data_modifica DESC";
|
||||
using var reader = command.ExecuteReader();
|
||||
while (reader.Read())
|
||||
{
|
||||
string codice = reader.GetString(0);
|
||||
string tipo = reader.GetString(1).ToLowerInvariant();
|
||||
var lista = codiciNoti.TryGetValue(codice, out var l) ? l : codiciNoti[codice] = new();
|
||||
if (!lista.Any(x => x.Tipo == tipo))
|
||||
lista.Add((tipo, reader.GetString(2)));
|
||||
}
|
||||
return codiciNoti;
|
||||
}
|
||||
|
||||
/// <summary>Risolve ricorsivamente l'albero dei componenti a partire dai file .asm indicati
|
||||
/// (già copiati in "raw"): per ogni assieme legge i codici referenziati al suo interno, li
|
||||
/// cerca nell'indice, copia in "raw" quelli trovati e, se sono a loro volta assiemi, li
|
||||
/// accoda per essere scansionati finché non restano solo PRT (foglie) o codici non presenti
|
||||
/// nell'indice. Usata dal bottone "Trova componenti referenziati".</summary>
|
||||
private (List<(string Codice, string Tipo, string Percorso)> Trovati, List<string> NonTrovati) RisolviAlberoComponenti(
|
||||
IEnumerable<string> asmIniziali)
|
||||
IEnumerable<string> asmIniziali, IProgress<(int Fatti, int Totale)>? progress = null)
|
||||
{
|
||||
var daScansionare = new Queue<string>(asmIniziali);
|
||||
var codiciIniziali = daScansionare.Select(f => Path.GetFileName(f).Split('.')[0]);
|
||||
var codiciVisti = new HashSet<string>(
|
||||
daScansionare.Select(f => Path.GetFileName(f).Split('.')[0]), StringComparer.OrdinalIgnoreCase);
|
||||
codiciIniziali.Concat(codiciIniziali.Select(AssemblyComponentScanner.CodiceRadice)),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var trovati = new List<(string Codice, string Tipo, string Percorso)>();
|
||||
var codiciNonTrovati = new List<string>();
|
||||
if (daScansionare.Count == 0 || _dbPath is null)
|
||||
return (trovati, codiciNonTrovati);
|
||||
|
||||
using var connection = new SqliteConnection($"Data Source={_dbPath};Mode=ReadOnly");
|
||||
connection.Open();
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = "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";
|
||||
var pCodice = command.Parameters.Add("@codice", Microsoft.Data.Sqlite.SqliteType.Text);
|
||||
var codiciNoti = CaricaCodiciNoti(_dbPath);
|
||||
|
||||
int processati = 0;
|
||||
while (daScansionare.Count > 0)
|
||||
{
|
||||
var asm = daScansionare.Dequeue();
|
||||
foreach (var codice in AssemblyComponentScanner.EstraiCodiciReferenziati(asm))
|
||||
processati++;
|
||||
foreach (var codice in AssemblyComponentScanner.EstraiCodiciReferenziati(asm, codiciNoti.Keys))
|
||||
{
|
||||
if (!codiciVisti.Add(codice))
|
||||
continue;
|
||||
|
||||
pCodice.Value = codice;
|
||||
// Lo stesso codice+tipo può esistere in più cartelle radice (duplicati sulla
|
||||
// share): ordinato per data_modifica DESC, si tiene solo il più recente per tipo.
|
||||
var componenti = new List<(string Tipo, string Percorso)>();
|
||||
var tipiVisti = new HashSet<string>();
|
||||
using (var reader = command.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
string tipoLower = reader.GetString(0).ToLowerInvariant();
|
||||
if (tipiVisti.Add(tipoLower))
|
||||
{
|
||||
componenti.Add((tipoLower, reader.GetString(1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (componenti.Count == 0)
|
||||
if (!codiciNoti.TryGetValue(codice, out var componenti))
|
||||
{
|
||||
codiciNonTrovati.Add(codice);
|
||||
continue;
|
||||
@@ -696,12 +734,13 @@ public partial class Form1 : Form
|
||||
}
|
||||
}
|
||||
}
|
||||
progress?.Report((processati, processati + daScansionare.Count));
|
||||
}
|
||||
|
||||
return (trovati, codiciNonTrovati);
|
||||
}
|
||||
|
||||
private void TrovaPrtReferenziati()
|
||||
private async void TrovaPrtReferenziati()
|
||||
{
|
||||
if (_ultimaCartellaRaw is null || !Directory.Exists(_ultimaCartellaRaw))
|
||||
return;
|
||||
@@ -714,8 +753,19 @@ public partial class Form1 : Form
|
||||
|
||||
// L'assieme generale è già in raw come qualsiasi altro assieme importato (vedi
|
||||
// CreaCartellaRaw), quindi rientra qui senza bisogno di un caso speciale.
|
||||
var asmIniziali = Directory.GetFiles(_ultimaCartellaRaw).Where(f => Path.GetFileName(f).Contains(".asm."));
|
||||
var (trovati, codiciNonTrovati) = RisolviAlberoComponenti(asmIniziali);
|
||||
var asmIniziali = Directory.GetFiles(_ultimaCartellaRaw).Where(f => Path.GetFileName(f).Contains(".asm.")).ToList();
|
||||
|
||||
using var progressForm = new ProgressoComponentiForm();
|
||||
var progress = new Progress<(int Fatti, int Totale)>(p => progressForm.Aggiorna(p.Fatti, p.Totale));
|
||||
var task = Task.Run(() => RisolviAlberoComponenti(asmIniziali, progress));
|
||||
progressForm.Shown += async (_, _) =>
|
||||
{
|
||||
await task;
|
||||
progressForm.Close();
|
||||
};
|
||||
progressForm.ShowDialog(this);
|
||||
|
||||
var (trovati, codiciNonTrovati) = await task;
|
||||
|
||||
int numPrt = trovati.Count(r => r.Tipo == "prt");
|
||||
int numAsm = trovati.Count(r => r.Tipo == "asm");
|
||||
@@ -742,46 +792,23 @@ public partial class Form1 : Form
|
||||
return;
|
||||
}
|
||||
|
||||
using var connection = new SqliteConnection($"Data Source={_dbPath};Mode=ReadOnly");
|
||||
connection.Open();
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = "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";
|
||||
var pCodice = command.Parameters.Add("@codice", Microsoft.Data.Sqlite.SqliteType.Text);
|
||||
|
||||
var cacheComponenti = new Dictionary<string, List<(string Tipo, string Percorso)>>();
|
||||
var codiciNoti = CaricaCodiciNoti(_dbPath);
|
||||
var cacheCodiciFile = new Dictionary<string, HashSet<string>>();
|
||||
|
||||
List<(string Tipo, string Percorso)> CercaComponenti(string codice)
|
||||
{
|
||||
if (cacheComponenti.TryGetValue(codice, out var giaTrovati))
|
||||
return giaTrovati;
|
||||
|
||||
pCodice.Value = codice;
|
||||
var risultato = new List<(string Tipo, string Percorso)>();
|
||||
var tipiVisti = new HashSet<string>();
|
||||
using (var reader = command.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
string tipo = reader.GetString(0).ToLowerInvariant();
|
||||
if (tipiVisti.Add(tipo))
|
||||
{
|
||||
risultato.Add((tipo, reader.GetString(1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
cacheComponenti[codice] = risultato;
|
||||
return risultato;
|
||||
}
|
||||
List<(string Tipo, string Percorso)> CercaComponenti(string codice) =>
|
||||
codiciNoti.TryGetValue(codice, out var trovati) ? trovati : new List<(string, string)>();
|
||||
|
||||
bool AssiemeCompleto(string percorsoAsmIniziale)
|
||||
{
|
||||
var daVisitare = new Queue<string>();
|
||||
daVisitare.Enqueue(percorsoAsmIniziale);
|
||||
var fileVisitati = new HashSet<string> { percorsoAsmIniziale };
|
||||
var codiciVisti = new HashSet<string> { Path.GetFileName(percorsoAsmIniziale).Split('.')[0] };
|
||||
var codiceIniziale = Path.GetFileName(percorsoAsmIniziale).Split('.')[0];
|
||||
var codiciVisti = new HashSet<string>
|
||||
{
|
||||
codiceIniziale,
|
||||
AssemblyComponentScanner.CodiceRadice(codiceIniziale),
|
||||
};
|
||||
|
||||
while (daVisitare.Count > 0)
|
||||
{
|
||||
@@ -790,7 +817,7 @@ public partial class Form1 : Form
|
||||
{
|
||||
try
|
||||
{
|
||||
codiciReferenziati = AssemblyComponentScanner.EstraiCodiciReferenziati(file);
|
||||
codiciReferenziati = AssemblyComponentScanner.EstraiCodiciReferenziati(file, codiciNoti.Keys);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
|
||||
+17
-1
@@ -65,9 +65,25 @@ public static class Indexer
|
||||
schemaCmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
// Le cartelle "temp_*" (temp_paris, temp_davide, temp_studi esterni, ...) sono lavori
|
||||
// personali/temporanei: contengono spesso copie datate o versioni scollegate di codici
|
||||
// che esistono ANCHE nel resto della share, con lo stesso codice ma contenuto diverso.
|
||||
// Un .asm nella struttura "ufficiale" può referenziare per coincidenza numerica un file
|
||||
// dentro una di queste cartelle (vedi docs/formato-asm.md, falso positivo su
|
||||
// "2502020809"), trascinando dentro un intero assieme di un progetto scollegato. Vanno
|
||||
// escluse a monte, non solo filtrate caso per caso nelle singole query.
|
||||
using (var deleteTempCmd = connection.CreateCommand())
|
||||
{
|
||||
deleteTempCmd.CommandText = @"DELETE FROM files WHERE cartella_radice LIKE 'temp\_%' ESCAPE '\'";
|
||||
deleteTempCmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
// Indicizza tutte le cartelle di primo livello sotto la radice (non un elenco fisso),
|
||||
// così una nuova cartella creata sulla share entra nell'indice senza modificare il codice.
|
||||
var cartelleRadice = Directory.GetDirectories(root).Select(p => Path.GetFileName(p)!).ToArray();
|
||||
var cartelleRadice = Directory.GetDirectories(root)
|
||||
.Select(p => Path.GetFileName(p)!)
|
||||
.Where(c => !c.StartsWith("temp_", StringComparison.OrdinalIgnoreCase))
|
||||
.ToArray();
|
||||
|
||||
var previsti = new Dictionary<string, long>();
|
||||
foreach (var cartella in cartelleRadice)
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
namespace CadSearchGui;
|
||||
|
||||
public sealed class ProgressoComponentiForm : Form
|
||||
{
|
||||
private readonly Label _lbl = new();
|
||||
private readonly ProgressBar _bar = new();
|
||||
|
||||
public ProgressoComponentiForm()
|
||||
{
|
||||
Text = "Ricerca componenti referenziati in corso...";
|
||||
FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||
StartPosition = FormStartPosition.CenterParent;
|
||||
MinimizeBox = false;
|
||||
MaximizeBox = false;
|
||||
ControlBox = false;
|
||||
ClientSize = new Size(400, 88);
|
||||
|
||||
_lbl.Text = "Preparazione...";
|
||||
_lbl.AutoSize = true;
|
||||
_lbl.Location = new Point(16, 16);
|
||||
|
||||
_bar.Location = new Point(16, 44);
|
||||
_bar.Size = new Size(368, 24);
|
||||
_bar.Minimum = 0;
|
||||
_bar.Maximum = 100;
|
||||
|
||||
Controls.Add(_lbl);
|
||||
Controls.Add(_bar);
|
||||
}
|
||||
|
||||
public void Aggiorna(int fatti, int totale)
|
||||
{
|
||||
int percento = totale > 0 ? (int)Math.Clamp(100.0 * fatti / totale, 0, 100) : 0;
|
||||
_bar.Value = percento;
|
||||
_lbl.Text = $"{fatti} / {totale} componenti analizzati ({percento}%)";
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,28 @@ using System.Diagnostics;
|
||||
|
||||
namespace CadSearchGui;
|
||||
|
||||
/// <summary>Mostra, per ogni componente della distinta base, se lo STEP corrispondente
|
||||
/// è già stato esportato (file .stp presente in "step") o manca ancora.</summary>
|
||||
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, bool Realizzato)> righe, string cartellaStep)
|
||||
public ReportStepForm(List<(string Componente, string Descrizione, EsitoStep Esito)> righe, string cartellaStepFinali)
|
||||
{
|
||||
int realizzati = righe.Count(r => r.Realizzato);
|
||||
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 realizzati";
|
||||
Text = "Report STEP finali";
|
||||
ClientSize = new Size(720, 440);
|
||||
StartPosition = FormStartPosition.CenterParent;
|
||||
Font = new Font("Segoe UI", 9.5f);
|
||||
@@ -21,8 +34,8 @@ public class ReportStepForm : Form
|
||||
AutoSize = false,
|
||||
Height = 32,
|
||||
Padding = new Padding(12, 8, 12, 8),
|
||||
Text = $"{realizzati} componenti realizzati su {righe.Count} ({righe.Count - realizzati} mancanti), "
|
||||
+ $"confrontati con \"{cartellaStep}\".",
|
||||
Text = $"{creati} copiati, {stepMancanti} da esportare (CAD presente), "
|
||||
+ $"{nonTrovati} non nell'indice, su {righe.Count} totali. In \"{cartellaStepFinali}\".",
|
||||
};
|
||||
|
||||
var grid = new DataGridView
|
||||
@@ -42,16 +55,22 @@ public class ReportStepForm : Form
|
||||
grid.Columns["descrizione"]!.FillWeight = 55;
|
||||
grid.Columns["step"]!.FillWeight = 25;
|
||||
|
||||
foreach (var (componente, descrizione, realizzato) in righe.OrderBy(r => r.Realizzato).ThenBy(r => r.Componente))
|
||||
foreach (var (componente, descrizione, esito) in righe.OrderBy(r => r.Esito).ThenBy(r => r.Componente))
|
||||
{
|
||||
int i = grid.Rows.Add(componente, descrizione, realizzato ? "Realizzato" : "Mancante");
|
||||
grid.Rows[i].DefaultCellStyle.ForeColor = realizzato ? Color.DarkGreen : Color.DarkRed;
|
||||
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", Dock = DockStyle.Bottom, Height = 34 };
|
||||
var btnApriCartella = new Button { Text = "Apri cartella step_finali", Dock = DockStyle.Bottom, Height = 34 };
|
||||
btnApriCartella.Click += (_, _) =>
|
||||
{
|
||||
try { Process.Start("explorer.exe", cartellaStep); }
|
||||
try { Process.Start("explorer.exe", cartellaStepFinali); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show(this, $"Impossibile aprire Esplora risorse:\n{ex.Message}", "Errore",
|
||||
|
||||
+198
-5
@@ -41,6 +41,17 @@ regex: \b\d{8,12}(?:_[A-Za-z]\d{2,4}|[A-Za-z]{0,2})\b
|
||||
occorrenze: >= 2
|
||||
```
|
||||
|
||||
`scripts/ExtractCreoStrings.cs` ha una versione più raffinata della regex e un filtro
|
||||
aggiuntivo (vedi sotto), non ancora portati in `AssemblyComponentScanner.cs`:
|
||||
|
||||
```
|
||||
regex: \b\d{8,12}[A-Za-z]{0,2}(?:_[A-Za-z]?\d{2,4})?\b
|
||||
```
|
||||
|
||||
(lettera di revisione e suffisso "_..." possono comparire anche insieme, es.
|
||||
`9284200366A_C01`; il suffisso dopo "_" può essere puramente numerico, es.
|
||||
`2702220182_8188`, non solo lettera+cifre come `_C01`/`_D100`.)
|
||||
|
||||
## Falsi positivi noti
|
||||
|
||||
**ID di viste salvate.** Il pattern `ModelView#<numero>` (es.
|
||||
@@ -54,6 +65,71 @@ volte nei metadati (`CODICE`, `part_table`, `STAMPIGLIATURA`, ecc.) e supera
|
||||
quindi la soglia di occorrenze. Va escluso esplicitamente confrontandolo con
|
||||
il codice derivato dal nome del file in ingresso (`nomefile.split('.')[0]`).
|
||||
|
||||
**Il codice dell'assieme padre (o di qualunque altro assieme che lo
|
||||
contiene).** Variante dello stesso problema: un sotto-assieme può contenere
|
||||
anche il codice dell'assieme che lo referenzia (riferimento al nome
|
||||
sessione/file salvato nei metadati), che supera la soglia di occorrenze pur
|
||||
non essendo un componente. A differenza del proprio codice, non è noto a
|
||||
priori: dipende da dove/come il file è stato aperto o da chi lo referenzia,
|
||||
quindi non si può escludere per confronto diretto come per il caso
|
||||
precedente. La regola generale che discrimina questo caso: un componente
|
||||
vero compare **sempre** anche dentro 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 con cui il file è stato salvato -
|
||||
osservate sia italiana sia inglese);
|
||||
- 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_...` - vedi sotto; il placement testuale
|
||||
"Componente ID" non li copre, ma questa coppia doppia sì);
|
||||
- il campo `family_name\0<codice>\0` (con `name` e `family_name` uguali,
|
||||
entrambi pari al codice). Osservato una sola volta per file, su un
|
||||
sotto-assieme referenziato come family table generica invece che con un
|
||||
placement testuale esplicito - caso `9284202452` dentro
|
||||
`9284208188_D400.asm`: il sotto-assieme (che contiene a sua volta
|
||||
`9284202452_C01/_C02/_C03`) non ha nessun `Componente ID (...)` né coppia
|
||||
NUL, solo questo campo `family_name`. Verificato su 5 file diversi
|
||||
(`9284208188_D400/D600/D610/D620/D630.asm`): l'unica occorrenza di
|
||||
`family_name\0<codice>\0` in ciascuno corrisponde sempre a un componente
|
||||
reale confermato dalla BOM Creo, mai al riferimento fantasma verso l'alto;
|
||||
- il codice seguito immediatamente da un bracket, `<codice><GENERICO>`
|
||||
(senza NUL né spazio in mezzo) - l'istanza di una family table non-UNI
|
||||
(es. `2510060303<CUSCI_ACBB7>`, un cuscinetto, confermato aprendo l'assieme
|
||||
in Creo: descrizione "CUSC.OBL.SFERE 2COR 17X40"). Il codice dell'istanza
|
||||
non è mai indicizzato a sé sulla share (solo il generico tra `<` e `>` lo
|
||||
è - stesso meccanismo di UNI, vedi sotto), quindi anche riconosciuto come
|
||||
placement reale finisce comunque negli "scartati" alla ricerca nel DB - ma
|
||||
in modo tracciabile, invece di sparire in silenzio prima ancora di essere
|
||||
cercato.
|
||||
|
||||
Un riferimento incidentale al nome di un altro assieme, anche se ripetuto più
|
||||
volte, non compare mai in nessuna delle quattro forme. `scripts/ExtractCreoStrings.cs`
|
||||
e `AssemblyComponentScanner.cs` applicano questa regola ai codici "nudi" (senza
|
||||
suffisso `_...` e senza bracket family table - vedi sotto perché limitata a
|
||||
questi): un codice ripetuto ma mai trovato dentro uno dei quattro segnali
|
||||
viene scartato. Questo NON copre i casi fantasma/soppressi sotto, che hanno un
|
||||
placement sintatticamente identico a uno vero e restano quindi indistinguibili
|
||||
a livello testuale.
|
||||
|
||||
**Il codice radice del proprio assieme (assieme generale, senza suffisso
|
||||
`_D.../_C...`).** Caso particolare e sistematico del precedente, non solo
|
||||
incidentale: un assieme suddiviso in sotto-assiemi con suffisso
|
||||
(`9284208188_D600.asm`, `_D610.asm`, ...) referenzia **sempre** al suo interno
|
||||
anche il codice radice dell'assieme generale (`9284208188`, senza suffisso) -
|
||||
il "padre" a cui tutti i `_D...` appartengono nella struttura Creo - con un
|
||||
vero placement sintatticamente identico a un componente reale. Non è un
|
||||
componente di quel sotto-assieme, è la risalita verso l'assieme che lo
|
||||
contiene. Osservato su `9284208188_D600.asm`: `Componente ID N (9284208188...)`.
|
||||
Se seguito in una risoluzione ricorsiva, riporta dentro l'intero albero del
|
||||
prodotto invece del solo sotto-assieme richiesto: 598 componenti raccolti
|
||||
invece dei ~65 reali. A differenza del caso generico sopra, qui il codice
|
||||
"padre" _è_ prevedibile a priori (è la parte del proprio codice prima del
|
||||
primo `_`), quindi va escluso esplicitamente seminando l'insieme dei codici
|
||||
già visitati anche con la radice del codice di partenza, non solo con il
|
||||
codice stesso — non basta il controllo generico sul placement, perché il
|
||||
placement qui è reale. Vedi `AssemblyComponentScanner.CodiceRadice`.
|
||||
|
||||
**Riferimenti fantasma a codici superati/rinominati.** Caso osservato:
|
||||
`9244207061.asm` contiene un riferimento a `9044207061` (stesso numero, prima
|
||||
di una rinumerazione) con la stessa identica struttura sintattica di un
|
||||
@@ -70,10 +146,120 @@ riesce a discriminarlo in modo affidabile senza rischiare di scartare
|
||||
componenti veri con pattern simili altrove. Se ricompare, l'unica verifica
|
||||
sicura resta l'apertura manuale in Creo del sotto-assieme sospetto.
|
||||
|
||||
**Componenti soppressi/rimossi con placement residuo.** Stessa natura del
|
||||
caso precedente: capita che un codice compaia con un vero
|
||||
`Componente ID N (<codice>.PRT)`/`Component id N (<codice>.PRT)` - con un ID
|
||||
numerico nello stesso intervallo di quelli dei componenti realmente visibili
|
||||
nell'albero Creo, quindi non distinguibile per posizione o formato - ma il
|
||||
codice non compare nell'albero/BOM che Creo mostra per quell'assieme.
|
||||
Osservato su più assiemi in questa sessione (`9044207061` dentro
|
||||
`9244207061.asm`, e diversi codici dentro `9284208188_D200.asm`). Ipotesi più
|
||||
probabile: feature soppresse, cancellate o superate in una revisione
|
||||
successiva, il cui placement resta comunque nella storia binaria del file
|
||||
(Creo non riscrive da zero il file a ogni salvataggio, accumula stato). Nella
|
||||
pratica, quando compaiono, sono un numero ridotto di codici extra rispetto al
|
||||
reale (falsi positivi, non falsi negativi): la BOM generata resta un
|
||||
sovrainsieme sicuro dei componenti veri, mai un sottoinsieme. Nessuna
|
||||
euristica testuale può escluderli con certezza; se serve un elenco esatto
|
||||
l'unica verifica è aprire l'assieme in Creo e controllare a mano quali ID
|
||||
sono effettivamente presenti/attivi.
|
||||
|
||||
**Placement senza il dettaglio del suffisso/bracket.** Il testo dentro
|
||||
`Componente ID N (...)` a volte riporta solo il nome generico/istanza, senza
|
||||
il suffisso di variante dimensionale o il bracket family-table, che compaiono
|
||||
invece solo altrove nei metadati dello stesso file (non vicino al placement).
|
||||
Osservato in due forme sullo stesso assieme (`9284208188_D200.asm`):
|
||||
`Component id 82 (R8--_-_U6592.PRT)` (nessun `<UNI6592>` nel placement, pure
|
||||
presente nel file) e `Component id 150 (2702220182.PRT)` (nessun `_8188`
|
||||
nel placement, mentre la variante dimensionale reale `2702220182_8188`
|
||||
compare solo come stringa ripetuta altrove). Per questo il requisito del
|
||||
placement esplicito (caso sopra) si applica solo ai codici "nudi" (senza
|
||||
suffisso `_...` e senza bracket UNI): per le varianti con suffisso o family
|
||||
table il placement può mancare del dettaglio, quindi resta necessaria la
|
||||
sola euristica delle occorrenze ripetute - meno precisa, ma l'unica
|
||||
applicabile senza rischiare falsi negativi su casi come questi.
|
||||
|
||||
## Parti commerciali con nome alfabetico (validazione via DB)
|
||||
|
||||
Oltre il 90% dei codici indicizzati sono numerici Pro-E (regex sopra), ma una minoranza
|
||||
sono parti commerciali scaricate come STEP da cataloghi online (anelli, boccole, guarnizioni,
|
||||
manopole...) che in Creo hanno un nome **alfabetico**, assegnato dal file STEP di origine, non
|
||||
un codice del gestionale — es. `NOME_PARTE-1-SOLID1`, senza nessuna cifra riconoscibile. Non
|
||||
esiste un pattern comune da ipotizzare a priori per questi nomi: variano da fornitore a
|
||||
fornitore.
|
||||
|
||||
Per questa famiglia l'estrazione si inverte: invece di restringere la regex a una forma nota
|
||||
(come per i codici numerici), si tokenizza il binario in modo generico —
|
||||
|
||||
```
|
||||
regex: [A-Za-z0-9_-]{3,}
|
||||
```
|
||||
|
||||
— e si lascia decidere al DB (`cad_index.db`, già caricato in memoria una sola volta dal
|
||||
chiamante, non per-file) se il token è un codice reale. Senza questo filtro il rumore binario
|
||||
sarebbe ingestibile: su un assieme con poche migliaia di stringhe, la tokenizzazione generica
|
||||
produce tipicamente qualche migliaio di candidati, di cui solo una manciata sopravvive al
|
||||
controllo DB.
|
||||
|
||||
L'essere nel DB da solo non basta, per lo stesso motivo dei codici numerici "nudi": codici
|
||||
corti o generici (es. una keyword interna di Creo come `start_part`, o un codice indicizzato
|
||||
per errore/caso limite) possono comparire per puro rumore. Si applica quindi la stessa regola
|
||||
di validazione della famiglia numerica:
|
||||
|
||||
- un token con un suffisso di revisione/variante (`_[A-Za-z]?\d{2,4}` in coda, es. `_C01`,
|
||||
`_D100`) o un prefisso di family table nota (`UNI`, `CUSCI_`) è considerato "forma nota":
|
||||
basta che occorra almeno 2 volte nel file;
|
||||
- un token "nudo" (nessuno dei due segnali) richiede invece un vero placement testuale, come
|
||||
per i codici numerici nudi (stesse quattro forme viste sopra).
|
||||
|
||||
Una parte commerciale referenziata ma **non ancora indicizzata** non è distinguibile dal
|
||||
rumore con questo metodo: resta invisibile, come lo era prima che questa famiglia venisse
|
||||
gestita. La famiglia numerica invece resta rilevabile anche quando manca dall'indice
|
||||
(rinominata/spostata), perché si riconosce dalla forma senza bisogno del DB — è per questo
|
||||
che le due famiglie restano due passaggi distinti invece di un unico tokenizer generico
|
||||
sempre validato contro il DB: unificarle farebbe perdere la segnalazione dei codici numerici
|
||||
mancanti (vedi "Riferimenti fantasma" sopra).
|
||||
|
||||
`AssemblyComponentScanner.EstraiCodiciReferenziati` accetta quindi un parametro opzionale
|
||||
`codiciNoti` (l'insieme di tutti i codici prt/asm noti all'indice) usato solo per questa
|
||||
seconda famiglia; se omesso, si estraggono solo i codici numerici. `scripts/BomViaDb.cs`
|
||||
è il prototipo standalone usato per validare l'approccio prima di portarlo nella GUI.
|
||||
|
||||
## Componenti normalizzati (family table UNI, CUSCI, ...)
|
||||
|
||||
I componenti di minuteria standard (viti, dadi, rosette UNI) e i cuscinetti
|
||||
(`CUSCI_...`) sono definiti in Creo come **family table**: un file generico
|
||||
(es. `uni6592.prt`, `cusci_acbb7.prt`) con tante righe/istanze quante sono le
|
||||
varianti dimensionali. Nel binario dell'assieme compaiono così:
|
||||
|
||||
```
|
||||
Componente ID 28755 (R6--_-_U6592<UNI6592>.PRT)
|
||||
2510060303<CUSCI_ACBB7>
|
||||
```
|
||||
|
||||
`R6--_-_U6592` e `2510060303` sono il nome/codice della singola istanza (riga
|
||||
della family table), **non un file separato su disco** — sulla share esiste
|
||||
solo il generico (`uni6592.prt.<versione>`, `cusci_acbb7.prt.<versione>`,
|
||||
verificato in `Unificati/`). Cercare l'istanza intera nell'indice fallisce
|
||||
sempre; va estratto solo il contenuto tra `<` e `>` (`UNI6592`, `CUSCI_ACBB7`),
|
||||
che coincide col nome del file generico realmente indicizzato:
|
||||
|
||||
```
|
||||
regex aggiuntiva: (?<=<)(?:UNI\d+|CUSCI_[A-Za-z0-9]+)(?=>)
|
||||
```
|
||||
|
||||
Implementata sia in [`scripts/ExtractCreoStrings.cs`](../scripts/ExtractCreoStrings.cs) sia in
|
||||
[`AssemblyComponentScanner.cs`](../CadSearchGui/AssemblyComponentScanner.cs) (fase "Trova componenti
|
||||
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,
|
||||
verificando prima con `scripts/Analyze.cs` che il contenuto tra `<` e `>` corrisponda a un codice
|
||||
realmente indicizzato in `Unificati/`.
|
||||
|
||||
## Risoluzione ricorsiva dell'albero
|
||||
|
||||
Un `.asm` referenzia solo i propri sotto-componenti diretti, non l'intero
|
||||
albero annidato. Per ricostruire la distinta base completa serve una BFS:
|
||||
albero annidato. Per ricostruire la distinta base completa (non solo il primo
|
||||
livello) serve una BFS:
|
||||
|
||||
1. Estrarre i codici candidati dal file corrente (euristica sopra).
|
||||
2. Per ogni codice, cercarlo nell'indice `cad_index.db`
|
||||
@@ -82,11 +268,18 @@ albero annidato. Per ricostruire la distinta base completa serve una BFS:
|
||||
3. Un codice non trovato nell'indice va scartato/segnalato: o è rumore
|
||||
binario, o è un componente non ancora indicizzato.
|
||||
|
||||
Stessa logica in tre punti del codice, con scopi diversi:
|
||||
Stessa logica di estrazione candidati ed euristica del codice radice (sopra),
|
||||
duplicata in due punti del codice per scopi diversi:
|
||||
|
||||
- [`AssemblyComponentScanner.EstraiCodiciReferenziati`](../CadSearchGui/AssemblyComponentScanner.cs) — solo estrazione dei candidati da un file.
|
||||
- [`Form1.AssiemeCompleto`](../CadSearchGui/Form1.cs) — BFS per verificare se un assieme è completo nell'indice (fase "Verifica completezza").
|
||||
- [`scripts/ExtractCreoStrings.cs`](../scripts/ExtractCreoStrings.cs) — stesso BFS, standalone, per generare un `BOM.txt` da riga di comando.
|
||||
- [`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.
|
||||
- [`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
|
||||
`(cartella_radice, codice, tipo_file)` - se lo stesso codice esiste in più
|
||||
cartelle radice sulla share (duplicati/copie storiche), va sempre ordinato
|
||||
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
|
||||
diversa nel contenuto) da quella realmente referenziata.
|
||||
|
||||
## Strumento di analisi manuale
|
||||
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
#:package Microsoft.Data.Sqlite@10.0.12
|
||||
// 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
|
||||
// pattern di codice, tarato sui codici numerici Pro-E), qui l'estrazione è generica -
|
||||
// qualunque run di lettere/cifre/underscore/trattino - e il DB decide se è un codice
|
||||
// vero, così si intercettano anche i nomi alfabetici delle parti commerciali scaricate
|
||||
// come STEP (es. "ANELLO_ESTERNO-1-SOLID1"). L'essere nel DB da solo non basta però:
|
||||
// codici corti o generici (es. "907", "NONE") comparirebbero per puro rumore binario,
|
||||
// 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;
|
||||
|
||||
if (args.Length < 1)
|
||||
{
|
||||
Console.WriteLine("Uso: dotnet run BomViaDb.cs -- <file.asm> [cad_index.db] [output.txt]");
|
||||
return 1;
|
||||
}
|
||||
|
||||
string asmIniziale = args[0];
|
||||
string dbPath = args.Length > 1 ? args[1] : "cad_index.db";
|
||||
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");
|
||||
connection.Open();
|
||||
|
||||
// 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.
|
||||
var codiciNoti = new Dictionary<string, List<(string Tipo, string Percorso)>>(StringComparer.OrdinalIgnoreCase);
|
||||
using (var command = connection.CreateCommand())
|
||||
{
|
||||
command.CommandText = "SELECT codice, tipo_file, percorso_completo FROM files_ultima_versione "
|
||||
+ "WHERE LOWER(tipo_file) IN ('prt', 'asm') ORDER BY codice, tipo_file, data_modifica DESC";
|
||||
using var reader = command.ExecuteReader();
|
||||
while (reader.Read())
|
||||
{
|
||||
string codice = reader.GetString(0);
|
||||
string tipo = reader.GetString(1).ToLowerInvariant();
|
||||
var lista = codiciNoti.TryGetValue(codice, out var l) ? l : codiciNoti[codice] = new();
|
||||
if (!lista.Any(x => x.Tipo == tipo))
|
||||
lista.Add((tipo, reader.GetString(2)));
|
||||
}
|
||||
}
|
||||
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];
|
||||
// Stessa esclusione di ExtractCreoStrings.cs: un assieme "_D..." referenzia sempre al
|
||||
// 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 scartatiNonInIndice = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var scartatiSenzaPlacement = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var codiciVisti = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { codiceProprio, codiceRadice };
|
||||
var daScansionare = new Queue<string>(new[] { asmIniziale });
|
||||
|
||||
while (daScansionare.Count > 0)
|
||||
{
|
||||
var asm = daScansionare.Dequeue();
|
||||
var testo = Encoding.Latin1.GetString(File.ReadAllBytes(asm));
|
||||
var candidati = codiceRegex.Matches(testo).Select(m => m.Value).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var candidato in candidati)
|
||||
{
|
||||
if (!codiciVisti.Add(candidato))
|
||||
continue;
|
||||
if (!codiciNoti.TryGetValue(candidato, out var componenti))
|
||||
{
|
||||
scartatiNonInIndice.Add(candidato);
|
||||
continue;
|
||||
}
|
||||
if (!HaPlacement(testo, candidato))
|
||||
{
|
||||
scartatiSenzaPlacement.Add(candidato);
|
||||
continue;
|
||||
}
|
||||
componentiTrovati.Add(candidato);
|
||||
foreach (var (tipo, percorso) in componenti)
|
||||
if (tipo == "asm")
|
||||
daScansionare.Enqueue(percorso);
|
||||
}
|
||||
}
|
||||
|
||||
File.WriteAllLines(outPath, componentiTrovati);
|
||||
Console.WriteLine($"{componentiTrovati.Count} componenti scritti in {outPath}");
|
||||
Console.WriteLine($"Candidati nell'indice ma senza placement (scartati, {scartatiSenzaPlacement.Count}): {string.Join(", ", scartatiSenzaPlacement.Take(30))}"
|
||||
+ (scartatiSenzaPlacement.Count > 30 ? ", ..." : ""));
|
||||
Console.WriteLine($"Candidati non nell'indice (rumore binario): {scartatiNonInIndice.Count}");
|
||||
return 0;
|
||||
+106
-34
@@ -1,13 +1,12 @@
|
||||
#:package Microsoft.Data.Sqlite@10.0.12
|
||||
// dotnet run scripts/ExtractCreoStrings.cs -- <file.asm> [cad_index.db] [output.txt]
|
||||
//
|
||||
// Estrae dal file .asm i codici dei componenti referenziati (stessa euristica di
|
||||
// CadSearchGui/AssemblyComponentScanner.cs: i codici veri compaiono più volte nel
|
||||
// file, il rumore binario quasi sempre una sola volta; gli ID interni
|
||||
// "ModelView#<numero>" sono esclusi via lookbehind - vedi scripts/Analyze.cs) e poi
|
||||
// risolve ricorsivamente i sotto-assiemi cercandoli nell'indice cad_index.db, con lo
|
||||
// stesso BFS di Form1.AssiemeCompleto: un assieme può referenziare solo i propri
|
||||
// sotto-assiemi diretti, non l'intero albero, quindi va camminato livello per livello.
|
||||
// Estrae dal file .asm i codici dei componenti referenziati, espandendo ricorsivamente
|
||||
// ogni sotto-assieme (.asm) trovato nell'indice, finché non restano solo PRT (foglie) o
|
||||
// codici non presenti nell'indice. Stessa euristica di CadSearchGui/AssemblyComponentScanner.cs:
|
||||
// i codici veri compaiono più volte nel file, il rumore binario quasi sempre una sola volta;
|
||||
// gli ID interni "ModelView#<numero>" sono esclusi via lookbehind - vedi scripts/Analyze.cs.
|
||||
// Ogni codice trovato viene validato contro l'indice cad_index.db.
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Data.Sqlite;
|
||||
@@ -22,7 +21,20 @@ string asmIniziale = args[0];
|
||||
string dbPath = args.Length > 1 ? args[1] : "cad_index.db";
|
||||
string outPath = args.Length > 2 ? args[2] : "BOM.txt";
|
||||
|
||||
var codiceRegex = new Regex(@"(?<!ModelView#)\b\d{8,12}(?:_[A-Za-z]\d{2,4}|[A-Za-z]{0,2})\b", RegexOptions.Compiled);
|
||||
// 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)
|
||||
@@ -31,56 +43,116 @@ HashSet<string> EstraiCodiciReferenziati(string percorsoAsm)
|
||||
var occorrenze = new Dictionary<string, int>();
|
||||
foreach (Match m in codiceRegex.Matches(testo))
|
||||
occorrenze[m.Value] = occorrenze.GetValueOrDefault(m.Value) + 1;
|
||||
return occorrenze.Where(kv => kv.Value >= occorrenzeMinime).Select(kv => kv.Key).ToHashSet();
|
||||
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");
|
||||
connection.Open();
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = "SELECT tipo_file, percorso_completo FROM files_ultima_versione "
|
||||
+ "WHERE LOWER(codice) = LOWER(@codice) AND LOWER(tipo_file) IN ('prt', 'asm')";
|
||||
var pCodice = command.Parameters.Add("@codice", SqliteType.Text);
|
||||
|
||||
List<(string Tipo, string Percorso)> CercaComponenti(string codice)
|
||||
// "files_ultima_versione" è una VIEW con ROW_NUMBER() OVER (...), che SQLite non
|
||||
// materializza: ogni query la ricalcola sull'intera tabella "files" (~230ms anche per un
|
||||
// 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)
|
||||
{
|
||||
pCodice.Value = codice;
|
||||
var risultato = new List<(string, string)>();
|
||||
var tipiVisti = new HashSet<string>();
|
||||
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 "
|
||||
+ $"WHERE LOWER(codice) IN ({string.Join(",", nomiParametri)}) AND LOWER(tipo_file) IN ('prt', 'asm') "
|
||||
+ "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();
|
||||
while (reader.Read())
|
||||
{
|
||||
string tipo = reader.GetString(0).ToLowerInvariant();
|
||||
if (tipiVisti.Add(tipo))
|
||||
risultato.Add((tipo, reader.GetString(1)));
|
||||
string codiceDb = reader.GetString(0);
|
||||
string tipo = reader.GetString(1).ToLowerInvariant();
|
||||
var visti = tipiVisti.TryGetValue(codiceDb, out var v) ? v : tipiVisti[codiceDb] = new HashSet<string>();
|
||||
if (visti.Add(tipo))
|
||||
risultato[codiceDb].Add((tipo, reader.GetString(2)));
|
||||
}
|
||||
return risultato;
|
||||
}
|
||||
|
||||
var codiciVisti = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { Path.GetFileName(asmIniziale).Split('.')[0] };
|
||||
var daVisitare = new Queue<string>();
|
||||
daVisitare.Enqueue(asmIniziale);
|
||||
// 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];
|
||||
// Un assieme con suffisso "_D..." referenzia sempre al suo interno anche l'assieme
|
||||
// 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 nonTrovati = new List<string>();
|
||||
var codiciVisti = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { codiceProprio, codiceRadice };
|
||||
var daScansionare = new Queue<string>(new[] { asmIniziale });
|
||||
|
||||
while (daVisitare.Count > 0)
|
||||
while (daScansionare.Count > 0)
|
||||
{
|
||||
var file = daVisitare.Dequeue();
|
||||
foreach (var codice in EstraiCodiciReferenziati(file))
|
||||
var asm = daScansionare.Dequeue();
|
||||
var candidati = EstraiCodiciReferenziati(asm)
|
||||
.Where(codiciVisti.Add)
|
||||
.ToList();
|
||||
var trovatiPerCodice = CercaComponenti(candidati);
|
||||
foreach (var codice in candidati)
|
||||
{
|
||||
if (!codiciVisti.Add(codice))
|
||||
continue;
|
||||
|
||||
var componenti = CercaComponenti(codice);
|
||||
var componenti = trovatiPerCodice[codice];
|
||||
if (componenti.Count == 0)
|
||||
{
|
||||
nonTrovati.Add(codice);
|
||||
continue;
|
||||
}
|
||||
|
||||
componentiTrovati.Add(codice);
|
||||
var percorsoAsm = componenti.FirstOrDefault(c => c.Tipo == "asm").Percorso;
|
||||
if (percorsoAsm is not null)
|
||||
daVisitare.Enqueue(percorsoAsm);
|
||||
foreach (var (tipo, percorso) in componenti)
|
||||
if (tipo == "asm")
|
||||
daScansionare.Enqueue(percorso);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
#:package Microsoft.Data.Sqlite@10.0.12
|
||||
// dotnet run scripts/Query.cs -- <cad_index.db> <sql>
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
using var connection = new SqliteConnection($"Data Source={args[0]};Mode=ReadOnly");
|
||||
connection.Open();
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = args[1];
|
||||
using var reader = command.ExecuteReader();
|
||||
int righe = 0;
|
||||
while (reader.Read())
|
||||
{
|
||||
righe++;
|
||||
var valori = Enumerable.Range(0, reader.FieldCount).Select(i => reader.IsDBNull(i) ? "" : reader.GetValue(i).ToString());
|
||||
Console.WriteLine(string.Join(" | ", valori));
|
||||
}
|
||||
Console.WriteLine($"({righe} righe)");
|
||||
return 0;
|
||||
Reference in new issue
Block a user