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