@@ -13,7 +13,11 @@ mod param_init;
1313mod plot_utils;
1414mod points_text;
1515
16- use self :: formula:: { formula_svg_bytes, formula_svg_uri, model_formula_info} ;
16+ #[ cfg( target_arch = "wasm32" ) ]
17+ use self :: formula:: formula_plain_text;
18+ use self :: formula:: model_formula_info;
19+ #[ cfg( not( target_arch = "wasm32" ) ) ]
20+ use self :: formula:: { formula_svg_bytes, formula_svg_uri} ;
1721use self :: i18n:: {
1822 center_origin_icon_image, clear_icon_image, family_label, fit_icon_image,
1923 fit_to_content_icon_image, github_mark_image, language_flag_image, model_choice_label,
@@ -80,6 +84,37 @@ impl UiLanguage {
8084 Self :: Russian => "Русский" ,
8185 }
8286 }
87+
88+ fn from_locale_tag ( locale : & str ) -> Self {
89+ let language = locale
90+ . trim ( )
91+ . split ( [ '-' , '_' , '.' , '@' , ':' , ',' ] )
92+ . next ( )
93+ . unwrap_or_default ( ) ;
94+
95+ if language. eq_ignore_ascii_case ( "ru" ) {
96+ Self :: Russian
97+ } else {
98+ Self :: English
99+ }
100+ }
101+
102+ fn from_system_locale ( ) -> Self {
103+ system_locale_tag ( )
104+ . as_deref ( )
105+ . map ( Self :: from_locale_tag)
106+ . unwrap_or ( Self :: English )
107+ }
108+ }
109+
110+ #[ cfg( not( target_arch = "wasm32" ) ) ]
111+ fn system_locale_tag ( ) -> Option < String > {
112+ sys_locale:: get_locale ( )
113+ }
114+
115+ #[ cfg( target_arch = "wasm32" ) ]
116+ fn system_locale_tag ( ) -> Option < String > {
117+ web_sys:: window ( ) . and_then ( |window| window. navigator ( ) . language ( ) )
83118}
84119
85120#[ derive( Debug , Clone , Copy , PartialEq , Eq , Default ) ]
@@ -384,6 +419,7 @@ struct ModelFormulaInfo {
384419 notes : String ,
385420}
386421
422+ #[ cfg( not( target_arch = "wasm32" ) ) ]
387423#[ derive( Debug , Clone ) ]
388424struct FormulaSvgCache {
389425 formula : String ,
@@ -657,6 +693,7 @@ pub struct CurveFitApp {
657693 result_metrics : Option < ExtendedMetrics > ,
658694 residual_plot_points : Vec < PlotPoint > ,
659695 spline_plot_curve : Option < Vec < PlotPoint > > ,
696+ #[ cfg( not( target_arch = "wasm32" ) ) ]
660697 formula_svg_cache : Option < FormulaSvgCache > ,
661698 sampled_curve_cache : Option < SampledCurveCache > ,
662699 iteration_diagnostics : IterationDiagnostics ,
@@ -675,7 +712,10 @@ impl CurveFitApp {
675712 /// Создает приложение и настраивает загрузчики изображений для иконок/формул.
676713 pub fn new ( cc : & eframe:: CreationContext < ' _ > ) -> Self {
677714 egui_extras:: install_image_loaders ( & cc. egui_ctx ) ;
678- Self :: default ( )
715+ Self {
716+ ui_language : UiLanguage :: from_system_locale ( ) ,
717+ ..Self :: default ( )
718+ }
679719 }
680720
681721 fn resolved_model ( & self ) -> ResolvedModel {
@@ -841,6 +881,7 @@ impl CurveFitApp {
841881 self . refresh_status_after_points_edit ( ) ;
842882 }
843883
884+ #[ cfg( not( target_arch = "wasm32" ) ) ]
844885 fn cached_formula_svg ( & mut self , formula : & str , dark_mode : bool ) -> ( String , Arc < [ u8 ] > ) {
845886 if let Some ( cache) = & self . formula_svg_cache
846887 && cache. formula == formula
@@ -1111,10 +1152,56 @@ impl CurveFitApp {
11111152 }
11121153 }
11131154
1114- fn write_points_text ( & mut self , points : & [ Point ] ) {
1115- self . push_points_undo_snapshot ( self . points_text . clone ( ) ) ;
1116- self . apply_points_text_change ( points_to_text ( points) , false ) ;
1117- self . refresh_status_after_points_edit ( ) ;
1155+ fn set_points_cache_from_valid_points ( & mut self , points : & [ Point ] ) {
1156+ let parsed_points = points. to_vec ( ) ;
1157+ let plot_points = parsed_points
1158+ . iter ( )
1159+ . map ( |point| PlotPoint :: new ( point. x ( ) , point. y ( ) ) )
1160+ . collect ( ) ;
1161+ self . points_cache = Some ( ParsedPointsCache {
1162+ parsed_points : Ok ( parsed_points) ,
1163+ parse_error_line : None ,
1164+ plot_points,
1165+ } ) ;
1166+ self . points_cache_dirty = false ;
1167+ self . points_parse_debounce_deadline = None ;
1168+ }
1169+
1170+ fn clear_points_text ( & mut self , record_undo : bool ) {
1171+ if self . points_text . is_empty ( ) {
1172+ return ;
1173+ }
1174+ let previous = std:: mem:: take ( & mut self . points_text ) ;
1175+ if record_undo {
1176+ self . push_points_undo_snapshot ( previous) ;
1177+ }
1178+ self . points_redo_stack . clear ( ) ;
1179+ self . set_points_cache_from_valid_points ( & [ ] ) ;
1180+ if matches ! (
1181+ self . status. as_ref( ) ,
1182+ Some ( StatusMessage :: Error ( message) ) if message. starts_with( POINTS_PARSE_ERROR_PREFIX )
1183+ ) {
1184+ self . status = Some ( self . idle_status_after_points_edit ( ) ) ;
1185+ }
1186+ }
1187+
1188+ fn write_points_text ( & mut self , points : & [ Point ] , record_undo : bool ) {
1189+ let new_text = points_to_text ( points) ;
1190+ if self . points_text == new_text {
1191+ return ;
1192+ }
1193+ if record_undo {
1194+ self . push_points_undo_snapshot ( self . points_text . clone ( ) ) ;
1195+ }
1196+ self . points_text = new_text;
1197+ self . points_redo_stack . clear ( ) ;
1198+ self . set_points_cache_from_valid_points ( points) ;
1199+ if matches ! (
1200+ self . status. as_ref( ) ,
1201+ Some ( StatusMessage :: Error ( message) ) if message. starts_with( POINTS_PARSE_ERROR_PREFIX )
1202+ ) {
1203+ self . status = Some ( self . idle_status_after_points_edit ( ) ) ;
1204+ }
11181205 }
11191206
11201207 fn clear_fit_preview ( & mut self ) {
@@ -1812,7 +1899,7 @@ impl CurveFitApp {
18121899 match Point :: try_new ( x, y) {
18131900 Ok ( point) => {
18141901 points. push ( point) ;
1815- self . write_points_text ( & points) ;
1902+ self . write_points_text ( & points, true ) ;
18161903 }
18171904 Err ( error) => {
18181905 self . status = Some ( StatusMessage :: Error ( error. to_string ( ) ) ) ;
@@ -1844,7 +1931,7 @@ impl CurveFitApp {
18441931 }
18451932 }
18461933
1847- self . write_points_text ( & points) ;
1934+ self . write_points_text ( & points, false ) ;
18481935 }
18491936
18501937 fn erase_points_from_plot (
@@ -1872,7 +1959,7 @@ impl CurveFitApp {
18721959 dx * dx + dy * dy > 1.0
18731960 } ) ;
18741961
1875- self . write_points_text ( & points) ;
1962+ self . write_points_text ( & points, false ) ;
18761963 }
18771964
18781965 fn plot_position_from_screen (
@@ -1893,8 +1980,12 @@ impl CurveFitApp {
18931980 }
18941981
18951982 let response = & plot_response. response ;
1983+ let is_continuous_tool = matches ! ( self . plot_tool, PlotTool :: Spray | PlotTool :: Eraser ) ;
18961984 let primary_down_on_plot = response. is_pointer_button_down_on ( ) ;
1897- if matches ! ( self . plot_tool, PlotTool :: Spray | PlotTool :: Eraser ) && primary_down_on_plot {
1985+ if is_continuous_tool && primary_down_on_plot {
1986+ if self . active_tool_bounds . is_none ( ) {
1987+ self . push_points_undo_snapshot ( self . points_text . clone ( ) ) ;
1988+ }
18981989 self . active_tool_bounds
18991990 . get_or_insert ( * plot_response. transform . bounds ( ) ) ;
19001991 } else {
@@ -2180,8 +2271,7 @@ impl CurveFitApp {
21802271 )
21812272 . clicked ( )
21822273 {
2183- self . push_points_undo_snapshot ( self . points_text . clone ( ) ) ;
2184- self . apply_points_text_change ( String :: new ( ) , false ) ;
2274+ self . clear_points_text ( true ) ;
21852275 self . clear_fit_outputs ( ) ;
21862276 self . status = Some ( StatusMessage :: Cleared ) ;
21872277 }
@@ -2335,14 +2425,23 @@ impl CurveFitApp {
23352425 ui. add_space ( 6.0 ) ;
23362426 ui. group ( |ui| {
23372427 ui. label ( egui:: RichText :: new ( tr ( language, "Model Formula" , "Формула модели" ) ) . strong ( ) ) ;
2338- let dark_mode = ui. visuals ( ) . dark_mode ;
2339- let ( svg_uri, svg_bytes) =
2340- self . cached_formula_svg ( & formula_info. full_formula , dark_mode) ;
2341- ui. add (
2342- egui:: Image :: from_bytes ( svg_uri, svg_bytes)
2343- . max_width ( ui. available_width ( ) )
2344- . fit_to_original_size ( 1.0 ) ,
2345- ) ;
2428+ #[ cfg( not( target_arch = "wasm32" ) ) ]
2429+ {
2430+ let dark_mode = ui. visuals ( ) . dark_mode ;
2431+ let ( svg_uri, svg_bytes) =
2432+ self . cached_formula_svg ( & formula_info. full_formula , dark_mode) ;
2433+ ui. add (
2434+ egui:: Image :: from_bytes ( svg_uri, svg_bytes)
2435+ . max_width ( ui. available_width ( ) )
2436+ . fit_to_original_size ( 1.0 ) ,
2437+ ) ;
2438+ }
2439+ #[ cfg( target_arch = "wasm32" ) ]
2440+ {
2441+ let plain_formula = formula_plain_text ( & formula_info. full_formula ) ;
2442+ let formula_label = egui:: RichText :: new ( plain_formula) . monospace ( ) ;
2443+ ui. label ( formula_label) ;
2444+ }
23462445 ui. label ( egui:: RichText :: new ( formula_info. notes ) . small ( ) ) ;
23472446 } ) ;
23482447
@@ -3077,7 +3176,7 @@ impl Default for CurveFitApp {
30773176 lbfgs_preset : LbfgsPreset :: infer_from_config ( & default_lbfgs) ,
30783177 ui_language : UiLanguage :: English ,
30793178 plot_tool : PlotTool :: SinglePoint ,
3080- spray_density : 8 ,
3179+ spray_density : 5 ,
30813180 spray_radius_rel : 0.02 ,
30823181 spray_brush : SprayBrush :: Uniform ,
30833182 eraser_radius_rel : 0.03 ,
@@ -3108,6 +3207,7 @@ impl Default for CurveFitApp {
31083207 result_metrics : None ,
31093208 residual_plot_points : Vec :: new ( ) ,
31103209 spline_plot_curve : None ,
3210+ #[ cfg( not( target_arch = "wasm32" ) ) ]
31113211 formula_svg_cache : None ,
31123212 sampled_curve_cache : None ,
31133213 iteration_diagnostics : IterationDiagnostics :: default ( ) ,
@@ -3233,7 +3333,7 @@ fn diagnostics_plot_y_axis_width(plot_response: &PlotResponse<()>) -> f32 {
32333333#[ cfg( test) ]
32343334mod tests {
32353335 use super :: {
3236- CurveFitApp , IterationDiagnostics , ModelChoice , ParamInitMethod , StatusMessage ,
3336+ CurveFitApp , IterationDiagnostics , ModelChoice , ParamInitMethod , StatusMessage , UiLanguage ,
32373337 data_based_params_for_family,
32383338 } ;
32393339 use crate :: domain:: { CurveFamily , CurveParams , FitResult , Point , Points } ;
@@ -3267,6 +3367,30 @@ mod tests {
32673367 ) ;
32683368 }
32693369
3370+ #[ test]
3371+ fn ui_language_from_locale_tag_uses_russian_for_ru_tags ( ) {
3372+ assert_eq ! ( UiLanguage :: from_locale_tag( "ru" ) , UiLanguage :: Russian ) ;
3373+ assert_eq ! ( UiLanguage :: from_locale_tag( "ru-RU" ) , UiLanguage :: Russian ) ;
3374+ assert_eq ! (
3375+ UiLanguage :: from_locale_tag( "ru_RU.UTF-8" ) ,
3376+ UiLanguage :: Russian
3377+ ) ;
3378+ assert_eq ! (
3379+ UiLanguage :: from_locale_tag( "ru-RU,en-US;q=0.9" ) ,
3380+ UiLanguage :: Russian
3381+ ) ;
3382+ }
3383+
3384+ #[ test]
3385+ fn ui_language_from_locale_tag_uses_english_for_other_tags ( ) {
3386+ assert_eq ! ( UiLanguage :: from_locale_tag( "en-US" ) , UiLanguage :: English ) ;
3387+ assert_eq ! (
3388+ UiLanguage :: from_locale_tag( "de_DE.UTF-8" ) ,
3389+ UiLanguage :: English
3390+ ) ;
3391+ assert_eq ! ( UiLanguage :: from_locale_tag( "" ) , UiLanguage :: English ) ;
3392+ }
3393+
32703394 #[ test]
32713395 fn diagnostics_initialize_stores_iteration_zero_state ( ) {
32723396 let points = line_points ( ) ;
0 commit comments