Skip to content

Commit b660a5c

Browse files
committed
major refactoring
1 parent 6c03f9d commit b660a5c

66 files changed

Lines changed: 10331 additions & 10285 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/app.rs

Lines changed: 27 additions & 1604 deletions
Large diffs are not rendered by default.

src/app/bootstrap.rs

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
//! Создание `CurveFitApp` и инициализация значений по умолчанию для UI и рантайма.
2+
3+
use super::*;
4+
5+
impl CurveFitApp {
6+
/// Создает приложение и настраивает загрузчики изображений для иконок/формул.
7+
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
8+
egui_extras::install_image_loaders(&cc.egui_ctx);
9+
Self {
10+
ui_language: UiLanguage::from_system_locale(),
11+
..Self::default()
12+
}
13+
}
14+
}
15+
16+
impl Default for CurveFitApp {
17+
fn default() -> Self {
18+
let selected_model = ModelChoice::Polynomial;
19+
let polynomial_degree = 1;
20+
let selected_family = polynomial_family(polynomial_degree);
21+
let default_lbfgs = LbfgsConfig::default();
22+
let default_nelder_mead = NelderMeadConfig::default();
23+
let default_steepest_descent = SteepestDescentConfig::default();
24+
let default_newton_cg = NewtonCgConfig::default();
25+
let default_sgd = SgdConfig::default();
26+
let default_adam = AdamConfig::default();
27+
28+
Self {
29+
points: PointsEditorState::default(),
30+
#[cfg(not(target_arch = "wasm32"))]
31+
points_file_import_dialog: FileDialog::new()
32+
.title("Import points from file")
33+
.add_file_filter_extensions("Point files", vec!["csv", "CSV", "xlsx", "XLSX"])
34+
.add_file_filter_extensions("CSV files", vec!["csv", "CSV"])
35+
.add_file_filter_extensions("Excel files", vec!["xlsx", "XLSX"])
36+
.default_file_filter("Point files"),
37+
#[cfg(not(target_arch = "wasm32"))]
38+
points_file_import_last_directory: None,
39+
#[cfg(not(target_arch = "wasm32"))]
40+
clipboard_import_request_pending: false,
41+
#[cfg(not(target_arch = "wasm32"))]
42+
clipboard_import_requested_at: None,
43+
#[cfg(target_arch = "wasm32")]
44+
clipboard_import_web_in_flight: false,
45+
#[cfg(target_arch = "wasm32")]
46+
clipboard_import_web_result: Rc::new(RefCell::new(None)),
47+
#[cfg(target_arch = "wasm32")]
48+
clipboard_copy_web_in_flight: false,
49+
#[cfg(target_arch = "wasm32")]
50+
clipboard_copy_web_result: Rc::new(RefCell::new(None)),
51+
selected_model,
52+
polynomial_degree,
53+
parameter_inputs: params_to_input_strings(&selected_family.default_params()),
54+
optimizer_method: OptimizerMethod::Lbfgs,
55+
optimizer_mode: OptimizerUiMode::Basic,
56+
optimization_loss_metric: OptimizationLossMetric::default(),
57+
metric_quantization_enabled: false,
58+
metric_quantization_decimal_places: DEFAULT_METRIC_QUANTIZATION_DECIMAL_PLACES,
59+
normalize_parametric_data: false,
60+
lbfgs_inputs: LbfgsInputState::from_config(&default_lbfgs),
61+
lbfgs_preset: infer_lbfgs_preset(&default_lbfgs),
62+
nelder_mead_inputs: NelderMeadInputState::from_config(&default_nelder_mead),
63+
nelder_mead_preset: infer_nelder_mead_preset(&default_nelder_mead),
64+
steepest_descent_inputs: SteepestDescentInputState::from_config(
65+
&default_steepest_descent,
66+
),
67+
steepest_descent_preset: infer_steepest_descent_preset(&default_steepest_descent),
68+
newton_cg_inputs: NewtonCgInputState::from_config(&default_newton_cg),
69+
newton_cg_preset: infer_newton_cg_preset(&default_newton_cg),
70+
sgd_inputs: SgdInputState::from_config(&default_sgd),
71+
sgd_preset: infer_sgd_preset(&default_sgd),
72+
adam_inputs: AdamInputState::from_config(&default_adam),
73+
adam_preset: infer_adam_preset(&default_adam),
74+
ui_language: UiLanguage::English,
75+
plot_tool: PlotTool::SinglePoint,
76+
spray_points_per_second: 140,
77+
spray_radius_rel: 0.02,
78+
spray_brush: SprayBrush::Uniform,
79+
eraser_radius_rel: 0.03,
80+
spray_seed: 0xDEADBEEFCAFEBABE,
81+
spray_points_budget: 0.0,
82+
spray_last_emit_at: None,
83+
fit_to_content_requested: false,
84+
center_origin_requested: false,
85+
origin_bottom_left_requested: true,
86+
last_plot_bounds: None,
87+
active_tool_bounds: None,
88+
panel: PanelState::default(),
89+
spline_knots: crate::fit::DEFAULT_SPLINE_KNOTS,
90+
spline_knot_strategy: SplineKnotStrategy::default(),
91+
spline_extrapolation: SplineExtrapolation::default(),
92+
spline_duplicate_x_policy: SplineDuplicateXPolicy::default(),
93+
spline_initial_knot_y_inputs: Vec::new(),
94+
auto_refit_enabled: false,
95+
auto_refit_pending_rerun: false,
96+
last_right_panel_fit_snapshot: None,
97+
replay: ReplayState::default(),
98+
fit_in_progress: false,
99+
fit_loss_metric: OptimizationLossMetric::default(),
100+
fit_metric_quantization: MetricQuantization::Disabled,
101+
fit_optimizer_method: OptimizerMethod::default(),
102+
fit_export_record: None,
103+
#[cfg(not(target_arch = "wasm32"))]
104+
fit_export_file_dialog: FileDialog::new()
105+
.title("Save fit result JSON")
106+
.add_save_extension("JSON files", "json")
107+
.default_save_extension("JSON files")
108+
.default_file_name("fit-result.json"),
109+
#[cfg(not(target_arch = "wasm32"))]
110+
fit_export_last_directory: None,
111+
#[cfg(not(target_arch = "wasm32"))]
112+
fit_export_pending_json: None,
113+
fit_preview_params: None,
114+
fit_preview_iteration: None,
115+
fit_started_at: None,
116+
last_fit_duration: None,
117+
fit_result: None,
118+
spline_result: None,
119+
active_fit_points: None,
120+
fit_run_ui_seed: None,
121+
result_metrics: None,
122+
residual_plot_points: Vec::new(),
123+
spline_plot_curve: None,
124+
#[cfg(not(target_arch = "wasm32"))]
125+
formula_svg_cache: None,
126+
sampled_curve_cache: None,
127+
iteration_diagnostics: IterationDiagnostics::default(),
128+
status: Some(StatusMessage::Ready),
129+
#[cfg(not(target_arch = "wasm32"))]
130+
fit_worker_rx: None,
131+
#[cfg(not(target_arch = "wasm32"))]
132+
fit_cancel_flag: None,
133+
#[cfg(not(target_arch = "wasm32"))]
134+
discard_fit_worker_updates: false,
135+
#[cfg(target_arch = "wasm32")]
136+
wasm_fit_job: None,
137+
}
138+
}
139+
}

