@@ -15,7 +15,7 @@ fn model_formula_full(model: ModelChoice, polynomial_degree: usize) -> String {
1515 r"y = a_{1}·exp(-k_{1}·x) + a_{2}·exp(-k_{2}·x) + c" . to_string ( )
1616 }
1717 ModelChoice :: DampedSinusoid => r"y = A·exp(-k·x)·sin(\omega·x + \phi) + c" . to_string ( ) ,
18- ModelChoice :: Lorentzian => r"y = C + \frac{A}{1 + (\frac{x - x_0}{gamma})^{2}}" . to_string ( ) ,
18+ ModelChoice :: Lorentzian => r"y = C + \frac{A}{1 + (\frac{x - x_0}{\ gamma})^{2}}" . to_string ( ) ,
1919 ModelChoice :: NaturalLog => r"y = A·ln(\frac{x}{B})" . to_string ( ) ,
2020 ModelChoice :: FourPl => r"y = d + \frac{a - d}{1 + (\frac{x}{c})^{b}}" . to_string ( ) ,
2121 ModelChoice :: FivePl => r"y = d + \frac{a - d}{(1 + (\frac{x}{c})^{b})^{m}}" . to_string ( ) ,
@@ -31,6 +31,19 @@ fn model_formula_full(model: ModelChoice, polynomial_degree: usize) -> String {
3131 ModelChoice :: Softplus => r"y = a·ln(1 + exp(b·(x - c))) + d" . to_string ( ) ,
3232 ModelChoice :: Power => r"y = a·x^{b}" . to_string ( ) ,
3333 ModelChoice :: Gaussian => r"y = a·exp(-\frac{(x - b)^{2}}{2·c^{2}})" . to_string ( ) ,
34+ ModelChoice :: Rational11 => r"y = d + \frac{a·x + b}{1 + c·x}" . to_string ( ) ,
35+ ModelChoice :: Rational22 => {
36+ r"y = \frac{a·x^{2} + b·x + c}{1 + d·x + e·x^{2}}" . to_string ( )
37+ }
38+ ModelChoice :: Emg => {
39+ r"y = c + \frac{a}{2·\tau}·exp(\frac{\sigma^{2}}{2·\tau^{2}} - \frac{x - \mu}{\tau})·erfc(\frac{1}{\sqrt{2}}(\frac{\sigma}{|\tau|} - \frac{x - \mu}{\sigma}))" . to_string ( )
40+ }
41+ ModelChoice :: PseudoVoigt => {
42+ r"y = c + a·(\eta·G(x; x_0, \sigma) + (1-\eta)·L(x; x_0, \gamma)), \eta = sigmoid(\eta_{raw})
43+ G(x; x_0, \sigma) = exp(-\frac{(x - x_0)^{2}}{2·\sigma^{2}})
44+ L(x; x_0, \gamma) = \frac{1}{1 + (\frac{x - x_0}{\gamma})^{2}}"
45+ . to_string ( )
46+ }
3447 ModelChoice :: LinearSpline => {
3548 r"y(x) = y_{i} + \frac{y_{i+1} - y_{i}}{x_{i+1} - x_{i}}·(x - x_{i})" . to_string ( )
3649 }
@@ -127,6 +140,18 @@ fn model_ml_note(language: UiLanguage, model: ModelChoice) -> &'static str {
127140 ( UiLanguage :: English , ModelChoice :: ExponentialLinear ) => {
128141 "Exponential trend with linear drift background."
129142 }
143+ ( UiLanguage :: English , ModelChoice :: Rational11 ) => {
144+ "Compact rational model with one linear pole term."
145+ }
146+ ( UiLanguage :: English , ModelChoice :: Rational22 ) => {
147+ "Flexible rational model with quadratic numerator/denominator."
148+ }
149+ ( UiLanguage :: English , ModelChoice :: Emg ) => {
150+ "Asymmetric peak (EMG); signed tau controls left/right tail."
151+ }
152+ ( UiLanguage :: English , ModelChoice :: PseudoVoigt ) => {
153+ "Mixture of Gaussian and Lorentzian peaks with learnable blend."
154+ }
130155 ( UiLanguage :: English , ModelChoice :: HyperbolicTangent ) => {
131156 "Smooth S-curve transition with bounded tails."
132157 }
@@ -179,6 +204,18 @@ fn model_ml_note(language: UiLanguage, model: ModelChoice) -> &'static str {
179204 ( UiLanguage :: Russian , ModelChoice :: ExponentialLinear ) => {
180205 "Экспоненциальный тренд с линейным дрейфом фона."
181206 }
207+ ( UiLanguage :: Russian , ModelChoice :: Rational11 ) => {
208+ "Компактная рациональная модель с линейным полюсом."
209+ }
210+ ( UiLanguage :: Russian , ModelChoice :: Rational22 ) => {
211+ "Гибкая рациональная модель с квадратами в числителе и знаменателе."
212+ }
213+ ( UiLanguage :: Russian , ModelChoice :: Emg ) => {
214+ "Асимметричный пик EMG; знак tau задаёт левый или правый хвост."
215+ }
216+ ( UiLanguage :: Russian , ModelChoice :: PseudoVoigt ) => {
217+ "Смесь гауссового и лоренцевого пиков с обучаемой долей."
218+ }
182219 ( UiLanguage :: Russian , ModelChoice :: HyperbolicTangent ) => {
183220 "Гладкий S-переход с ограниченными хвостами."
184221 }
@@ -240,23 +277,44 @@ pub(super) fn formula_plain_text(formula: &str) -> String {
240277/// чтобы не тянуть тяжёлые зависимости ради ограниченного подмножества LaTeX.
241278#[ cfg( not( target_arch = "wasm32" ) ) ]
242279pub ( super ) fn formula_svg_bytes ( formula : & str , dark_mode : bool ) -> Vec < u8 > {
243- let spans = parse_formula_spans ( formula) ;
244- let visible_chars: usize = spans. iter ( ) . map ( |span| span. text . chars ( ) . count ( ) ) . sum ( ) ;
245- let width = ( ( visible_chars. max ( 24 ) as u32 ) * 14 + 48 ) . clamp ( 380 , 2100 ) ;
246- let height = 68_u32 ;
280+ let lines = formula. lines ( ) . collect :: < Vec < _ > > ( ) ;
281+ let line_count = lines. len ( ) . max ( 1 ) ;
282+ let line_spans = lines
283+ . iter ( )
284+ . map ( |line| parse_formula_spans ( line) )
285+ . collect :: < Vec < _ > > ( ) ;
286+ let max_visible_chars = line_spans
287+ . iter ( )
288+ . map ( |spans| {
289+ spans
290+ . iter ( )
291+ . map ( |span| span. text . chars ( ) . count ( ) )
292+ . sum :: < usize > ( )
293+ } )
294+ . max ( )
295+ . unwrap_or ( 24 ) ;
296+ let width = ( ( max_visible_chars. max ( 24 ) as u32 ) * 14 + 48 ) . clamp ( 380 , 2100 ) ;
297+ let height = ( ( line_count as u32 ) * 30 + 22 ) . clamp ( 68 , 720 ) ;
247298 let rect_width = width - 2 ;
248299 let rect_height = height - 2 ;
249300 let ( background, border, text) = if dark_mode {
250301 ( "#0f172a" , "#334155" , "#f8fafc" )
251302 } else {
252303 ( "#ffffff" , "#cbd5e1" , "#111827" )
253304 } ;
254- let tspan_markup = formula_spans_to_svg ( & spans) ;
305+ let mut text_markup = String :: new ( ) ;
306+ for ( index, spans) in line_spans. iter ( ) . enumerate ( ) {
307+ let tspan_markup = formula_spans_to_svg ( spans) ;
308+ let y = 32 + ( index as u32 ) * 30 ;
309+ text_markup. push_str ( & format ! (
310+ r#"<text x="16" y="{y}" font-family="Cambria Math, STIX Two Math, DejaVu Serif, serif" font-size="24" fill="{text}">{tspan_markup}</text>"#
311+ ) ) ;
312+ }
255313
256314 format ! (
257315 r#"<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}">
258316<rect x="1" y="1" width="{rect_width}" height="{rect_height}" rx="10" fill="{background}" stroke="{border}" stroke-width="1.4"/>
259- <text x="16" y="44" font-family="Cambria Math, STIX Two Math, DejaVu Serif, serif" font-size="24" fill="{text}">{tspan_markup}</text>
317+ {text_markup}
260318</svg>"#
261319 )
262320 . into_bytes ( )
@@ -292,7 +350,7 @@ fn parse_formula_spans(formula: &str) -> Vec<FormulaSpan> {
292350 if !normal. is_empty ( ) {
293351 spans. push ( FormulaSpan {
294352 kind : FormulaSpanKind :: Normal ,
295- text : std:: mem:: take ( & mut normal) ,
353+ text : latex_group_to_text ( & std:: mem:: take ( & mut normal) ) ,
296354 } ) ;
297355 }
298356 let numerator = read_braced_group ( & mut chars) ;
@@ -323,7 +381,7 @@ fn parse_formula_spans(formula: &str) -> Vec<FormulaSpan> {
323381 if !normal. is_empty ( ) {
324382 spans. push ( FormulaSpan {
325383 kind : FormulaSpanKind :: Normal ,
326- text : std:: mem:: take ( & mut normal) ,
384+ text : latex_group_to_text ( & std:: mem:: take ( & mut normal) ) ,
327385 } ) ;
328386 }
329387 chars. next ( ) ;
@@ -334,7 +392,7 @@ fn parse_formula_spans(formula: &str) -> Vec<FormulaSpan> {
334392 } else {
335393 FormulaSpanKind :: Subscript
336394 } ,
337- text : content,
395+ text : latex_group_to_text ( & content) ,
338396 } ) ;
339397 } else {
340398 normal. push ( ch) ;
@@ -344,7 +402,7 @@ fn parse_formula_spans(formula: &str) -> Vec<FormulaSpan> {
344402 if !normal. is_empty ( ) {
345403 spans. push ( FormulaSpan {
346404 kind : FormulaSpanKind :: Normal ,
347- text : normal,
405+ text : latex_group_to_text ( & normal) ,
348406 } ) ;
349407 }
350408 spans
@@ -386,38 +444,125 @@ fn read_braced_group(chars: &mut std::iter::Peekable<std::str::Chars<'_>>) -> St
386444 content
387445}
388446
447+ fn read_latex_command ( chars : & mut std:: iter:: Peekable < std:: str:: Chars < ' _ > > ) -> String {
448+ let mut command = String :: new ( ) ;
449+ while let Some ( ch) = chars. peek ( ) . copied ( ) {
450+ if ch. is_ascii_alphabetic ( ) {
451+ command. push ( ch) ;
452+ chars. next ( ) ;
453+ } else {
454+ break ;
455+ }
456+ }
457+ command
458+ }
459+
460+ fn latex_symbol ( command : & str ) -> Option < & ' static str > {
461+ match command {
462+ "alpha" => Some ( "α" ) ,
463+ "beta" => Some ( "β" ) ,
464+ "gamma" => Some ( "γ" ) ,
465+ "delta" => Some ( "δ" ) ,
466+ "epsilon" => Some ( "ε" ) ,
467+ "zeta" => Some ( "ζ" ) ,
468+ "eta" => Some ( "η" ) ,
469+ "theta" => Some ( "θ" ) ,
470+ "iota" => Some ( "ι" ) ,
471+ "kappa" => Some ( "κ" ) ,
472+ "lambda" => Some ( "λ" ) ,
473+ "mu" => Some ( "μ" ) ,
474+ "nu" => Some ( "ν" ) ,
475+ "xi" => Some ( "ξ" ) ,
476+ "pi" => Some ( "π" ) ,
477+ "rho" => Some ( "ρ" ) ,
478+ "sigma" => Some ( "σ" ) ,
479+ "tau" => Some ( "τ" ) ,
480+ "upsilon" => Some ( "υ" ) ,
481+ "phi" => Some ( "φ" ) ,
482+ "chi" => Some ( "χ" ) ,
483+ "psi" => Some ( "ψ" ) ,
484+ "omega" => Some ( "ω" ) ,
485+ "Gamma" => Some ( "Γ" ) ,
486+ "Delta" => Some ( "Δ" ) ,
487+ "Theta" => Some ( "Θ" ) ,
488+ "Lambda" => Some ( "Λ" ) ,
489+ "Xi" => Some ( "Ξ" ) ,
490+ "Pi" => Some ( "Π" ) ,
491+ "Sigma" => Some ( "Σ" ) ,
492+ "Upsilon" => Some ( "Υ" ) ,
493+ "Phi" => Some ( "Φ" ) ,
494+ "Psi" => Some ( "Ψ" ) ,
495+ "Omega" => Some ( "Ω" ) ,
496+ "cdot" => Some ( "·" ) ,
497+ "times" => Some ( "×" ) ,
498+ "pm" => Some ( "±" ) ,
499+ "leq" => Some ( "≤" ) ,
500+ "geq" => Some ( "≥" ) ,
501+ "infty" => Some ( "∞" ) ,
502+ _ => None ,
503+ }
504+ }
505+
389506fn latex_group_to_text ( text : & str ) -> String {
390507 let mut output = String :: new ( ) ;
391508 let mut chars = text. chars ( ) . peekable ( ) ;
392509 while let Some ( ch) = chars. next ( ) {
393510 if ch == '\\' {
394- if try_consume_keyword ( & mut chars, "frac" ) && matches ! ( chars. next( ) , Some ( '{' ) ) {
395- let numerator = read_braced_group ( & mut chars) ;
396- if matches ! ( chars. next( ) , Some ( '{' ) ) {
397- let denominator = read_braced_group ( & mut chars) ;
398- output. push ( '(' ) ;
399- output. push_str ( & latex_group_to_text ( & numerator) ) ;
400- output. push ( ')' ) ;
401- output. push ( '∕' ) ;
402- output. push ( '(' ) ;
403- output. push_str ( & latex_group_to_text ( & denominator) ) ;
404- output. push ( ')' ) ;
405- continue ;
406- }
407- output. push_str ( "\\ frac{" ) ;
408- output. push_str ( & numerator) ;
409- continue ;
410- }
411- if try_consume_keyword ( & mut chars, "quad" ) {
412- output. push ( ' ' ) ;
511+ let command = read_latex_command ( & mut chars) ;
512+ if command. is_empty ( ) {
513+ output. push ( ch) ;
413514 continue ;
414515 }
415- if try_consume_keyword ( & mut chars, "text" ) && matches ! ( chars. next( ) , Some ( '{' ) ) {
416- let content = read_braced_group ( & mut chars) ;
417- output. push_str ( & latex_group_to_text ( & content) ) ;
418- continue ;
516+ match command. as_str ( ) {
517+ "frac" => {
518+ if matches ! ( chars. next( ) , Some ( '{' ) ) {
519+ let numerator = read_braced_group ( & mut chars) ;
520+ if matches ! ( chars. next( ) , Some ( '{' ) ) {
521+ let denominator = read_braced_group ( & mut chars) ;
522+ output. push ( '(' ) ;
523+ output. push_str ( & latex_group_to_text ( & numerator) ) ;
524+ output. push ( ')' ) ;
525+ output. push ( '∕' ) ;
526+ output. push ( '(' ) ;
527+ output. push_str ( & latex_group_to_text ( & denominator) ) ;
528+ output. push ( ')' ) ;
529+ continue ;
530+ }
531+ output. push_str ( "\\ frac{" ) ;
532+ output. push_str ( & numerator) ;
533+ continue ;
534+ }
535+ output. push_str ( "\\ frac" ) ;
536+ }
537+ "quad" => output. push ( ' ' ) ,
538+ "text" => {
539+ if matches ! ( chars. next( ) , Some ( '{' ) ) {
540+ let content = read_braced_group ( & mut chars) ;
541+ output. push_str ( & latex_group_to_text ( & content) ) ;
542+ } else {
543+ output. push_str ( "\\ text" ) ;
544+ }
545+ }
546+ "sqrt" => {
547+ if matches ! ( chars. next( ) , Some ( '{' ) ) {
548+ let radicand = read_braced_group ( & mut chars) ;
549+ output. push ( '√' ) ;
550+ output. push ( '(' ) ;
551+ output. push_str ( & latex_group_to_text ( & radicand) ) ;
552+ output. push ( ')' ) ;
553+ } else {
554+ output. push ( '√' ) ;
555+ }
556+ }
557+ _ => {
558+ if let Some ( symbol) = latex_symbol ( & command) {
559+ output. push_str ( symbol) ;
560+ } else {
561+ output. push ( '\\' ) ;
562+ output. push_str ( & command) ;
563+ }
564+ }
419565 }
420- output. push ( ch) ;
421566 continue ;
422567 }
423568 if ( ch == '^' || ch == '_' ) && matches ! ( chars. peek( ) , Some ( '{' ) ) {
0 commit comments