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
|
/*.csv
|
||||||
/raw/
|
/raw/
|
||||||
/step/
|
/step/
|
||||||
|
/step_finali/
|
||||||
*.dxc
|
*.dxc
|
||||||
|
|
||||||
# Report BOM generati manualmente da Creo per debug/validazione: mai una dipendenza
|
# 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
|
sotto-assiemi/parti che contengono (`RisolviAlberoComponenti`), copiandoli anch'essi
|
||||||
in `raw/`.
|
in `raw/`.
|
||||||
5. **Verifica completezza e STEP** — "Verifica completezza assiemi" (ricorsivo sui
|
5. **Verifica completezza e STEP** — "Verifica completezza assiemi" (ricorsivo sui
|
||||||
percorsi originali in indice, non su `raw/`) e "Report STEP realizzati" (confronto
|
percorsi originali in indice, non su `raw/`) e "Crea cartella step_finali": solo per i
|
||||||
con `step/`).
|
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
|
- **Indexer.cs** — logica di scansione del disco e scrittura/aggiornamento del DB
|
||||||
SQLite (`Indexer.ScanAsync`). Cammina tutte le cartelle di primo livello trovate sotto
|
SQLite (`Indexer.ScanAsync`). Cammina tutte le cartelle di primo livello trovate sotto
|
||||||
la radice (scoperte dinamicamente con `Directory.GetDirectories`, non un elenco fisso)
|
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`),
|
`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
|
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
|
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`
|
sotto-assiemi contro `cad_index.db` (stessa logica di `Form1.AssiemeCompleto`, solo codici
|
||||||
dumpa il contesto testo/hex attorno a un pattern in un file binario, per investigare a mano
|
numerici Pro-E); `BomViaDb.cs` fa lo stesso ma con la tokenizzazione generica validata contro
|
||||||
nuovi falsi positivi prima di toccare la regex.
|
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 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
|
`ModelView#`, self-reference, riferimenti fantasma a codici superati/rinominati — sintatticamente
|
||||||
|
|||||||
@@ -14,8 +14,10 @@ public class AssemblyComponentScannerTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Scarta_codici_con_una_sola_occorrenza()
|
public void Scarta_codici_con_una_sola_occorrenza()
|
||||||
{
|
{
|
||||||
// "11112222" compare 2 volte (riferimento vero), "99998888" una sola (rumore).
|
// "11112222" compare 2 volte con un vero placement (riferimento vero), "99998888"
|
||||||
string percorso = ScriviAsmFinto("blah 11112222 blah 99998888 blah 11112222 fine");
|
// una sola volta senza placement (rumore).
|
||||||
|
string percorso = ScriviAsmFinto(
|
||||||
|
"blah Componente ID 1 (11112222.PRT) blah 99998888 blah 11112222 fine");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var codici = AssemblyComponentScanner.EstraiCodiciReferenziati(percorso);
|
var codici = AssemblyComponentScanner.EstraiCodiciReferenziati(percorso);
|
||||||
@@ -25,6 +27,20 @@ public class AssemblyComponentScannerTests
|
|||||||
finally { File.Delete(percorso); }
|
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]
|
[Fact]
|
||||||
public void Riconosce_sotto_assiemi_con_suffisso_stile_D100()
|
public void Riconosce_sotto_assiemi_con_suffisso_stile_D100()
|
||||||
{
|
{
|
||||||
@@ -67,8 +83,69 @@ public class AssemblyComponentScannerTests
|
|||||||
finally { File.Delete(percorso); }
|
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]
|
[Fact]
|
||||||
public void File_inesistente_lancia_eccezione_IO() =>
|
public void File_inesistente_lancia_eccezione_IO() =>
|
||||||
Assert.Throws<DirectoryNotFoundException>(() =>
|
Assert.Throws<DirectoryNotFoundException>(() =>
|
||||||
AssemblyComponentScanner.EstraiCodiciReferenziati(@"C:\non\esiste\pippo.asm.1"));
|
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>
|
/// <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.
|
||||||
/// Il formato è per lo più binario, ma i codici dei sotto-componenti compaiono
|
/// 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
|
/// come stringhe ASCII leggibili al suo interno.
|
||||||
/// 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).
|
|
||||||
///
|
///
|
||||||
/// 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),
|
/// 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
|
/// 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
|
/// Falso positivo noto ed escluso (famiglia 1): gli ID interni di viste salvate, nel
|
||||||
/// "ModelView#<numero>", hanno la stessa forma di un codice (8-12 cifre) e
|
/// 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.
|
/// 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>
|
/// </summary>
|
||||||
static class AssemblyComponentScanner
|
static class AssemblyComponentScanner
|
||||||
{
|
{
|
||||||
private static readonly Regex CodiceRegex = new(
|
private static readonly Regex CodiceNumericoRegex = new(
|
||||||
@"(?<!ModelView#)\b\d{8,12}(?:_[A-Za-z]\d{2,4}|[A-Za-z]{0,2})\b", RegexOptions.Compiled);
|
@"(?<!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;
|
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 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,
|
AutoSize = true,
|
||||||
ForeColor = SystemColors.GrayText,
|
ForeColor = SystemColors.GrayText,
|
||||||
Margin = new Padding(0, 0, 0, 8),
|
Margin = new Padding(0, 0, 0, 8),
|
||||||
Text = "Dopo aver esportato gli STEP con Creo, premi qui per confrontare i "
|
Text = "Dopo aver esportato gli STEP con Creo, premi qui per confrontare i componenti "
|
||||||
+ "componenti della distinta con i file .stp presenti in 'step' e vedere quali "
|
+ "della distinta (e i sotto-componenti trovati in fase 4) con i file .stp presenti "
|
||||||
+ "sono già stati realizzati e quali mancano ancora.",
|
+ "in 'step', copiare quelli trovati in 'step_finali' e vedere quali mancano ancora.",
|
||||||
};
|
};
|
||||||
contenuto.Controls.Add(lblIstruzioniStep);
|
contenuto.Controls.Add(lblIstruzioniStep);
|
||||||
|
|
||||||
_btnReport.Text = "Report STEP realizzati";
|
_btnReport.Text = "Crea cartella step_finali";
|
||||||
_btnReport.Size = new Size(220, 32);
|
_btnReport.Size = new Size(220, 32);
|
||||||
_btnReport.Click += (_, _) => GeneraReportStep();
|
_btnReport.Click += (_, _) => GeneraReportStep();
|
||||||
contenuto.Controls.Add(_btnReport);
|
contenuto.Controls.Add(_btnReport);
|
||||||
@@ -551,33 +551,65 @@ public partial class Form1 : Form
|
|||||||
if (_percorsoInputCorrente is null || _righeCorrenti is null)
|
if (_percorsoInputCorrente is null || _righeCorrenti is null)
|
||||||
return;
|
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))
|
if (!Directory.Exists(cartellaStep))
|
||||||
{
|
{
|
||||||
ImpostaStatoImport($"Cartella 'step' non trovata in {cartellaStep}.", Color.DarkRed);
|
ImpostaStatoImport($"Cartella 'step' non trovata in {cartellaStep}.", Color.DarkRed);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var codiciConStep = new HashSet<string>(
|
var descrizioni = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
Directory.GetFiles(cartellaStep, "*.stp").Select(Path.GetFileNameWithoutExtension)!);
|
foreach (var r in _righeCorrenti)
|
||||||
|
descrizioni[r.Componente] = r.Descrizione;
|
||||||
|
|
||||||
var righeReport = _righeCorrenti
|
// Solo i componenti della distinta importata dal CSV: i sotto-componenti trovati in
|
||||||
.Select(r => (r.Componente, r.Descrizione, Realizzato: codiciConStep.Contains(r.Componente)))
|
// fase 4 (_componentiRisolti) servono a completare "raw", non entrano nello STEP finale.
|
||||||
|
var tuttiICodici = _righeCorrenti.Select(r => r.Componente)
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
.ToList();
|
.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(
|
ImpostaStatoImport(
|
||||||
$"Report STEP: {realizzati} componenti realizzati su {righeReport.Count} "
|
$"STEP finali: {creati} copiati, {stepMancanti} da esportare (CAD presente), "
|
||||||
+ $"({righeReport.Count - realizzati} mancanti).",
|
+ $"{nonTrovati} componenti non nell'indice, su {righeReport.Count} totali. "
|
||||||
|
+ $"Cartella: {cartellaStepFinali}.",
|
||||||
Color.DarkGreen);
|
Color.DarkGreen);
|
||||||
|
|
||||||
using var dialog = new ReportStepForm(righeReport, cartellaStep);
|
using var dialog = new ReportStepForm(righeReport, cartellaStepFinali);
|
||||||
dialog.ShowDialog(this);
|
dialog.ShowDialog(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Svuota in un colpo solo le cartelle "raw" e "step" nella root (stessa cartella
|
/// <summary>Svuota in un colpo solo le cartelle "raw", "step" e "step_finali" nella root
|
||||||
/// base del file distinta importato), previa un'unica conferma. Non tocca l'indice CAD: solo
|
/// (stessa cartella base del file distinta importato), previa un'unica conferma. Non tocca
|
||||||
/// le copie locali generate dalla pipeline.</summary>
|
/// l'indice CAD: solo le copie locali generate dalla pipeline.</summary>
|
||||||
private void PulisciRawEStep()
|
private void PulisciRawEStep()
|
||||||
{
|
{
|
||||||
string radice;
|
string radice;
|
||||||
@@ -595,12 +627,12 @@ public partial class Form1 : Form
|
|||||||
radice = dialog.SelectedPath;
|
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)
|
.Where(Directory.Exists)
|
||||||
.ToList();
|
.ToList();
|
||||||
if (cartelle.Count == 0)
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -630,57 +662,63 @@ public partial class Form1 : Form
|
|||||||
ImpostaStatoImport($"{eliminati} file eliminati da {string.Join(" e ", cartelle)}.", Color.DarkGreen);
|
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
|
/// <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
|
/// (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
|
/// 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
|
/// accoda per essere scansionati finché non restano solo PRT (foglie) o codici non presenti
|
||||||
/// nell'indice. Usata dal bottone "Trova componenti referenziati".</summary>
|
/// nell'indice. Usata dal bottone "Trova componenti referenziati".</summary>
|
||||||
private (List<(string Codice, string Tipo, string Percorso)> Trovati, List<string> NonTrovati) RisolviAlberoComponenti(
|
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 daScansionare = new Queue<string>(asmIniziali);
|
||||||
|
var codiciIniziali = daScansionare.Select(f => Path.GetFileName(f).Split('.')[0]);
|
||||||
var codiciVisti = new HashSet<string>(
|
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 trovati = new List<(string Codice, string Tipo, string Percorso)>();
|
||||||
var codiciNonTrovati = new List<string>();
|
var codiciNonTrovati = new List<string>();
|
||||||
if (daScansionare.Count == 0 || _dbPath is null)
|
if (daScansionare.Count == 0 || _dbPath is null)
|
||||||
return (trovati, codiciNonTrovati);
|
return (trovati, codiciNonTrovati);
|
||||||
|
|
||||||
using var connection = new SqliteConnection($"Data Source={_dbPath};Mode=ReadOnly");
|
var codiciNoti = CaricaCodiciNoti(_dbPath);
|
||||||
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);
|
|
||||||
|
|
||||||
|
int processati = 0;
|
||||||
while (daScansionare.Count > 0)
|
while (daScansionare.Count > 0)
|
||||||
{
|
{
|
||||||
var asm = daScansionare.Dequeue();
|
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))
|
if (!codiciVisti.Add(codice))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
pCodice.Value = codice;
|
if (!codiciNoti.TryGetValue(codice, out var componenti))
|
||||||
// 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)
|
|
||||||
{
|
{
|
||||||
codiciNonTrovati.Add(codice);
|
codiciNonTrovati.Add(codice);
|
||||||
continue;
|
continue;
|
||||||
@@ -696,12 +734,13 @@ public partial class Form1 : Form
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
progress?.Report((processati, processati + daScansionare.Count));
|
||||||
}
|
}
|
||||||
|
|
||||||
return (trovati, codiciNonTrovati);
|
return (trovati, codiciNonTrovati);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void TrovaPrtReferenziati()
|
private async void TrovaPrtReferenziati()
|
||||||
{
|
{
|
||||||
if (_ultimaCartellaRaw is null || !Directory.Exists(_ultimaCartellaRaw))
|
if (_ultimaCartellaRaw is null || !Directory.Exists(_ultimaCartellaRaw))
|
||||||
return;
|
return;
|
||||||
@@ -714,8 +753,19 @@ public partial class Form1 : Form
|
|||||||
|
|
||||||
// L'assieme generale è già in raw come qualsiasi altro assieme importato (vedi
|
// L'assieme generale è già in raw come qualsiasi altro assieme importato (vedi
|
||||||
// CreaCartellaRaw), quindi rientra qui senza bisogno di un caso speciale.
|
// CreaCartellaRaw), quindi rientra qui senza bisogno di un caso speciale.
|
||||||
var asmIniziali = Directory.GetFiles(_ultimaCartellaRaw).Where(f => Path.GetFileName(f).Contains(".asm."));
|
var asmIniziali = Directory.GetFiles(_ultimaCartellaRaw).Where(f => Path.GetFileName(f).Contains(".asm.")).ToList();
|
||||||
var (trovati, codiciNonTrovati) = RisolviAlberoComponenti(asmIniziali);
|
|
||||||
|
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 numPrt = trovati.Count(r => r.Tipo == "prt");
|
||||||
int numAsm = trovati.Count(r => r.Tipo == "asm");
|
int numAsm = trovati.Count(r => r.Tipo == "asm");
|
||||||
@@ -742,46 +792,23 @@ public partial class Form1 : Form
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
using var connection = new SqliteConnection($"Data Source={_dbPath};Mode=ReadOnly");
|
var codiciNoti = CaricaCodiciNoti(_dbPath);
|
||||||
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 cacheCodiciFile = new Dictionary<string, HashSet<string>>();
|
var cacheCodiciFile = new Dictionary<string, HashSet<string>>();
|
||||||
|
|
||||||
List<(string Tipo, string Percorso)> CercaComponenti(string codice)
|
List<(string Tipo, string Percorso)> CercaComponenti(string codice) =>
|
||||||
{
|
codiciNoti.TryGetValue(codice, out var trovati) ? trovati : new List<(string, string)>();
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool AssiemeCompleto(string percorsoAsmIniziale)
|
bool AssiemeCompleto(string percorsoAsmIniziale)
|
||||||
{
|
{
|
||||||
var daVisitare = new Queue<string>();
|
var daVisitare = new Queue<string>();
|
||||||
daVisitare.Enqueue(percorsoAsmIniziale);
|
daVisitare.Enqueue(percorsoAsmIniziale);
|
||||||
var fileVisitati = new HashSet<string> { 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)
|
while (daVisitare.Count > 0)
|
||||||
{
|
{
|
||||||
@@ -790,7 +817,7 @@ public partial class Form1 : Form
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
codiciReferenziati = AssemblyComponentScanner.EstraiCodiciReferenziati(file);
|
codiciReferenziati = AssemblyComponentScanner.EstraiCodiciReferenziati(file, codiciNoti.Keys);
|
||||||
}
|
}
|
||||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||||
{
|
{
|
||||||
|
|||||||
+17
-1
@@ -65,9 +65,25 @@ public static class Indexer
|
|||||||
schemaCmd.ExecuteNonQuery();
|
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),
|
// 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.
|
// 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>();
|
var previsti = new Dictionary<string, long>();
|
||||||
foreach (var cartella in cartelleRadice)
|
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;
|
namespace CadSearchGui;
|
||||||
|
|
||||||
/// <summary>Mostra, per ogni componente della distinta base, se lo STEP corrispondente
|
public enum EsitoStep
|
||||||
/// è già stato esportato (file .stp presente in "step") o manca ancora.</summary>
|
{
|
||||||
|
/// <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 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);
|
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);
|
||||||
@@ -21,8 +34,8 @@ public class ReportStepForm : Form
|
|||||||
AutoSize = false,
|
AutoSize = false,
|
||||||
Height = 32,
|
Height = 32,
|
||||||
Padding = new Padding(12, 8, 12, 8),
|
Padding = new Padding(12, 8, 12, 8),
|
||||||
Text = $"{realizzati} componenti realizzati su {righe.Count} ({righe.Count - realizzati} mancanti), "
|
Text = $"{creati} copiati, {stepMancanti} da esportare (CAD presente), "
|
||||||
+ $"confrontati con \"{cartellaStep}\".",
|
+ $"{nonTrovati} non nell'indice, su {righe.Count} totali. In \"{cartellaStepFinali}\".",
|
||||||
};
|
};
|
||||||
|
|
||||||
var grid = new DataGridView
|
var grid = new DataGridView
|
||||||
@@ -42,16 +55,22 @@ public class ReportStepForm : Form
|
|||||||
grid.Columns["descrizione"]!.FillWeight = 55;
|
grid.Columns["descrizione"]!.FillWeight = 55;
|
||||||
grid.Columns["step"]!.FillWeight = 25;
|
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");
|
var (testo, colore) = esito switch
|
||||||
grid.Rows[i].DefaultCellStyle.ForeColor = realizzato ? Color.DarkGreen : Color.DarkRed;
|
{
|
||||||
|
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 += (_, _) =>
|
btnApriCartella.Click += (_, _) =>
|
||||||
{
|
{
|
||||||
try { Process.Start("explorer.exe", cartellaStep); }
|
try { Process.Start("explorer.exe", cartellaStepFinali); }
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
MessageBox.Show(this, $"Impossibile aprire Esplora risorse:\n{ex.Message}", "Errore",
|
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
|
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
|
## Falsi positivi noti
|
||||||
|
|
||||||
**ID di viste salvate.** Il pattern `ModelView#<numero>` (es.
|
**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
|
quindi la soglia di occorrenze. Va escluso esplicitamente confrontandolo con
|
||||||
il codice derivato dal nome del file in ingresso (`nomefile.split('.')[0]`).
|
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:
|
**Riferimenti fantasma a codici superati/rinominati.** Caso osservato:
|
||||||
`9244207061.asm` contiene un riferimento a `9044207061` (stesso numero, prima
|
`9244207061.asm` contiene un riferimento a `9044207061` (stesso numero, prima
|
||||||
di una rinumerazione) con la stessa identica struttura sintattica di un
|
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
|
componenti veri con pattern simili altrove. Se ricompare, l'unica verifica
|
||||||
sicura resta l'apertura manuale in Creo del sotto-assieme sospetto.
|
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
|
## Risoluzione ricorsiva dell'albero
|
||||||
|
|
||||||
Un `.asm` referenzia solo i propri sotto-componenti diretti, non l'intero
|
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).
|
1. Estrarre i codici candidati dal file corrente (euristica sopra).
|
||||||
2. Per ogni codice, cercarlo nell'indice `cad_index.db`
|
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
|
3. Un codice non trovato nell'indice va scartato/segnalato: o è rumore
|
||||||
binario, o è un componente non ancora indicizzato.
|
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.
|
- [`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.
|
||||||
- [`Form1.AssiemeCompleto`](../CadSearchGui/Form1.cs) — BFS per verificare se un assieme è completo nell'indice (fase "Verifica completezza").
|
- [`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) — stesso BFS, standalone, per generare un `BOM.txt` da riga di comando.
|
|
||||||
|
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
|
## 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
|
#: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 (stessa euristica di
|
// Estrae dal file .asm i codici dei componenti referenziati, espandendo ricorsivamente
|
||||||
// CadSearchGui/AssemblyComponentScanner.cs: i codici veri compaiono più volte nel
|
// ogni sotto-assieme (.asm) trovato nell'indice, finché non restano solo PRT (foglie) o
|
||||||
// file, il rumore binario quasi sempre una sola volta; gli ID interni
|
// codici non presenti nell'indice. Stessa euristica di CadSearchGui/AssemblyComponentScanner.cs:
|
||||||
// "ModelView#<numero>" sono esclusi via lookbehind - vedi scripts/Analyze.cs) e poi
|
// i codici veri compaiono più volte nel file, il rumore binario quasi sempre una sola volta;
|
||||||
// risolve ricorsivamente i sotto-assiemi cercandoli nell'indice cad_index.db, con lo
|
// gli ID interni "ModelView#<numero>" sono esclusi via lookbehind - vedi scripts/Analyze.cs.
|
||||||
// stesso BFS di Form1.AssiemeCompleto: un assieme può referenziare solo i propri
|
// Ogni codice trovato viene validato contro l'indice cad_index.db.
|
||||||
// sotto-assiemi diretti, non l'intero albero, quindi va camminato livello per livello.
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Microsoft.Data.Sqlite;
|
using Microsoft.Data.Sqlite;
|
||||||
@@ -22,7 +21,20 @@ 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";
|
||||||
|
|
||||||
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;
|
const int occorrenzeMinime = 2;
|
||||||
|
|
||||||
HashSet<string> EstraiCodiciReferenziati(string percorsoAsm)
|
HashSet<string> EstraiCodiciReferenziati(string percorsoAsm)
|
||||||
@@ -31,56 +43,116 @@ HashSet<string> EstraiCodiciReferenziati(string percorsoAsm)
|
|||||||
var occorrenze = new Dictionary<string, int>();
|
var occorrenze = new Dictionary<string, int>();
|
||||||
foreach (Match m in codiceRegex.Matches(testo))
|
foreach (Match m in codiceRegex.Matches(testo))
|
||||||
occorrenze[m.Value] = occorrenze.GetValueOrDefault(m.Value) + 1;
|
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");
|
using var connection = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly");
|
||||||
connection.Open();
|
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 Dictionary<string, List<(string, string)>>(StringComparer.OrdinalIgnoreCase);
|
||||||
var risultato = new List<(string, string)>();
|
foreach (var codice in codici)
|
||||||
var tipiVisti = new HashSet<string>();
|
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();
|
using var reader = command.ExecuteReader();
|
||||||
while (reader.Read())
|
while (reader.Read())
|
||||||
{
|
{
|
||||||
string tipo = reader.GetString(0).ToLowerInvariant();
|
string codiceDb = reader.GetString(0);
|
||||||
if (tipiVisti.Add(tipo))
|
string tipo = reader.GetString(1).ToLowerInvariant();
|
||||||
risultato.Add((tipo, reader.GetString(1)));
|
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;
|
return risultato;
|
||||||
}
|
}
|
||||||
|
|
||||||
var codiciVisti = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { Path.GetFileName(asmIniziale).Split('.')[0] };
|
// Espansione ricorsiva: ogni sotto-assieme (.asm) trovato viene a sua volta letto e i suoi
|
||||||
var daVisitare = new Queue<string>();
|
// componenti aggiunti alla coda, finché non restano solo PRT (foglie) o codici non presenti
|
||||||
daVisitare.Enqueue(asmIniziale);
|
// 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 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 daScansionare = new Queue<string>(new[] { asmIniziale });
|
||||||
|
|
||||||
while (daVisitare.Count > 0)
|
while (daScansionare.Count > 0)
|
||||||
{
|
{
|
||||||
var file = daVisitare.Dequeue();
|
var asm = daScansionare.Dequeue();
|
||||||
foreach (var codice in EstraiCodiciReferenziati(file))
|
var candidati = EstraiCodiciReferenziati(asm)
|
||||||
|
.Where(codiciVisti.Add)
|
||||||
|
.ToList();
|
||||||
|
var trovatiPerCodice = CercaComponenti(candidati);
|
||||||
|
foreach (var codice in candidati)
|
||||||
{
|
{
|
||||||
if (!codiciVisti.Add(codice))
|
var componenti = trovatiPerCodice[codice];
|
||||||
continue;
|
|
||||||
|
|
||||||
var componenti = CercaComponenti(codice);
|
|
||||||
if (componenti.Count == 0)
|
if (componenti.Count == 0)
|
||||||
{
|
{
|
||||||
nonTrovati.Add(codice);
|
nonTrovati.Add(codice);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentiTrovati.Add(codice);
|
componentiTrovati.Add(codice);
|
||||||
var percorsoAsm = componenti.FirstOrDefault(c => c.Tipo == "asm").Percorso;
|
foreach (var (tipo, percorso) in componenti)
|
||||||
if (percorsoAsm is not null)
|
if (tipo == "asm")
|
||||||
daVisitare.Enqueue(percorsoAsm);
|
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