Skip to main content

motion_content/particles/
collision.rs

1//! Swept geometric contacts and immutable, budgeted signed-distance grids.
2//!
3//! # Particle radius
4//!
5//! Every collider treats `Particle::size` as the particle's collision radius in
6//! the same local design units as the collider geometry; the particle is a disc,
7//! not a point. A contact occurs when the signed distance from the particle
8//! centre to the collider surface is at most `size` (plus a `1e-6` tolerance):
9//!
10//! * circles and segments measure the distance to the circle/segment boundary;
11//! * polygons use the distance to the nearest edge, negative inside, so a disc
12//!   straddling an edge or fully inside a solid polygon is pushed out along the
13//!   edge normal;
14//! * SDF grids sample the stored signed distance (negative inside) and use its
15//!   gradient as the contact normal.
16//!
17//! `size` is the birth size multiplied by any `SizeOverLifetime`, so a particle
18//! that grows collides earlier. Set `size` to zero for point-like behaviour.
19//! Because `size` is also the render scale, styles whose base radius differs from
20//! one design unit should scale collider geometry accordingly.
21
22use super::*;
23use motion_core::{Point, Rect};
24
25/// Largest coordinate magnitude for which [`swept_from`] may skip a particle using collider
26/// bounds; probe rounding there stays far below [`BOUNDS_MARGIN`].
27const BOUNDS_EXTENT: f64 = 1e7;
28
29/// Absolute slack, in design units, added to the bounds test for probe rounding.
30const BOUNDS_MARGIN: f64 = 1e-3;
31
32/// Bounded normal restitution and tangential friction.
33#[derive(Clone, Copy, Debug)]
34pub struct CollisionResponse {
35    /// Normal velocity restitution in 0..=1.
36    pub restitution: f32,
37    /// Fraction of tangential velocity removed in 0..=1.
38    pub friction: f32,
39    /// Remove the particle at its first contact.
40    pub kill_on_hit: bool,
41}
42
43impl Default for CollisionResponse {
44    fn default() -> Self {
45        Self {
46            restitution: 1.0,
47            friction: 0.0,
48            kill_on_hit: false,
49        }
50    }
51}
52
53impl CollisionResponse {
54    pub(super) fn validate(self) -> Result<(), ValidationError> {
55        if !self.restitution.is_finite()
56            || !self.friction.is_finite()
57            || !(0.0..=1.0).contains(&self.restitution)
58            || !(0.0..=1.0).contains(&self.friction)
59        {
60            return Err(invalid(
61                "collision",
62                "Restitution/friction must be finite in 0..=1",
63            ));
64        }
65        Ok(())
66    }
67}
68
69fn nearest(point: Point, start: Point, end: Point) -> (f64, [f64; 2]) {
70    let delta = [end.x - start.x, end.y - start.y];
71    let square = delta[0] * delta[0] + delta[1] * delta[1];
72    let progress = if square == 0.0 {
73        0.0
74    } else {
75        ((point.x - start.x) * delta[0] + (point.y - start.y) * delta[1]) / square
76    }
77    .clamp(0.0, 1.0);
78    let difference = [
79        point.x - start.x - delta[0] * progress,
80        point.y - start.y - delta[1] * progress,
81    ];
82    let distance = libm::hypot(difference[0], difference[1]);
83    let normal = if distance > 1e-12 {
84        difference.map(|value| value / distance)
85    } else {
86        let length = libm::hypot(delta[0], delta[1]).max(1e-12);
87        [-delta[1] / length, delta[0] / length]
88    };
89    (distance, normal)
90}
91
92fn inside_polygon(point: Point, vertices: &[Point]) -> bool {
93    let mut inside = false;
94    for index in 0..vertices.len() {
95        let first = vertices[index];
96        let second = vertices[(index + 1) % vertices.len()];
97        if (first.y > point.y) != (second.y > point.y)
98            && point.x < (second.x - first.x) * (point.y - first.y) / (second.y - first.y) + first.x
99        {
100            inside = !inside;
101        }
102    }
103    inside
104}
105
106fn swept(
107    particle: &mut Particle,
108    step: f32,
109    response: CollisionResponse,
110    bounds: Option<Rect>,
111    distance: impl Fn(Point) -> (f64, [f64; 2]),
112) -> Result<bool, ValidationError> {
113    swept_from(particle, step, response, bounds, distance, 0.0)
114}
115
116/// `bounds` encloses all collider geometry, or is `None` to always run the probe loop.
117fn swept_from(
118    particle: &mut Particle,
119    step: f32,
120    response: CollisionResponse,
121    bounds: Option<Rect>,
122    distance: impl Fn(Point) -> (f64, [f64; 2]),
123    mut progress: f64,
124) -> Result<bool, ValidationError> {
125    particle.validate()?;
126    response.validate()?;
127    if !step.is_finite() || step <= 0.0 {
128        return Err(invalid(
129            "collision",
130            "Contact step must be positive and finite",
131        ));
132    }
133    let start = particle.position.map(f64::from);
134    let velocity = particle.velocity.map(f64::from);
135    let travel = libm::hypot(velocity[0], velocity[1]) * f64::from(step);
136    // `size` is the collision radius for all collider shapes.
137    let radius = f64::from(particle.size);
138    if let Some(bounds) = bounds {
139        // A path farther than `radius + travel / 32` from the bounds never contacts, and each
140        // probe below then advances by more than 1/32 of the travel, so the loop would end
141        // without contact well inside its iteration allowance. Skipping it is exact.
142        let end = [
143            start[0] + velocity[0] * f64::from(step),
144            start[1] + velocity[1] * f64::from(step),
145        ];
146        let margin = radius + travel / 32.0 + BOUNDS_MARGIN;
147        if start
148            .iter()
149            .chain(&end)
150            .all(|value| value.abs() <= BOUNDS_EXTENT)
151            && (start[0].min(end[0]) - margin > bounds.right
152                || start[0].max(end[0]) + margin < bounds.left
153                || start[1].min(end[1]) - margin > bounds.bottom
154                || start[1].max(end[1]) + margin < bounds.top)
155        {
156            return Ok(false);
157        }
158    }
159    for _ in 0..64 {
160        let point = Point::new(
161            start[0] + velocity[0] * f64::from(step) * progress,
162            start[1] + velocity[1] * f64::from(step) * progress,
163        );
164        let (signed, normal) = distance(point);
165        if !signed.is_finite() || normal.iter().any(|value| !value.is_finite()) {
166            return Err(invalid("collision", "Nonfinite contact geometry"));
167        }
168        if signed <= radius + 1e-6 {
169            let contact = [
170                point.x + normal[0] * (radius - signed + 1e-5),
171                point.y + normal[1] * (radius - signed + 1e-5),
172            ];
173            let incoming = velocity[0] * normal[0] + velocity[1] * normal[1];
174            let normal_speed = if incoming < 0.0 {
175                -incoming * f64::from(response.restitution)
176            } else {
177                incoming
178            };
179            let tangent = [
180                velocity[0] - incoming * normal[0],
181                velocity[1] - incoming * normal[1],
182            ];
183            particle.velocity = std::array::from_fn(|axis| {
184                (normal[axis] * normal_speed + tangent[axis] * (1.0 - f64::from(response.friction)))
185                    as f32
186            });
187            particle.position = std::array::from_fn(|axis| {
188                (contact[axis] - f64::from(particle.velocity[axis]) * f64::from(step) * progress)
189                    as f32
190            });
191            if response.kill_on_hit {
192                particle.lifetime = particle.age.max(f32::MIN_POSITIVE);
193                particle.position = contact.map(|value| value as f32);
194            }
195            particle.validate()?;
196            return Ok(true);
197        }
198        if progress >= 1.0 || travel == 0.0 {
199            return Ok(false);
200        }
201        progress = (progress + ((signed - radius) / travel).max(1e-7)).min(1.0);
202    }
203    Err(limit("Swept collision iteration allowance exhausted"))
204}
205
206/// Solid circular obstacle with swept collision detection.
207#[derive(Clone, Copy, Debug)]
208pub struct CircleCollider {
209    /// Obstacle center.
210    pub center: Point,
211    /// Nonnegative obstacle radius.
212    pub radius: f64,
213    /// Contact response.
214    pub response: CollisionResponse,
215}
216
217impl CircleCollider {
218    fn contact(&self, particle: &mut Particle, step: f32) -> Result<bool, ValidationError> {
219        if !self.radius.is_finite()
220            || self.radius < 0.0
221            || !self.center.x.is_finite()
222            || !self.center.y.is_finite()
223        {
224            return Err(invalid("circle", "Invalid collider circle"));
225        }
226        swept(particle, step, self.response, None, |point| {
227            let delta = [point.x - self.center.x, point.y - self.center.y];
228            let length = libm::hypot(delta[0], delta[1]);
229            (
230                length - self.radius,
231                if length == 0.0 {
232                    [1.0, 0.0]
233                } else {
234                    delta.map(|value| value / length)
235                },
236            )
237        })
238    }
239}
240
241/// Two-sided finite segment, including endpoint contacts.
242#[derive(Clone, Copy, Debug)]
243pub struct SegmentCollider {
244    /// Segment start.
245    pub start: Point,
246    /// Segment end.
247    pub end: Point,
248    /// Contact response.
249    pub response: CollisionResponse,
250}
251
252impl SegmentCollider {
253    fn contact(&self, particle: &mut Particle, step: f32) -> Result<bool, ValidationError> {
254        if [self.start.x, self.start.y, self.end.x, self.end.y]
255            .iter()
256            .any(|value| !value.is_finite())
257            || self.start == self.end
258        {
259            return Err(invalid("segment", "Require finite distinct endpoints"));
260        }
261        particle.validate()?;
262        self.response.validate()?;
263        if !step.is_finite() || step <= 0.0 {
264            return Err(invalid(
265                "collision",
266                "Contact step must be positive and finite",
267            ));
268        }
269        let origin = Point::new(
270            f64::from(particle.position[0]),
271            f64::from(particle.position[1]),
272        );
273        let motion = particle
274            .velocity
275            .map(|value| f64::from(value) * f64::from(step));
276        let radius = f64::from(particle.size);
277        let mut hit = if nearest(origin, self.start, self.end).0 <= radius + 1e-6 {
278            Some(0.0)
279        } else {
280            None
281        };
282        let delta = [self.end.x - self.start.x, self.end.y - self.start.y];
283        let length = libm::hypot(delta[0], delta[1]);
284        let axis = delta.map(|value| value / length);
285        let normal = [-axis[1], axis[0]];
286        let relative = [origin.x - self.start.x, origin.y - self.start.y];
287        let side = relative[0] * normal[0] + relative[1] * normal[1];
288        let crossing = motion[0] * normal[0] + motion[1] * normal[1];
289        let mut admit = |progress: f64| {
290            if (0.0..=1.0).contains(&progress) {
291                hit = Some(hit.map_or(progress, |prior: f64| prior.min(progress)));
292            }
293        };
294        if crossing != 0.0 {
295            for edge in [-radius, radius] {
296                let progress = (edge - side) / crossing;
297                let along = (relative[0] + motion[0] * progress) * axis[0]
298                    + (relative[1] + motion[1] * progress) * axis[1];
299                if (0.0..=length).contains(&along) {
300                    admit(progress);
301                }
302            }
303        }
304        let square = motion[0] * motion[0] + motion[1] * motion[1];
305        if square > 0.0 {
306            for end in [self.start, self.end] {
307                let offset = [origin.x - end.x, origin.y - end.y];
308                let projection = offset[0] * motion[0] + offset[1] * motion[1];
309                let constant = offset[0] * offset[0] + offset[1] * offset[1] - radius * radius;
310                let discriminant = projection * projection - square * constant;
311                if discriminant >= 0.0 {
312                    admit((-projection - libm::sqrt(discriminant)) / square);
313                }
314            }
315        }
316        let Some(progress) = hit else {
317            return Ok(false);
318        };
319        swept_from(
320            particle,
321            step,
322            self.response,
323            None,
324            |point| nearest(point, self.start, self.end),
325            progress,
326        )
327    }
328}
329
330/// Bounded simple polygon obstacle, prepared once rather than per particle.
331#[derive(Clone, Debug)]
332pub struct PolygonCollider {
333    vertices: Arc<[Point]>,
334    response: CollisionResponse,
335    /// Vertex bounds for the swept early-out, absent beyond [`BOUNDS_EXTENT`].
336    bounds: Option<Rect>,
337}
338
339impl PolygonCollider {
340    /// Validates 3..=256 finite vertices and rejects intersecting nonadjacent edges.
341    pub fn new(
342        vertices: Arc<[Point]>,
343        response: CollisionResponse,
344    ) -> Result<Self, ValidationError> {
345        response.validate()?;
346        if !(3..=256).contains(&vertices.len())
347            || vertices
348                .iter()
349                .any(|point| !point.x.is_finite() || !point.y.is_finite())
350        {
351            return Err(invalid("polygon", "Require 3..=256 finite vertices"));
352        }
353        let cross = |first: Point, second: Point, third: Point| {
354            (second.x - first.x) * (third.y - first.y) - (second.y - first.y) * (third.x - first.x)
355        };
356        let area = (0..vertices.len())
357            .map(|index| {
358                vertices[index].x * vertices[(index + 1) % vertices.len()].y
359                    - vertices[(index + 1) % vertices.len()].x * vertices[index].y
360            })
361            .sum::<f64>();
362        if !area.is_finite() || area.abs() < 1e-12 {
363            return Err(invalid("polygon", "Degenerate polygon"));
364        }
365        for first in 0..vertices.len() {
366            if vertices[first] == vertices[(first + 1) % vertices.len()] {
367                return Err(invalid("polygon", "Duplicate adjacent vertices"));
368            }
369            for second in first + 2..vertices.len() {
370                if first == 0 && second == vertices.len() - 1 {
371                    continue;
372                }
373                let ends = [
374                    vertices[first],
375                    vertices[(first + 1) % vertices.len()],
376                    vertices[second],
377                    vertices[(second + 1) % vertices.len()],
378                ];
379                let overlap = ends[0].x.min(ends[1].x) <= ends[2].x.max(ends[3].x)
380                    && ends[2].x.min(ends[3].x) <= ends[0].x.max(ends[1].x)
381                    && ends[0].y.min(ends[1].y) <= ends[2].y.max(ends[3].y)
382                    && ends[2].y.min(ends[3].y) <= ends[0].y.max(ends[1].y);
383                if overlap
384                    && cross(ends[0], ends[1], ends[2]) * cross(ends[0], ends[1], ends[3]) <= 0.0
385                    && cross(ends[2], ends[3], ends[0]) * cross(ends[2], ends[3], ends[1]) <= 0.0
386                {
387                    return Err(invalid("polygon", "Polygon edges intersect"));
388                }
389            }
390        }
391        let bounds = vertices
392            .iter()
393            .all(|point| point.x.abs() <= BOUNDS_EXTENT && point.y.abs() <= BOUNDS_EXTENT)
394            .then(|| {
395                vertices.iter().skip(1).fold(
396                    Rect::new(vertices[0].x, vertices[0].y, vertices[0].x, vertices[0].y),
397                    |bounds, point| {
398                        Rect::new(
399                            bounds.left.min(point.x),
400                            bounds.top.min(point.y),
401                            bounds.right.max(point.x),
402                            bounds.bottom.max(point.y),
403                        )
404                    },
405                )
406            });
407        Ok(Self {
408            vertices,
409            response,
410            bounds,
411        })
412    }
413
414    fn contact(&self, particle: &mut Particle, step: f32) -> Result<bool, ValidationError> {
415        swept(particle, step, self.response, self.bounds, |point| {
416            let (distance, normal) = (0..self.vertices.len())
417                .map(|index| {
418                    nearest(
419                        point,
420                        self.vertices[index],
421                        self.vertices[(index + 1) % self.vertices.len()],
422                    )
423                })
424                .min_by(|first, second| first.0.total_cmp(&second.0))
425                .unwrap_or((0.0, [1.0, 0.0]));
426            if inside_polygon(point, &self.vertices) {
427                (-distance, normal.map(|value| -value))
428            } else {
429                (distance, normal)
430            }
431        })
432    }
433}
434
435macro_rules! collider {
436    ($name:ident, $work:expr) => {
437        impl ParticleModifier for $name {
438            fn apply(
439                &self,
440                particle: &mut Particle,
441                step: f32,
442                _: Time,
443                _: &mut Random,
444            ) -> Result<(), ValidationError> {
445                self.contact(particle, step).map(|_| ())
446            }
447            fn apply_event(
448                &self,
449                particle: &mut Particle,
450                step: f32,
451                _: Time,
452                _: &mut Random,
453            ) -> Result<bool, ValidationError> {
454                self.contact(particle, step)
455            }
456            fn work(&self) -> u64 {
457                $work(self)
458            }
459        }
460    };
461}
462collider!(CircleCollider, |_: &CircleCollider| 64);
463collider!(SegmentCollider, |_: &SegmentCollider| 64);
464collider!(
465    PolygonCollider,
466    |collider: &PolygonCollider| collider.vertices.len() as u64 * 128
467);
468
469/// Precomputed signed samples plus exact cached boundary guards for thin features.
470/// All data is immutable and can be shared through SceneContext::cache.
471#[derive(Clone, Debug)]
472pub struct SignedDistanceGrid {
473    bounds: Rect,
474    dimensions: [u32; 2],
475    values: Arc<[f32]>,
476    surface: Arc<crate::VectorSurface>,
477    boundary: Arc<[(Point, Point)]>,
478}
479
480impl SignedDistanceGrid {
481    /// Shares prepared immutable grids in the existing bounded SceneCache.
482    /// The key includes geometry identity, resolution and preparation budgets.
483    #[track_caller]
484    pub fn cached<Key: Eq + std::hash::Hash + Send + Sync + 'static>(
485        context: &motion_core::SceneContext<'_>,
486        key: Key,
487        surface: Arc<crate::VectorSurface>,
488        bounds: Rect,
489        dimensions: [u32; 2],
490        limits: SimulationLimits,
491    ) -> Result<Arc<Self>, ValidationError> {
492        let identity = Arc::as_ptr(&surface) as usize;
493        let geometry = [
494            bounds.left.to_bits(),
495            bounds.top.to_bits(),
496            bounds.right.to_bits(),
497            bounds.bottom.to_bits(),
498        ];
499        context
500            .cache_with_cost(
501                (
502                    key,
503                    identity,
504                    geometry,
505                    dimensions,
506                    limits.max_bytes,
507                    limits.max_work,
508                ),
509                || {
510                    let result =
511                        Self::new(surface.clone(), bounds, dimensions, limits).map(Arc::new);
512                    (surface, result)
513                },
514                |(_, result)| result.as_ref().map_or(0, |grid| grid.bytes() as usize),
515            )
516            .1
517            .clone()
518    }
519
520    /// Prepares an explicitly resolved grid under SimulationLimits; no downsampling fallback.
521    pub fn new(
522        surface: Arc<crate::VectorSurface>,
523        bounds: Rect,
524        dimensions: [u32; 2],
525        limits: SimulationLimits,
526    ) -> Result<Self, ValidationError> {
527        motion_core::drawing::validate_rect(bounds, "particles.sdf", "bounds")?;
528        let [width, height] = dimensions;
529        let count = u64::from(width) * u64::from(height);
530        if width < 2
531            || height < 2
532            || width > 2048
533            || height > 2048
534            || bounds.width() <= 0.0
535            || bounds.height() <= 0.0
536            || count * 4 + surface.triangles().len() as u64 * 512 > limits.max_bytes
537        {
538            return Err(limit("SDF resolution/byte allowance exhausted"));
539        }
540        let contains = |point: Point| {
541            surface
542                .triangles()
543                .iter()
544                .any(|triangle| inside_polygon(point, &triangle.vertices))
545        };
546        let mut boundary = Vec::new();
547        let mut work = 0u64;
548        for triangle in surface.triangles() {
549            for index in 0..3 {
550                let start = triangle.vertices[index];
551                let end = triangle.vertices[(index + 1) % 3];
552                let delta = [end.x - start.x, end.y - start.y];
553                let length = libm::hypot(delta[0], delta[1]);
554                if length == 0.0 {
555                    continue;
556                }
557                let margin = surface.tolerance().min(length * 0.001).max(1e-9);
558                let middle = Point::new((start.x + end.x) * 0.5, (start.y + end.y) * 0.5);
559                work += surface.triangles().len() as u64 * 6;
560                if work > limits.max_work {
561                    return Err(limit("SDF boundary work exhausted"));
562                }
563                if contains(Point::new(
564                    middle.x - delta[1] / length * margin,
565                    middle.y + delta[0] / length * margin,
566                )) != contains(Point::new(
567                    middle.x + delta[1] / length * margin,
568                    middle.y - delta[0] / length * margin,
569                )) {
570                    boundary.push((start, end));
571                }
572            }
573        }
574        if boundary.is_empty() {
575            return Err(invalid("sdf", "Surface has no resolved boundary"));
576        }
577        let mut values = Vec::new();
578        values
579            .try_reserve_exact(count as usize)
580            .map_err(|_| limit("SDF grid allocation failed"))?;
581        for row in 0..height {
582            for column in 0..width {
583                work += surface.triangles().len() as u64 * 3 + boundary.len() as u64;
584                if work > limits.max_work {
585                    return Err(limit("SDF grid work exhausted"));
586                }
587                let point = Point::new(
588                    bounds.left + f64::from(column) / f64::from(width - 1) * bounds.width(),
589                    bounds.top + f64::from(row) / f64::from(height - 1) * bounds.height(),
590                );
591                let distance = boundary
592                    .iter()
593                    .map(|(start, end)| nearest(point, *start, *end).0)
594                    .fold(f64::INFINITY, f64::min);
595                values.push((distance * if contains(point) { -1.0 } else { 1.0 }) as f32);
596            }
597        }
598        Ok(Self {
599            bounds,
600            dimensions,
601            values: values.into(),
602            surface,
603            boundary: boundary.into(),
604        })
605    }
606
607    /// Exact retained grid and boundary payload, suitable for SceneCache cost accounting.
608    pub fn bytes(&self) -> u64 {
609        self.values.len() as u64 * 4
610            + self.boundary.len() as u64 * 32
611            + self.surface.triangles().len() as u64
612                * std::mem::size_of::<crate::SurfaceTriangle>() as u64
613    }
614
615    /// Bilinear grid sampling, with an exact immutable boundary guard within one cell diagonal.
616    pub fn sample(&self, point: Point) -> (f64, [f64; 2]) {
617        let cell = [
618            self.bounds.width() / f64::from(self.dimensions[0] - 1),
619            self.bounds.height() / f64::from(self.dimensions[1] - 1),
620        ];
621        let coordinate = [
622            (point.x - self.bounds.left) / cell[0],
623            (point.y - self.bounds.top) / cell[1],
624        ];
625        let outside = coordinate[0] < 0.0
626            || coordinate[1] < 0.0
627            || coordinate[0] > f64::from(self.dimensions[0] - 1)
628            || coordinate[1] > f64::from(self.dimensions[1] - 1);
629        let position = [
630            coordinate[0].clamp(0.0, f64::from(self.dimensions[0] - 1)),
631            coordinate[1].clamp(0.0, f64::from(self.dimensions[1] - 1)),
632        ];
633        let low = position.map(|value| libm::floor(value) as u32);
634        let high = [
635            (low[0] + 1).min(self.dimensions[0] - 1),
636            (low[1] + 1).min(self.dimensions[1] - 1),
637        ];
638        let value =
639            |column, row| f64::from(self.values[(row * self.dimensions[0] + column) as usize]);
640        let values = [
641            value(low[0], low[1]),
642            value(high[0], low[1]),
643            value(low[0], high[1]),
644            value(high[0], high[1]),
645        ];
646        let fraction = [
647            position[0] - f64::from(low[0]),
648            position[1] - f64::from(low[1]),
649        ];
650        let distance = (values[0] * (1.0 - fraction[0]) + values[1] * fraction[0])
651            * (1.0 - fraction[1])
652            + (values[2] * (1.0 - fraction[0]) + values[3] * fraction[0]) * fraction[1];
653        if outside || distance.abs() <= libm::hypot(cell[0], cell[1]) * 2.0 {
654            let (distance, normal) = self
655                .boundary
656                .iter()
657                .map(|(start, end)| nearest(point, *start, *end))
658                .min_by(|first, second| first.0.total_cmp(&second.0))
659                .unwrap_or((0.0, [1.0, 0.0]));
660            if self
661                .surface
662                .triangles()
663                .iter()
664                .any(|triangle| inside_polygon(point, &triangle.vertices))
665            {
666                return (-distance, normal.map(|value| -value));
667            }
668            return (distance, normal);
669        }
670        let gradient = [
671            ((values[1] - values[0]) * (1.0 - fraction[1]) + (values[3] - values[2]) * fraction[1])
672                / cell[0],
673            ((values[2] - values[0]) * (1.0 - fraction[0]) + (values[3] - values[1]) * fraction[0])
674                / cell[1],
675        ];
676        let length = libm::hypot(gradient[0], gradient[1]);
677        (
678            distance,
679            if length == 0.0 {
680                [1.0, 0.0]
681            } else {
682                gradient.map(|value| value / length)
683            },
684        )
685    }
686}
687
688/// Grid collision; velocity*step must remain below the smallest cell dimension.
689#[derive(Clone, Debug)]
690pub struct SdfCollider {
691    /// Cached immutable collision grid.
692    pub grid: Arc<SignedDistanceGrid>,
693    /// Normal/tangential contact response.
694    pub response: CollisionResponse,
695}
696
697impl SdfCollider {
698    fn contact(&self, particle: &mut Particle, step: f32) -> Result<bool, ValidationError> {
699        let cell = (self.grid.bounds.width() / f64::from(self.grid.dimensions[0] - 1))
700            .min(self.grid.bounds.height() / f64::from(self.grid.dimensions[1] - 1));
701        if f64::from(libm::hypotf(particle.velocity[0], particle.velocity[1]) * step) >= cell {
702            return Err(invalid(
703                "sdf",
704                "Velocity times step must be smaller than a grid cell",
705            ));
706        }
707        swept(particle, step, self.response, None, |point| {
708            self.grid.sample(point)
709        })
710    }
711}
712collider!(
713    SdfCollider,
714    |collider: &SdfCollider| (collider.grid.boundary.len() as u64
715        + collider.grid.surface.triangles().len() as u64 * 3
716        + 8)
717        * 64
718);