src/app/clipboard_import.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
//! Импорт точек из буфера обмена для native- и wasm-режимов.
2+
13
use super::*;
24

35
impl CurveFitApp {

src/app/diagnostics.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
//! Накопление рядов метрик и параметров для графиков итерационной диагностики.
2+
13
use super::*;
24

35
#[derive(Debug, Clone, Default)]

src/app/file_import.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
//! Импорт точек из CSV/XLSX и интеграция с файловым диалогом пользовательского интерфейса.
2+
13
#[cfg(not(target_arch = "wasm32"))]
24
use super::points_text::parse_point_from_clipboard_like_fragments;
35
use super::*;

src/app/fit_worker.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
//! Координация фонового и wasm-раннера фитинга и перенос результатов обратно в UI.
2+
13
use super::replay::{ReplayFrame, ReplayFramePayload, upsert_replay_frame_in};
24
use super::*;
35

46
#[cfg(not(target_arch = "wasm32"))]
7+
/// Полный снимок входных данных, передаваемых в фоновый поток параметрического фитинга.
58
struct ParametricFitWorkerInput {
69
family: CurveFamily,
710
optimization_points: Points,
@@ -195,6 +198,7 @@ impl CurveFitApp {
195198
let mut replay_frames = Vec::with_capacity(trace.len().saturating_add(2));
196199
if let (Some(points), Some(initial_params)) = (fit_points.as_ref(), initial_params.as_ref())
197200
{
201+
// Для диагностики и replay сохраняем начальное состояние как итерацию 0.
198202
diagnostics.initialize(
199203
points,
200204
initial_params,
@@ -212,6 +216,7 @@ impl CurveFitApp {
212216
let mut result_metrics = None;
213217
let mut residual_plot_points = Vec::new();
214218
if let Some(points) = fit_points.as_ref() {
219+
// Финальные метрики пересчитываем по тем точкам и параметрам, которые видит пользователь.
215220
let (metrics, snapshot_metrics, snapshot_residuals) =
216221
self.parametric_metrics_and_residuals(points, &result.params);
217222
diagnostics.append(result.iterations, metrics, &result.params);
@@ -247,6 +252,7 @@ impl CurveFitApp {
247252
let mut diagnostics = IterationDiagnostics::default();
248253
let mut replay_frames = Vec::with_capacity(trace.len().saturating_add(2));
249254
if let Some(initial_curve) = initial_curve {
255+
// Начальная кривая нужна, чтобы replay показывал эволюцию сплайна с нулевого шага.
250256
Self::upsert_buffered_spline_replay_frame(&mut replay_frames, 0, initial_curve);
251257
}
252258
Self::apply_spline_trace_to_buffers(trace, &mut diagnostics, &mut replay_frames);
@@ -787,9 +793,13 @@ impl CurveFitApp {
787793

788794
let selected_model = self.resolved_model();
789795
if let Some(spline_family) = selected_model.spline_family() {
790-
let spline_config = self
791-
.spline_config_for_model(selected_model, points.len())
792-
.expect("spline model must provide spline config");
796+
let Some(spline_config) = self.spline_config_for_model(selected_model, points.len())
797+
else {
798+
self.status = Some(StatusMessage::Error(
799+
"Selected spline model has no spline configuration".to_string(),
800+
));
801+
return;
802+
};
793803
self.sync_spline_initial_knot_y_inputs(spline_config.knots);
794804
let initial_knot_y = match self.parse_spline_initial_knot_y(spline_config.knots) {
795805
Ok(values) => values,

src/app/formula.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
//! Формулы моделей и пояснения, показываемые в карточке и отдельном окне формулы.
2+
13
#[cfg(not(target_arch = "wasm32"))]
24
use std::hash::{DefaultHasher, Hash, Hasher};
35

src/app/i18n.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ macro_rules! tabler_icon {
1515
};
1616
}
1717

18-
/// Небольшой helper для выбора строки по текущему языку интерфейса.
18+
/// Небольшая утилита для выбора строки по текущему языку интерфейса.
1919
pub(super) fn tr(language: UiLanguage, en: &'static str, ru: &'static str) -> &'static str {
2020
match language {
2121
UiLanguage::English => en,

src/app/input_parse.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
//! Парсинг пользовательских строк в типизированные начальные параметры и узлы сплайна.
2+
13
use super::*;
24

5+
/// Уже распарсенные и типизированные начальные параметры параметрической модели.
36
#[derive(Debug, Clone)]
47
pub(super) struct ParsedInitialParams(CurveParams);
58

@@ -29,6 +32,7 @@ impl ParsedInitialParams {
2932
}
3033
}
3134

35+
/// Провалидированные начальные значения `y` для внутренних узлов сплайна.
3236
#[derive(Debug, Clone)]
3337
pub(super) struct ParsedSplineInitialKnotY {
3438
values: Vec<f64>,

src/app/layout.rs

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
//! Главный `eframe`-цикл и раскладка верхней, боковых и нижней панелей приложения.
2+
3+
use super::*;
4+
impl eframe::App for CurveFitApp {
5+
fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
6+
Self::apply_visual_style(ctx);
7+
self.poll_fit_worker(ctx);
8+
self.maybe_run_pending_auto_refit();
9+
self.tick_replay(ctx);
10+
self.poll_points_clipboard_import(ctx);
11+
self.poll_clipboard_copy(ctx);
12+
#[cfg(not(target_arch = "wasm32"))]
13+
self.poll_points_file_import_dialog(ctx);
14+
#[cfg(not(target_arch = "wasm32"))]
15+
self.poll_fit_export_save_dialog(ctx);
16+
self.maybe_refresh_points_cache_after_debounce();
17+
18+
if !self.fit_in_progress {
19+
let undo_requested = ctx.input(|input| {
20+
input.modifiers.command && !input.modifiers.shift && input.key_pressed(egui::Key::Z)
21+
});
22+
let redo_requested = ctx.input(|input| {
23+
input.modifiers.command
24+
&& (input.key_pressed(egui::Key::Y)
25+
|| (input.modifiers.shift && input.key_pressed(egui::Key::Z)))
26+
});
27+
if undo_requested {
28+
self.undo_points_edit();
29+
} else if redo_requested {
30+
self.redo_points_edit();
31+
}
32+
}
33+
}
34+
35+
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
36+
let ctx = ui.ctx().clone();
37+
let panel_style = ctx.global_style();
38+
let panel_style = panel_style.as_ref();
39+
40+
egui::Panel::top("header_panel")
41+
.frame(Self::top_bottom_panel_frame(panel_style))
42+
.show_inside(ui, |ui| {
43+
self.ui_header(ui);
44+
});
45+
46+
egui::Panel::bottom("status_bar")
47+
.frame(Self::top_bottom_panel_frame(panel_style))
48+
.show_inside(ui, |ui| {
49+
self.ui_status_bar(ui);
50+
});
51+
52+
if self.panel.show_left {
53+
egui::Panel::left("points_panel")
54+
.default_size(LEFT_PANEL_DEFAULT_WIDTH)
55+
.min_size(LEFT_PANEL_MIN_WIDTH)
56+
.resizable(true)
57+
.frame(Self::side_panel_frame(panel_style))
58+
.show_inside(ui, |ui| {
59+
let language = self.ui_language;
60+
ui.spacing_mut().item_spacing = egui::vec2(10.0, 8.0);
61+
ui.set_width(ui.available_width());
62+
Self::panel_card_collapsible(
63+
ui,
64+
"left_section_tools",
65+
tr(language, "Tools", "Инструменты"),
66+
|ui| {
67+
self.ui_tools(ui);
68+
},
69+
);
70+
Self::panel_card_collapsible(
71+
ui,
72+
"left_section_points",
73+
tr(language, "Input Points", "Точки"),
74+
|ui| {
75+
self.ui_points_editor(ui);
76+
},
77+
);
78+
});
79+
}
80+
81+
if self.panel.show_right {
82+
egui::Panel::right("settings_panel")
83+
.default_size(RIGHT_PANEL_DEFAULT_WIDTH)
84+
.min_size(RIGHT_PANEL_MIN_WIDTH)
85+
.resizable(true)
86+
.frame(Self::side_panel_frame(panel_style))
87+
.show_inside(ui, |ui| {
88+
let language = self.ui_language;
89+
ui.spacing_mut().item_spacing = egui::vec2(10.0, 8.0);
90+
ui.set_width(ui.available_width());
91+
Self::panel_card_collapsible_with_collapsed_trailing(
92+
self,
93+
ui,
94+
"right_section_model",
95+
tr(language, "Model", "Модель"),
96+
CurveFitApp::ui_family_and_params,
97+
CurveFitApp::ui_model_selector_compact,
98+
);
99+
Self::panel_card_collapsible_with_collapsed_trailing(
100+
self,
101+
ui,
102+
"right_section_metric",
103+
tr(language, "Optimization metric", "Метрика оптимизации"),
104+
CurveFitApp::ui_optimization_metric,
105+
CurveFitApp::ui_optimization_metric_selector_compact,
106+
);
107+
Self::panel_card_collapsible_with_collapsed_trailing(
108+
self,
109+
ui,
110+
"right_section_optimizer",
111+
tr(language, "Optimizer", "Оптимизатор"),
112+
CurveFitApp::ui_optimizer,
113+
CurveFitApp::ui_optimizer_action_button_compact,
114+
);
115+
Self::panel_card_collapsible(
116+
ui,
117+
"right_section_result",
118+
tr(language, "Result", "Результат"),
119+
|ui| {
120+
self.ui_result(ui);
121+
},
122+
);
123+
});
124+
self.track_right_panel_fit_changes_and_maybe_refit();
125+
}
126+
127+
self.ui_formula_window(&ctx);
128+
129+
if self.panel.show_diagnostics {
130+
egui::Panel::bottom("diagnostics_panel")
131+
.resizable(true)
132+
.default_size(DIAGNOSTICS_PANEL_DEFAULT_HEIGHT)
133+
.min_size(DIAGNOSTICS_PANEL_MIN_HEIGHT)
134+
.frame(Self::top_bottom_panel_frame(panel_style))
135+
.show_inside(ui, |ui| {
136+
let available_height = ui.available_height();
137+
ui.set_height(available_height);
138+
self.ui_iteration_diagnostics(ui);
139+
});
140+
}
141+
142+
egui::CentralPanel::default().show_inside(ui, |ui| {
143+
self.ui_plot(ui, ui.available_height().max(2.0));
144+
});
145+
}
146+
}

0 commit comments

Comments
 (0)