1use crate::field::{Field, NodeId, NodeKind, NodeState, Rect, Vec2};
2use crate::viewport::Viewport;
3
4#[derive(Clone, Debug, PartialEq)]
7pub struct NodeVisual {
8 pub id: NodeId,
9
10 pub pos: Vec2,
12 pub size: Vec2,
13
14 pub kind: NodeKind,
16 pub state: NodeState,
17
18 pub anchored: bool,
21
22 pub label: String,
24 pub label_scale: f32,
25
26 pub is_cluster_core: bool,
28 pub cluster_member_count: Option<usize>,
29
30 pub z: i32,
32
33 pub alpha: f32,
35}
36
37#[derive(Clone, Copy, Debug)]
39pub struct VisualParams {
40 pub zoom: f32,
42
43 pub focused: Option<NodeId>,
45
46 pub min_label_scale: f32,
48 pub max_label_scale: f32,
49}
50
51impl Default for VisualParams {
52 fn default() -> Self {
53 Self {
54 zoom: 1.0,
55 focused: None,
56 min_label_scale: 1.0,
57 max_label_scale: 4.0,
58 }
59 }
60}
61
62fn make_visual(field: &Field, id: NodeId, params: VisualParams) -> NodeVisual {
63 let n = field
64 .node(id)
65 .expect("make_visual called with missing node");
66
67 let zoom = params.zoom.max(0.0001);
68
69 let label_scale = (1.0 / zoom).clamp(params.min_label_scale, params.max_label_scale);
73
74 let is_cluster_core = n.kind == NodeKind::Core;
75
76 let cluster_member_count = if is_cluster_core {
78 field
79 .cluster_id_for_core_public(id)
80 .and_then(|cid| field.cluster(cid))
81 .map(|c| c.members().len())
82 } else {
83 None
84 };
85
86 let mut z = 0;
91 if params.focused == Some(id) {
92 z += 10_000;
93 }
94 if is_cluster_core {
95 z += 1_000;
96 }
97 z += match n.state {
98 NodeState::Active => 300,
99 NodeState::Drifting => 150,
100 NodeState::Node => 100,
101 NodeState::Core => 400,
102 };
103
104 let alpha = match n.state {
106 NodeState::Node => 0.85,
107 _ => 1.0,
108 };
109
110 NodeVisual {
111 id,
112 pos: n.pos,
113 size: n.footprint,
114 kind: n.kind.clone(),
115 state: n.state.clone(),
116
117 anchored: n.pinned,
119
120 label: n.label.clone(),
121 label_scale,
122
123 is_cluster_core,
124 cluster_member_count,
125
126 z,
127 alpha,
128 }
129}
130
131pub fn build_visuals(field: &Field, _vp: &Viewport, params: VisualParams) -> Vec<NodeVisual> {
136 let mut out = Vec::new();
137
138 for (&id, _) in field.nodes().iter() {
139 if !field.participates_in_field_view(id) {
140 continue;
141 }
142 if !field.is_visible(id) {
143 continue;
144 }
145 out.push(make_visual(field, id, params));
146 }
147
148 out.sort_by(|a, b| {
150 a.z.cmp(&b.z)
151 .then_with(|| a.id.as_u64().cmp(&b.id.as_u64()))
152 });
153 out
154}
155
156pub fn build_visuals_in_view(
158 field: &Field,
159 _vp: &Viewport,
160 view: Rect,
161 params: VisualParams,
162) -> Vec<NodeVisual> {
163 let mut out = Vec::new();
164
165 for (&id, _) in field.nodes().iter() {
166 if !field.participates_in_field_view(id) {
167 continue;
168 }
169 if !field.is_visible(id) {
170 continue;
171 }
172 if field.bounds(id).is_some_and(|b| b.intersects(view)) {
173 out.push(make_visual(field, id, params));
174 }
175 }
176
177 out.sort_by(|a, b| {
178 a.z.cmp(&b.z)
179 .then_with(|| a.id.as_u64().cmp(&b.id.as_u64()))
180 });
181 out
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187 use crate::field::Field;
188
189 #[test]
190 fn visuals_skip_hidden_nodes() {
191 let mut f = Field::new();
192 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
193 let b = f.spawn_surface("B", Vec2 { x: 20.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
194
195 assert!(f.set_hidden(b, true));
196
197 let vp = Viewport::new(Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 100.0, y: 100.0 });
198 let visuals = build_visuals(&f, &vp, VisualParams::default());
199
200 assert_eq!(visuals.len(), 1);
201 assert_eq!(visuals[0].id, a);
202 assert_eq!(visuals[0].label, "A");
203 }
204
205 #[test]
206 fn label_scale_grows_when_zoomed_out() {
207 let mut f = Field::new();
208 let _a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
209
210 let vp = Viewport::new(Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 100.0, y: 100.0 });
211
212 let v1 = build_visuals(
213 &f,
214 &vp,
215 VisualParams {
216 zoom: 1.0,
217 ..Default::default()
218 },
219 );
220 let v2 = build_visuals(
221 &f,
222 &vp,
223 VisualParams {
224 zoom: 0.5,
225 ..Default::default()
226 },
227 );
228
229 assert!(v2[0].label_scale > v1[0].label_scale);
230 assert_eq!(v1[0].label_scale, 1.0);
231 assert_eq!(v2[0].label_scale, 2.0);
232 }
233
234 #[test]
235 fn focused_node_draws_on_top() {
236 let mut f = Field::new();
237 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
238 let b = f.spawn_surface("B", Vec2 { x: 20.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
239
240 let vp = Viewport::new(Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 100.0, y: 100.0 });
241
242 let visuals = build_visuals(
243 &f,
244 &vp,
245 VisualParams {
246 focused: Some(b),
247 ..Default::default()
248 },
249 );
250
251 let za = visuals.iter().find(|v| v.id == a).unwrap().z;
252 let zb = visuals.iter().find(|v| v.id == b).unwrap().z;
253 assert!(zb > za);
254 }
255
256 #[test]
257 fn in_view_filters_nodes() {
258 let mut f = Field::new();
259 let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
260 let _b = f.spawn_surface("B", Vec2 { x: 100.0, y: 100.0 }, Vec2 { x: 10.0, y: 10.0 });
261
262 let vp = Viewport::new(Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 100.0, y: 100.0 });
263
264 let view = Rect {
265 min: Vec2 { x: -20.0, y: -20.0 },
266 max: Vec2 { x: 20.0, y: 20.0 },
267 };
268
269 let visuals = build_visuals_in_view(&f, &vp, view, VisualParams::default());
270 assert_eq!(visuals.len(), 1);
271 assert_eq!(visuals[0].id, a);
272 }
273}