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