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