1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
//! 视口模块:维护世界坐标与屏幕像素坐标之间的映射关系。
//! `ViewRect` 描述世界坐标下的轴对齐可视矩形,`Viewport` 在其之上记录屏幕尺寸与缩放倍率,
//! 渲染后端、捕捉与拾取等上层逻辑都应通过本模块换算坐标,避免各自实现一套变换。
use crate::geometry::Point;
use crate::math::Vector2;
/// 世界坐标系下的轴对齐矩形,用于表示视图范围或实体包围盒。
/// 四边均为闭边界,落在边界上的点也算作在矩形内;坐标单位为世界单位。
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ViewRect {
/// x 方向的较小边界。
pub min_x: f64,
/// y 方向的较小边界。
pub min_y: f64,
/// x 方向的较大边界。
pub max_x: f64,
/// y 方向的较大边界。
pub max_y: f64,
}
impl ViewRect {
/// 用四个边界值构造矩形,参数按原样保存,不做排序或合法性校验。
/// - `min_x`、`min_y`、`max_x`、`max_y`:世界坐标下的边界值。
pub fn new(min_x: f64, min_y: f64, max_x: f64, max_y: f64) -> Self {
Self { min_x, min_y, max_x, max_y }
}
/// x 方向的跨度 `max_x - min_x`;边界反向时为负值,调用方需自行保证边界有序。
pub fn width(&self) -> f64 {
self.max_x - self.min_x
}
/// y 方向的跨度 `max_y - min_y`;边界反向时为负值。
pub fn height(&self) -> f64 {
self.max_y - self.min_y
}
/// 矩形中心的世界坐标 `(x, y)`。
pub fn center(&self) -> (f64, f64) {
((self.min_x + self.max_x) / 2.0, (self.min_y + self.max_y) / 2.0)
}
/// 判断世界坐标点是否在矩形内,四条边界上的点视为包含(闭区间)。
/// # 示例
/// ```
/// use cadrs::render::ViewRect;
/// let rect = ViewRect::new(0.0, 0.0, 100.0, 100.0);
/// assert!(rect.contains(50.0, 50.0));
/// assert!(!rect.contains(150.0, 150.0));
/// ```
pub fn contains(&self, x: f64, y: f64) -> bool {
x >= self.min_x && x <= self.max_x && y >= self.min_y && y <= self.max_y
}
/// 以中心为基准按比例缩放,返回新矩形,`self` 保持不变。
/// - `factor`:宽高缩放因子,大于 1 表示扩大可视范围,小于 1 表示收缩。
pub fn expand(&self, factor: f64) -> Self {
let cx = (self.min_x + self.max_x) / 2.0;
let cy = (self.min_y + self.max_y) / 2.0;
let hw = self.width() / 2.0 * factor;
let hh = self.height() / 2.0 * factor;
Self::new(cx - hw, cy - hh, cx + hw, cy + hh)
}
}
/// 视口:把世界坐标映射到屏幕像素坐标。
/// `view_rect` 是当前可见的世界区域,映射时会把它拉伸铺满 `size` 指定的屏幕尺寸,
/// 并且屏幕 y 轴向下、世界 y 轴向上,因此 y 方向在换算中翻转。
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Viewport {
/// 视图平移量(世界单位),由 `pan` 累加,默认 `(0.0, 0.0)`。
pub origin: (f64, f64),
/// 屏幕像素尺寸 `(宽, 高)`,默认 `(800.0, 600.0)`。
pub size: (f64, f64),
/// 累计缩放倍率,`zoom_at` 会把新因子乘到该值上,默认 `1.0`。
pub zoom: f64,
/// 视口旋转角(弧度),仅作为状态保存,当前的坐标换算尚未使用该字段。
pub rotation: f64,
/// 当前可见的世界坐标矩形,决定 `world_to_screen` 的映射范围。
pub view_rect: ViewRect,
}
impl Viewport {
/// 创建默认视口:屏幕 800×600 像素、缩放 1.0、旋转 0,可视区域为 `(0, 0)-(800, 600)`。
pub fn new() -> Self {
Self {
origin: (0.0, 0.0),
size: (800.0, 600.0),
zoom: 1.0,
rotation: 0.0,
view_rect: ViewRect::new(0.0, 0.0, 800.0, 600.0),
}
}
/// 创建指定屏幕像素尺寸的视口,可视区域初始为 `(0, 0)-(width, height)`,缩放为 1.0。
/// - `width`、`height`:屏幕像素尺寸,不做正数校验。
pub fn with_size(width: f64, height: f64) -> Self {
let view_rect = ViewRect::new(0.0, 0.0, width, height);
Self {
origin: (0.0, 0.0),
size: (width, height),
zoom: 1.0,
rotation: 0.0,
view_rect,
}
}
/// 直接设置可视区域:以 `(x, y)` 为左下角、按给定宽高生成 `view_rect`。
/// 不修改 `origin`、`zoom`、`size`,因此不会改变屏幕上的缩放比例。
pub fn set_view(&mut self, x: f64, y: f64, width: f64, height: f64) {
self.view_rect = ViewRect::new(x, y, x + width, y + height);
}
/// 平移视图:`origin` 与 `view_rect` 同步偏移 `(dx, dy)`,缩放倍率与可视范围大小不变。
/// - `dx`、`dy`:世界单位下的位移量,正值表示视图向正方向移动。
pub fn pan(&mut self, dx: f64, dy: f64) {
self.origin.0 += dx;
self.origin.1 += dy;
self.view_rect.min_x += dx;
self.view_rect.max_x += dx;
self.view_rect.min_y += dy;
self.view_rect.max_y += dy;
}
/// 按因子缩放视图:可视范围宽高变为原来的 `1 / factor`,并围绕当前可视区域中心收缩或扩张。
/// 缩放结果通过直接改写 `view_rect` 体现,`zoom` 字段同步累乘。
/// - `factor`:缩放因子,大于 1 表示放大;必须为正,为 0 时结果无意义。
/// - `center_x`、`center_y`:为指定缩放中心预留,当前实现未使用该中心,始终以可视区域中心缩放。
pub fn zoom_at(&mut self, factor: f64, center_x: f64, center_y: f64) {
let before_width = self.view_rect.width();
let before_height = self.view_rect.height();
self.zoom *= factor;
let after_width = before_width / factor;
let after_height = before_height / factor;
let cx = (self.view_rect.min_x + self.view_rect.max_x) / 2.0;
let cy = (self.view_rect.min_y + self.view_rect.max_y) / 2.0;
self.view_rect.min_x = cx - after_width / 2.0;
self.view_rect.max_x = cx + after_width / 2.0;
self.view_rect.min_y = cy - after_height / 2.0;
self.view_rect.max_y = cy + after_height / 2.0;
}
/// 缩放到给定世界矩形:以 `rect` 为中心建立可视区域,四周额外留出 10% 余量以免图形贴边。
/// 仅改写 `view_rect`,不更新 `origin` 与 `zoom`。
/// - `rect`:目标世界矩形,通常来自 `document_bbox` 等包围盒计算。
pub fn zoom_extents(&mut self, rect: ViewRect) {
let padding = 1.1;
let width = rect.width() * padding;
let height = rect.height() * padding;
let cx = (rect.min_x + rect.max_x) / 2.0;
let cy = (rect.min_y + rect.max_y) / 2.0;
self.view_rect.min_x = cx - width / 2.0;
self.view_rect.max_x = cx + width / 2.0;
self.view_rect.min_y = cy - height / 2.0;
self.view_rect.max_y = cy + height / 2.0;
}
/// 世界坐标转屏幕像素坐标,y 方向翻转(世界 y 增大对应屏幕 y 减小)。
/// 可视区域内的点映射到 `0..=size` 的像素范围,区域外的点会得到越界值,不做裁剪。
/// - `x`、`y`:世界坐标,单位与 `view_rect` 一致。
/// 返回 `(screen_x, screen_y)`,单位是屏幕像素。
/// # 示例
/// ```
/// use cadrs::render::Viewport;
/// let mut viewport = Viewport::with_size(800.0, 600.0);
/// viewport.set_view(0.0, 0.0, 100.0, 100.0);
/// let (sx, sy) = viewport.world_to_screen(50.0, 50.0); // (400.0, 300.0)
/// ```
pub fn world_to_screen(&self, x: f64, y: f64) -> (f64, f64) {
let vw = self.view_rect.width();
let vh = self.view_rect.height();
let sw = self.size.0;
let sh = self.size.1;
let screen_x = (x - self.view_rect.min_x) / vw * sw;
let screen_y = sh - (y - self.view_rect.min_y) / vh * sh;
(screen_x, screen_y)
}
/// 屏幕像素坐标转世界坐标,是 `world_to_screen` 的逆变换,同样翻转 y 方向。
/// - `x`、`y`:以左上角为原点、y 轴向下、单位为像素的屏幕坐标。
/// 返回世界坐标 `(world_x, world_y)`。
pub fn screen_to_world(&self, x: f64, y: f64) -> (f64, f64) {
let vw = self.view_rect.width();
let vh = self.view_rect.height();
let sw = self.size.0;
let sh = self.size.1;
let world_x = self.view_rect.min_x + x / sw * vw;
let world_y = self.view_rect.min_y + (sh - y) / sh * vh;
(world_x, world_y)
}
}
impl Default for Viewport {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_view_rect() {
let rect = ViewRect::new(0.0, 0.0, 100.0, 100.0);
assert_eq!(rect.width(), 100.0);
assert_eq!(rect.height(), 100.0);
assert!(rect.contains(50.0, 50.0));
assert!(!rect.contains(150.0, 150.0));
}
#[test]
fn test_viewport_world_to_screen() {
let mut viewport = Viewport::with_size(800.0, 600.0);
viewport.set_view(0.0, 0.0, 100.0, 100.0);
let (sx, sy) = viewport.world_to_screen(50.0, 50.0);
assert!((sx - 400.0).abs() < 1e-10);
assert!((sy - 300.0).abs() < 1e-10);
}
#[test]
fn test_viewport_pan() {
let mut viewport = Viewport::new();
viewport.pan(100.0, 50.0);
assert_eq!(viewport.origin.0, 100.0);
assert_eq!(viewport.origin.1, 50.0);
}
}