Files

206 lines
6.5 KiB
Rust

//! 旧 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<Self, VmError> {
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<Size>) -> [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<Size>) -> 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);
}
}