Skip to main content

motion_core/drawing/mesh/
evaluate.rs

1//! Analytic inverse-bilinear mesh patches shared by independent paint backends.
2
3use crate::{MeshVertex, Point, ValidationError, color};
4
5/// Maximum absolute normalized coordinate admitted by analytic patch arithmetic.
6const MAX_POSITION: f64 = 1e6;
7
8/// Shared root slack for points on patch boundaries.
9const ROOT_SLACK: f64 = 1e-6;
10
11/// Maximum absolute Oklab component, keeping cubed shader intermediates finite.
12const MAX_LAB_COMPONENT: f64 = 1e10;
13
14/// Newton-free inverse-bilinear patch descriptor with straight Oklab and alpha.
15#[derive(Clone, Debug, PartialEq)]
16pub struct MeshPatch {
17    positions: [Point; 4],
18    colors: [[f64; 4]; 4],
19}
20
21impl MeshPatch {
22    pub(super) fn new(vertices: [MeshVertex; 4]) -> Result<Self, ValidationError> {
23        let positions = vertices.map(|vertex| vertex.position);
24        let colors = vertices.map(|vertex| {
25            let [red, green, blue, alpha] = vertex.color.to_rgba_float();
26            let [lightness, horizontal, vertical] = color::srgb_to_oklab([red, green, blue]);
27            [lightness, horizontal, vertical, alpha]
28        });
29        if positions.iter().any(|point| {
30            !point.x.is_finite()
31                || !point.y.is_finite()
32                || point.x.abs() > MAX_POSITION
33                || point.y.abs() > MAX_POSITION
34        }) || colors
35            .iter()
36            .flatten()
37            .any(|value| !value.is_finite() || value.abs() > MAX_LAB_COMPONENT)
38        {
39            return Err(ValidationError::invalid(
40                "mesh_gradient",
41                "patch",
42                "Analytic patch coordinates and colors exceed finite working support",
43            ));
44        }
45        Ok(Self { positions, colors })
46    }
47
48    /// Corners in top-left, top-right, bottom-left, bottom-right lattice order.
49    pub fn positions(&self) -> &[Point; 4] {
50        &self.positions
51    }
52
53    /// Straight Oklab plus linear alpha, in the same corner order as positions.
54    pub fn colors(&self) -> &[[f64; 4]; 4] {
55        &self.colors
56    }
57
58    /// Evaluates a unit-object-space point; returns `None` outside this patch.
59    ///
60    /// Folded patches choose the first valid quadratic root. Degenerate patches
61    /// have no area. Color weights are cubic smoothstep in each patch coordinate.
62    /// Rejects nonfinite sample coordinates or nonfinite working-color results.
63    pub fn sample(&self, point: Point) -> Result<Option<[f32; 4]>, ValidationError> {
64        if !point.x.is_finite() || !point.y.is_finite() {
65            return Err(ValidationError::invalid(
66                "mesh_gradient",
67                "point",
68                "Mesh samples must be finite",
69            ));
70        }
71        let [a, b, c, d] = self.positions;
72        let b = [b.x - a.x, b.y - a.y];
73        let c = [c.x - a.x, c.y - a.y];
74        let d = [d.x - a.x - b[0] - c[0], d.y - a.y - b[1] - c[1]];
75        let q = [point.x - a.x, point.y - a.y];
76        let aa = -cross(b, d);
77        let bb = cross(q, d) - cross(b, c);
78        let cc = cross(q, c);
79        let scale = aa.abs().max(bb.abs()).max(cc.abs());
80        if scale == 0.0 {
81            return Ok(None);
82        }
83        let roots = if aa.abs() <= scale * 1e-12 {
84            if bb.abs() <= scale * 1e-12 {
85                return Ok(None);
86            }
87            [-cc / bb; 2]
88        } else {
89            let discriminant = bb * bb - 4.0 * aa * cc;
90            if discriminant < 0.0 {
91                return Ok(None);
92            }
93            let root = libm::sqrt(discriminant);
94            // Avoid cancellation near affine patches, retaining positive-root-first order.
95            let quotient = -0.5 * (bb + if bb >= 0.0 { root } else { -root });
96            if quotient == 0.0 {
97                [0.0; 2]
98            } else if bb >= 0.0 {
99                [cc / quotient, quotient / aa]
100            } else {
101                [quotient / aa, cc / quotient]
102            }
103        };
104        for u in roots {
105            let direction = [c[0] + d[0] * u, c[1] + d[1] * u];
106            let axis = usize::from(direction[1].abs() > direction[0].abs());
107            if direction[axis] == 0.0 {
108                continue;
109            }
110            let v = (q[axis] - b[axis] * u) / direction[axis];
111            if !(-ROOT_SLACK..=1.0 + ROOT_SLACK).contains(&u)
112                || !(-ROOT_SLACK..=1.0 + ROOT_SLACK).contains(&v)
113            {
114                continue;
115            }
116            let smooth = |value: f64| {
117                let value = value.clamp(0.0, 1.0);
118                value * value * (3.0 - 2.0 * value)
119            };
120            let (u, v) = (smooth(u), smooth(v));
121            let mixed: [f64; 4] = std::array::from_fn(|channel| {
122                let top = self.colors[0][channel]
123                    + (self.colors[1][channel] - self.colors[0][channel]) * u;
124                let bottom = self.colors[2][channel]
125                    + (self.colors[3][channel] - self.colors[2][channel]) * u;
126                top + (bottom - top) * v
127            });
128            let rgb =
129                color::oklab_to_srgb([mixed[0], mixed[1], mixed[2]]).map(color::srgb_to_linear);
130            let result = [
131                rgb[0] * mixed[3],
132                rgb[1] * mixed[3],
133                rgb[2] * mixed[3],
134                mixed[3],
135            ]
136            .map(|value| value as f32);
137            if result.iter().any(|value| !value.is_finite()) {
138                return Err(ValidationError::invalid(
139                    "mesh_gradient",
140                    "color",
141                    "Mesh sample exceeds finite working storage",
142                ));
143            }
144            return Ok(Some(result));
145        }
146        Ok(None)
147    }
148}
149
150fn cross(first: [f64; 2], second: [f64; 2]) -> f64 {
151    first[0] * second[1] - first[1] * second[0]
152}