jellyflow-runtime 0.1.0

Headless store, rules, schema, profile, and change pipeline for Jellyflow.
Documentation
use jellyflow_core::core::{CanvasPoint, CanvasRect};

use super::bounds::CanvasBounds;

#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct ViewportFitFrame {
    width_px: f32,
    height_px: f32,
    margin_x: f32,
    margin_y: f32,
}

impl ViewportFitFrame {
    pub(crate) fn from_viewport_and_padding(
        width_px: f32,
        height_px: f32,
        padding: f32,
        margin_px_fallback: f32,
    ) -> Self {
        let (margin_x, margin_y) = if padding > 0.0 {
            (width_px * padding, height_px * padding)
        } else {
            (margin_px_fallback, margin_px_fallback)
        };

        Self {
            width_px,
            height_px,
            margin_x,
            margin_y,
        }
    }

    pub(crate) fn available_width(self) -> f32 {
        self.width_px - 2.0 * self.margin_x
    }

    pub(crate) fn available_height(self) -> f32 {
        self.height_px - 2.0 * self.margin_y
    }

    pub(crate) fn pan_for_center(self, center: CanvasPoint, zoom: f32) -> CanvasPoint {
        CanvasPoint {
            x: 0.5 * self.width_px / zoom - center.x,
            y: 0.5 * self.height_px / zoom - center.y,
        }
    }

    pub(crate) fn fit_rect(
        self,
        target_canvas: CanvasRect,
        min_zoom: f32,
        max_zoom: f32,
    ) -> Option<(CanvasPoint, f32)> {
        if !target_canvas.is_positive_finite() {
            return None;
        }

        let zoom_x = self.available_width() / target_canvas.size.width;
        let zoom_y = self.available_height() / target_canvas.size.height;
        if !zoom_x.is_finite() || !zoom_y.is_finite() {
            return None;
        }

        let zoom = zoom_x.min(zoom_y).clamp(min_zoom, max_zoom);
        if !zoom.is_finite() || zoom <= 0.0 {
            return None;
        }

        let bounds = CanvasBounds::from_rect(target_canvas)?;
        let pan = self.pan_for_center(bounds.center(), zoom);
        if !pan.is_finite() {
            return None;
        }

        Some((pan, zoom))
    }
}

#[cfg(test)]
mod tests {
    use super::ViewportFitFrame;
    use jellyflow_core::core::{CanvasPoint, CanvasRect, CanvasSize};

    #[test]
    fn viewport_fit_frame_targets_canvas_rect() {
        let frame = ViewportFitFrame::from_viewport_and_padding(800.0, 600.0, 0.0, 24.0);
        let (pan, zoom) = frame
            .fit_rect(
                CanvasRect {
                    origin: CanvasPoint { x: 100.0, y: 50.0 },
                    size: CanvasSize {
                        width: 400.0,
                        height: 200.0,
                    },
                },
                0.1,
                4.0,
            )
            .expect("target");

        assert!((zoom - 1.88).abs() <= 1.0e-6);
        assert!((pan.x - (-87.23404)).abs() <= 1.0e-4);
        assert!((pan.y - 9.574471).abs() <= 1.0e-4);
    }
}