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
//! Per-view clip bridge: imperative polygon clips against named XAML elements on a
//! single [`NoesisView`](crate::NoesisView). The clip counterpart of
//! [`crate::geometry`] — where that assigns a `Path`'s `Data`, this assigns any
//! element's [`UIElement::Clip`](noesis_runtime::view::FrameworkElement::set_clip_points).
//!
//! Add a [`NoesisClip`] component to the view's camera entity. Its `clips` map is
//! the desired clip polygon per `x:Name`, applied whenever the component changes
//! (Bevy change detection). Each set of points (in the element's own coordinate
//! space) becomes a filled Noesis `StreamGeometry` set as the element's `Clip`; an
//! empty polygon clears the clip. Rewriting the polygon each frame animates a
//! moving clip region.
//!
//! Everything runs on the main thread (Noesis is thread-affine and lives there):
//! the reconcile system reads each view's component and applies the writes against
//! that view's live scene. No cross-world queues.
use std::collections::HashMap;
use bevy::prelude::*;
use crate::render::{NoesisRenderState, NoesisSet};
/// Per-view clip bridge. Attach to a [`NoesisView`](crate::NoesisView) entity.
#[derive(Component, Clone, Default, Debug)]
pub struct NoesisClip {
/// Desired clip polygon per element `x:Name`. Written to the view's elements
/// whenever this component changes. Each value is a closed polygon through
/// `[x, y]` pairs in the element's own coordinate space; an empty `Vec` clears
/// the element's clip. A degenerate polygon (1 or 2 points) is skipped with a
/// warning on apply.
pub clips: HashMap<String, Vec<[f32; 2]>>,
}
impl NoesisClip {
/// Creates an empty bridge with no clips. Chain [`clip`](Self::clip) to add
/// polygons before inserting it on the [`NoesisView`](crate::NoesisView) camera.
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// Builder: set element `name`'s clip to the closed polygon through `points`
/// (empty clears it).
#[must_use]
pub fn clip(mut self, name: impl Into<String>, points: Vec<[f32; 2]>) -> Self {
self.clips.insert(name.into(), points);
self
}
/// Set element `name`'s clip from a system holding `&mut NoesisClip`. The
/// runtime counterpart of [`clip`](Self::clip): the next reconcile assigns the
/// polygon (or clears it, when empty) on the live element.
pub fn set(&mut self, name: impl Into<String>, points: Vec<[f32; 2]>) {
self.clips.insert(name.into(), points);
}
}
/// Reconcile every view's [`NoesisClip`]: apply the desired clip writes when the
/// component changed.
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn sync_clip_bridge(
views: Query<(Entity, Ref<NoesisClip>)>,
state: Option<NonSendMut<NoesisRenderState>>,
) {
let Some(mut state) = state else {
return;
};
for (entity, clip) in &views {
if clip.is_changed()
|| state.scene_rebuilt_this_frame(entity)
|| state.panel_mounted_this_frame(entity)
{
state.apply_clip_for(entity, &clip.clips);
}
}
}
/// Wires the per-view clip bridge. Added transitively by [`crate::NoesisPlugin`].
pub struct NoesisClipPlugin;
impl Plugin for NoesisClipPlugin {
fn build(&self, app: &mut App) {
// After `sync_panels` so a panel's `NoesisClip` re-applies the same frame
// its fragment mounts (the bridge reads `panel_mounted_this_frame`, set by
// `sync_panels`); mirrors the geometry bridge's ordering.
app.add_systems(
PostUpdate,
sync_clip_bridge
.in_set(NoesisSet::Apply)
.after(crate::panel::sync_panels),
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builder_collects_clips() {
let c = NoesisClip::new()
.clip("Panel", vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0]])
.clip("Cleared", vec![]);
assert_eq!(c.clips.get("Panel").map(Vec::len), Some(3));
assert_eq!(c.clips.get("Cleared").map(Vec::len), Some(0));
}
}