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