206 lines
6.5 KiB
Rust
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);
|
|
}
|
|
}
|