//! 旧 Page.mode 的兼容空间策略,以及页面级的正逆相机变换。 //! 新组合应优先使用普通布局、ScrollView 和独立场景视口;保留这里是为了兼容已有 window/vertical/horizontal/canvas 页面。 //! //! Page space is independent of the native window. The camera is applied once //! for painting and inverted once for input; layout and Mounts stay in world units. use crate::{geometry::Point, layout::Size}; use endor_core::VmError; use serde::{Deserialize, Serialize}; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] /// 旧 Page 的有限或自由空间模式,新场景视口不依赖它表达布局方向。 pub enum Mode { #[default] Window, Vertical, Horizontal, Canvas, } impl Mode { /// 返回内部枚举或对象对应的稳定源名称。 pub fn name(self) -> &'static str { match self { Self::Window => "window", Self::Vertical => "vertical", Self::Horizontal => "horizontal", Self::Canvas => "canvas", } } /// 把外部源码或属性表示转换为内部枚举/结构,并反馈不支持的输入。 pub fn parse(name: &str) -> Result { match name { "window" => Ok(Self::Window), "vertical" => Ok(Self::Vertical), "horizontal" => Ok(Self::Horizontal), "canvas" => Ok(Self::Canvas), _ => Err(VmError::InvalidArgument( "Page.mode expects window, vertical, horizontal or canvas", )), } } /// 为旧页面模式生成有限轴和可延伸轴的根约束。 pub fn constraints(self, viewport: Option) -> [f32; 4] { let Some(size) = viewport else { return crate::layout::root_constraints(None); }; let limit = crate::ui_bridge::MAX_DIMENSION as f32; match self { Self::Window => [size.width, size.height, size.width, size.height], Self::Vertical => [size.width, size.height, size.width, limit], Self::Horizontal => [size.width, size.height, limit, size.height], Self::Canvas => [0.0, 0.0, limit, limit], } } } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] /// 页面级兼容相机状态和有限内容范围,支持绘制与命中正逆变换。 pub struct Surface { pub mode: Mode, pub page: String, pub pan: Point, #[serde(default = "unit")] pub zoom: f32, pub extent: Size, } /// 生成页面相机计算所需的单位比例或边界默认值。 fn unit() -> f32 { 1.0 } impl Default for Surface { /// 建立类型约定的初始值,供尚未发布或尚未交互的状态使用。 fn default() -> Self { Self { mode: Mode::Window, page: String::new(), pan: Point::default(), zoom: 1.0, extent: Size::default(), } } } impl Surface { /// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。 pub fn new(mode: Mode) -> Self { Self { mode, zoom: 1.0, ..Self::default() } } /// 把页面视图坐标反算为旧 Page 相机下的世界坐标。 pub fn to_world(&self, point: Point) -> Point { Point::new( point.x / self.zoom + self.pan.x, point.y / self.zoom + self.pan.y, ) } /// 把旧页面世界坐标变换到可见视图坐标。 pub fn to_view(&self, point: Point) -> Point { Point::new( (point.x - self.pan.x) * self.zoom, (point.y - self.pan.y) * self.zoom, ) } /// 判断给定点是否位于目标范围;需要相机转换的类型会先转换再判断。 pub fn contains(&self, point: [f32; 2], viewport: Option) -> bool { let point = self.to_view(Point::new(point[0], point[1])); point.x >= 0.0 && point.y >= 0.0 && viewport.is_none_or(|size| point.x < size.width && point.y < size.height) } /// 只对有限页面按实际内容范围钳制偏移;自由画布不按内容边缘停止导航。 pub fn clamp(&mut self, viewport: Size) { match self.mode { Mode::Window => { self.pan = Point::default(); self.zoom = 1.0; } Mode::Vertical => { self.zoom = 1.0; self.pan.x = 0.0; self.pan.y = self .pan .y .clamp(0.0, (self.extent.height - viewport.height).max(0.0)); } Mode::Horizontal => { self.zoom = 1.0; self.pan.y = 0.0; self.pan.x = self .pan .x .clamp(0.0, (self.extent.width - viewport.width).max(0.0)); } Mode::Canvas => {} } } } #[cfg(test)] mod tests { use super::*; #[test] fn camera_inverse_and_cursor_anchored_zoom_preserve_world_position() { let mut view = Surface::new(Mode::Canvas); view.pan = Point::new(-240.0, 120.0); view.zoom = 0.75; let pointer = Point::new(300.0, 180.0); let world = view.to_world(pointer); assert_eq!(view.to_view(world), pointer); view.zoom = 2.0; view.pan = Point::new( world.x - pointer.x / view.zoom, world.y - pointer.y / view.zoom, ); assert_eq!(view.to_view(world), pointer); assert!(view.contains( [world.x, world.y], Some(Size { width: 500.0, height: 400.0 }) )); assert!(!view.contains( [-1000.0, -1000.0], Some(Size { width: 500.0, height: 400.0 }) )); } #[test] fn finite_pages_clamp_after_content_shrinks() { let mut view = Surface::new(Mode::Vertical); view.pan = Point::new(25.0, 900.0); view.extent = Size { width: 400.0, height: 800.0, }; view.clamp(Size { width: 400.0, height: 600.0, }); assert_eq!(view.pan, Point::new(0.0, 200.0)); view.extent.height = 100.0; view.clamp(Size { width: 400.0, height: 600.0, }); assert_eq!(view.pan.y, 0.0); } }