Navigazione verticale e strumenti di misura nell'interfaccia
Rifa' l'impaginazione attorno a una barra verticale sul fianco sinistro che governa insieme il contenuto principale e la colonna delle impostazioni: una sezione, una vista, i suoi comandi. Prima c'erano due gerarchie di schede da tenere allineate a mano; adesso ce n'e' una sola, in verticale ci sta il nome per esteso, e la sezione scelta resta leggibile mentre si lavora - cosa che in una fila di schede in cima si perde appena l'occhio scende sul contenuto. Accanto a ogni voce compare il numero di avvisi che la riguardano. L'anteprima non e' piu' il contenuto di una scheda fra le altre: resta in alto sempre, perche' in un programma che tratta immagini l'immagine si guarda mentre si regola qualunque cosa. Guadagna le quattro cose che la rendono utile per giudicare e non solo per guardare. Zoom e trascinamento, perche' una stabilizzazione sotto il pixel non si vede su un'immagine rimpicciolita per stare in un riquadro, e da scala uno a uno in su i pixel si mostrano come sono invece di essere interpolati. Confronto a tendina fra originale e corretto, perche' l'unico modo di capire cosa una correzione stia facendo e' vedere accanto ciо' che c'era prima. Campo vettoriale sovrapposto, che il motore calcola comunque e che spiega perche' la sfocatura viene come viene, soprattutto quando e' sbagliata. Confine delle regioni, campionato attraverso la stessa mappatura del ritaglio cosi' resta al suo posto anche sotto una panoramica virtuale. Ogni sezione porta lo strumento di misura che le riguarda: striscia di provini e pannello degli avvisi sulla sequenza, istogramma e forma d'onda sull'esposizione, percorso ricostruito della stabilizzazione sul movimento, piano temporale sul tempo. Sono tutte grandezze che il motore gia' calcolava e che finivano in due numeri in fondo a una riga di stato, cioe' invisibili. Il pilota automatico. I parametri deducibili dalle misure non si chiedono piu': un cursore con l'indicatore auto mostra il valore scelto e il motivo, toccarlo passa il comando all'utente, l'indicatore lo restituisce. E' lo stesso modello che il menu dell'orientamento usava da solo, esteso a larghezza di analisi, finestra del deflicker, lunghezza della transizione, finestra della stabilizzazione e tetto di memoria. Non sono valori di comodo: la finestra del deflicker viene da quattro periodi dello sfarfallio misurati sull'autocorrelazione, quella della stabilizzazione e' la piu' corta che rende liscio il percorso, la transizione e' meta' della distanza tipica fra i cambi. Dove il valore giusto non si puo' misurare il direttore non inventa: restituisce meno decisioni e lascia il cursore dov'e'. Il riquadro sponsor sta soltanto nella scheda Esportazione, perche' quello e' l'unico momento in cui non c'e' niente da fare e uno spazio pubblicitario non toglie niente a nessuno; accanto a un cursore che si sta regolando sarebbe un ostacolo. Gli annunci si leggono da una cartella locale con un listino in formato testo, ed e' importante dire cosa NON fa: nessuna rete, nessun identificativo, nessun clic registrato. Non e' prudenza eccessiva - un circuito pubblicitario vero richiederebbe il suo SDK, che il vincolo sulle dipendenze esclude, e comunque significherebbe far uscire dati dalla macchina di chi sta montando un time-lapse. Le campagne si aggiornano copiando file; a listino vuoto compaiono note interne; il riquadro si spegne dalle preferenze. Aggiunge anche le preferenze dell'applicazione, distinte da quelle del progetto, con persistenza in un file di testo scritto a mano nello stesso spirito del resto: una riga per voce, correggibile con un editor. Il progetto descrive come trattare questi fotogrammi, le preferenze come si comporta il programma. Piu' due cose piccole che pesavano: immissione numerica sui cursori, perche' trascinare fino a 0,35 e' un esercizio di mira e non una regolazione, e anteprima dei fotogrammi mentre vengono codificati, che non migliora il risultato di un pixel ma cambia molto un'attesa di mezz'ora. Verifica: 54 controlli invariati, Debug e Release puliti, tutte e sei le sezioni ispezionate a video sulla scena di prova. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+238
-133
@@ -6,10 +6,13 @@ using Titano.Video;
|
||||
namespace Titano.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Pannello di configurazione avanzata, diviso nelle sezioni Generale, Immagine, Movimento,
|
||||
/// Tempo ed Esportazione. Ogni controllo scrive direttamente nel progetto e dichiara quale
|
||||
/// parte della pipeline va rifatta: c'è differenza fra spostare un cursore che cambia solo
|
||||
/// una curva già calcolata e sceglierne uno che obbliga a rileggere ogni file.
|
||||
/// Colonna delle impostazioni. Non ha più schede proprie: mostra la pagina della sezione
|
||||
/// scelta nella barra di navigazione, così la barra governa insieme il contenuto principale
|
||||
/// e i comandi che lo riguardano, e non esistono due gerarchie di schede da tenere allineate.
|
||||
///
|
||||
/// Ogni controllo dichiara quale parte della pipeline invalida. C'è differenza fra spostare
|
||||
/// un cursore che ricalcola una curva già misurata e sceglierne uno che obbliga a rileggere
|
||||
/// mille file, e il programma deve saperla.
|
||||
/// </summary>
|
||||
internal sealed class SettingsPanel : Panel
|
||||
{
|
||||
@@ -17,8 +20,9 @@ internal sealed class SettingsPanel : Panel
|
||||
private static readonly int?[] OrientationValues = [null, 1, 2, 4, 6, 3, 8];
|
||||
|
||||
private readonly TitanoProject _project;
|
||||
private readonly TabStrip _tabs;
|
||||
private readonly Panel[] _pages;
|
||||
private readonly AppSettings _app;
|
||||
private readonly Dictionary<WorkspaceSection, Panel> _pages = [];
|
||||
private readonly Dictionary<AutoKey, ParameterSlider> _autoSliders = [];
|
||||
|
||||
private LabeledCombo? _orientationCombo;
|
||||
private Label? _analysisNote;
|
||||
@@ -31,30 +35,19 @@ internal sealed class SettingsPanel : Panel
|
||||
private ParameterSlider? _keyframeEaseIn;
|
||||
private bool _syncingKeyframe;
|
||||
|
||||
/// <summary>Un parametro del deflicker è cambiato: basta ricalcolare le curve già misurate.</summary>
|
||||
public event EventHandler? DeflickerChanged;
|
||||
|
||||
/// <summary>È cambiato un parametro che invalida l'analisi già svolta.</summary>
|
||||
public event EventHandler? AnalysisInvalidated;
|
||||
|
||||
/// <summary>È cambiato un parametro che modifica solo l'anteprima o l'esportazione.</summary>
|
||||
public event EventHandler? PreviewInvalidated;
|
||||
|
||||
public event EventHandler? BrowseOutputRequested;
|
||||
public event EventHandler? AppSettingsChanged;
|
||||
|
||||
public TextBox OutputPathBox { get; }
|
||||
|
||||
public SettingsPanel(TitanoProject project)
|
||||
public SettingsPanel(TitanoProject project, AppSettings app)
|
||||
{
|
||||
_project = project;
|
||||
_app = app;
|
||||
BackColor = Theme.Surface;
|
||||
Padding = new Padding(0);
|
||||
|
||||
_tabs = new TabStrip("Generale", "Immagine", "Movimento", "Tempo", "Esportazione")
|
||||
{
|
||||
Dock = DockStyle.Top,
|
||||
};
|
||||
_tabs.SelectedChanged += (_, _) => ShowPage(_tabs.SelectedIndex);
|
||||
|
||||
OutputPathBox = new TextBox
|
||||
{
|
||||
@@ -70,24 +63,30 @@ internal sealed class SettingsPanel : Panel
|
||||
PreviewInvalidated?.Invoke(this, EventArgs.Empty);
|
||||
};
|
||||
|
||||
// L'ordine di inserimento determina l'ordine di ancoraggio: i controlli in coda alla
|
||||
// collezione vengono disposti per primi, quindi la barra delle schede va aggiunta
|
||||
// dopo le pagine per riservarsi la propria fascia in alto.
|
||||
_pages = [BuildGeneralPage(), BuildImagePage(), BuildMotionPage(), BuildTimePage(), BuildExportPage()];
|
||||
foreach (var page in _pages)
|
||||
_pages[WorkspaceSection.Sequence] = BuildSequencePage();
|
||||
_pages[WorkspaceSection.Exposure] = BuildExposurePage();
|
||||
_pages[WorkspaceSection.Motion] = BuildMotionPage();
|
||||
_pages[WorkspaceSection.Timing] = BuildTimingPage();
|
||||
_pages[WorkspaceSection.Export] = BuildExportPage();
|
||||
_pages[WorkspaceSection.Preferences] = BuildPreferencesPage();
|
||||
|
||||
foreach (var page in _pages.Values)
|
||||
{
|
||||
page.Dock = DockStyle.Fill;
|
||||
page.Visible = false;
|
||||
Controls.Add(page);
|
||||
}
|
||||
Controls.Add(_tabs);
|
||||
|
||||
ShowPage(0);
|
||||
ShowSection(WorkspaceSection.Sequence);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Riporta nel menu l'esito del rilevamento, così la voce "Automatico" dice cosa ha deciso.
|
||||
/// </summary>
|
||||
public void ShowSection(WorkspaceSection section)
|
||||
{
|
||||
foreach (var (key, page) in _pages) page.Visible = key == section;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ aggiornamenti dall'esterno
|
||||
|
||||
public void ShowDetectedOrientation(Imaging.OrientationDetection detection)
|
||||
{
|
||||
if (_orientationCombo is null || _orientationCombo.Combo.Items.Count == 0) return;
|
||||
@@ -97,7 +96,24 @@ internal sealed class SettingsPanel : Panel
|
||||
_orientationCombo.Combo.SelectedIndex = selected;
|
||||
}
|
||||
|
||||
/// <summary>Aggiorna le voci che dipendono dall'analisi: maschera, transizioni, tremolio.</summary>
|
||||
/// <summary>
|
||||
/// Riporta nei cursori i valori che il direttore ha dedotto, con il motivo di ciascuno.
|
||||
/// Quelli di cui l'utente ha preso il controllo restano dove sono.
|
||||
/// </summary>
|
||||
public void ShowAutoDecisions(IReadOnlyDictionary<AutoKey, AutoDecision> decisions)
|
||||
{
|
||||
foreach (var (key, slider) in _autoSliders)
|
||||
{
|
||||
bool manual = _project.ManualParameters.Contains(key);
|
||||
slider.IsAuto = !manual;
|
||||
|
||||
if (!decisions.TryGetValue(key, out var decision)) continue;
|
||||
slider.AutoReason = decision.Reason;
|
||||
if (!manual) slider.SetValueSilently(decision.Value);
|
||||
}
|
||||
Invalidate(true);
|
||||
}
|
||||
|
||||
public void ShowAnalysis()
|
||||
{
|
||||
if (_analysisNote is null) return;
|
||||
@@ -115,16 +131,16 @@ internal sealed class SettingsPanel : Panel
|
||||
{
|
||||
lines.Add(transitions.StepCount == 0
|
||||
? "Transizioni: nessun cambio di impostazione rilevato."
|
||||
: $"Transizioni: {transitions.StepCount} cambi di impostazione, " +
|
||||
$"il maggiore di {transitions.LargestStepStops:0.00} EV" +
|
||||
(transitions.MetadataUsable ? " (dai metadati)." : " (dedotti dalla luminanza)."));
|
||||
: $"Transizioni: {transitions.StepCount} cambi" +
|
||||
(transitions.UnobservedSteps > 0 ? $", {transitions.UnobservedSteps} non recepiti" : string.Empty) +
|
||||
$", il maggiore di {transitions.LargestStepStops:0.00} EV.");
|
||||
}
|
||||
|
||||
if (_project.Stabilization.Enabled && _project.Motion is { } motion)
|
||||
{
|
||||
var (width, _) = _project.ResolveSourceSize();
|
||||
lines.Add($"Stabilizzazione: tremolio medio {motion.MeanShake * Math.Max(1, width):0.0} px, " +
|
||||
$"ritaglio necessario {(_project.StabilizationZoom - 1) * 100:0.#}%.");
|
||||
$"ritaglio {(_project.StabilizationZoom - 1) * 100:0.#}%.");
|
||||
}
|
||||
|
||||
_analysisNote.Text = lines.Count > 0
|
||||
@@ -133,7 +149,6 @@ internal sealed class SettingsPanel : Panel
|
||||
LayoutNote(_analysisNote);
|
||||
}
|
||||
|
||||
/// <summary>Aggiorna l'editor del movimento quando cambia la sequenza caricata.</summary>
|
||||
public void ShowSequenceGeometry()
|
||||
{
|
||||
if (_cameraEditor is null) return;
|
||||
@@ -142,17 +157,9 @@ internal sealed class SettingsPanel : Panel
|
||||
_cameraEditor.Invalidate();
|
||||
}
|
||||
|
||||
/// <summary>Seleziona una delle sezioni; usata anche dalla modalità di cattura.</summary>
|
||||
internal void SelectPage(int index) => _tabs.SelectedIndex = index;
|
||||
// ------------------------------------------------------------------ Sequenza
|
||||
|
||||
private void ShowPage(int index)
|
||||
{
|
||||
for (int i = 0; i < _pages.Length; i++) _pages[i].Visible = i == index;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ Generale
|
||||
|
||||
private Panel BuildGeneralPage()
|
||||
private Panel BuildSequencePage()
|
||||
{
|
||||
var stack = NewStack();
|
||||
|
||||
@@ -166,13 +173,6 @@ internal sealed class SettingsPanel : Panel
|
||||
AnalysisInvalidated?.Invoke(this, EventArgs.Empty);
|
||||
}));
|
||||
|
||||
stack.Add(Slider("Tolleranza sulla cadenza", 0.05, 1.0, _project.General.CadenceTolerance, 0.05, "0.00", "×",
|
||||
value =>
|
||||
{
|
||||
_project.General.CadenceTolerance = value;
|
||||
AnalysisInvalidated?.Invoke(this, EventArgs.Empty);
|
||||
}));
|
||||
|
||||
_orientationCombo = Combo("Orientamento dei fotogrammi",
|
||||
["Automatico", "Nessuna rotazione", "Specchia orizzontalmente", "Specchia verticalmente",
|
||||
"Ruota 90° in senso orario", "Ruota 180°", "Ruota 90° in senso antiorario"],
|
||||
@@ -184,13 +184,14 @@ internal sealed class SettingsPanel : Panel
|
||||
});
|
||||
stack.Add(_orientationCombo);
|
||||
|
||||
stack.Add(Note("In automatico l'orientamento viene dedotto dai metadati del primo scatto e " +
|
||||
"confrontato con ciò che il decodificatore di sistema restituisce, così la " +
|
||||
"trasformazione non viene applicata due volte. Se il risultato non convince, " +
|
||||
"l'anteprima mostra subito l'effetto di una scelta manuale."));
|
||||
stack.Add(Slider("Tolleranza sulla cadenza", 0.05, 1.0, _project.General.CadenceTolerance, 0.05, "0.00", "×",
|
||||
value =>
|
||||
{
|
||||
_project.General.CadenceTolerance = value;
|
||||
AnalysisInvalidated?.Invoke(this, EventArgs.Empty);
|
||||
}));
|
||||
|
||||
stack.Add(new SectionHeader("Qualità di elaborazione"));
|
||||
|
||||
stack.Add(Combo("Profilo",
|
||||
["Bozza — veloce", "Standard — equilibrio", "Massima — resa migliore"],
|
||||
(int)_project.General.Quality,
|
||||
@@ -201,11 +202,12 @@ internal sealed class SettingsPanel : Panel
|
||||
}));
|
||||
|
||||
stack.Add(Note("Il profilo governa finezza del campo vettoriale, campioni della sfocatura, " +
|
||||
"riquadri della correlazione di fase e risoluzione delle passate di analisi, " +
|
||||
"e sovrascrive i cursori delle sezioni avanzate."));
|
||||
"riquadri della correlazione di fase e risoluzione delle passate di analisi."));
|
||||
|
||||
stack.Add(new SectionHeader("Prestazioni e memoria"));
|
||||
stack.Add(Slider("Larghezza della passata di analisi", 256, 2048, _project.General.AnalysisWidth, 64, "0", "px",
|
||||
|
||||
stack.Add(AutoSlider(AutoKey.AnalysisWidth, "Larghezza della passata di analisi", 256, 2048,
|
||||
_project.General.AnalysisWidth, 64, "0", "px",
|
||||
value =>
|
||||
{
|
||||
_project.General.AnalysisWidth = (int)value;
|
||||
@@ -222,16 +224,16 @@ internal sealed class SettingsPanel : Panel
|
||||
stack.Add(Slider("Lettura in anticipo", 0, 32, _project.Cache.PrefetchDepth, 1, "0", "fotogrammi",
|
||||
value => _project.Cache.PrefetchDepth = (int)value));
|
||||
|
||||
stack.Add(Slider("Tetto di memoria per i fotogrammi", 256, 32768, _project.Cache.MemoryBudgetMiB, 256, "0", "MiB",
|
||||
stack.Add(AutoSlider(AutoKey.MemoryBudget, "Tetto di memoria", 256, 32768,
|
||||
_project.Cache.MemoryBudgetMiB, 256, "0", "MiB",
|
||||
value => _project.Cache.MemoryBudgetMiB = (int)value));
|
||||
|
||||
stack.Add(Check("Parcheggia su disco i fotogrammi in eccesso", _project.Cache.AllowDiskSpill,
|
||||
value => _project.Cache.AllowDiskSpill = value));
|
||||
|
||||
stack.Add(Note("La finestra attiva sta sempre in memoria; il tetto governa la lettura in " +
|
||||
"anticipo, che serve a tenere occupati tutti i processori sulla decodifica dei " +
|
||||
"RAW. Oltre il tetto i fotogrammi già letti aspettano su disco e vengono ripresi " +
|
||||
"una volta sola. Il file di parcheggio si cancella da sé alla chiusura."));
|
||||
"anticipo. Oltre il tetto i fotogrammi già letti aspettano su disco e vengono " +
|
||||
"ripresi una volta sola. Il file di parcheggio si cancella da sé."));
|
||||
|
||||
stack.Add(new SectionHeader("Esito dell'analisi"));
|
||||
_analysisNote = Note("Esegui l'analisi per vedere cosa il motore ha dedotto dalla sequenza.");
|
||||
@@ -240,9 +242,9 @@ internal sealed class SettingsPanel : Panel
|
||||
return stack.Panel;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ Immagine
|
||||
// ------------------------------------------------------------------ Esposizione
|
||||
|
||||
private Panel BuildImagePage()
|
||||
private Panel BuildExposurePage()
|
||||
{
|
||||
var stack = NewStack();
|
||||
|
||||
@@ -254,7 +256,8 @@ internal sealed class SettingsPanel : Panel
|
||||
DeflickerChanged?.Invoke(this, EventArgs.Empty);
|
||||
}));
|
||||
|
||||
stack.Add(Slider("Finestra temporale", 3, 121, _project.Deflicker.WindowFrames, 2, "0", "fotogrammi",
|
||||
stack.Add(AutoSlider(AutoKey.DeflickerWindow, "Finestra temporale", 3, 121,
|
||||
_project.Deflicker.WindowFrames, 2, "0", "fotogrammi",
|
||||
value => { _project.Deflicker.WindowFrames = (int)value; DeflickerChanged?.Invoke(this, EventArgs.Empty); }));
|
||||
|
||||
stack.Add(Slider("Intensità della correzione", 0, 1, _project.Deflicker.Strength, 0.05, "0.00", string.Empty,
|
||||
@@ -275,10 +278,10 @@ internal sealed class SettingsPanel : Panel
|
||||
PreviewInvalidated?.Invoke(this, EventArgs.Empty);
|
||||
}));
|
||||
|
||||
stack.Add(Slider("Innesco della compressione", 0.4, 0.98, _project.Deflicker.HighlightKnee, 0.02, "0.00", string.Empty,
|
||||
stack.Add(Slider("Innesco della compressione", 0.4, 0.98, _project.Deflicker.HighlightKnee, 0.02, "0.00",
|
||||
string.Empty,
|
||||
value => { _project.Deflicker.HighlightKnee = value; PreviewInvalidated?.Invoke(this, EventArgs.Empty); }));
|
||||
|
||||
// ---- Regioni
|
||||
stack.Add(new SectionHeader("Deflicker per regioni"));
|
||||
|
||||
stack.Add(Combo("Divisione del fotogramma",
|
||||
@@ -305,11 +308,9 @@ internal sealed class SettingsPanel : Panel
|
||||
value => { _project.Regions.SampleFrames = (int)value; AnalysisInvalidated?.Invoke(this, EventArgs.Empty); }));
|
||||
|
||||
stack.Add(Note("La maschera nasce dalla mediana temporale di un campione di fotogrammi, che " +
|
||||
"toglie di mezzo nuvole e passanti e lascia la struttura fissa della scena. " +
|
||||
"Serve a impedire che il transito di una nuvola densa sul cielo faccia " +
|
||||
"schiarire anche il paesaggio, che invece non è cambiato."));
|
||||
"toglie di mezzo nuvole e passanti. Impedisce che il transito di una nuvola sul " +
|
||||
"cielo faccia schiarire anche il paesaggio, che invece non è cambiato."));
|
||||
|
||||
// ---- Holy Grail
|
||||
stack.Add(new SectionHeader("Transizioni giorno-notte"));
|
||||
|
||||
stack.Add(Check("Ammorbidisci i cambi di impostazione", _project.HolyGrail.Enabled, value =>
|
||||
@@ -318,7 +319,8 @@ internal sealed class SettingsPanel : Panel
|
||||
DeflickerChanged?.Invoke(this, EventArgs.Empty);
|
||||
}));
|
||||
|
||||
stack.Add(Slider("Lunghezza della transizione", 4, 240, _project.HolyGrail.TransitionFrames, 2, "0", "fotogrammi",
|
||||
stack.Add(AutoSlider(AutoKey.TransitionFrames, "Lunghezza della transizione", 4, 240,
|
||||
_project.HolyGrail.TransitionFrames, 2, "0", "fotogrammi",
|
||||
value => { _project.HolyGrail.TransitionFrames = (int)value; DeflickerChanged?.Invoke(this, EventArgs.Empty); }));
|
||||
|
||||
stack.Add(Slider("Soglia di riconoscimento", 0.05, 1.0, _project.HolyGrail.StepThresholdStops, 0.05, "0.00", "EV",
|
||||
@@ -336,17 +338,16 @@ internal sealed class SettingsPanel : Panel
|
||||
DeflickerChanged?.Invoke(this, EventArgs.Empty);
|
||||
}));
|
||||
|
||||
stack.Add(Slider("Finestra della lisciatura cromatica", 5, 201, _project.HolyGrail.ColorWindowFrames, 2, "0", "fotogrammi",
|
||||
stack.Add(Slider("Finestra della lisciatura cromatica", 5, 201, _project.HolyGrail.ColorWindowFrames, 2, "0",
|
||||
"fotogrammi",
|
||||
value => { _project.HolyGrail.ColorWindowFrames = (int)value; DeflickerChanged?.Invoke(this, EventArgs.Empty); }));
|
||||
|
||||
stack.Add(Slider("Intensità cromatica", 0, 1, _project.HolyGrail.ColorStrength, 0.05, "0.00", string.Empty,
|
||||
value => { _project.HolyGrail.ColorStrength = value; DeflickerChanged?.Invoke(this, EventArgs.Empty); }));
|
||||
|
||||
stack.Add(Note("Il valore di esposizione si legge nei metadati, quindi l'ampiezza di ogni " +
|
||||
"salto è nota senza incertezza e viene ridistribuita su una transizione a " +
|
||||
"derivata nulla agli estremi. La lisciatura cromatica agisce sul rapporto fra " +
|
||||
"i canali: toglie il tremolio del bilanciamento automatico e lascia intatto " +
|
||||
"il viaggio verso il caldo del tramonto."));
|
||||
stack.Add(Note("Il valore di esposizione si legge nei metadati, quindi l'ampiezza di ogni salto " +
|
||||
"è nota senza incertezza. Un salto che la luminanza non ha recepito — fotogramma " +
|
||||
"già saturo — viene lasciato stare."));
|
||||
|
||||
return stack.Panel;
|
||||
}
|
||||
@@ -365,13 +366,15 @@ internal sealed class SettingsPanel : Panel
|
||||
AnalysisInvalidated?.Invoke(this, EventArgs.Empty);
|
||||
}));
|
||||
|
||||
stack.Add(Slider("Finestra del percorso", 5, 151, _project.Stabilization.SmoothingFrames, 2, "0", "fotogrammi",
|
||||
stack.Add(AutoSlider(AutoKey.StabilizationWindow, "Finestra del percorso", 5, 151,
|
||||
_project.Stabilization.SmoothingFrames, 2, "0", "fotogrammi",
|
||||
value => { _project.Stabilization.SmoothingFrames = (int)value; DeflickerChanged?.Invoke(this, EventArgs.Empty); }));
|
||||
|
||||
stack.Add(Slider("Intensità", 0, 1, _project.Stabilization.Strength, 0.05, "0.00", string.Empty,
|
||||
value => { _project.Stabilization.Strength = value; DeflickerChanged?.Invoke(this, EventArgs.Empty); }));
|
||||
|
||||
stack.Add(Slider("Correzione massima", 0.005, 0.20, _project.Stabilization.MaxCorrectionFraction, 0.005, "0.000", "×L",
|
||||
stack.Add(Slider("Correzione massima", 0.005, 0.20, _project.Stabilization.MaxCorrectionFraction, 0.005,
|
||||
"0.000", "×L",
|
||||
value => { _project.Stabilization.MaxCorrectionFraction = value; DeflickerChanged?.Invoke(this, EventArgs.Empty); }));
|
||||
|
||||
stack.Add(Check("Compensa anche la rotazione", _project.Stabilization.CompensateRotation, value =>
|
||||
@@ -383,7 +386,6 @@ internal sealed class SettingsPanel : Panel
|
||||
stack.Add(Slider("Lato dei riquadri di correlazione", 64, 512, _project.Stabilization.PatchSize, 64, "0", "px",
|
||||
value =>
|
||||
{
|
||||
// La trasformata vuole una potenza di due: il cursore si muove per gradini validi.
|
||||
_project.Stabilization.PatchSize = Fourier.FloorPowerOfTwo((int)value);
|
||||
AnalysisInvalidated?.Invoke(this, EventArgs.Empty);
|
||||
}));
|
||||
@@ -391,13 +393,6 @@ internal sealed class SettingsPanel : Panel
|
||||
stack.Add(Slider("Riquadri per lato", 1, 5, _project.Stabilization.Grid, 1, "0", string.Empty,
|
||||
value => { _project.Stabilization.Grid = (int)value; AnalysisInvalidated?.Invoke(this, EventArgs.Empty); }));
|
||||
|
||||
stack.Add(Note("Lo spostamento fra fotogrammi adiacenti si misura con la correlazione di " +
|
||||
"fase, che ignora le differenze di luminosità e reagisce alla sola geometria. " +
|
||||
"Il percorso ricostruito viene lisciato: quello che resta fra percorso vero e " +
|
||||
"percorso liscio è il tremolio, e la sua inversa è la correzione. Una " +
|
||||
"panoramica voluta sopravvive perché è già liscia."));
|
||||
|
||||
// ---- Virtual camera
|
||||
stack.Add(new SectionHeader("Movimento di macchina virtuale"));
|
||||
|
||||
stack.Add(Check("Panoramiche e zoom virtuali", _project.Camera.Enabled, value =>
|
||||
@@ -406,7 +401,7 @@ internal sealed class SettingsPanel : Panel
|
||||
AnalysisInvalidated?.Invoke(this, EventArgs.Empty);
|
||||
}));
|
||||
|
||||
_cameraEditor = new CameraEditor { Settings = _project.Camera, Height = 210 };
|
||||
_cameraEditor = new CameraEditor { Settings = _project.Camera, Height = 200 };
|
||||
_cameraEditor.Changed += (_, _) =>
|
||||
{
|
||||
SyncKeyframeControls();
|
||||
@@ -416,8 +411,8 @@ internal sealed class SettingsPanel : Panel
|
||||
stack.Add(_cameraEditor);
|
||||
|
||||
var keyframeButtons = new Panel { Height = 32, BackColor = Theme.Surface };
|
||||
var addButton = new DarkButton { Text = "Aggiungi nodo", Width = 130, Height = 28, Left = 0, Top = 0 };
|
||||
var removeButton = new DarkButton { Text = "Togli nodo", Width = 118, Height = 28, Left = 138, Top = 0 };
|
||||
var addButton = new DarkButton { Text = "Aggiungi nodo", Width = 128, Height = 28 };
|
||||
var removeButton = new DarkButton { Text = "Togli nodo", Width = 116, Height = 28, Left = 134 };
|
||||
addButton.Click += (_, _) => { _cameraEditor.AddKeyframe(); PreviewInvalidated?.Invoke(this, EventArgs.Empty); };
|
||||
removeButton.Click += (_, _) => { _cameraEditor.RemoveSelected(); PreviewInvalidated?.Invoke(this, EventArgs.Empty); };
|
||||
keyframeButtons.Controls.Add(addButton);
|
||||
@@ -447,13 +442,6 @@ internal sealed class SettingsPanel : Panel
|
||||
PreviewInvalidated?.Invoke(this, EventArgs.Empty);
|
||||
}));
|
||||
|
||||
stack.Add(Note("Le inquadrature si trascinano dentro il riquadro e si stringono dalla " +
|
||||
"maniglia d'angolo; la striscia in basso è la linea del tempo. Le tacche " +
|
||||
"lungo il percorso sono equispaziate nel tempo: dove si addensano il " +
|
||||
"movimento rallenta. Con il movimento attivo i fotogrammi vengono letti a " +
|
||||
"risoluzione nativa, perché il ritaglio deve avere pixel da cui attingere."));
|
||||
|
||||
// ---- Motion blur e campo vettoriale
|
||||
stack.Add(new SectionHeader("Motion blur sintetico"));
|
||||
|
||||
stack.Add(Check("Sfocatura di movimento attiva", _project.MotionBlur.Enabled, value =>
|
||||
@@ -474,9 +462,6 @@ internal sealed class SettingsPanel : Panel
|
||||
stack.Add(Slider("Campioni per pixel", 3, 65, _project.MotionBlur.MaxSamples, 2, "0", string.Empty,
|
||||
value => { _project.MotionBlur.MaxSamples = (int)value; PreviewInvalidated?.Invoke(this, EventArgs.Empty); }));
|
||||
|
||||
stack.Add(Note("La scia sintetizzata compensa in quadratura la sfocatura mancante: " +
|
||||
"√(obiettivo² − reale²). A 180° si ottiene la resa cinematografica."));
|
||||
|
||||
stack.Add(new SectionHeader("Campo vettoriale di movimento"));
|
||||
|
||||
stack.Add(Slider("Larghezza di analisi del movimento", 320, 1920, _project.Flow.AnalysisWidth, 32, "0", "px",
|
||||
@@ -506,7 +491,6 @@ internal sealed class SettingsPanel : Panel
|
||||
PreviewInvalidated?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>Riporta nei cursori i valori del nodo scelto, senza farli reagire.</summary>
|
||||
private void SyncKeyframeControls()
|
||||
{
|
||||
if (_cameraEditor?.Selected is not { } keyframe) return;
|
||||
@@ -522,7 +506,7 @@ internal sealed class SettingsPanel : Panel
|
||||
|
||||
// ------------------------------------------------------------------ Tempo
|
||||
|
||||
private Panel BuildTimePage()
|
||||
private Panel BuildTimingPage()
|
||||
{
|
||||
var stack = NewStack();
|
||||
|
||||
@@ -541,7 +525,6 @@ internal sealed class SettingsPanel : Panel
|
||||
stack.Add(Slider("Dilatazione massima", 1.5, 8, _project.Export.MaxAdaptiveStretch, 0.5, "0.0", "×",
|
||||
value => _project.Export.MaxAdaptiveStretch = value));
|
||||
|
||||
// ---- Time ramping
|
||||
stack.Add(new SectionHeader("Rimappatura non lineare"));
|
||||
|
||||
stack.Add(Check("Curva di velocità attiva", _project.TimeRamp.Enabled, value =>
|
||||
@@ -554,7 +537,7 @@ internal sealed class SettingsPanel : Panel
|
||||
{
|
||||
Minimum = 0.1,
|
||||
Maximum = 8.0,
|
||||
Height = 168,
|
||||
Height = 160,
|
||||
StartLabel = "primo scatto",
|
||||
EndLabel = "ultimo scatto",
|
||||
};
|
||||
@@ -567,13 +550,9 @@ internal sealed class SettingsPanel : Panel
|
||||
stack.Add(_rampEditor);
|
||||
|
||||
stack.Add(Note("Trascina i nodi, aggiungine uno con un doppio clic, toglilo con il tasto destro. " +
|
||||
"La curva dice quanti scatti vengono consumati per ogni fotogramma d'uscita: " +
|
||||
"sopra 1 la sequenza accelera saltando scatti, sotto 1 rallenta e i fotogrammi " +
|
||||
"mancanti vengono sintetizzati dal campo vettoriale. La velocità del filmato " +
|
||||
"resta fissa. La spline è monotona per costruzione, quindi il tempo non può " +
|
||||
"tornare indietro fra due nodi."));
|
||||
"La curva dice quanti scatti vengono consumati per ogni fotogramma d'uscita: sopra 1 " +
|
||||
"la sequenza accelera, sotto 1 rallenta e i fotogrammi mancanti vengono sintetizzati."));
|
||||
|
||||
// ---- Stacking
|
||||
stack.Add(new SectionHeader("Accumulo temporale"));
|
||||
|
||||
stack.Add(Combo("Modalità",
|
||||
@@ -599,11 +578,9 @@ internal sealed class SettingsPanel : Panel
|
||||
stack.Add(Slider("Lunghezza delle scie", 0, 400, _project.Stacking.TrailFrames, 5, "0", "fotogrammi",
|
||||
value => { _project.Stacking.TrailFrames = value; PreviewInvalidated?.Invoke(this, EventArgs.Empty); }));
|
||||
|
||||
stack.Add(Note("La mediana tiene il valore centrale della serie temporale di ogni pixel: la " +
|
||||
"scena stabile resta se stessa, chi attraversa l'inquadratura una volta sola " +
|
||||
"sparisce. Il massimo conserva il valore più alto incontrato e trasforma le " +
|
||||
"stelle in archi continui; a zero le scie non si spengono mai. La finestra " +
|
||||
"della mediana tiene occupata memoria: sono tutti fotogrammi vivi insieme."));
|
||||
stack.Add(Note("La mediana tiene il valore centrale della serie temporale di ogni pixel: la scena " +
|
||||
"stabile resta se stessa, chi attraversa l'inquadratura una volta sola sparisce. " +
|
||||
"Il massimo trasforma le stelle in archi; a zero le scie non si spengono mai."));
|
||||
|
||||
return stack.Panel;
|
||||
}
|
||||
@@ -635,7 +612,7 @@ internal sealed class SettingsPanel : Panel
|
||||
value => { _project.Export.FrameRate = value; PreviewInvalidated?.Invoke(this, EventArgs.Empty); }));
|
||||
|
||||
stack.Add(Slider("Bitrate medio", 5, 250, _project.Export.BitrateMbps, 5, "0", "Mb/s",
|
||||
value => _project.Export.BitrateMbps = value));
|
||||
value => { _project.Export.BitrateMbps = value; PreviewInvalidated?.Invoke(this, EventArgs.Empty); }));
|
||||
|
||||
stack.Add(Slider("Intervallo fra fotogrammi chiave", 1, 10, _project.Export.KeyframeIntervalSeconds, 1, "0", "s",
|
||||
value => _project.Export.KeyframeIntervalSeconds = (int)value));
|
||||
@@ -652,8 +629,113 @@ internal sealed class SettingsPanel : Panel
|
||||
stack.Add(browse);
|
||||
|
||||
stack.Add(Note("Il video viene scritto in un unico flusso continuo. L'unico altro file che " +
|
||||
"l'elaborazione può creare è il parcheggio temporaneo dei fotogrammi, che si " +
|
||||
"cancella da sé e non contiene nulla di riutilizzabile."));
|
||||
"l'elaborazione può creare è il parcheggio temporaneo, che si cancella da sé."));
|
||||
|
||||
return stack.Panel;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ Impostazioni dell'applicazione
|
||||
|
||||
private Panel BuildPreferencesPage()
|
||||
{
|
||||
var stack = NewStack();
|
||||
|
||||
stack.Add(new SectionHeader("All'apertura"));
|
||||
|
||||
stack.Add(Check("Ricorda le impostazioni per cartella", _app.RememberPerFolder, value =>
|
||||
{
|
||||
_app.RememberPerFolder = value;
|
||||
AppSettingsChanged?.Invoke(this, EventArgs.Empty);
|
||||
}));
|
||||
|
||||
stack.Add(Check("Analizza appena una sequenza è caricata", _app.AnalyzeOnLoad, value =>
|
||||
{
|
||||
_app.AnalyzeOnLoad = value;
|
||||
AppSettingsChanged?.Invoke(this, EventArgs.Empty);
|
||||
}));
|
||||
|
||||
stack.Add(new SectionHeader("Valori predefiniti dei nuovi progetti"));
|
||||
|
||||
stack.Add(Combo("Profilo di qualità", ["Bozza", "Standard", "Massima"], (int)_app.DefaultQuality,
|
||||
index =>
|
||||
{
|
||||
_app.DefaultQuality = (QualityProfile)Math.Clamp(index, 0, 2);
|
||||
AppSettingsChanged?.Invoke(this, EventArgs.Empty);
|
||||
}));
|
||||
|
||||
stack.Add(Slider("Decodifiche simultanee", 1, 16, _app.DefaultDecodeParallelism, 1, "0", "thread",
|
||||
value => { _app.DefaultDecodeParallelism = (int)value; AppSettingsChanged?.Invoke(this, EventArgs.Empty); }));
|
||||
|
||||
stack.Add(Slider("Tetto di memoria", 256, 32768, _app.DefaultMemoryBudgetMiB, 256, "0", "MiB",
|
||||
value => { _app.DefaultMemoryBudgetMiB = (int)value; AppSettingsChanged?.Invoke(this, EventArgs.Empty); }));
|
||||
|
||||
stack.Add(Check("Parcheggia su disco i fotogrammi in eccesso", _app.DefaultAllowDiskSpill, value =>
|
||||
{
|
||||
_app.DefaultAllowDiskSpill = value;
|
||||
AppSettingsChanged?.Invoke(this, EventArgs.Empty);
|
||||
}));
|
||||
|
||||
stack.Add(new SectionHeader("Durante l'esportazione"));
|
||||
|
||||
stack.Add(Check("Chiedi conferma prima di sovrascrivere", _app.ConfirmOverwrite, value =>
|
||||
{
|
||||
_app.ConfirmOverwrite = value;
|
||||
AppSettingsChanged?.Invoke(this, EventArgs.Empty);
|
||||
}));
|
||||
|
||||
stack.Add(Check("Mostra i fotogrammi mentre vengono codificati", _app.LivePreviewDuringExport, value =>
|
||||
{
|
||||
_app.LivePreviewDuringExport = value;
|
||||
AppSettingsChanged?.Invoke(this, EventArgs.Empty);
|
||||
}));
|
||||
|
||||
stack.Add(Slider("Aggiorna l'anteprima ogni", 1, 60, _app.LivePreviewEvery, 1, "0", "fotogrammi",
|
||||
value => { _app.LivePreviewEvery = (int)value; AppSettingsChanged?.Invoke(this, EventArgs.Empty); }));
|
||||
|
||||
stack.Add(Check("Apri la cartella a esportazione conclusa", _app.RevealWhenFinished, value =>
|
||||
{
|
||||
_app.RevealWhenFinished = value;
|
||||
AppSettingsChanged?.Invoke(this, EventArgs.Empty);
|
||||
}));
|
||||
|
||||
stack.Add(new SectionHeader("Riquadro sponsor"));
|
||||
|
||||
stack.Add(Check("Mostra gli sponsor durante l'attesa", _app.ShowSponsors, value =>
|
||||
{
|
||||
_app.ShowSponsors = value;
|
||||
AppSettingsChanged?.Invoke(this, EventArgs.Empty);
|
||||
}));
|
||||
|
||||
stack.Add(Slider("Cambia annuncio ogni", 5, 180, _app.SponsorRotationSeconds, 5, "0", "s",
|
||||
value => { _app.SponsorRotationSeconds = (int)value; AppSettingsChanged?.Invoke(this, EventArgs.Empty); }));
|
||||
|
||||
var openFolder = new DarkButton { Text = "Apri la cartella delle campagne", Height = 30, Dock = DockStyle.Top };
|
||||
openFolder.Click += (_, _) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
SponsorCatalogue.EnsureExample(_app.ResolvedSponsorFolder);
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(_app.ResolvedSponsorFolder)
|
||||
{
|
||||
UseShellExecute = true,
|
||||
});
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or System.ComponentModel.Win32Exception)
|
||||
{
|
||||
// Cartella non apribile: non c'è nulla di sensato da fare.
|
||||
}
|
||||
};
|
||||
stack.Add(openFolder);
|
||||
|
||||
stack.Add(Note("Gli annunci si leggono da una cartella locale: il programma non contatta alcun " +
|
||||
"servizio, non invia identificativi e non registra clic. Le campagne si " +
|
||||
"aggiornano copiando file dentro quella cartella, con un elenco chiamato " +
|
||||
"campagne.txt. Il riquadro compare soltanto nella scheda Esportazione, dove per " +
|
||||
"forza di cose si aspetta."));
|
||||
|
||||
stack.Add(new SectionHeader("Dove stanno i file"));
|
||||
stack.Add(Note($"Impostazioni: {AppSettings.SettingsPath}"));
|
||||
stack.Add(Note($"Campagne: {_app.ResolvedSponsorFolder}"));
|
||||
|
||||
return stack.Panel;
|
||||
}
|
||||
@@ -673,8 +755,6 @@ internal sealed class SettingsPanel : Panel
|
||||
control.Width = Panel.ClientSize.Width - 34;
|
||||
control.Anchor = AnchorStyles.Left | AnchorStyles.Top | AnchorStyles.Right;
|
||||
|
||||
// Le note esplicative variano in lunghezza: si misurano sulla larghezza reale
|
||||
// della colonna, altrimenti le più lunghe finirebbero tagliate a metà frase.
|
||||
if (control is Label note && !note.AutoSize) MeasureNote(note);
|
||||
|
||||
Panel.Controls.Add(control);
|
||||
@@ -690,14 +770,12 @@ internal sealed class SettingsPanel : Panel
|
||||
note.Height = measured.Height + 8;
|
||||
}
|
||||
|
||||
/// <summary>Rimisura una nota il cui testo è cambiato dopo la costruzione della pagina.</summary>
|
||||
private static void LayoutNote(Label note)
|
||||
{
|
||||
int before = note.Height;
|
||||
MeasureNote(note);
|
||||
if (note.Height == before || note.Parent is null) return;
|
||||
|
||||
// Le note sotto vanno fatte scorrere: la colonna è posizionata a coordinate assolute.
|
||||
int delta = note.Height - before;
|
||||
foreach (Control sibling in note.Parent.Controls)
|
||||
{
|
||||
@@ -707,13 +785,8 @@ internal sealed class SettingsPanel : Panel
|
||||
|
||||
private static Stack NewStack()
|
||||
{
|
||||
var panel = new Panel
|
||||
{
|
||||
BackColor = Theme.Surface,
|
||||
Padding = new Padding(0, 8, 0, 12),
|
||||
Width = 360,
|
||||
};
|
||||
var host = new Panel { BackColor = Theme.Surface, Dock = DockStyle.Fill, AutoScroll = true, Width = 360 };
|
||||
var panel = new Panel { BackColor = Theme.Surface, Padding = new Padding(0, 8, 0, 12), Width = 372 };
|
||||
var host = new Panel { BackColor = Theme.Surface, Dock = DockStyle.Fill, AutoScroll = true, Width = 372 };
|
||||
panel.Controls.Add(host);
|
||||
return new Stack(host);
|
||||
}
|
||||
@@ -735,6 +808,38 @@ internal sealed class SettingsPanel : Panel
|
||||
return slider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cursore che il direttore sa impostare da sé. L'indicatore «auto» dice chi comanda;
|
||||
/// toccare il cursore passa il comando all'utente, e il pulsante lo restituisce.
|
||||
/// </summary>
|
||||
private ParameterSlider AutoSlider(AutoKey key, string caption, double min, double max, double value,
|
||||
double step, string format, string unit, Action<double> onChange)
|
||||
{
|
||||
var slider = Slider(caption, min, max, value, step, format, unit, onChange);
|
||||
slider.AutoSupported = true;
|
||||
slider.IsAuto = !_project.ManualParameters.Contains(key);
|
||||
|
||||
slider.AutoChanged += (_, _) =>
|
||||
{
|
||||
if (slider.IsAuto)
|
||||
{
|
||||
_project.ManualParameters.Remove(key);
|
||||
if (_project.AutoDecisions.TryGetValue(key, out var decision))
|
||||
{
|
||||
slider.SetValueSilently(decision.Value);
|
||||
onChange(decision.Value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_project.ManualParameters.Add(key);
|
||||
}
|
||||
};
|
||||
|
||||
_autoSliders[key] = slider;
|
||||
return slider;
|
||||
}
|
||||
|
||||
private static DarkCheckBox Check(string caption, bool value, Action<bool> onChange)
|
||||
{
|
||||
var box = new DarkCheckBox { Text = caption, Checked = value };
|
||||
|
||||
Reference in New Issue
Block a user