Files
prep-step/CadSearchGui/Form1.cs
T
davideandClaude Sonnet 5 e56a3be5b4 Add recursive component resolution for assembly dependencies
Assemblies reference other assemblies and parts internally; a flat
single-level copy into raw/ left Creo unable to load models ("Dati di
input non completi"). AssemblyComponentScanner heuristically extracts
component codes embedded as readable strings inside the otherwise
binary .asm files; each candidate is validated against the index (which
filters out scan noise), copied into raw/, and if it's itself an
assembly, queued for the same treatment until the whole tree resolves
to PRT leaves. PrtMancantiForm reports what was found and copied.

Also ignore Creo Distributed Batch job files (*.dxc).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 11:40:34 +02:00

596 lines
22 KiB
C#

using System.Diagnostics;
using Microsoft.Data.Sqlite;
namespace CadSearchGui;
public partial class Form1 : Form
{
private readonly Button _btnApriDb = new();
private readonly Button _btnAggiorna = new();
private readonly Button _btnCreaRaw = new();
private readonly Button _btnTrovaPrt = new();
private readonly Label _lblStatus = new();
private readonly Label _lblDb = new();
private readonly DataGridView _gridImport = new();
private readonly Label _lblImportStatus = new();
private readonly ToolTip _tipImportStatus = new();
private static readonly string[] TipiPredefiniti = { "prt", "asm" };
private const string RadiceDiscoDefault = @"X:\";
private string? _dbPath;
private string? _percorsoInputCorrente;
private List<(string Componente, string Descrizione)>? _righeCorrenti;
private string? _ultimaCartellaRaw;
public Form1()
{
InitializeComponent();
BuildUi();
Load += (_, _) => InizializzaDatabase();
}
// Palette: blu primario per l'azione principale del flusso ("Crea cartella raw"),
// il resto resta ai colori di sistema per restare coerente col tema Windows dell'utente.
private static readonly Color ColorePrimario = Color.FromArgb(0, 99, 177);
private void BuildUi()
{
AllowDrop = true;
Text = "Distinta Base -> CSV con percorsi CAD";
MinimumSize = new Size(760, 480);
Font = new Font("Segoe UI", 9.5f);
var contenutoTop = new TableLayoutPanel
{
Dock = DockStyle.Top,
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
ColumnCount = 1,
Padding = new Padding(12, 12, 12, 0),
};
contenutoTop.Controls.Add(BuildGruppoDatabase());
contenutoTop.Controls.Add(BuildGruppoImporta());
_lblImportStatus.Dock = DockStyle.Top;
_lblImportStatus.AutoSize = false;
_lblImportStatus.AutoEllipsis = true;
_lblImportStatus.Height = 28;
_lblImportStatus.Padding = new Padding(12, 6, 12, 6);
_lblImportStatus.ForeColor = SystemColors.GrayText;
_gridImport.Dock = DockStyle.Fill;
_gridImport.ReadOnly = true;
_gridImport.BorderStyle = BorderStyle.None;
_gridImport.BackgroundColor = SystemColors.Window;
_gridImport.RowHeadersVisible = false;
_gridImport.AllowUserToAddRows = false;
_gridImport.AllowUserToDeleteRows = false;
_gridImport.AllowUserToResizeRows = false;
_gridImport.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
_gridImport.MultiSelect = false;
_gridImport.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;
_gridImport.ColumnHeadersHeight = 32;
_gridImport.ColumnHeadersDefaultCellStyle.Font = new Font(Font, FontStyle.Bold);
_gridImport.ColumnHeadersDefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleLeft;
_gridImport.RowTemplate.Height = 26;
_gridImport.AlternatingRowsDefaultCellStyle.BackColor = Color.FromArgb(245, 247, 250);
_gridImport.DefaultCellStyle.SelectionBackColor = ColorePrimario;
_gridImport.DefaultCellStyle.Padding = new Padding(4, 0, 4, 0);
_gridImport.Columns.Add("componente", "Componente");
_gridImport.Columns.Add("descrizione", "Descrizione");
_gridImport.Columns.Add("percorso", "Percorso CAD");
_gridImport.Columns["componente"]!.FillWeight = 20;
_gridImport.Columns["descrizione"]!.FillWeight = 40;
_gridImport.Columns["percorso"]!.FillWeight = 40;
_gridImport.CellDoubleClick += (_, _) => ApriCartellaFileSelezionato();
var statusPanel = new Panel { Dock = DockStyle.Bottom, Height = 28, BackColor = SystemColors.ControlLight };
_lblStatus.Dock = DockStyle.Fill;
_lblStatus.TextAlign = ContentAlignment.MiddleLeft;
_lblStatus.Padding = new Padding(12, 0, 0, 0);
_lblStatus.Text = "Pronto.";
statusPanel.Controls.Add(_lblStatus);
Controls.Add(_gridImport);
Controls.Add(_lblImportStatus);
Controls.Add(statusPanel);
Controls.Add(contenutoTop);
DragEnter += (_, e) =>
e.Effect = e.Data?.GetDataPresent(DataFormats.FileDrop) == true
? DragDropEffects.Copy
: DragDropEffects.None;
DragDrop += (_, e) =>
{
if (e.Data?.GetData(DataFormats.FileDrop) is string[] { Length: > 0 } file)
ImportaDistinta(file[0]);
};
}
private GroupBox BuildGruppoDatabase()
{
var contenuto = new TableLayoutPanel
{
Dock = DockStyle.Top,
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
ColumnCount = 1,
};
_lblDb.AutoSize = true;
_lblDb.AutoEllipsis = true;
_lblDb.MaximumSize = new Size(900, 0);
_lblDb.ForeColor = SystemColors.GrayText;
_lblDb.Margin = new Padding(0, 0, 0, 8);
contenuto.Controls.Add(_lblDb);
var bottoni = new FlowLayoutPanel { AutoSize = true, FlowDirection = FlowDirection.LeftToRight };
_btnApriDb.Text = "Apri DB...";
_btnApriDb.Size = new Size(120, 32);
_btnApriDb.Margin = new Padding(0, 0, 8, 0);
_btnApriDb.Click += (_, _) => ScegliDatabase();
bottoni.Controls.Add(_btnApriDb);
_btnAggiorna.Text = "Aggiorna indice";
_btnAggiorna.Size = new Size(120, 32);
_btnAggiorna.Click += (_, _) => AggiornaIndice();
bottoni.Controls.Add(_btnAggiorna);
contenuto.Controls.Add(bottoni);
return new GroupBox
{
Text = "1. Indice CAD (caricato automaticamente all'avvio)",
Dock = DockStyle.Top,
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
Padding = new Padding(12, 8, 12, 12),
Margin = new Padding(0, 0, 0, 10),
Controls = { contenuto },
};
}
private GroupBox BuildGruppoImporta()
{
var contenuto = new TableLayoutPanel
{
Dock = DockStyle.Top,
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
ColumnCount = 1,
};
var lblIstruzioni1 = new Label
{
AutoSize = true,
ForeColor = SystemColors.GrayText,
Margin = new Padding(0, 0, 0, 0),
Text = "2. Trascina qui il file .xls/.xml esportato dal gestionale (foglio 'Distinta "
+ "Base'), oppure scegli il file col pulsante sotto: genera un CSV con Componente "
+ "e Descrizione, in anteprima nella griglia.",
};
contenuto.Controls.Add(lblIstruzioni1);
var btnSfoglia = new Button
{
Text = "Scegli file distinta...",
Size = new Size(220, 32),
Margin = new Padding(0, 8, 0, 16),
};
btnSfoglia.Click += (_, _) => ScegliFileDistinta();
contenuto.Controls.Add(btnSfoglia);
var lblIstruzioni2 = new Label
{
AutoSize = true,
ForeColor = SystemColors.GrayText,
Text = "3. Premi qui sotto per cercare il percorso CAD di ogni componente "
+ "nell'indice e copiare i file trovati in una cartella 'raw'.",
};
contenuto.Controls.Add(lblIstruzioni2);
_btnCreaRaw.Text = "Crea cartella raw";
_btnCreaRaw.Size = new Size(220, 36);
_btnCreaRaw.Margin = new Padding(0, 8, 0, 0);
_btnCreaRaw.Enabled = false;
_btnCreaRaw.FlatStyle = FlatStyle.Flat;
_btnCreaRaw.FlatAppearance.BorderSize = 0;
_btnCreaRaw.BackColor = ColorePrimario;
_btnCreaRaw.ForeColor = Color.White;
_btnCreaRaw.Font = new Font(Font, FontStyle.Bold);
_btnCreaRaw.Click += (_, _) => CreaCartellaRaw();
contenuto.Controls.Add(_btnCreaRaw);
var lblIstruzioni3 = new Label
{
AutoSize = true,
ForeColor = SystemColors.GrayText,
Margin = new Padding(0, 12, 0, 0),
Text = "4. Gli assiemi referenziano altri PRT e sotto-assiemi al loro interno: premi "
+ "qui per risolvere l'albero componenti (ricorsivamente) e copiare tutto ciò che "
+ "viene trovato nell'indice dentro 'raw'.",
};
contenuto.Controls.Add(lblIstruzioni3);
_btnTrovaPrt.Text = "Trova componenti referenziati";
_btnTrovaPrt.Size = new Size(220, 32);
_btnTrovaPrt.Margin = new Padding(0, 8, 0, 0);
_btnTrovaPrt.Enabled = false;
_btnTrovaPrt.Click += (_, _) => TrovaPrtReferenziati();
contenuto.Controls.Add(_btnTrovaPrt);
return new GroupBox
{
Text = "2-4. Importa Distinta Base, crea la cartella raw e risolvi i componenti referenziati",
Dock = DockStyle.Top,
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
Padding = new Padding(12, 8, 12, 12),
Controls = { contenuto },
};
}
private void ScegliFileDistinta()
{
using var dialog = new OpenFileDialog
{
Filter = "File distinta base (*.xls;*.xml)|*.xls;*.xml|Tutti i file|*.*",
Title = "Seleziona il file Distinta Base",
};
if (dialog.ShowDialog(this) == DialogResult.OK)
ImportaDistinta(dialog.FileName);
}
private void ImportaDistinta(string percorso)
{
try
{
var (csv, righe) = DistintaBaseImporter.ConvertiInCsv(percorso);
_percorsoInputCorrente = percorso;
_righeCorrenti = righe;
_gridImport.Rows.Clear();
foreach (var (componente, descrizione) in righe)
{
_gridImport.Rows.Add(componente, descrizione, "");
}
_btnCreaRaw.Enabled = righe.Count > 0;
ImpostaStatoImport($"CSV generato ({righe.Count} righe): {csv}", Color.DarkGreen);
}
catch (Exception ex)
{
_percorsoInputCorrente = null;
_righeCorrenti = null;
_btnCreaRaw.Enabled = false;
_gridImport.Rows.Clear();
ImpostaStatoImport($"Errore durante la conversione: {ex.Message}", Color.DarkRed);
}
}
/// <summary>Imposta testo/colore dello stato import e il tooltip con il testo completo
/// (la label tronca con "..." quando il percorso non ci sta in una riga).</summary>
private void ImpostaStatoImport(string testo, Color colore)
{
_lblImportStatus.ForeColor = colore;
_lblImportStatus.Text = testo;
_tipImportStatus.SetToolTip(_lblImportStatus, testo);
}
private void CreaCartellaRaw()
{
if (_percorsoInputCorrente is null || _righeCorrenti is null)
return;
var percorsiTrovati = TrovaPercorsiCad(_righeCorrenti.Select(r => r.Componente));
for (int i = 0; i < _righeCorrenti.Count; i++)
{
string percorsoCad = percorsiTrovati.TryGetValue(_righeCorrenti[i].Componente, out var lista)
? string.Join("; ", lista)
: "";
_gridImport.Rows[i].Cells[2].Value = percorsoCad;
}
var cartellaRaw = Path.Combine(Path.GetDirectoryName(Path.GetFullPath(_percorsoInputCorrente))!, "raw");
int copiati = CopiaFileTrovati(percorsiTrovati.Values.SelectMany(p => p).Distinct(), cartellaRaw);
_ultimaCartellaRaw = cartellaRaw;
_btnTrovaPrt.Enabled = true;
ImpostaStatoImport(
$"{percorsiTrovati.Count} componenti con percorso CAD trovato su {_righeCorrenti.Count}. "
+ $"{copiati} file copiati in {cartellaRaw}",
Color.DarkGreen);
}
/// <summary>Risolve ricorsivamente l'albero dei componenti a partire dagli .asm 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.</summary>
private void TrovaPrtReferenziati()
{
if (_ultimaCartellaRaw is null || !Directory.Exists(_ultimaCartellaRaw))
return;
var daScansionare = new Queue<string>(
Directory.GetFiles(_ultimaCartellaRaw).Where(f => Path.GetFileName(f).Contains(".asm.")));
var codiciVisti = new HashSet<string>(daScansionare.Select(f => Path.GetFileName(f).Split('.')[0]));
var trovati = new List<(string Codice, string Tipo, string Percorso)>();
int codiciNonTrovati = 0;
if (_dbPath is not null)
{
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 codice = @codice AND tipo_file IN ('prt', 'asm')";
var pCodice = command.Parameters.Add("@codice", Microsoft.Data.Sqlite.SqliteType.Text);
while (daScansionare.Count > 0)
{
var asm = daScansionare.Dequeue();
foreach (var codice in AssemblyComponentScanner.EstraiCodiciReferenziati(asm))
{
if (!codiciVisti.Add(codice))
continue;
pCodice.Value = codice;
var componenti = new List<(string Tipo, string Percorso)>();
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
componenti.Add((reader.GetString(0), reader.GetString(1)));
}
}
if (componenti.Count == 0)
{
codiciNonTrovati++;
continue;
}
foreach (var (tipo, percorso) in componenti)
{
CopiaFileTrovati(new[] { percorso }, _ultimaCartellaRaw);
trovati.Add((codice, tipo, percorso));
if (tipo == "asm")
{
daScansionare.Enqueue(Path.Combine(_ultimaCartellaRaw, Path.GetFileName(percorso)));
}
}
}
}
}
using var dialog = new PrtMancantiForm(trovati, codiciNonTrovati, _ultimaCartellaRaw);
dialog.ShowDialog(this);
}
/// <summary>Copia i file trovati nella cartella "raw" indicata, creandola se necessario.
/// Ritorna il numero di file copiati con successo.</summary>
internal static int CopiaFileTrovati(IEnumerable<string> percorsi, string cartellaRaw)
{
int copiati = 0;
foreach (var percorso in percorsi)
{
try
{
Directory.CreateDirectory(cartellaRaw);
File.Copy(percorso, Path.Combine(cartellaRaw, Path.GetFileName(percorso)), overwrite: true);
copiati++;
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
// ponytail: file non raggiungibile (share offline, permessi) -> lo si salta, non blocca gli altri
}
}
return copiati;
}
/// <summary>Cerca, per ogni codice componente, i percorsi dei file CAD (ultima versione,
/// solo prt/asm) corrispondenti nell'indice.</summary>
private Dictionary<string, List<string>> TrovaPercorsiCad(IEnumerable<string> codici)
{
var risultato = new Dictionary<string, List<string>>();
if (_dbPath is null)
return risultato;
using var connection = new SqliteConnection($"Data Source={_dbPath};Mode=ReadOnly");
connection.Open();
var nomiParametri = TipiPredefiniti.Select((_, i) => $"@tipo{i}");
using var command = connection.CreateCommand();
command.CommandText = $"""
SELECT percorso_completo FROM files_ultima_versione
WHERE codice = @codice AND tipo_file IN ({string.Join(",", nomiParametri)})
ORDER BY tipo_file
""";
var pCodice = command.Parameters.Add("@codice", Microsoft.Data.Sqlite.SqliteType.Text);
for (int i = 0; i < TipiPredefiniti.Length; i++)
{
command.Parameters.AddWithValue($"@tipo{i}", TipiPredefiniti[i]);
}
foreach (var codice in codici.Distinct())
{
pCodice.Value = codice;
using var reader = command.ExecuteReader();
var percorsi = new List<string>();
while (reader.Read())
{
percorsi.Add(reader.GetString(0));
}
if (percorsi.Count > 0)
risultato[codice] = percorsi;
}
return risultato;
}
private static readonly string PercorsoImpostazioni = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "CadSearchGui", "ultimo_db.txt");
private void InizializzaDatabase()
{
var candidati = new[]
{
LeggiUltimoDbUsato(),
Path.Combine(AppContext.BaseDirectory, "cad_index.db"),
Path.Combine(Directory.GetCurrentDirectory(), "cad_index.db"),
};
_dbPath = candidati.FirstOrDefault(p => p is not null && File.Exists(p));
if (_dbPath is null)
{
_lblStatus.Text = "Database non trovato. Usa 'Apri DB...' per selezionarlo oppure 'Aggiorna indice' per crearlo.";
_lblDb.Text = "Nessun database caricato. Premi 'Aggiorna indice' per crearlo da X:\\, oppure 'Apri DB...' se ne hai gi\u00e0 uno.";
return;
}
CaricaDatabase(_dbPath);
}
private void ScegliDatabase()
{
using var dialog = new OpenFileDialog
{
Filter = "Database SQLite (*.db)|*.db|Tutti i file (*.*)|*.*",
Title = "Seleziona cad_index.db",
};
if (dialog.ShowDialog(this) == DialogResult.OK)
{
CaricaDatabase(dialog.FileName);
}
}
private void CaricaDatabase(string path)
{
try
{
using var connection = new SqliteConnection($"Data Source={path};Mode=ReadOnly");
connection.Open();
long totale = (long)(new SqliteCommand("SELECT COUNT(*) FROM files", connection).ExecuteScalar() ?? 0L);
_dbPath = path;
_lblDb.Text = $"Database: {path} ({totale:N0} file indicizzati)";
_lblStatus.Text = "Pronto.";
SalvaUltimoDbUsato(path);
}
catch (Exception ex)
{
MessageBox.Show(this, $"Impossibile aprire il database:\n{ex.Message}", "Errore",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>Ricorda l'ultimo DB caricato con successo, così al prossimo avvio si
/// ricarica automaticamente anche se l'eseguibile gira da una cartella diversa.</summary>
private static string? LeggiUltimoDbUsato()
{
try
{
return File.Exists(PercorsoImpostazioni) ? File.ReadAllText(PercorsoImpostazioni).Trim() : null;
}
catch (IOException)
{
return null;
}
}
private static void SalvaUltimoDbUsato(string path)
{
try
{
Directory.CreateDirectory(Path.GetDirectoryName(PercorsoImpostazioni)!);
File.WriteAllText(PercorsoImpostazioni, path);
}
catch (IOException)
{
// ponytail: non critico se non si riesce a salvare, si torna a chiedere "Apri DB..." la prossima volta
}
}
private async void AggiornaIndice()
{
string dbPath = _dbPath ?? Path.Combine(AppContext.BaseDirectory, "cad_index.db");
if (!Directory.Exists(RadiceDiscoDefault))
{
MessageBox.Show(this, $"Disco non raggiungibile: {RadiceDiscoDefault}", "Errore",
MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
_btnAggiorna.Enabled = false;
_lblStatus.Text = "Aggiornamento indice in corso...";
using var cts = new CancellationTokenSource();
using var dialog = new ProgressForm();
var progress = new Progress<ScanProgress>(dialog.Aggiorna);
dialog.Annullato += (_, _) => cts.Cancel();
var scanTask = Indexer.ScanAsync(RadiceDiscoDefault, dbPath, progress, cts.Token);
// Chiude automaticamente il dialog quando lo scan termina (successo, errore o annullamento)
_ = scanTask.ContinueWith(_ =>
{
if (dialog.Visible)
{
dialog.BeginInvoke(dialog.Close);
}
}, TaskScheduler.Default);
dialog.ShowDialog(this);
try
{
await scanTask;
_lblStatus.Text = "Indice aggiornato.";
CaricaDatabase(dbPath);
}
catch (OperationCanceledException)
{
_lblStatus.Text = "Aggiornamento annullato.";
}
catch (Exception ex)
{
MessageBox.Show(this, $"Errore durante l'aggiornamento dell'indice:\n{ex.Message}", "Errore",
MessageBoxButtons.OK, MessageBoxIcon.Error);
_lblStatus.Text = "Aggiornamento fallito.";
}
finally
{
_btnAggiorna.Enabled = true;
}
}
private void ApriCartellaFileSelezionato()
{
if (_gridImport.CurrentRow is null)
{
return;
}
string? percorsi = _gridImport.CurrentRow.Cells[2].Value as string;
if (string.IsNullOrEmpty(percorsi))
{
return;
}
string primoPercorso = percorsi.Split("; ", 2)[0];
try
{
Process.Start("explorer.exe", $"/select,\"{primoPercorso}\"");
}
catch (Exception ex)
{
MessageBox.Show(this, $"Impossibile aprire Esplora risorse:\n{ex.Message}", "Errore",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}