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