1use super::sweep::Sweep;
7use super::sweep_toi::{sweep_time_of_impact, SweepToiOutput, SweepToiStatus};
8use super::toi_proxy::ToiProxy;
9use crate::bounding_volume::BoundingVolume;
10use crate::math::{Pose, Real, Vector};
11use crate::shape::{Shape, TypedShape};
12
13#[cfg(feature = "dim2")]
14use crate::shape::{PolylineFlags, Segment};
15#[cfg(feature = "dim3")]
16use crate::shape::{TriMeshFlags, Triangle};
17
18pub const CORE_FRACTION: Real = 0.25;
20
21#[derive(Copy, Clone)]
23pub struct SweepCompositeFastShape<'a, 'b> {
24 pub proxy: &'a ToiProxy<'b>,
26 pub sweep: &'a Sweep,
28 pub local_centroid: Vector,
30 pub min_extent: Real,
33}
34
35struct CompositeToiContext<'a, 'b> {
36 fast: SweepCompositeFastShape<'a, 'b>,
37 local_centroid1: Vector,
39 local_centroid2: Vector,
40 fallback_radius: Real,
41 one_sided: bool,
42 #[cfg_attr(feature = "dim2", allow(dead_code))]
43 target_is_sensor: bool,
44 linear_slop: Real,
45 max_fraction: Real,
46 best: Option<SweepToiOutput>,
47}
48
49impl CompositeToiContext<'_, '_> {
50 fn toi_against_element(&mut self, element_proxy: &ToiProxy, element_sweep: &Sweep) {
53 let output = sweep_time_of_impact(
54 element_proxy,
55 element_sweep,
56 self.fast.proxy,
57 self.fast.sweep,
58 self.max_fraction,
59 self.linear_slop,
60 );
61
62 if 0.0 < output.fraction && output.fraction < self.max_fraction {
63 self.max_fraction = output.fraction;
64 self.best = Some(output);
65 } else if output.fraction == 0.0 {
66 #[cfg(feature = "dim2")]
68 let radius = self.fallback_radius;
69 #[cfg(feature = "dim3")]
70 let radius = self.fallback_radius + self.linear_slop;
71
72 let fallback_proxy = ToiProxy::point(self.fast.local_centroid, radius);
73 let output = sweep_time_of_impact(
74 element_proxy,
75 element_sweep,
76 &fallback_proxy,
77 self.fast.sweep,
78 self.max_fraction,
79 self.linear_slop,
80 );
81
82 if 0.0 < output.fraction && output.fraction < self.max_fraction {
83 self.max_fraction = output.fraction;
84 self.best = Some(output);
85 }
86 }
87 }
88
89 #[cfg(feature = "dim2")]
92 fn one_sided_early_out(&self, segment: &Segment) -> bool {
93 if !self.one_sided {
94 return false;
95 }
96
97 let e = segment.b - segment.a;
98 let length = e.length();
99 if length <= self.linear_slop {
100 return false;
101 }
102 let e = e / length;
103
104 let separation1 = (self.local_centroid1 - segment.a).perp_dot(e);
105 let separation2 = (self.local_centroid2 - segment.a).perp_dot(e);
106 let core_distance = CORE_FRACTION * self.fast.min_extent;
107
108 separation1 < 0.0
109 || (separation1 - separation2 < core_distance && separation2 > core_distance)
110 }
111
112 #[cfg(feature = "dim3")]
115 fn one_sided_early_out(&self, triangle: &Triangle) -> bool {
116 if !self.one_sided {
117 return false;
118 }
119
120 let n = (triangle.b - triangle.a)
121 .cross(triangle.c - triangle.a)
122 .normalize_or_zero();
123 let offset1 = n.dot(self.local_centroid1 - triangle.a);
124 let offset2 = n.dot(self.local_centroid2 - triangle.a);
125
126 if offset1 < 0.0 {
127 return true;
129 }
130
131 if !self.target_is_sensor
132 && offset1 - offset2 < self.fallback_radius
133 && offset2 > self.fallback_radius
134 {
135 return true;
137 }
138
139 false
140 }
141}
142
143#[allow(clippy::too_many_arguments)]
154pub fn sweep_time_of_impact_composite(
155 composite: &dyn Shape,
156 composite_pose: &Pose,
157 fast: SweepCompositeFastShape,
158 one_sided: bool,
159 target_is_sensor: bool,
160 max_fraction: Real,
161 linear_slop: Real,
162) -> Option<SweepToiOutput> {
163 let typed = composite.as_typed_shape();
164
165 #[cfg(feature = "dim2")]
167 let fallback_radius = CORE_FRACTION * fast.min_extent;
168 #[cfg(feature = "dim3")]
169 let fallback_radius = match typed {
170 TypedShape::Compound(_) => (0.75 * fast.min_extent).max(4.0 * linear_slop),
171 _ => (0.5 * fast.min_extent).max(linear_slop),
172 };
173
174 let start_aabb = fast.proxy.compute_aabb(&fast.sweep.transform_at(0.0));
176 let end_aabb = fast
177 .proxy
178 .compute_aabb(&fast.sweep.transform_at(max_fraction));
179 let local_aabb = start_aabb
180 .merged(&end_aabb)
181 .transform_by(&composite_pose.inverse());
182
183 let centroid_world1 = fast
185 .sweep
186 .transform_at(0.0)
187 .transform_point(fast.local_centroid);
188 let centroid_world2 = fast
189 .sweep
190 .final_transform()
191 .transform_point(fast.local_centroid);
192
193 let mut context = CompositeToiContext {
194 fast,
195 local_centroid1: composite_pose.inverse_transform_point(centroid_world1),
196 local_centroid2: composite_pose.inverse_transform_point(centroid_world2),
197 fallback_radius,
198 one_sided,
199 target_is_sensor,
200 linear_slop,
201 max_fraction,
202 best: None,
203 };
204
205 let composite_sweep = Sweep::constant(composite_pose, Vector::ZERO);
208
209 match typed {
210 #[cfg(feature = "dim3")]
211 TypedShape::TriMesh(mesh) => {
212 let one_sided_mesh = mesh.flags().contains(TriMeshFlags::ORIENTED);
213 context.one_sided = one_sided || one_sided_mesh;
214 for tri_id in mesh.bvh().intersect_aabb(&local_aabb) {
215 let triangle = mesh.triangle(tri_id);
216 if context.one_sided_early_out(&triangle) {
217 continue;
218 }
219 let proxy = ToiProxy::from_array([triangle.a, triangle.b, triangle.c], 0.0);
220 context.toi_against_element(&proxy, &composite_sweep);
221 }
222 }
223 #[cfg(feature = "dim2")]
224 TypedShape::TriMesh(mesh) => {
225 for tri_id in mesh.bvh().intersect_aabb(&local_aabb) {
226 let triangle = mesh.triangle(tri_id);
227 let proxy = ToiProxy::from_array([triangle.a, triangle.b, triangle.c], 0.0);
228 context.toi_against_element(&proxy, &composite_sweep);
229 }
230 }
231 TypedShape::Polyline(polyline) => {
232 #[cfg(feature = "dim2")]
233 {
234 let oriented = polyline.flags().contains(PolylineFlags::ORIENTED);
235 context.one_sided = one_sided || oriented;
236 }
237 for seg_id in polyline.bvh().intersect_aabb(&local_aabb) {
238 let segment = polyline.segment(seg_id);
239 #[cfg(feature = "dim2")]
240 if context.one_sided_early_out(&segment) {
241 continue;
242 }
243 let proxy = ToiProxy::from_array([segment.a, segment.b], 0.0);
244 context.toi_against_element(&proxy, &composite_sweep);
245 }
246 }
247 TypedShape::HeightField(heightfield) => {
248 #[cfg(feature = "dim2")]
249 heightfield.map_elements_in_local_aabb(&local_aabb, &mut |_, segment| {
250 if !context.one_sided_early_out(segment) {
251 let proxy = ToiProxy::from_array([segment.a, segment.b], 0.0);
252 context.toi_against_element(&proxy, &composite_sweep);
253 }
254 });
255 #[cfg(feature = "dim3")]
256 heightfield.map_elements_in_local_aabb(&local_aabb, &mut |_, triangle| {
257 if !context.one_sided_early_out(triangle) {
258 let proxy = ToiProxy::from_array([triangle.a, triangle.b, triangle.c], 0.0);
259 context.toi_against_element(&proxy, &composite_sweep);
260 }
261 });
262 }
263 TypedShape::Compound(compound) => {
264 for child_id in compound.bvh().intersect_aabb(&local_aabb) {
265 let (child_pose, child_shape) = &compound.shapes()[child_id as usize];
266 let child_world_pose = *composite_pose * *child_pose;
267 if let Some(child_proxy) = ToiProxy::from_shape(child_shape.as_ref()) {
268 let child_sweep = Sweep::constant(&child_world_pose, Vector::ZERO);
269 context.toi_against_element(&child_proxy, &child_sweep);
270 } else if let Some(hit) = sweep_time_of_impact_composite(
271 child_shape.as_ref(),
272 &child_world_pose,
273 context.fast,
274 context.one_sided,
275 target_is_sensor,
276 context.max_fraction,
277 linear_slop,
278 ) {
279 if 0.0 < hit.fraction && hit.fraction < context.max_fraction {
280 context.max_fraction = hit.fraction;
281 context.best = Some(hit);
282 }
283 }
284 }
286 }
287 _ => return None,
288 }
289
290 Some(context.best.unwrap_or(SweepToiOutput {
291 status: SweepToiStatus::Separated,
292 fraction: max_fraction,
293 point: Vector::ZERO,
294 normal: Vector::ZERO,
295 }))
296}