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
use bevy::{prelude::*, ui::UiGlobalTransform};
use crate::viewport::SceneViewport;
/// Window-to-camera-space mapping for the scene viewport node.
///
/// `top_left` is where the viewport node starts in window logical pixels,
/// `vp_size` is its logical size, and `remap` scales window-local pixels
/// onto the camera's render target (which may differ from the UI node
/// size on `HiDPI` / fractional scaling).
pub(crate) struct ViewportRemap {
pub top_left: Vec2,
pub vp_size: Vec2,
pub remap: Vec2,
}
impl ViewportRemap {
/// Compute remap parameters from the camera and the scene viewport's
/// `ComputedNode` + `UiGlobalTransform`.
pub fn new(camera: &Camera, computed: &ComputedNode, vp_transform: &UiGlobalTransform) -> Self {
let scale = computed.inverse_scale_factor();
let vp_pos = vp_transform.translation * scale;
let vp_size = computed.size() * scale;
let top_left = vp_pos - vp_size / 2.0;
let target_size = camera.logical_viewport_size().unwrap_or(vp_size);
Self {
top_left,
vp_size,
remap: target_size / vp_size,
}
}
}
/// Convert a window cursor position to camera-space viewport coordinates,
/// remapping the cursor against a specific viewport UI-node entity rather
/// than assuming there's only one. Used by hover-routed systems that
/// already know which viewport the cursor is over (via `ActiveViewport`)
/// and by modal operators that captured a viewport at start.
///
/// The camera renders to an off-screen image whose logical size may differ
/// from the UI node's logical size (they diverge on `HiDPI` / fractional
/// scaling displays). This remaps UI-logical space into camera viewport
/// space so `camera.viewport_to_world()` / `camera.world_to_viewport()`
/// produce correct results.
pub(crate) fn window_to_viewport_cursor_for(
cursor_pos: Vec2,
camera: &Camera,
viewport_entity: Entity,
viewport_query: &Query<(&ComputedNode, &UiGlobalTransform), With<SceneViewport>>,
) -> Option<Vec2> {
let Ok((computed, vp_transform)) = viewport_query.get(viewport_entity) else {
return None;
};
remap_cursor(cursor_pos, camera, computed, vp_transform)
}
/// Like [`window_to_viewport_cursor_for`] but does not bounds-check
/// the cursor against the viewport rectangle. Returns `None` only if
/// `viewport_entity` is no longer a `SceneViewport`.
///
/// Used by modal operators that captured the viewport at drag-start
/// and need cursor coordinates to keep updating even when the user
/// drags past the viewport's edge into another panel. Bounds-checking
/// during a drag would force the operator to cancel mid-gesture and
/// snap the entity back to its start transform.
pub(crate) fn window_to_viewport_cursor_for_unbounded(
cursor_pos: Vec2,
camera: &Camera,
viewport_entity: Entity,
viewport_query: &Query<(&ComputedNode, &UiGlobalTransform), With<SceneViewport>>,
) -> Option<Vec2> {
let Ok((computed, vp_transform)) = viewport_query.get(viewport_entity) else {
return None;
};
let map = ViewportRemap::new(camera, computed, vp_transform);
Some((cursor_pos - map.top_left) * map.remap)
}
fn remap_cursor(
cursor_pos: Vec2,
camera: &Camera,
computed: &ComputedNode,
vp_transform: &UiGlobalTransform,
) -> Option<Vec2> {
let map = ViewportRemap::new(camera, computed, vp_transform);
let local = cursor_pos - map.top_left;
if local.x >= 0.0 && local.y >= 0.0 && local.x <= map.vp_size.x && local.y <= map.vp_size.y {
Some(local * map.remap)
} else {
None
}
}
/// Test whether a 2D point lies inside a convex or concave polygon (ray-casting algorithm).
pub(crate) fn point_in_polygon_2d(point: Vec2, polygon: &[Vec2]) -> bool {
let n = polygon.len();
if n < 3 {
return false;
}
let mut inside = false;
let mut j = n - 1;
for i in 0..n {
let pi = polygon[i];
let pj = polygon[j];
if ((pi.y > point.y) != (pj.y > point.y))
&& (point.x < (pj.x - pi.x) * (point.y - pi.y) / (pj.y - pi.y) + pi.x)
{
inside = !inside;
}
j = i;
}
inside
}
/// Distance from a point to a line segment.
pub(crate) fn point_to_segment_dist(point: Vec2, a: Vec2, b: Vec2) -> f32 {
let ab = b - a;
let ap = point - a;
let t = (ap.dot(ab) / ab.length_squared()).clamp(0.0, 1.0);
let closest = a + ab * t;
(point - closest).length()
}