Skip to main content

sectorsync_core/
interest.rs

1//! Interest query primitives used by replication planning.
2
3use crate::entity::{EntityRecord, EntityTags};
4use crate::ids::ClientId;
5use crate::spatial::{Frustum3, Position3};
6
7/// Viewer-side interest query.
8#[derive(Clone, Copy, Debug, PartialEq)]
9pub struct ViewerQuery {
10    /// Client requesting visible or interesting entities.
11    pub client_id: ClientId,
12    /// Viewer position.
13    pub position: Position3,
14    /// Primary spherical interest radius.
15    pub radius: f32,
16    /// Optional maximum number of selected entities.
17    pub max_entities: usize,
18}
19
20impl ViewerQuery {
21    /// Returns squared interest radius.
22    pub fn radius_squared(self) -> f32 {
23        self.radius * self.radius
24    }
25}
26
27/// Visibility hook. Embedders can provide frustum or occlusion-aware filters.
28pub trait VisibilityFilter {
29    /// Returns whether an entity is visible enough to be considered.
30    fn is_visible(&self, viewer: &ViewerQuery, entity: &EntityRecord) -> bool;
31
32    /// Returns visibility when the planner already computed squared distance.
33    ///
34    /// Custom filters can keep implementing only [`Self::is_visible`]. Range-aware
35    /// filters should override this method to avoid repeating distance work.
36    fn is_visible_with_distance(
37        &self,
38        viewer: &ViewerQuery,
39        entity: &EntityRecord,
40        _distance_squared: f32,
41    ) -> bool {
42        self.is_visible(viewer, entity)
43    }
44}
45
46/// Range-only visibility filter.
47#[derive(Clone, Copy, Debug, Default)]
48pub struct RangeOnlyVisibility;
49
50impl VisibilityFilter for RangeOnlyVisibility {
51    fn is_visible(&self, viewer: &ViewerQuery, entity: &EntityRecord) -> bool {
52        entity.position.distance_squared(viewer.position) <= viewer.radius_squared()
53    }
54
55    fn is_visible_with_distance(
56        &self,
57        viewer: &ViewerQuery,
58        _entity: &EntityRecord,
59        distance_squared: f32,
60    ) -> bool {
61        distance_squared <= viewer.radius_squared()
62    }
63}
64
65/// Frustum visibility filter.
66#[derive(Clone, Copy, Debug, PartialEq)]
67pub struct FrustumVisibility {
68    /// Six-plane visibility volume.
69    pub frustum: Frustum3,
70}
71
72impl FrustumVisibility {
73    /// Creates a frustum visibility filter.
74    pub const fn new(frustum: Frustum3) -> Self {
75        Self { frustum }
76    }
77}
78
79impl VisibilityFilter for FrustumVisibility {
80    fn is_visible(&self, _viewer: &ViewerQuery, entity: &EntityRecord) -> bool {
81        self.frustum
82            .intersects_bounds(entity.position, entity.bounds)
83    }
84}
85
86/// Tag visibility filter using business-defined entity tag bits.
87#[derive(Clone, Copy, Debug, PartialEq, Eq)]
88pub struct TagVisibility {
89    /// Tags that must all be present.
90    pub required: EntityTags,
91    /// Tags where any match rejects the entity.
92    pub excluded: EntityTags,
93}
94
95impl TagVisibility {
96    /// Creates a tag filter.
97    pub const fn new(required: EntityTags, excluded: EntityTags) -> Self {
98        Self { required, excluded }
99    }
100
101    /// Creates a tag filter requiring all bits in `required`.
102    pub const fn require(required: EntityTags) -> Self {
103        Self::new(required, EntityTags::EMPTY)
104    }
105
106    /// Creates a tag filter excluding any bit in `excluded`.
107    pub const fn exclude(excluded: EntityTags) -> Self {
108        Self::new(EntityTags::EMPTY, excluded)
109    }
110}
111
112impl VisibilityFilter for TagVisibility {
113    fn is_visible(&self, _viewer: &ViewerQuery, entity: &EntityRecord) -> bool {
114        entity.tags.contains(self.required) && !entity.tags.intersects(self.excluded)
115    }
116}
117
118/// Visibility filter that requires both child filters to pass.
119#[derive(Clone, Copy, Debug, PartialEq, Eq)]
120pub struct AndVisibility<A, B> {
121    /// First filter.
122    pub left: A,
123    /// Second filter.
124    pub right: B,
125}
126
127impl<A, B> AndVisibility<A, B> {
128    /// Creates a filter that accepts only entities accepted by both filters.
129    pub const fn new(left: A, right: B) -> Self {
130        Self { left, right }
131    }
132}
133
134impl<A, B> VisibilityFilter for AndVisibility<A, B>
135where
136    A: VisibilityFilter,
137    B: VisibilityFilter,
138{
139    fn is_visible(&self, viewer: &ViewerQuery, entity: &EntityRecord) -> bool {
140        self.left.is_visible(viewer, entity) && self.right.is_visible(viewer, entity)
141    }
142
143    fn is_visible_with_distance(
144        &self,
145        viewer: &ViewerQuery,
146        entity: &EntityRecord,
147        distance_squared: f32,
148    ) -> bool {
149        self.left
150            .is_visible_with_distance(viewer, entity, distance_squared)
151            && self
152                .right
153                .is_visible_with_distance(viewer, entity, distance_squared)
154    }
155}