@@ -15,8 +15,12 @@ use egui::WidgetText;
1515use egui:: emath:: Rot2 ;
1616use egui:: emath:: remap_clamp;
1717use egui:: epaint:: TextShape ;
18+ use emath:: Vec2b ;
19+ use emath:: pos2;
20+ use emath:: remap;
1821
19- use super :: transform:: PlotTransform ;
22+ use crate :: bounds:: PlotBounds ;
23+ use crate :: bounds:: PlotPoint ;
2024use crate :: colors;
2125use crate :: grid:: GridMark ;
2226use crate :: placement:: HPlacement ;
@@ -329,3 +333,277 @@ impl<'a> AxisWidget<'a> {
329333 thickness
330334 }
331335}
336+
337+ /// Contains the screen rectangle and the plot bounds and provides methods to
338+ /// transform between them.
339+ #[ cfg_attr( feature = "serde" , derive( serde:: Deserialize , serde:: Serialize ) ) ]
340+ #[ derive( Clone , Copy , Debug ) ]
341+ pub struct PlotTransform {
342+ /// The screen rectangle.
343+ frame : Rect ,
344+
345+ /// The plot bounds.
346+ bounds : PlotBounds ,
347+
348+ /// Whether to always center the x-range or y-range of the bounds.
349+ centered : Vec2b ,
350+
351+ /// Whether to always invert the x and/or y axis
352+ inverted_axis : Vec2b ,
353+ }
354+
355+ impl PlotTransform {
356+ pub fn new ( frame : Rect , bounds : PlotBounds , center_axis : impl Into < Vec2b > ) -> Self {
357+ debug_assert ! (
358+ 0.0 <= frame. width( ) && 0.0 <= frame. height( ) ,
359+ "Bad plot frame: {frame:?}"
360+ ) ;
361+ let center_axis = center_axis. into ( ) ;
362+
363+ // Since the current Y bounds an affect the final X bounds and vice versa, we
364+ // need to keep the original version of the `bounds` before we start
365+ // modifying it.
366+ let mut new_bounds = bounds;
367+
368+ // Sanitize bounds.
369+ //
370+ // When a given bound axis is "thin" (e.g. width or height is 0) but finite, we
371+ // center the bounds around that value. If the other axis is "fat", we
372+ // reuse its extent for the thin axis, and default to +/- 1.0 otherwise.
373+ if !bounds. is_finite_x ( ) {
374+ new_bounds. set_x ( & PlotBounds :: new_symmetrical ( 1.0 ) ) ;
375+ } else if bounds. width ( ) <= 0.0 {
376+ new_bounds. set_x_center_width (
377+ bounds. center ( ) . x ,
378+ if bounds. is_valid_y ( ) { bounds. height ( ) } else { 1.0 } ,
379+ ) ;
380+ }
381+
382+ if !bounds. is_finite_y ( ) {
383+ new_bounds. set_y ( & PlotBounds :: new_symmetrical ( 1.0 ) ) ;
384+ } else if bounds. height ( ) <= 0.0 {
385+ new_bounds. set_y_center_height (
386+ bounds. center ( ) . y ,
387+ if bounds. is_valid_x ( ) { bounds. width ( ) } else { 1.0 } ,
388+ ) ;
389+ }
390+
391+ // Scale axes so that the origin is in the center.
392+ if center_axis. x {
393+ new_bounds. make_x_symmetrical ( ) ;
394+ }
395+ if center_axis. y {
396+ new_bounds. make_y_symmetrical ( ) ;
397+ }
398+
399+ debug_assert ! ( new_bounds. is_valid( ) , "Bad final plot bounds: {new_bounds:?}" ) ;
400+
401+ Self {
402+ frame,
403+ bounds : new_bounds,
404+ centered : center_axis,
405+ inverted_axis : Vec2b :: new ( false , false ) ,
406+ }
407+ }
408+
409+ pub fn new_with_invert_axis (
410+ frame : Rect ,
411+ bounds : PlotBounds ,
412+ center_axis : impl Into < Vec2b > ,
413+ invert_axis : impl Into < Vec2b > ,
414+ ) -> Self {
415+ let mut new = Self :: new ( frame, bounds, center_axis) ;
416+ new. inverted_axis = invert_axis. into ( ) ;
417+ new
418+ }
419+
420+ /// ui-space rectangle.
421+ #[ inline]
422+ pub fn frame ( & self ) -> & Rect {
423+ & self . frame
424+ }
425+
426+ /// Plot-space bounds.
427+ #[ inline]
428+ pub fn bounds ( & self ) -> & PlotBounds {
429+ & self . bounds
430+ }
431+
432+ #[ inline]
433+ pub fn set_bounds ( & mut self , bounds : PlotBounds ) {
434+ self . bounds = bounds;
435+ }
436+
437+ pub fn translate_bounds ( & mut self , mut delta_pos : ( f64 , f64 ) ) {
438+ if self . centered . x {
439+ delta_pos. 0 = 0. ;
440+ }
441+ if self . centered . y {
442+ delta_pos. 1 = 0. ;
443+ }
444+ delta_pos. 0 *= self . dvalue_dpos ( ) [ 0 ] ;
445+ delta_pos. 1 *= self . dvalue_dpos ( ) [ 1 ] ;
446+ self . bounds . translate ( ( delta_pos. 0 , delta_pos. 1 ) ) ;
447+ }
448+
449+ /// Zoom by a relative factor with the given screen position as center.
450+ pub fn zoom ( & mut self , zoom_factor : Vec2 , center : Pos2 ) {
451+ let center = self . value_from_position ( center) ;
452+
453+ let mut new_bounds = self . bounds ;
454+ new_bounds. zoom ( zoom_factor, center) ;
455+
456+ if new_bounds. is_valid ( ) {
457+ self . bounds = new_bounds;
458+ }
459+ }
460+
461+ pub fn position_from_point_x ( & self , value : f64 ) -> f32 {
462+ remap (
463+ value,
464+ self . bounds . min [ 0 ] ..=self . bounds . max [ 0 ] ,
465+ if self . inverted_axis [ 0 ] {
466+ ( self . frame . right ( ) as f64 ) ..=( self . frame . left ( ) as f64 )
467+ } else {
468+ ( self . frame . left ( ) as f64 ) ..=( self . frame . right ( ) as f64 )
469+ } ,
470+ ) as f32
471+ }
472+
473+ pub fn position_from_point_y ( & self , value : f64 ) -> f32 {
474+ remap (
475+ value,
476+ self . bounds . min [ 1 ] ..=self . bounds . max [ 1 ] ,
477+ // negated y axis by default
478+ if self . inverted_axis [ 1 ] {
479+ ( self . frame . top ( ) as f64 ) ..=( self . frame . bottom ( ) as f64 )
480+ } else {
481+ ( self . frame . bottom ( ) as f64 ) ..=( self . frame . top ( ) as f64 )
482+ } ,
483+ ) as f32
484+ }
485+
486+ /// Screen/ui position from point on plot.
487+ pub fn position_from_point ( & self , value : & PlotPoint ) -> Pos2 {
488+ pos2 ( self . position_from_point_x ( value. x ) , self . position_from_point_y ( value. y ) )
489+ }
490+
491+ /// Plot point from screen/ui position.
492+ pub fn value_from_position ( & self , pos : Pos2 ) -> PlotPoint {
493+ let x = remap (
494+ pos. x as f64 ,
495+ if self . inverted_axis [ 0 ] {
496+ ( self . frame . right ( ) as f64 ) ..=( self . frame . left ( ) as f64 )
497+ } else {
498+ ( self . frame . left ( ) as f64 ) ..=( self . frame . right ( ) as f64 )
499+ } ,
500+ self . bounds . range_x ( ) ,
501+ ) ;
502+ let y = remap (
503+ pos. y as f64 ,
504+ // negated y axis by default
505+ if self . inverted_axis [ 1 ] {
506+ ( self . frame . top ( ) as f64 ) ..=( self . frame . bottom ( ) as f64 )
507+ } else {
508+ ( self . frame . bottom ( ) as f64 ) ..=( self . frame . top ( ) as f64 )
509+ } ,
510+ self . bounds . range_y ( ) ,
511+ ) ;
512+
513+ PlotPoint :: new ( x, y)
514+ }
515+
516+ /// Transform a rectangle of plot values to a screen-coordinate rectangle.
517+ ///
518+ /// This typically means that the rect is mirrored vertically (top becomes
519+ /// bottom and vice versa), since the plot's coordinate system has +Y
520+ /// up, while egui has +Y down.
521+ pub fn rect_from_values ( & self , value1 : & PlotPoint , value2 : & PlotPoint ) -> Rect {
522+ let pos1 = self . position_from_point ( value1) ;
523+ let pos2 = self . position_from_point ( value2) ;
524+
525+ let mut rect = Rect :: NOTHING ;
526+ rect. extend_with ( pos1) ;
527+ rect. extend_with ( pos2) ;
528+ rect
529+ }
530+
531+ /// delta position / delta value = how many ui points per step in the X axis
532+ /// in "plot space"
533+ pub fn dpos_dvalue_x ( & self ) -> f64 {
534+ let flip = if self . inverted_axis [ 0 ] { -1.0 } else { 1.0 } ;
535+ flip * ( self . frame . width ( ) as f64 ) / self . bounds . width ( )
536+ }
537+
538+ /// delta position / delta value = how many ui points per step in the Y axis
539+ /// in "plot space"
540+ pub fn dpos_dvalue_y ( & self ) -> f64 {
541+ let flip = if self . inverted_axis [ 1 ] { 1.0 } else { -1.0 } ;
542+ flip * ( self . frame . height ( ) as f64 ) / self . bounds . height ( )
543+ }
544+
545+ /// delta position / delta value = how many ui points per step in "plot
546+ /// space"
547+ pub fn dpos_dvalue ( & self ) -> [ f64 ; 2 ] {
548+ [ self . dpos_dvalue_x ( ) , self . dpos_dvalue_y ( ) ]
549+ }
550+
551+ /// delta value / delta position = how much ground do we cover in "plot
552+ /// space" per ui point?
553+ pub fn dvalue_dpos ( & self ) -> [ f64 ; 2 ] {
554+ [ 1.0 / self . dpos_dvalue_x ( ) , 1.0 / self . dpos_dvalue_y ( ) ]
555+ }
556+
557+ /// scale.x/scale.y ratio.
558+ ///
559+ /// If 1.0, it means the scale factor is the same in both axes.
560+ fn aspect ( & self ) -> f64 {
561+ let rw = self . frame . width ( ) as f64 ;
562+ let rh = self . frame . height ( ) as f64 ;
563+ ( self . bounds . width ( ) / rw) / ( self . bounds . height ( ) / rh)
564+ }
565+
566+ /// Sets the aspect ratio by expanding the x- or y-axis.
567+ ///
568+ /// This never contracts, so we don't miss out on any data.
569+ pub ( crate ) fn set_aspect_by_expanding ( & mut self , aspect : f64 ) {
570+ let current_aspect = self . aspect ( ) ;
571+
572+ let epsilon = 1e-5 ;
573+ if ( current_aspect - aspect) . abs ( ) < epsilon {
574+ // Don't make any changes when the aspect is already almost correct.
575+ return ;
576+ }
577+
578+ if current_aspect < aspect {
579+ self . bounds
580+ . expand_x ( ( aspect / current_aspect - 1.0 ) * self . bounds . width ( ) * 0.5 ) ;
581+ } else {
582+ self . bounds
583+ . expand_y ( ( current_aspect / aspect - 1.0 ) * self . bounds . height ( ) * 0.5 ) ;
584+ }
585+ }
586+
587+ /// Sets the aspect ratio by changing either the X or Y axis (callers
588+ /// choice).
589+ pub ( crate ) fn set_aspect_by_changing_axis ( & mut self , aspect : f64 , axis : Axis ) {
590+ let current_aspect = self . aspect ( ) ;
591+
592+ let epsilon = 1e-5 ;
593+ if ( current_aspect - aspect) . abs ( ) < epsilon {
594+ // Don't make any changes when the aspect is already almost correct.
595+ return ;
596+ }
597+
598+ match axis {
599+ Axis :: X => {
600+ self . bounds
601+ . expand_x ( ( aspect / current_aspect - 1.0 ) * self . bounds . width ( ) * 0.5 ) ;
602+ }
603+ Axis :: Y => {
604+ self . bounds
605+ . expand_y ( ( current_aspect / aspect - 1.0 ) * self . bounds . height ( ) * 0.5 ) ;
606+ }
607+ }
608+ }
609+ }
0 commit comments