Skip to main content

universal_weave/layout/
mod.rs

1//! [`Weave`] rendering helpers.
2//!
3//! This library provides 3 different 2D [`Layouter`] implementations with identical behavior:
4//! - [`DependentLayouter`] - Takes a [`DependentWeave`] as an input.
5//! - [`IndependentLayouter`] - Takes an [`IndependentWeave`] as an input.
6//! - [`TopologicalLayouter`] - Takes any [`Weave`] as an input.
7
8use core::{
9    hash::{BuildHasher, Hash},
10    marker::PhantomData,
11    num::FpCategory,
12};
13
14use alloc::vec::Vec;
15use glam::Vec2;
16use scratchpads::Scratchpad;
17use tinyvec::ArrayVec;
18
19use crate::{
20    IndependentContents, LayoutItem, Layouter, Node, Weave,
21    dependent::{DependentNode, DependentWeave},
22    independent::{IndependentNode, IndependentWeave},
23    layout::positioner::Layout2D,
24};
25
26mod positioner;
27
28/// Minimum gaps in a [`Weave`] layout.
29///
30/// All values must be finite normal numbers >= 0.
31#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
32#[must_use]
33pub struct Spacing {
34    /// Gap between adjacent nodes.
35    pub node: f32,
36    /// Gap between layers of nodes.
37    pub layer: f32,
38    /// Reserved space for edges.
39    pub corridor: f32,
40    /// Gap between edges and adjacent items.
41    pub edge: f32,
42}
43
44impl Default for Spacing {
45    fn default() -> Self {
46        Self {
47            node: 16.0,
48            layer: 16.0,
49            corridor: 0.0,
50            edge: 8.0,
51        }
52    }
53}
54
55impl Spacing {
56    /// Validates that all spacing values are finite normal numbers >= 0.
57    #[must_use]
58    pub const fn validate(&self) -> bool {
59        validate_float(self.node)
60            && validate_float(self.layer)
61            && validate_float(self.corridor)
62            && validate_float(self.edge)
63    }
64}
65
66/// A 2D [`Layouter`] which takes a [`DependentWeave`] as input.
67///
68/// This layout algorithm has identical behavior to [`TopologicalLayouter`].
69#[derive(Default, Debug, Clone)]
70#[must_use]
71pub struct DependentLayouter<K>
72where
73    K: Hash + Copy + Eq + Ord,
74{
75    /// The [`Spacing`] used to arrange contents.
76    pub spacing: Spacing,
77
78    layout: Layout2D<K>,
79}
80
81impl<K> DependentLayouter<K>
82where
83    K: Hash + Copy + Eq + Ord,
84{
85    /// Creates a new [`DependentLayouter`] with the specified spacing.
86    pub fn new(spacing: Spacing) -> Self {
87        Self {
88            spacing,
89            layout: Layout2D::default(),
90        }
91    }
92}
93
94impl<K, T, M, S>
95    Layouter<DependentWeave<K, T, M, S>, K, DependentNode<K, T, S>, T, Vec2, ArrayVec<[Vec2; 6]>>
96    for DependentLayouter<K>
97where
98    K: Hash + Copy + Eq + Ord + 'static,
99    S: BuildHasher + Default + Clone + 'static,
100{
101    fn layout(&mut self, weave: &mut DependentWeave<K, T, M, S>, sizes: impl FnMut(&K) -> Vec2) {
102        self.layout.layout_dependent(weave, sizes, &self.spacing);
103    }
104    fn size(&self) -> Vec2 {
105        self.layout.size()
106    }
107    fn view(
108        &mut self,
109        min: Vec2,
110        max: Vec2,
111        callback: impl FnMut(LayoutItem<K, Vec2, ArrayVec<[Vec2; 6]>>),
112    ) {
113        self.layout.view(min, max, callback);
114    }
115}
116
117/// A 2D [`Layouter`] which takes an [`IndependentWeave`] as input.
118///
119/// This layout algorithm has identical behavior to [`TopologicalLayouter`].
120#[derive(Default, Debug, Clone)]
121#[must_use]
122pub struct IndependentLayouter<K>
123where
124    K: Hash + Copy + Eq + Ord,
125{
126    /// The [`Spacing`] used to arrange contents.
127    pub spacing: Spacing,
128
129    layout: Layout2D<K>,
130    topological: Vec<K>,
131}
132
133impl<K> IndependentLayouter<K>
134where
135    K: Hash + Copy + Eq + Ord,
136{
137    /// Creates a new [`IndependentLayouter`] with the specified spacing.
138    pub fn new(spacing: Spacing) -> Self {
139        Self {
140            spacing,
141            layout: Layout2D::default(),
142            topological: Vec::new(),
143        }
144    }
145}
146
147impl<K, T, M, S>
148    Layouter<
149        IndependentWeave<K, T, M, S>,
150        K,
151        IndependentNode<K, T, S>,
152        T,
153        Vec2,
154        ArrayVec<[Vec2; 6]>,
155    > for IndependentLayouter<K>
156where
157    K: Hash + Copy + Eq + Ord + 'static,
158    T: IndependentContents,
159    S: BuildHasher + Default + Clone + 'static,
160{
161    fn layout(&mut self, weave: &mut IndependentWeave<K, T, M, S>, sizes: impl FnMut(&K) -> Vec2) {
162        weave.get_ordered_identifiers(&mut self.topological);
163
164        self.layout
165            .layout_independent(weave, sizes, &self.spacing, &mut self.topological);
166    }
167    fn size(&self) -> Vec2 {
168        self.layout.size()
169    }
170    fn view(
171        &mut self,
172        min: Vec2,
173        max: Vec2,
174        callback: impl FnMut(LayoutItem<K, Vec2, ArrayVec<[Vec2; 6]>>),
175    ) {
176        self.layout.view(min, max, callback);
177    }
178}
179
180/// A 2D [`Layouter`] which orders nodes using [`Weave::get_ordered_identifiers()`].
181///
182/// However, this additional flexibility may result in worse performance and memory usage characteristics compared to [`DependentLayouter`] or [`IndependentLayouter`].
183#[derive(Debug, Clone)]
184#[must_use]
185pub struct TopologicalLayouter<K, S>
186where
187    K: Hash + Copy + Eq + Ord,
188    S: BuildHasher + Default + Clone,
189{
190    /// The [`Spacing`] used to arrange contents.
191    pub spacing: Spacing,
192
193    layout: Layout2D<K>,
194    topological: Vec<K>,
195    scratchpad: Scratchpad,
196    _hasher: PhantomData<S>,
197}
198
199impl<K, S> Default for TopologicalLayouter<K, S>
200where
201    K: Hash + Copy + Eq + Ord,
202    S: BuildHasher + Default + Clone,
203{
204    fn default() -> Self {
205        Self::new(Spacing::default())
206    }
207}
208
209impl<K, S> TopologicalLayouter<K, S>
210where
211    K: Hash + Copy + Eq + Ord,
212    S: BuildHasher + Default + Clone,
213{
214    /// Creates a new [`TopologicalLayouter`] with the specified spacing.
215    pub fn new(spacing: Spacing) -> Self {
216        Self {
217            spacing,
218            layout: Layout2D::default(),
219            topological: Vec::new(),
220            scratchpad: Scratchpad::new(),
221            _hasher: PhantomData,
222        }
223    }
224}
225
226impl<W, K, N, T, S> Layouter<W, K, N, T, Vec2, ArrayVec<[Vec2; 6]>> for TopologicalLayouter<K, S>
227where
228    W: Weave<K, N, T>,
229    K: Hash + Copy + Eq + Ord + 'static,
230    N: Node<K, T>,
231    S: BuildHasher + Default + Clone + 'static,
232    for<'a> &'a N::From: IntoIterator<Item = &'a K>,
233{
234    fn layout(&mut self, weave: &mut W, sizes: impl FnMut(&K) -> Vec2) {
235        weave.get_ordered_identifiers(&mut self.topological);
236
237        assert_eq!(
238            weave.len(),
239            self.topological.len(),
240            "Malformed topological order"
241        );
242
243        self.layout.layout_topological::<W, N, T, S, _>(
244            weave,
245            sizes,
246            &self.spacing,
247            &mut self.scratchpad,
248            &mut self.topological,
249        );
250    }
251    fn size(&self) -> Vec2 {
252        self.layout.size()
253    }
254    fn view(
255        &mut self,
256        min: Vec2,
257        max: Vec2,
258        callback: impl FnMut(LayoutItem<K, Vec2, ArrayVec<[Vec2; 6]>>),
259    ) {
260        self.layout.view(min, max, callback);
261    }
262}
263
264/// Smooths a polyline produced by this module's [`Layouter`] implementation into a chain of cubic Bézier segments.
265///
266/// This function may produce incorrect results if used to process polylines from other [`Layouter`] implementations.
267#[allow(
268    clippy::float_arithmetic,
269    clippy::arithmetic_side_effects,
270    reason = "Coordinate calculation"
271)]
272#[must_use]
273pub fn smooth(points: ArrayVec<[Vec2; 6]>) -> ArrayVec<[[Vec2; 4]; 5]> {
274    let mut segments = ArrayVec::new();
275
276    for [start, end] in points.array_windows::<2>().copied() {
277        let y_diff = end.y - start.y;
278
279        segments.push(if y_diff == 0.0 {
280            let x_diff = end.x - start.x;
281
282            [
283                start,
284                start + Vec2::new(x_diff * (1.0 / 3.0), 0.0),
285                start + Vec2::new(x_diff * (2.0 / 3.0), 0.0),
286                end,
287            ]
288        } else {
289            let arm = Vec2::new(0.0, y_diff * 0.5);
290
291            [start, start + arm, end - arm, end]
292        });
293    }
294
295    segments
296}
297
298#[must_use]
299const fn validate_float(value: f32) -> bool {
300    matches!(value.classify(), FpCategory::Normal | FpCategory::Zero) && value.is_sign_positive()
301}
302
303#[must_use]
304const fn validate_vec2(value: Vec2) -> bool {
305    validate_float(value.x) && validate_float(value.y)
306}
307
308#[must_use]
309const fn validate_output_float(value: f32) -> bool {
310    matches!(
311        value.classify(),
312        FpCategory::Normal | FpCategory::Zero | FpCategory::Subnormal
313    ) && value.is_sign_positive()
314}