Skip to main content

libsurfer/
analog_renderer.rs

1//! Analog signal rendering: command generation and waveform drawing.
2
3use crate::analog_signal_cache::{AnalogSignalCache, CacheQueryResult, is_nan_highimp};
4use crate::displayed_item::{
5    AnalogSettings, DisplayedFieldRef, DisplayedItemRef, DisplayedVariable,
6};
7use crate::drawing_canvas::{AnalogDrawingCommands, DrawingCommands, VariableDrawCommands};
8use crate::message::Message;
9use crate::translation::TranslatorList;
10use crate::view::DrawingContext;
11use crate::viewport::Viewport;
12use crate::wave_data::WaveData;
13use egui::{Color32, Pos2, Stroke, emath};
14use epaint::PathShape;
15use num::{BigInt, ToPrimitive};
16use std::collections::HashMap;
17
18pub enum AnalogDrawingCommand {
19    /// Constant value from `start_px` to `end_px`.
20    /// In Step mode: horizontal line at `start_val`, vertical transition to next.
21    /// In Interpolated mode: line from (`start_px`, `start_val`) to (`end_px`, `end_val`).
22    Flat {
23        start_px: f32,
24        start_val: f64,
25        end_px: f32,
26        end_val: f64,
27    },
28    /// Multiple transitions in one pixel (anti-aliased vertical bar).
29    /// Rendered identically in both Step and Interpolated modes.
30    Range { px: f32, min_val: f64, max_val: f64 },
31}
32
33/// Generate draw commands for a displayed analog variable.
34/// Returns `None` if unrenderable, or a cache-build command if cache not ready.
35pub(crate) fn variable_analog_draw_commands(
36    displayed_variable: &DisplayedVariable,
37    display_id: DisplayedItemRef,
38    waves: &WaveData,
39    translators: &TranslatorList,
40    view_width: f32,
41    viewport_idx: usize,
42) -> Option<VariableDrawCommands> {
43    let render_mode = displayed_variable.analog.as_ref()?;
44
45    let wave_container = waves.inner.as_waves()?;
46    let displayed_field_ref: DisplayedFieldRef = display_id.into();
47    let translator = waves.variable_translator(&displayed_field_ref, translators);
48    let viewport = &waves.viewports[viewport_idx];
49    let num_timestamps = waves.safe_num_timestamps();
50
51    let signal_id = wave_container
52        .signal_id(&displayed_variable.variable_ref)
53        .ok()?;
54    let translator_name = translator.name();
55    let cache_key = (signal_id, translator_name.clone());
56
57    // Check if cache exists and is valid (correct generation and matching key)
58    let cache = match &render_mode.cache {
59        Some(entry)
60            if entry.generation == waves.cache_generation && entry.cache_key == cache_key =>
61        {
62            if let Some(cache) = entry.get() {
63                cache
64            } else {
65                // Cache is building, return loading state
66                let mut local_commands = HashMap::new();
67                local_commands.insert(
68                    vec![],
69                    DrawingCommands::Analog(AnalogDrawingCommands::Loading),
70                );
71                return Some(VariableDrawCommands {
72                    clock_edges: vec![],
73                    display_id,
74                    local_commands,
75                    local_msgs: vec![],
76                });
77            }
78        }
79        _ => {
80            // Cache missing or stale - request build and show loading
81            let mut local_commands = HashMap::new();
82            local_commands.insert(
83                vec![],
84                DrawingCommands::Analog(AnalogDrawingCommands::Loading),
85            );
86            return Some(VariableDrawCommands {
87                clock_edges: vec![],
88                display_id,
89                local_commands,
90                local_msgs: vec![Message::BuildAnalogCache {
91                    display_id,
92                    cache_key,
93                }],
94            });
95        }
96    };
97
98    let analog_commands = CommandBuilder::new(
99        cache,
100        viewport,
101        &num_timestamps,
102        view_width,
103        render_mode.settings,
104    )
105    .build();
106
107    let mut local_commands = HashMap::new();
108    local_commands.insert(vec![], DrawingCommands::Analog(analog_commands));
109
110    Some(VariableDrawCommands {
111        clock_edges: vec![],
112        display_id,
113        local_commands,
114        local_msgs: vec![],
115    })
116}
117
118/// Render analog waveform from pre-computed commands.
119pub fn draw_analog(
120    analog_commands: &AnalogDrawingCommands,
121    color: Color32,
122    offset: f32,
123    height_scaling_factor: f32,
124    frame_width: f32,
125    ctx: &mut DrawingContext,
126) {
127    let AnalogDrawingCommands::Ready {
128        viewport_min,
129        viewport_max,
130        global_min,
131        global_max,
132        values,
133        min_valid_pixel,
134        max_valid_pixel,
135        analog_settings,
136    } = analog_commands
137    else {
138        draw_building_indicator(offset, height_scaling_factor, frame_width, ctx);
139        return;
140    };
141
142    let (min_val, max_val) = select_value_range(
143        *viewport_min,
144        *viewport_max,
145        *global_min,
146        *global_max,
147        analog_settings,
148    );
149
150    let render_ctx = RenderContext::new(
151        color,
152        min_val,
153        max_val,
154        *min_valid_pixel,
155        *max_valid_pixel,
156        offset,
157        height_scaling_factor,
158        ctx,
159    );
160
161    // Use the appropriate strategy based on settings
162    match analog_settings.render_style {
163        crate::displayed_item::AnalogRenderStyle::Step => {
164            let mut strategy = StepStrategy::default();
165            render_with_strategy(values, &render_ctx, &mut strategy, ctx);
166        }
167        crate::displayed_item::AnalogRenderStyle::Interpolated => {
168            let mut strategy = InterpolatedStrategy::default();
169            render_with_strategy(values, &render_ctx, &mut strategy, ctx);
170        }
171    }
172
173    draw_amplitude_labels(&render_ctx, frame_width, ctx);
174}
175
176/// Draw a building indicator with animated dots while analog cache is being built.
177fn draw_building_indicator(
178    offset: f32,
179    height_scaling_factor: f32,
180    frame_width: f32,
181    ctx: &mut DrawingContext,
182) {
183    // Animate dots: cycle through ".", "..", "..." every 333ms
184    let elapsed = ctx.painter.ctx().input(|i| i.time);
185    let dot_index = (elapsed / 0.333) as usize % 3;
186    let text = ["Building.  ", "Building.. ", "Building..."][dot_index];
187
188    let text_size = ctx.cfg.text_size;
189    let row_height = ctx.cfg.line_height * height_scaling_factor;
190    let center_y = offset + row_height / 2.0;
191    let center_x = frame_width / 2.0;
192    let pos = (ctx.to_screen)(center_x, center_y);
193
194    ctx.painter.text(
195        pos,
196        egui::Align2::CENTER_CENTER,
197        text,
198        egui::FontId::monospace(text_size),
199        ctx.theme.foreground.gamma_multiply(0.6),
200    );
201}
202
203fn select_value_range(
204    viewport_min: f64,
205    viewport_max: f64,
206    global_min: f64,
207    global_max: f64,
208    settings: &AnalogSettings,
209) -> (f64, f64) {
210    let (min, max) = match settings.y_axis_scale {
211        crate::displayed_item::AnalogYAxisScale::Viewport => (viewport_min, viewport_max),
212        crate::displayed_item::AnalogYAxisScale::Global => (global_min, global_max),
213    };
214
215    // Handle all-NaN case: min=INFINITY, max=NEG_INFINITY
216    if !min.is_finite() || !max.is_finite() || min > max {
217        return (-0.5, 0.5);
218    }
219
220    // Avoid division by zero
221    if (min - max).abs() < f64::EPSILON {
222        (min - 0.5, max + 0.5)
223    } else {
224        (min, max)
225    }
226}
227
228/// Builds drawing commands by iterating viewport pixels.
229struct CommandBuilder<'a> {
230    cache: &'a AnalogSignalCache,
231    viewport: &'a Viewport,
232    num_timestamps: &'a BigInt,
233    view_width: f32,
234    min_valid_pixel: f32,
235    max_valid_pixel: f32,
236    output: CommandOutput,
237    analog_settings: AnalogSettings,
238}
239
240/// Accumulates commands and tracks value bounds.
241struct CommandOutput {
242    commands: Vec<AnalogDrawingCommand>,
243    pending_flat: Option<(f32, f64)>,
244    viewport_min: f64,
245    viewport_max: f64,
246}
247
248impl CommandOutput {
249    fn new() -> Self {
250        Self {
251            commands: Vec::new(),
252            pending_flat: None,
253            viewport_min: f64::INFINITY,
254            viewport_max: f64::NEG_INFINITY,
255        }
256    }
257
258    fn update_bounds(&mut self, value: f64) {
259        if value.is_finite() {
260            self.viewport_min = self.viewport_min.min(value);
261            self.viewport_max = self.viewport_max.max(value);
262        }
263    }
264
265    fn emit_flat(&mut self, px: f32, value: f64) {
266        match self.pending_flat {
267            // Bit compare to distinguish different NaN payloads ( Undef / HighZ )
268            Some((_, v)) if v.to_bits() == value.to_bits() => {
269                // Same value, extend the flat region (no-op, end_px updated on flush)
270            }
271            Some((start, start_val)) => {
272                // Value changed: flush previous flat
273                let end_val = if start_val.is_finite() && value.is_finite() {
274                    value
275                } else {
276                    start_val
277                };
278                self.commands.push(AnalogDrawingCommand::Flat {
279                    start_px: start,
280                    start_val,
281                    end_px: px,
282                    end_val,
283                });
284                self.pending_flat = Some((px, value));
285            }
286            None => self.pending_flat = Some((px, value)),
287        }
288    }
289
290    fn emit_range(&mut self, px: f32, min: f64, max: f64, entry_val: f64, exit_val: f64) {
291        // Flush pending flat - end_val is the first transition value (entry to range)
292        // for correct interpolation in Interpolated mode
293        if let Some((start, start_val)) = self.pending_flat.take() {
294            let end_val = if start_val.is_finite() && entry_val.is_finite() {
295                entry_val
296            } else {
297                start_val
298            };
299            self.commands.push(AnalogDrawingCommand::Flat {
300                start_px: start,
301                start_val,
302                end_px: px,
303                end_val,
304            });
305        }
306        self.commands.push(AnalogDrawingCommand::Range {
307            px,
308            min_val: min,
309            max_val: max,
310        });
311        // Start new flat from exit_val
312        self.pending_flat = Some((px + 1.0, exit_val));
313    }
314}
315
316impl<'a> CommandBuilder<'a> {
317    fn new(
318        cache: &'a AnalogSignalCache,
319        viewport: &'a Viewport,
320        num_timestamps: &'a BigInt,
321        view_width: f32,
322        analog_settings: AnalogSettings,
323    ) -> Self {
324        let min_valid_pixel =
325            viewport.pixel_from_time(&BigInt::from(0), view_width, num_timestamps);
326        let max_valid_pixel = viewport.pixel_from_time(num_timestamps, view_width, num_timestamps);
327
328        Self {
329            cache,
330            viewport,
331            num_timestamps,
332            view_width,
333            min_valid_pixel,
334            max_valid_pixel,
335            output: CommandOutput::new(),
336            analog_settings,
337        }
338    }
339
340    fn build(mut self) -> AnalogDrawingCommands {
341        let end_px = self.view_width.floor().max(0.0) + 1.0;
342
343        let before_px = self.add_before_viewport_sample();
344        self.iterate_pixels(0.0, end_px);
345        self.add_after_viewport_sample(end_px);
346
347        self.finalize(before_px)
348    }
349
350    fn time_at_pixel(&self, px: f64) -> u64 {
351        self.viewport
352            .as_absolute_time(px, self.view_width, self.num_timestamps)
353            .0
354            .to_u64()
355            .unwrap_or(0)
356    }
357
358    fn pixel_at_time(&self, time: u64) -> f32 {
359        self.viewport
360            .pixel_from_time(&BigInt::from(time), self.view_width, self.num_timestamps)
361    }
362
363    fn query(&self, time: u64) -> CacheQueryResult {
364        self.cache.query_at_time(time)
365    }
366
367    /// Captures the most recent sample occurring before the visible viewport.
368    /// This method ensures rendering continuity when a signal value extends from before
369    /// the viewport into the visible area.
370    fn add_before_viewport_sample(&mut self) -> Option<f32> {
371        let query = self.query(self.time_at_pixel(0.0));
372
373        if let Some((time, value)) = query.current {
374            let px = self.pixel_at_time(time);
375            if px < 0.0 {
376                self.output.update_bounds(value);
377                self.output.pending_flat = Some((px, value));
378                return Some(px);
379            }
380        }
381        None
382    }
383
384    fn iterate_pixels(&mut self, start_px: f32, end_px: f32) {
385        let mut px = start_px as u32;
386        let end = end_px as u32;
387        let mut next_query_time: Option<u64> = None;
388        let mut last_queried_time: Option<u64> = None;
389
390        while px < end {
391            // Track if we jumped to this pixel for a specific transition
392            let jumped_to_transition = next_query_time.is_some();
393            let t0 = next_query_time.unwrap_or_else(|| self.time_at_pixel(f64::from(px)));
394            let t1 = self.time_at_pixel(f64::from(px) + 1.0);
395            next_query_time = None;
396
397            // Skip if we already queried this exact time (optimization for zoomed-out views
398            // where multiple pixels map to the same integer time). Don't skip if we jumped
399            // here for a specific transition.
400            if !jumped_to_transition && last_queried_time == Some(t0) {
401                px += 1;
402                continue;
403            }
404
405            let query = self.query(t0);
406            last_queried_time = Some(t0);
407            let next_change = query.next;
408            let is_flat = next_change.is_none_or(|nc| nc >= t1);
409
410            if is_flat {
411                px = self.process_flat(px, end, &query, next_change, &mut next_query_time);
412            } else {
413                self.process_range(px, t0, t1);
414                px += 1;
415            }
416        }
417    }
418
419    fn process_flat(
420        &mut self,
421        px: u32,
422        end: u32,
423        query: &CacheQueryResult,
424        next_change: Option<u64>,
425        next_query_time: &mut Option<u64>,
426    ) -> u32 {
427        if let Some((_, value)) = query.current {
428            self.output.update_bounds(value);
429            self.output.emit_flat(px as f32, value);
430        }
431
432        // Skip ahead to next transition
433        if let Some(next) = next_change {
434            let next_px = self.pixel_at_time(next);
435            if next_px.is_finite() {
436                let jump = next_px.floor().max(0.0) as u32;
437                if jump > px {
438                    *next_query_time = Some(next);
439                    return jump.min(end);
440                }
441            }
442            (px + 1).min(end)
443        } else {
444            end
445        }
446    }
447
448    fn process_range(&mut self, px: u32, t0: u64, t1: u64) {
449        if let Some((min, max)) = self.cache.query_time_range(t0, t1.saturating_sub(1)) {
450            self.output.update_bounds(min);
451            self.output.update_bounds(max);
452
453            // Query the value at the first transition within the pixel (entry value)
454            // This is used as end_val for the preceding Flat in interpolated mode
455            let t0_query = self.query(t0);
456            let entry_val = match t0_query.current {
457                // If t0 is exactly on a transition (jumped here via next_query_time),
458                // the current value is already the first transition value
459                Some((time, value)) if time == t0 => value,
460                // Otherwise t0 is at pixel start, so first transition is at t0_query.next
461                _ => {
462                    if let Some(first_change) = t0_query.next {
463                        self.query(first_change).current.map_or(min, |(_, v)| v)
464                    } else {
465                        min
466                    }
467                }
468            };
469
470            // Query the value at the end of the range (exit value)
471            let exit_query = self.query(t1.saturating_sub(1));
472            let exit_val = exit_query.current.map_or(max, |(_, v)| v);
473
474            self.output
475                .emit_range(px as f32, min, max, entry_val, exit_val);
476        }
477    }
478
479    /// Extends rendering to include the first sample occurring after the visible viewport.
480    fn add_after_viewport_sample(&mut self, end_px: f32) {
481        let query = self.query(self.time_at_pixel(f64::from(end_px)));
482
483        let Some(next_time) = query.next else {
484            return;
485        };
486
487        let after_px = self.pixel_at_time(next_time);
488        if after_px <= end_px {
489            return;
490        }
491
492        let after_query = self.query(next_time);
493
494        if let Some((_, value)) = after_query.current {
495            self.output.update_bounds(value);
496
497            if let Some((start, start_val)) = self.output.pending_flat.take() {
498                self.output.commands.push(AnalogDrawingCommand::Flat {
499                    start_px: start,
500                    start_val,
501                    end_px: after_px,
502                    end_val: value,
503                });
504            }
505        }
506    }
507
508    fn finalize(mut self, before_px: Option<f32>) -> AnalogDrawingCommands {
509        // Flush remaining pending flat with same end_val (constant to end)
510        if let Some((start, start_val)) = self.output.pending_flat.take() {
511            self.output.commands.push(AnalogDrawingCommand::Flat {
512                start_px: start,
513                start_val,
514                end_px: self.max_valid_pixel,
515                end_val: start_val, // Signal stays constant
516            });
517        }
518
519        // Extend first command to include before-viewport sample
520        if let Some(before) = before_px
521            && let Some(AnalogDrawingCommand::Flat { start_px, .. }) =
522                self.output.commands.first_mut()
523        {
524            *start_px = (*start_px).min(before);
525        }
526
527        AnalogDrawingCommands::Ready {
528            viewport_min: self.output.viewport_min,
529            viewport_max: self.output.viewport_max,
530            global_min: self.cache.global_min,
531            global_max: self.cache.global_max,
532            values: self.output.commands,
533            min_valid_pixel: self.min_valid_pixel,
534            max_valid_pixel: self.max_valid_pixel,
535            analog_settings: self.analog_settings,
536        }
537    }
538}
539
540/// Rendering strategy for analog waveforms.
541pub trait RenderStrategy {
542    /// Reset state after encountering undefined values.
543    fn reset_state(&mut self);
544
545    /// Get the last rendered point (for Range connection).
546    fn last_point(&self) -> Option<Pos2>;
547
548    /// Set the last rendered point (after Range draws).
549    fn set_last_point(&mut self, point: Pos2);
550
551    /// Render a flat segment.
552    /// Step: horizontal line at `start_val`, connect to next.
553    /// Interpolated: line from (`start_px`, `start_val`) to (`end_px`, `end_val`).
554    fn render_flat(
555        &mut self,
556        ctx: &mut DrawingContext,
557        render_ctx: &RenderContext,
558        start_px: f32,
559        start_val: f64,
560        end_px: f32,
561        end_val: f64,
562    );
563
564    /// Render a range segment (default impl, same for both strategies).
565    /// Draws vertical bar at px from `min_val` to `max_val`.
566    fn render_range(
567        &mut self,
568        ctx: &mut DrawingContext,
569        render_ctx: &RenderContext,
570        px: f32,
571        min_val: f64,
572        max_val: f64,
573    ) {
574        if !min_val.is_finite() || !max_val.is_finite() {
575            let nan = if min_val.is_finite() {
576                max_val
577            } else {
578                min_val
579            };
580            render_ctx.draw_undefined(px, px + 1.0, nan, ctx);
581            self.reset_state();
582            return;
583        }
584
585        let p_min = render_ctx.to_screen(px, min_val, ctx);
586        let p_max = render_ctx.to_screen(px, max_val, ctx);
587
588        // Connect from previous to closer endpoint
589        let (connect, other) = match self.last_point() {
590            Some(prev) if (prev.y - p_min.y).abs() < (prev.y - p_max.y).abs() => (p_min, p_max),
591            _ => (p_max, p_min),
592        };
593
594        if let Some(prev) = self.last_point() {
595            render_ctx.draw_line(prev, connect, ctx);
596        }
597
598        // Vertical bar
599        render_ctx.draw_line(connect, other, ctx);
600        self.set_last_point(other);
601    }
602}
603
604/// Coordinate transformation state shared between rendering strategies.
605/// Invariant: `min_val` and `max_val` are always finite.
606pub struct RenderContext {
607    pub stroke: Stroke,
608    pub min_val: f64,
609    pub max_val: f64,
610    /// Pixel position of timestamp 0 (start of signal data).
611    pub min_valid_pixel: f32,
612    /// Pixel position of last timestamp (end of signal data).
613    pub max_valid_pixel: f32,
614    pub offset: f32,
615    pub height_scale: f32,
616    pub line_height: f32,
617}
618
619impl RenderContext {
620    #[allow(clippy::too_many_arguments)]
621    fn new(
622        color: Color32,
623        min_val: f64,
624        max_val: f64,
625        min_valid_pixel: f32,
626        max_valid_pixel: f32,
627        offset: f32,
628        height_scale: f32,
629        ctx: &DrawingContext,
630    ) -> Self {
631        Self {
632            stroke: Stroke::new(ctx.theme.linewidth, color),
633            min_val,
634            max_val,
635            min_valid_pixel,
636            max_valid_pixel,
637            offset,
638            height_scale,
639            line_height: ctx.cfg.line_height,
640        }
641    }
642
643    /// Normalize value to [0, 1].
644    /// Invariant: `min_val` and `max_val` are always finite (guaranteed by `AnalogSignalCache`).
645    #[must_use]
646    pub fn normalize(&self, value: f64) -> f32 {
647        debug_assert!(
648            self.min_val.is_finite() && self.max_val.is_finite(),
649            "RenderContext min_val and max_val must be finite"
650        );
651        let range = self.max_val - self.min_val;
652        if range.abs() <= f64::EPSILON {
653            0.5
654        } else {
655            ((value - self.min_val) / range) as f32
656        }
657    }
658
659    /// Convert value to screen position.
660    #[must_use]
661    pub fn to_screen(&self, x: f32, y: f64, ctx: &DrawingContext) -> Pos2 {
662        let y_norm = self.normalize(y);
663        (ctx.to_screen)(
664            x,
665            (1.0 - y_norm) * self.line_height * self.height_scale + self.offset,
666        )
667    }
668
669    /// Clamp x to valid pixel range (within VCD file bounds).
670    #[must_use]
671    pub fn clamp_x(&self, x: f32) -> f32 {
672        x.clamp(self.min_valid_pixel, self.max_valid_pixel)
673    }
674
675    pub fn draw_line(&self, from: Pos2, to: Pos2, ctx: &mut DrawingContext) {
676        ctx.painter
677            .add(PathShape::line(vec![from, to], self.stroke));
678    }
679
680    pub fn draw_undefined(&self, start_x: f32, end_x: f32, value: f64, ctx: &mut DrawingContext) {
681        let color = if value == f64::INFINITY {
682            ctx.theme.accent_error.background
683        } else if value == f64::NEG_INFINITY {
684            ctx.theme.variable_dontcare
685        } else if is_nan_highimp(value) {
686            ctx.theme.variable_highimp
687        } else {
688            ctx.theme.variable_undef
689        };
690        let min = (ctx.to_screen)(start_x, self.offset);
691        let max = (ctx.to_screen)(end_x, self.offset + self.line_height * self.height_scale);
692        ctx.painter
693            .rect_filled(egui::Rect::from_min_max(min, max), 0.0, color);
694    }
695}
696
697/// Step-style rendering: horizontal segments with vertical transitions.
698#[derive(Default)]
699pub struct StepStrategy {
700    last_point: Option<Pos2>,
701}
702
703impl RenderStrategy for StepStrategy {
704    fn reset_state(&mut self) {
705        self.last_point = None;
706    }
707
708    fn last_point(&self) -> Option<Pos2> {
709        self.last_point
710    }
711
712    fn set_last_point(&mut self, point: Pos2) {
713        self.last_point = Some(point);
714    }
715
716    fn render_flat(
717        &mut self,
718        ctx: &mut DrawingContext,
719        render_ctx: &RenderContext,
720        start_px: f32,
721        start_val: f64,
722        end_px: f32,
723        _end_val: f64, // Ignored in Step mode
724    ) {
725        let start_px = render_ctx.clamp_x(start_px);
726        let end_px = render_ctx.clamp_x(end_px);
727
728        if !start_val.is_finite() {
729            render_ctx.draw_undefined(start_px, end_px, start_val, ctx);
730            self.reset_state();
731            return;
732        }
733
734        let p1 = render_ctx.to_screen(start_px, start_val, ctx);
735        let p2 = render_ctx.to_screen(end_px, start_val, ctx);
736
737        // Vertical transition from previous
738        if let Some(prev) = self.last_point {
739            render_ctx.draw_line(Pos2::new(p1.x, prev.y), p1, ctx);
740        }
741
742        // Horizontal line
743        render_ctx.draw_line(p1, p2, ctx);
744        self.last_point = Some(p2);
745    }
746}
747
748/// Interpolated rendering: diagonal lines connecting consecutive values.
749#[derive(Default)]
750pub struct InterpolatedStrategy {
751    last_point: Option<Pos2>,
752    started: bool,
753}
754
755impl RenderStrategy for InterpolatedStrategy {
756    fn reset_state(&mut self) {
757        self.last_point = None;
758        self.started = true;
759    }
760
761    fn last_point(&self) -> Option<Pos2> {
762        self.last_point
763    }
764
765    fn set_last_point(&mut self, point: Pos2) {
766        self.last_point = Some(point);
767        self.started = true;
768    }
769
770    fn render_flat(
771        &mut self,
772        ctx: &mut DrawingContext,
773        render_ctx: &RenderContext,
774        start_px: f32,
775        start_val: f64,
776        end_px: f32,
777        end_val: f64,
778    ) {
779        let start_px = render_ctx.clamp_x(start_px);
780        let end_px = render_ctx.clamp_x(end_px);
781
782        if !start_val.is_finite() {
783            render_ctx.draw_undefined(start_px, end_px, start_val, ctx);
784            self.reset_state();
785            return;
786        }
787
788        // If end_val is NaN but start_val is finite, render as flat line using start_val
789        let end_val = if end_val.is_finite() {
790            end_val
791        } else {
792            start_val
793        };
794
795        let p1 = render_ctx.to_screen(start_px, start_val, ctx);
796        let p2 = render_ctx.to_screen(end_px, end_val, ctx);
797
798        // Connect from previous point
799        if let Some(prev) = self.last_point {
800            render_ctx.draw_line(prev, p1, ctx);
801        } else if !self.started {
802            // Connect from viewport edge
803            let edge = render_ctx.to_screen(render_ctx.min_valid_pixel.max(0.0), start_val, ctx);
804            render_ctx.draw_line(edge, p1, ctx);
805        }
806
807        render_ctx.draw_line(p1, p2, ctx);
808        self.last_point = Some(p2);
809        self.started = true;
810    }
811}
812
813/// Render commands using the given strategy.
814fn render_with_strategy<S: RenderStrategy>(
815    commands: &[AnalogDrawingCommand],
816    render_ctx: &RenderContext,
817    strategy: &mut S,
818    ctx: &mut DrawingContext,
819) {
820    for cmd in commands {
821        match cmd {
822            AnalogDrawingCommand::Flat {
823                start_px,
824                start_val,
825                end_px,
826                end_val,
827            } => {
828                strategy.render_flat(ctx, render_ctx, *start_px, *start_val, *end_px, *end_val);
829            }
830            AnalogDrawingCommand::Range {
831                px,
832                min_val,
833                max_val,
834            } => {
835                strategy.render_range(ctx, render_ctx, *px, *min_val, *max_val);
836            }
837        }
838    }
839}
840
841/// Format amplitude value for display, using scientific notation for extreme values.
842fn format_amplitude_value(value: f64) -> String {
843    const SCIENTIFIC_THRESHOLD_HIGH: f64 = 1e4;
844    const SCIENTIFIC_THRESHOLD_LOW: f64 = 1e-3;
845    let abs_val = value.abs();
846    if abs_val == 0.0 {
847        "0.00".to_string()
848    } else if !(SCIENTIFIC_THRESHOLD_LOW..SCIENTIFIC_THRESHOLD_HIGH).contains(&abs_val) {
849        format!("{value:.2e}")
850    } else {
851        format!("{value:.2}")
852    }
853}
854
855fn draw_amplitude_labels(render_ctx: &RenderContext, frame_width: f32, ctx: &mut DrawingContext) {
856    const SPLIT_LABEL_HEIGHT_THRESHOLD: f32 = 2.0;
857    const LABEL_ALPHA: f32 = 0.7;
858    const BACKGROUND_ALPHA: u8 = 200;
859
860    let text_size = ctx.cfg.text_size;
861
862    let text_color = render_ctx.stroke.color.gamma_multiply(LABEL_ALPHA);
863    let bg_color = Color32::from_rgba_unmultiplied(0, 0, 0, BACKGROUND_ALPHA);
864    let font = egui::FontId::monospace(text_size);
865
866    if render_ctx.height_scale < SPLIT_LABEL_HEIGHT_THRESHOLD {
867        let combined_text = format!(
868            "[{}, {}]",
869            format_amplitude_value(render_ctx.min_val),
870            format_amplitude_value(render_ctx.max_val)
871        );
872        let galley = ctx
873            .painter
874            .layout_no_wrap(combined_text.clone(), font.clone(), text_color);
875
876        let label_x = frame_width - galley.size().x - 5.0;
877        let label_pos = render_ctx.to_screen(
878            label_x,
879            f64::midpoint(render_ctx.min_val, render_ctx.max_val),
880            ctx,
881        );
882
883        let rect = egui::Rect::from_min_size(
884            Pos2::new(label_pos.x - 2.0, label_pos.y - galley.size().y / 2.0 - 2.0),
885            egui::Vec2::new(galley.size().x + 4.0, galley.size().y + 4.0),
886        );
887        ctx.painter.rect_filled(rect, 2.0, bg_color);
888        ctx.painter.text(
889            Pos2::new(label_pos.x, label_pos.y - galley.size().y / 2.0),
890            emath::Align2::LEFT_TOP,
891            combined_text,
892            font,
893            text_color,
894        );
895    } else {
896        let max_text = format_amplitude_value(render_ctx.max_val);
897        let min_text = format_amplitude_value(render_ctx.min_val);
898
899        let max_galley = ctx
900            .painter
901            .layout_no_wrap(max_text.clone(), font.clone(), text_color);
902        let min_galley = ctx
903            .painter
904            .layout_no_wrap(min_text.clone(), font.clone(), text_color);
905
906        let label_x = frame_width - max_galley.size().x.max(min_galley.size().x) - 5.0;
907
908        let max_pos = render_ctx.to_screen(label_x, render_ctx.max_val, ctx);
909        let max_rect = egui::Rect::from_min_size(
910            Pos2::new(max_pos.x - 2.0, max_pos.y - 2.0),
911            egui::Vec2::new(max_galley.size().x + 4.0, max_galley.size().y + 4.0),
912        );
913        ctx.painter.rect_filled(max_rect, 2.0, bg_color);
914        ctx.painter.text(
915            max_pos,
916            emath::Align2::LEFT_TOP,
917            max_text,
918            font.clone(),
919            text_color,
920        );
921
922        let min_pos = render_ctx.to_screen(label_x, render_ctx.min_val, ctx);
923        let min_rect = egui::Rect::from_min_size(
924            Pos2::new(min_pos.x - 2.0, min_pos.y - min_galley.size().y - 2.0),
925            egui::Vec2::new(min_galley.size().x + 4.0, min_galley.size().y + 4.0),
926        );
927        ctx.painter.rect_filled(min_rect, 2.0, bg_color);
928        ctx.painter.text(
929            min_pos,
930            emath::Align2::LEFT_BOTTOM,
931            min_text,
932            font,
933            text_color,
934        );
935    }
936}