Skip to main content

motion_core/authoring/
spatial.rs

1//! Logical placement and double-precision bounds.
2
3use crate::{Pivot, Point, Rect, Transform, ValidationError, Vector2};
4
5/// An edge in the downward-positive parent coordinate system.
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7pub enum Side {
8    /// The left edge.
9    Left,
10    /// The right edge.
11    Right,
12    /// The upper edge.
13    Top,
14    /// The lower edge.
15    Bottom,
16}
17
18impl Side {
19    pub(crate) fn adjacent(self, subject: Rect, reference: Rect, gap: f64) -> Vector2 {
20        match self {
21            Self::Left => Vector2::new(
22                reference.left - subject.right - gap,
23                reference.center().y - subject.center().y,
24            ),
25            Self::Right => Vector2::new(
26                reference.right - subject.left + gap,
27                reference.center().y - subject.center().y,
28            ),
29            Self::Top => Vector2::new(
30                reference.center().x - subject.center().x,
31                reference.top - subject.bottom - gap,
32            ),
33            Self::Bottom => Vector2::new(
34                reference.center().x - subject.center().x,
35                reference.bottom - subject.top + gap,
36            ),
37        }
38    }
39
40    pub(crate) fn aligned(self, subject: Rect, reference: Rect) -> Vector2 {
41        match self {
42            Self::Left => Vector2::new(reference.left - subject.left, 0.0),
43            Self::Right => Vector2::new(reference.right - subject.right, 0.0),
44            Self::Top => Vector2::new(0.0, reference.top - subject.top),
45            Self::Bottom => Vector2::new(0.0, reference.bottom - subject.bottom),
46        }
47    }
48}
49
50#[derive(Clone, Debug)]
51pub(super) struct Relative {
52    pub(super) node: super::Node,
53    pub(super) reference: super::Node,
54    side: Side,
55    gap: Option<f64>,
56}
57
58pub(super) fn relative(
59    node: super::Node,
60    reference: super::Node,
61    side: Side,
62    gap: Option<f64>,
63) -> super::Node {
64    super::Node::new(super::node::NodeKind::Relative(Relative {
65        node,
66        reference,
67        side,
68        gap,
69    }))
70}
71
72impl Relative {
73    pub(super) fn translation(
74        &self,
75        subject: Rect,
76        reference: Rect,
77    ) -> Result<Transform, ValidationError> {
78        if self.gap.is_some_and(|gap| !gap.is_finite()) {
79            return Err(ValidationError::invalid(
80                "layout",
81                "gap",
82                "Relative layout gap must be finite",
83            ));
84        }
85        let offset = match self.gap {
86            Some(gap) => self.side.adjacent(subject, reference, gap),
87            None => self.side.aligned(subject, reference),
88        };
89        if !offset.x.is_finite() || !offset.y.is_finite() {
90            return Err(ValidationError::invalid(
91                "layout",
92                "translation",
93                "Relative layout offset must remain finite",
94            ));
95        }
96        Ok(Transform::translate(offset.x, offset.y))
97    }
98}
99
100#[derive(Clone, Copy, Debug)]
101pub(super) enum Bounds {
102    Empty,
103    Known(Rect),
104}
105
106impl Bounds {
107    pub fn declared(rect: Rect, path: &str) -> Result<Self, ValidationError> {
108        if !rect.is_finite() || rect.right < rect.left || rect.bottom < rect.top {
109            return Err(ValidationError::invalid(
110                path,
111                "layout_bounds",
112                "Require finite, noninverted declared layout bounds",
113            ));
114        }
115        Ok(Self::Known(rect))
116    }
117
118    pub fn rect(self, _path: &str) -> Result<Rect, ValidationError> {
119        match self {
120            Self::Empty => Ok(Rect::default()),
121            Self::Known(rect) => Ok(rect),
122        }
123    }
124
125    pub fn union(self, other: Self) -> Self {
126        match (self, other) {
127            (Self::Empty, bounds) | (bounds, Self::Empty) => bounds,
128            (Self::Known(first), Self::Known(second)) => Self::Known(Rect::new(
129                first.left.min(second.left),
130                first.top.min(second.top),
131                first.right.max(second.right),
132                first.bottom.max(second.bottom),
133            )),
134        }
135    }
136
137    pub fn transformed(self, transform: Transform, path: &str) -> Result<Self, ValidationError> {
138        let Self::Known(rect) = self else {
139            return Ok(self);
140        };
141        let corners = [
142            Point::new(rect.left, rect.top),
143            Point::new(rect.right, rect.top),
144            Point::new(rect.right, rect.bottom),
145            Point::new(rect.left, rect.bottom),
146        ]
147        .map(|point| transform.apply(point));
148        if corners
149            .iter()
150            .any(|point| !point.x.is_finite() || !point.y.is_finite())
151        {
152            return Err(ValidationError::invalid(
153                path,
154                "transform",
155                "Transformed layout bounds must remain finite",
156            ));
157        }
158        Ok(Self::Known(Rect::new(
159            corners
160                .iter()
161                .map(|point| point.x)
162                .fold(f64::INFINITY, f64::min),
163            corners
164                .iter()
165                .map(|point| point.y)
166                .fold(f64::INFINITY, f64::min),
167            corners
168                .iter()
169                .map(|point| point.x)
170                .fold(f64::NEG_INFINITY, f64::max),
171            corners
172                .iter()
173                .map(|point| point.y)
174                .fold(f64::NEG_INFINITY, f64::max),
175        )))
176    }
177}
178
179#[derive(Clone, Copy, Debug)]
180pub(super) struct Placement {
181    pub affine: Transform,
182    pub position: Option<Point>,
183    pub translation: Vector2,
184    pub rotation: f64,
185    pub scale: Vector2,
186    pub pivot: Pivot,
187}
188
189impl Default for Placement {
190    fn default() -> Self {
191        Self {
192            affine: Transform::IDENTITY,
193            position: None,
194            translation: Vector2::ZERO,
195            rotation: 0.0,
196            scale: Vector2::new(1.0, 1.0),
197            pivot: Pivot::Center,
198        }
199    }
200}
201
202pub(super) struct Resolved {
203    pub transform: Transform,
204}
205
206impl Placement {
207    pub fn resolve(self, bounds: Bounds, path: &str) -> Result<Resolved, ValidationError> {
208        if !self.affine.is_finite() {
209            return Err(ValidationError::invalid(
210                path,
211                "transform",
212                "Local affine matrix must be finite",
213            ));
214        }
215        let placed_bounds = bounds.transformed(self.affine, path)?;
216        let finite = [
217            self.translation.x,
218            self.translation.y,
219            self.rotation,
220            self.scale.x,
221            self.scale.y,
222        ]
223        .iter()
224        .all(|value| value.is_finite())
225            && self
226                .position
227                .is_none_or(|position| position.x.is_finite() && position.y.is_finite())
228            && match self.pivot {
229                Pivot::Relative(horizontal, vertical) => {
230                    horizontal.is_finite() && vertical.is_finite()
231                }
232                Pivot::Point(point) => point.x.is_finite() && point.y.is_finite(),
233                _ => true,
234            };
235        if !finite {
236            return Err(ValidationError::invalid(
237                path,
238                "transform",
239                "Placement parameters must be finite",
240            ));
241        }
242        let needs_pivot =
243            self.position.is_some() || self.rotation != 0.0 || self.scale != Vector2::new(1.0, 1.0);
244        let pivot = if !needs_pivot {
245            Point::ORIGIN
246        } else if let Pivot::Point(point) = self.pivot {
247            point
248        } else {
249            placed_bounds.rect(path)?.anchor(self.pivot)
250        };
251        let offset = self
252            .position
253            .map_or(Vector2::ZERO, |position| position - pivot)
254            + self.translation;
255        let transform = if self.rotation == 0.0 && self.scale == Vector2::new(1.0, 1.0) {
256            Transform::translate(offset.x, offset.y)
257        } else {
258            Transform::translate(-pivot.x, -pivot.y)
259                .then(Transform::scale_non_uniform(self.scale.x, self.scale.y))
260                .then(Transform::rotate(self.rotation))
261                .then(Transform::translate(pivot.x + offset.x, pivot.y + offset.y))
262        };
263        let transform = self.affine.then(transform);
264        if !transform.is_finite() {
265            return Err(ValidationError::invalid(
266                path,
267                "transform",
268                "Resolved placement must remain finite",
269            ));
270        }
271        Ok(Resolved { transform })
272    }
273}