Skip to main content

boxddd/
debug_draw.rs

1#![cfg_attr(all(target_arch = "wasm32", boxddd_wasm_provider), allow(dead_code))]
2
3use crate::core::foundation::ReplayLease;
4use crate::core::{callback_state, validation};
5#[cfg(all(target_arch = "wasm32", boxddd_wasm_provider))]
6use crate::error::Error;
7use crate::error::Result;
8use crate::shapes::ShapeType;
9use crate::types::{Aabb, Plane, Pos, ShapeId, Transform, Vec3, WorldTransform};
10use crate::world::World;
11use crate::world::ledger::CallbackProvenanceIndex;
12use boxddd_sys::ffi;
13use std::cell::{Cell, RefCell};
14use std::ffi::{CStr, c_void};
15use std::fmt;
16use std::mem;
17use std::panic::{AssertUnwindSafe, catch_unwind};
18use std::slice;
19
20#[cfg(all(target_arch = "wasm32", boxddd_wasm_provider))]
21mod provider;
22
23#[cfg(all(test, target_arch = "wasm32", boxddd_wasm_provider))]
24pub(crate) use provider::provider_debug_registry_count;
25#[cfg(all(target_arch = "wasm32", boxddd_wasm_provider))]
26pub(crate) use provider::{
27    ProviderDebugFrameGuard, register_provider_debug_registry, take_provider_debug_error,
28    unregister_provider_debug_registry,
29};
30#[cfg(all(target_arch = "wasm32", boxddd_wasm_provider))]
31pub use provider::{
32    boxddd_debug_draw_bounds, boxddd_debug_draw_box, boxddd_debug_draw_capsule,
33    boxddd_debug_draw_point, boxddd_debug_draw_segment, boxddd_debug_draw_shape,
34    boxddd_debug_draw_sphere, boxddd_debug_draw_string, boxddd_debug_draw_transform,
35    boxddd_debug_report_error, boxddd_debug_shape_create, boxddd_debug_shape_destroy,
36};
37
38/// Packed Box3D debug color.
39///
40/// Box3D stores RGB in the low 24 bits and may use the high byte for a debug
41/// material preset. Use [`HexColor::rgb_u32`] when only the visible color is
42/// needed, and [`HexColor::raw_u32`] when preserving renderer metadata.
43#[repr(transparent)]
44#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
45#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
46pub struct HexColor(u32);
47
48impl HexColor {
49    /// Black.
50    pub const BLACK: Self = Self::from_rgb_u32(0x000000);
51    /// White.
52    pub const WHITE: Self = Self::from_rgb_u32(0xffffff);
53    /// Red.
54    pub const RED: Self = Self::from_rgb_u32(0xff0000);
55    /// Green.
56    pub const GREEN: Self = Self::from_rgb_u32(0x00ff00);
57    /// Blue.
58    pub const BLUE: Self = Self::from_rgb_u32(0x0000ff);
59
60    /// Creates a color from red, green, and blue components.
61    #[inline]
62    pub const fn from_rgb(red: u8, green: u8, blue: u8) -> Self {
63        Self(((red as u32) << 16) | ((green as u32) << 8) | blue as u32)
64    }
65
66    /// Creates a color from a packed `0xRRGGBB` value.
67    #[inline]
68    pub const fn from_rgb_u32(rgb: u32) -> Self {
69        Self(rgb & 0x00ff_ffff)
70    }
71
72    /// Creates a color from a raw Box3D debug color payload.
73    #[inline]
74    pub const fn from_raw(raw: u32) -> Self {
75        Self(raw)
76    }
77
78    /// Returns the full raw payload, including the high material byte.
79    #[inline]
80    pub const fn raw_u32(self) -> u32 {
81        self.0
82    }
83
84    /// Returns this color as a packed `0xRRGGBB` value.
85    #[inline]
86    pub const fn rgb_u32(self) -> u32 {
87        self.0 & 0x00ff_ffff
88    }
89
90    /// Returns the full raw Box3D color payload.
91    #[inline]
92    pub const fn into_raw(self) -> u32 {
93        self.0
94    }
95
96    #[inline]
97    fn from_ffi(raw: ffi::b3HexColor) -> Self {
98        Self(raw)
99    }
100}
101
102/// Stable typed handle for a persistent Box3D debug shape asset.
103#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
104#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
105pub struct DebugShapeHandle {
106    /// Slot index owned by the debug shape store.
107    pub index: u32,
108    /// Generation used to reject stale renderer cache entries.
109    pub generation: u32,
110}
111
112impl DebugShapeHandle {
113    /// Creates a handle. Generation zero is reserved as invalid.
114    #[inline]
115    pub const fn new(index: u32, generation: u32) -> Option<Self> {
116        if generation == 0 {
117            None
118        } else {
119            Some(Self { index, generation })
120        }
121    }
122
123    /// Returns whether this handle is non-zero and usable as a renderer cache key.
124    #[inline]
125    pub const fn is_valid(self) -> bool {
126        self.generation != 0
127    }
128}
129
130/// Owned asset emitted when Box3D creates a persistent debug shape.
131#[derive(Clone, Debug, PartialEq)]
132pub struct DebugShapeAsset {
133    /// Stable handle referenced by subsequent shape draw commands.
134    pub handle: DebugShapeHandle,
135    /// Box3D shape that produced this debug asset.
136    pub shape_id: ShapeId,
137    /// Box3D shape kind.
138    pub shape_type: ShapeType,
139    /// Owned renderer-agnostic geometry snapshot.
140    pub geometry: DebugShapeGeometry,
141}
142
143/// Lifecycle event for persistent debug shape assets.
144#[derive(Clone, Debug, PartialEq)]
145pub enum DebugShapeEvent {
146    /// A new renderer asset should be created or refreshed.
147    Created(DebugShapeAsset),
148    /// A previously emitted asset should be removed from renderer caches.
149    Destroyed {
150        /// Retired handle.
151        handle: DebugShapeHandle,
152    },
153    /// The owning world has invalidated every debug handle.
154    ClearAll,
155}
156
157/// Diagnostic captured while collecting a debug draw frame.
158#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
159#[derive(Clone, Debug, PartialEq, Eq)]
160pub struct DebugDrawDiagnostic {
161    /// Human-readable message.
162    pub message: String,
163}
164
165/// Face polygon copied from a convex hull.
166#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
167#[derive(Clone, Debug, PartialEq)]
168pub struct DebugHullFace {
169    /// Indices into [`DebugShapeGeometry::Hull::points`].
170    pub indices: Vec<u32>,
171    /// Local plane for the face.
172    pub plane: Plane,
173}
174
175/// Owned triangle mesh snapshot used by mesh-like debug geometry.
176#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
177#[derive(Clone, Debug, PartialEq)]
178pub struct DebugMesh {
179    /// Local-space bounds.
180    pub bounds: Aabb,
181    /// Mesh vertices.
182    pub vertices: Vec<Vec3>,
183    /// Mesh triangles.
184    pub triangles: Vec<DebugMeshTriangle>,
185    /// Number of material slots stored by the source shape.
186    pub material_count: i32,
187}
188
189/// Triangle copied from cooked mesh-like data.
190#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
191#[derive(Copy, Clone, Debug, PartialEq, Eq)]
192pub struct DebugMeshTriangle {
193    /// Indices into [`DebugMesh::vertices`].
194    pub indices: [u32; 3],
195    /// Optional per-triangle material index.
196    pub material_index: Option<u8>,
197}
198
199/// Owned child geometry inside a compound debug shape.
200#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
201#[derive(Clone, Debug, PartialEq)]
202pub struct DebugCompoundChild {
203    /// Transform from compound-local space to child-local space.
204    pub transform: Transform,
205    /// Material indices reported by Box3D for this child.
206    pub material_indices: [i32; ffi::B3_MAX_COMPOUND_MESH_MATERIALS as usize],
207    /// Owned child geometry.
208    pub geometry: DebugShapeGeometry,
209}
210
211/// Renderer-agnostic owned geometry for persistent debug shapes.
212#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
213#[derive(Clone, Debug, PartialEq)]
214pub enum DebugShapeGeometry {
215    /// Sphere geometry in the shape's local space.
216    Sphere {
217        /// Local center.
218        center: Vec3,
219        /// Radius.
220        radius: f32,
221    },
222    /// Capsule geometry in the shape's local space.
223    Capsule {
224        /// First local endpoint.
225        center1: Vec3,
226        /// Second local endpoint.
227        center2: Vec3,
228        /// Radius.
229        radius: f32,
230    },
231    /// Convex hull geometry.
232    Hull {
233        /// Local-space bounds.
234        aabb: Aabb,
235        /// Hull points.
236        points: Vec<Vec3>,
237        /// Convex faces as point index polygons.
238        faces: Vec<DebugHullFace>,
239    },
240    /// Cooked triangle mesh geometry.
241    Mesh {
242        /// Owned mesh data.
243        mesh: DebugMesh,
244        /// Per-shape scale.
245        scale: Vec3,
246    },
247    /// Height-field data expanded into a mesh snapshot.
248    HeightField {
249        /// Owned mesh data.
250        mesh: DebugMesh,
251    },
252    /// Compound geometry flattened into owned children.
253    Compound {
254        /// Flattened child shapes.
255        children: Vec<DebugCompoundChild>,
256    },
257}
258
259impl DebugShapeGeometry {
260    /// Returns the Box3D shape type represented by this geometry.
261    #[inline]
262    pub const fn shape_type(&self) -> Option<ShapeType> {
263        match self {
264            Self::Sphere { .. } => Some(ShapeType::Sphere),
265            Self::Capsule { .. } => Some(ShapeType::Capsule),
266            Self::Hull { .. } => Some(ShapeType::Hull),
267            Self::Mesh { .. } => Some(ShapeType::Mesh),
268            Self::HeightField { .. } => Some(ShapeType::HeightField),
269            Self::Compound { .. } => Some(ShapeType::Compound),
270        }
271    }
272}
273
274/// Debug draw command emitted for one frame.
275///
276/// Positions and transforms are in world coordinates. Geometry referenced by a
277/// [`DebugShapeHandle`] is owned separately by the corresponding lifecycle asset and remains in the
278/// shape's local space.
279#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
280#[derive(Clone, Debug, PartialEq)]
281pub enum DebugDrawCommand {
282    /// Draw a persistent shape asset.
283    Shape {
284        /// Optional persistent debug shape handle.
285        handle: Option<DebugShapeHandle>,
286        /// World transform of the shape.
287        transform: WorldTransform,
288        /// Shape color.
289        color: HexColor,
290    },
291    /// Draw a line segment.
292    Segment {
293        /// First endpoint.
294        p1: Pos,
295        /// Second endpoint.
296        p2: Pos,
297        /// Segment color.
298        color: HexColor,
299    },
300    /// Draw a transform basis.
301    Transform(WorldTransform),
302    /// Draw a point marker.
303    Point {
304        /// Point position.
305        position: Pos,
306        /// Point size in debug-draw units.
307        size: f32,
308        /// Point color.
309        color: HexColor,
310    },
311    /// Draw a sphere.
312    Sphere {
313        /// Sphere center.
314        center: Pos,
315        /// Sphere radius.
316        radius: f32,
317        /// Sphere color.
318        color: HexColor,
319        /// Sphere alpha.
320        alpha: f32,
321    },
322    /// Draw a capsule.
323    Capsule {
324        /// First capsule endpoint.
325        p1: Pos,
326        /// Second capsule endpoint.
327        p2: Pos,
328        /// Capsule radius.
329        radius: f32,
330        /// Capsule color.
331        color: HexColor,
332        /// Capsule alpha.
333        alpha: f32,
334    },
335    /// Draw an AABB.
336    Bounds {
337        /// Bounds to draw.
338        aabb: Aabb,
339        /// Bounds color.
340        color: HexColor,
341    },
342    /// Draw an oriented box.
343    Box {
344        /// Box half extents.
345        extents: Vec3,
346        /// Box world transform.
347        transform: WorldTransform,
348        /// Box color.
349        color: HexColor,
350    },
351    /// Draw text.
352    String {
353        /// Text position.
354        position: Pos,
355        /// Text content.
356        text: String,
357        /// Text color.
358        color: HexColor,
359    },
360}
361
362/// Complete, owned data collected from one debug draw pass.
363///
364/// The frame contains no borrowed Box3D memory and may outlive the call that produced it. Apply
365/// `events` to a renderer asset cache in order before consuming `commands`: a `Created` event may
366/// introduce a handle used by a command in the same frame, `Destroyed` retires one cached asset,
367/// and `ClearAll` invalidates the entire cache. Handles are scoped to the owning [`World`]; clear
368/// the renderer cache when that world is dropped or replaced even if no later frame is collected.
369#[derive(Clone, Debug, Default, PartialEq)]
370pub struct DebugDrawFrame {
371    /// Persistent shape lifecycle events emitted since the previous drain.
372    pub events: Vec<DebugShapeEvent>,
373    /// Immediate draw commands for the current frame.
374    pub commands: Vec<DebugDrawCommand>,
375    /// Non-fatal diagnostics captured while copying debug shape data.
376    pub diagnostics: Vec<DebugDrawDiagnostic>,
377}
378
379impl DebugDrawFrame {
380    /// Clears all events, commands, and diagnostics while preserving capacity.
381    pub fn clear(&mut self) {
382        self.events.clear();
383        self.commands.clear();
384        self.diagnostics.clear();
385    }
386}
387
388/// Trait implemented by low-level debug draw sinks.
389///
390/// Most users should prefer [`World::debug_draw_frame`], which exposes a
391/// lifecycle-correct, owned data model. Box3D invokes this sink synchronously
392/// during [`World::debug_draw`] and never retains it. Callback positions and
393/// transforms are in world coordinates; shift them into the renderer's camera
394/// frame inside the sink when needed.
395///
396/// Sink methods run in Box3D callback context, so safe API reentry returns
397/// [`Error::InCallback`]. On unwind-capable targets, the first sink panic is
398/// contained, later sink calls are suppressed, and [`World::debug_draw`]
399/// returns [`Error::CallbackPanicked`]. `panic=abort` targets cannot contain a
400/// panic. Custom Rust sinks are unavailable in provider-mode WASM; use frame
401/// collection there.
402pub trait DebugDraw {
403    /// Draws a persistent shape outline.
404    fn draw_shape(
405        &mut self,
406        _handle: Option<DebugShapeHandle>,
407        _transform: WorldTransform,
408        _color: HexColor,
409    ) {
410    }
411
412    /// Draws a line segment.
413    fn draw_segment(&mut self, _p1: Pos, _p2: Pos, _color: HexColor) {}
414    /// Draws a transform basis.
415    fn draw_transform(&mut self, _transform: WorldTransform) {}
416    /// Draws a point marker.
417    fn draw_point(&mut self, _position: Pos, _size: f32, _color: HexColor) {}
418    /// Draws a sphere.
419    fn draw_sphere(&mut self, _center: Pos, _radius: f32, _color: HexColor, _alpha: f32) {}
420    /// Draws a capsule.
421    fn draw_capsule(&mut self, _p1: Pos, _p2: Pos, _radius: f32, _color: HexColor, _alpha: f32) {}
422    /// Draws an AABB.
423    fn draw_bounds(&mut self, _aabb: Aabb, _color: HexColor) {}
424    /// Draws an oriented box.
425    fn draw_box(&mut self, _extents: Vec3, _transform: WorldTransform, _color: HexColor) {}
426    /// Draws text.
427    fn draw_string(&mut self, _position: Pos, _text: &str, _color: HexColor) {}
428}
429
430/// Options passed to Box3D debug drawing.
431#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
432#[derive(Copy, Clone, Debug)]
433pub struct DebugDrawOptions {
434    /// Bounds limiting debug drawing.
435    pub drawing_bounds: Aabb,
436    /// Collision mask used by the query.
437    pub mask_bits: u64,
438    /// Scale applied to force visualizations.
439    pub force_scale: f32,
440    /// Scale applied to joint visualizations.
441    pub joint_scale: f32,
442    /// Whether shape outlines are drawn.
443    pub draw_shapes: bool,
444    /// Whether joints are drawn.
445    pub draw_joints: bool,
446    /// Whether joint extra details are drawn.
447    pub draw_joint_extras: bool,
448    /// Whether AABBs are drawn.
449    pub draw_bounds: bool,
450    /// Whether mass data is drawn.
451    pub draw_mass: bool,
452    /// Whether sleep information is drawn for dynamic and kinematic bodies.
453    pub draw_sleep: bool,
454    /// Whether body names are drawn.
455    pub draw_body_names: bool,
456    /// Whether contacts are drawn.
457    pub draw_contacts: bool,
458    /// Whether contact anchor A is drawn instead of contact anchor B.
459    pub draw_anchor_a: bool,
460    /// Whether graph-color debug coloring is drawn.
461    pub draw_graph_colors: bool,
462    /// Whether contact feature ids are drawn.
463    pub draw_contact_features: bool,
464    /// Whether contact normals are drawn.
465    pub draw_contact_normals: bool,
466    /// Whether contact forces are drawn.
467    pub draw_contact_forces: bool,
468    /// Whether solver islands are drawn.
469    pub draw_islands: bool,
470}
471
472impl Default for DebugDrawOptions {
473    fn default() -> Self {
474        Self {
475            drawing_bounds: Aabb {
476                lower_bound: Vec3::new(-1.0e9, -1.0e9, -1.0e9),
477                upper_bound: Vec3::new(1.0e9, 1.0e9, 1.0e9),
478            },
479            mask_bits: u64::MAX,
480            force_scale: 1.0,
481            joint_scale: 1.0,
482            draw_shapes: true,
483            draw_joints: true,
484            draw_joint_extras: false,
485            draw_bounds: false,
486            draw_mass: false,
487            draw_sleep: false,
488            draw_body_names: false,
489            draw_contacts: false,
490            draw_anchor_a: false,
491            draw_graph_colors: false,
492            draw_contact_features: false,
493            draw_contact_normals: false,
494            draw_contact_forces: false,
495            draw_islands: false,
496        }
497    }
498}
499
500pub(crate) struct DebugShapeRegistry {
501    inner: RefCell<DebugShapeStore>,
502    provenance: CallbackProvenanceIndex,
503    failures: callback_state::CallbackInvocationSlot,
504    poisoned: Cell<bool>,
505    failure_generation: Cell<u64>,
506    #[cfg(test)]
507    panic_next_native_callback: Cell<bool>,
508}
509
510#[derive(Default)]
511struct DebugShapeStore {
512    slots: Vec<DebugShapeSlot>,
513    free: Vec<usize>,
514    events: Vec<DebugShapeEvent>,
515    diagnostics: Vec<DebugDrawDiagnostic>,
516    cleared: bool,
517}
518
519#[derive(Debug)]
520struct DebugShapeSlot {
521    generation: u32,
522    asset: Option<DebugShapeAsset>,
523}
524
525impl fmt::Debug for DebugShapeRegistry {
526    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
527        let inner = self.inner.borrow();
528        f.debug_struct("DebugShapeRegistry")
529            .field("slots", &inner.slots.len())
530            .field("free", &inner.free.len())
531            .field("pending_events", &inner.events.len())
532            .field("pending_diagnostics", &inner.diagnostics.len())
533            .finish()
534    }
535}
536
537#[derive(Copy, Clone, Debug)]
538struct DebugShapeResource {
539    handle: DebugShapeHandle,
540}
541
542impl DebugShapeRegistry {
543    pub(crate) fn new(
544        provenance: CallbackProvenanceIndex,
545        failures: callback_state::CallbackInvocationSlot,
546    ) -> Self {
547        Self {
548            inner: RefCell::new(DebugShapeStore::default()),
549            provenance,
550            failures,
551            poisoned: Cell::new(false),
552            failure_generation: Cell::new(0),
553            #[cfg(test)]
554            panic_next_native_callback: Cell::new(false),
555        }
556    }
557
558    fn invoke_native_callback<R: Copy>(&self, fallback: R, callback: impl FnOnce() -> R) -> R {
559        match catch_unwind(AssertUnwindSafe(|| {
560            let _guard = callback_state::CallbackGuard::enter();
561            #[cfg(test)]
562            if self.panic_next_native_callback.replace(false) {
563                panic!("injected debug-shape callback panic");
564            }
565            callback()
566        })) {
567            Ok(value) => value,
568            Err(_) => {
569                self.record_callback_failure(callback_state::CallbackFailure::Panicked);
570                fallback
571            }
572        }
573    }
574
575    fn record_callback_failure(&self, failure: callback_state::CallbackFailure) {
576        self.poisoned.set(true);
577        self.failure_generation
578            .set(self.failure_generation.get().saturating_add(1));
579        self.failures.record(failure);
580    }
581
582    pub(crate) fn is_poisoned(&self) -> bool {
583        self.poisoned.get()
584    }
585
586    pub(crate) fn failure_generation(&self) -> u64 {
587        self.failure_generation.get()
588    }
589
590    #[cfg(test)]
591    fn panic_next_native_callback(&self) {
592        self.panic_next_native_callback.set(true);
593    }
594
595    fn create_asset_handle(&self, raw: &ffi::b3DebugShape) -> Option<DebugShapeHandle> {
596        let mut store = self.inner.borrow_mut();
597        let snapshot = match unsafe { snapshot_debug_shape(raw, &self.provenance) } {
598            Ok(snapshot) => snapshot,
599            Err(message) => {
600                store.diagnostics.push(DebugDrawDiagnostic {
601                    message: message.to_owned(),
602                });
603                drop(store);
604                self.record_callback_failure(callback_state::CallbackFailure::InvalidNativeInput);
605                return None;
606            }
607        };
608        let handle = store.alloc_handle();
609        let asset = DebugShapeAsset {
610            handle,
611            shape_id: snapshot.shape_id,
612            shape_type: snapshot.shape_type,
613            geometry: snapshot.geometry,
614        };
615        store.slots[handle.index as usize].asset = Some(asset.clone());
616        store.events.push(DebugShapeEvent::Created(asset));
617        Some(handle)
618    }
619
620    fn create_native_resource(&self, raw: &ffi::b3DebugShape) -> *mut c_void {
621        let Some(handle) = self.create_asset_handle(raw) else {
622            return std::ptr::null_mut();
623        };
624        Box::into_raw(Box::new(DebugShapeResource { handle })) as *mut c_void
625    }
626
627    fn destroy_native_resource(&self, resource: DebugShapeResource) {
628        let _ = self.destroy_handle(resource.handle);
629    }
630
631    fn destroy_handle(&self, handle: DebugShapeHandle) -> bool {
632        let destroyed = self.inner.borrow_mut().destroy_handle(handle);
633        if !destroyed {
634            self.record_callback_failure(callback_state::CallbackFailure::InvalidNativeInput);
635        }
636        destroyed
637    }
638
639    pub(crate) fn drain_into(&self, frame: &mut DebugDrawFrame) {
640        let mut store = self.inner.borrow_mut();
641        frame.events.append(&mut store.events);
642        frame.diagnostics.append(&mut store.diagnostics);
643    }
644
645    #[cfg(all(target_arch = "wasm32", boxddd_wasm_provider))]
646    fn push_diagnostic(&self, message: impl Into<String>) {
647        self.inner
648            .borrow_mut()
649            .diagnostics
650            .push(DebugDrawDiagnostic {
651                message: message.into(),
652            });
653    }
654
655    pub(crate) fn clear_all(&self) {
656        let mut store = self.inner.borrow_mut();
657        for slot in &mut store.slots {
658            if slot.asset.take().is_some() {
659                slot.generation = next_generation(slot.generation);
660            }
661        }
662        let slot_len = store.slots.len();
663        store.free.clear();
664        store.free.extend(0..slot_len);
665        store.events.clear();
666        store.diagnostics.clear();
667        store.events.push(DebugShapeEvent::ClearAll);
668        store.cleared = true;
669    }
670}
671
672impl DebugShapeStore {
673    fn alloc_handle(&mut self) -> DebugShapeHandle {
674        self.cleared = false;
675        if let Some(index) = self.free.pop() {
676            let generation = self.slots[index].generation;
677            DebugShapeHandle {
678                index: index as u32,
679                generation,
680            }
681        } else {
682            let index = self.slots.len();
683            self.slots.push(DebugShapeSlot {
684                generation: 1,
685                asset: None,
686            });
687            DebugShapeHandle {
688                index: index as u32,
689                generation: 1,
690            }
691        }
692    }
693
694    fn destroy_handle(&mut self, handle: DebugShapeHandle) -> bool {
695        let Some(slot) = self.slots.get_mut(handle.index as usize) else {
696            self.diagnostics.push(DebugDrawDiagnostic {
697                message: "debug shape destroy referenced an unknown handle".to_owned(),
698            });
699            return false;
700        };
701        if slot.generation != handle.generation || slot.asset.is_none() {
702            if self.cleared {
703                return true;
704            }
705            self.diagnostics.push(DebugDrawDiagnostic {
706                message: "debug shape destroy referenced a stale handle".to_owned(),
707            });
708            return false;
709        }
710
711        slot.asset = None;
712        slot.generation = next_generation(slot.generation);
713        self.events.push(DebugShapeEvent::Destroyed { handle });
714        self.free.push(handle.index as usize);
715        true
716    }
717}
718
719#[inline]
720const fn next_generation(current: u32) -> u32 {
721    let next = current.wrapping_add(1);
722    if next == 0 { 1 } else { next }
723}
724
725#[cfg(not(all(target_arch = "wasm32", boxddd_wasm_provider)))]
726pub(crate) unsafe extern "C" fn create_debug_shape(
727    debug_shape: *const ffi::b3DebugShape,
728    user_context: *mut c_void,
729) -> *mut c_void {
730    if user_context.is_null() {
731        return std::ptr::null_mut();
732    }
733    let registry = unsafe { &*(user_context as *const DebugShapeRegistry) };
734    registry.invoke_native_callback(std::ptr::null_mut(), || {
735        let Some(debug_shape) = (unsafe { debug_shape.as_ref() }) else {
736            registry.record_callback_failure(callback_state::CallbackFailure::InvalidNativeInput);
737            return std::ptr::null_mut();
738        };
739        registry.create_native_resource(debug_shape)
740    })
741}
742
743#[cfg(not(all(target_arch = "wasm32", boxddd_wasm_provider)))]
744pub(crate) unsafe extern "C" fn destroy_debug_shape(
745    user_shape: *mut c_void,
746    user_context: *mut c_void,
747) {
748    if user_shape.is_null() {
749        return;
750    }
751    if !user_context.is_null() {
752        let registry = unsafe { &*(user_context as *const DebugShapeRegistry) };
753        registry.invoke_native_callback((), || {
754            let resource = unsafe { *Box::from_raw(user_shape as *mut DebugShapeResource) };
755            registry.destroy_native_resource(resource);
756        });
757    } else {
758        let _ = catch_unwind(AssertUnwindSafe(|| {
759            let _guard = callback_state::CallbackGuard::enter();
760            drop(unsafe { Box::from_raw(user_shape as *mut DebugShapeResource) });
761        }));
762    }
763}
764
765struct DebugShapeSnapshot {
766    shape_id: ShapeId,
767    shape_type: ShapeType,
768    geometry: DebugShapeGeometry,
769}
770
771type SnapshotResult<T> = std::result::Result<T, &'static str>;
772
773unsafe fn snapshot_debug_shape(
774    raw: &ffi::b3DebugShape,
775    provenance: &CallbackProvenanceIndex,
776) -> SnapshotResult<DebugShapeSnapshot> {
777    let shape_type = ShapeType::from_raw(raw.type_).ok_or("debug shape has unknown shape type")?;
778    let geometry = unsafe {
779        match shape_type {
780            ShapeType::Sphere => snapshot_sphere_ptr(raw.__bindgen_anon_1.sphere)?,
781            ShapeType::Capsule => snapshot_capsule_ptr(raw.__bindgen_anon_1.capsule)?,
782            ShapeType::Hull => snapshot_hull_ptr(raw.__bindgen_anon_1.hull)?,
783            ShapeType::Mesh => snapshot_mesh_ptr(raw.__bindgen_anon_1.mesh)?,
784            ShapeType::HeightField => snapshot_height_field_ptr(raw.__bindgen_anon_1.heightField)?,
785            ShapeType::Compound => snapshot_compound_ptr(raw.__bindgen_anon_1.compound)?,
786        }
787    };
788    let shape_id = provenance
789        .resolve_shape(raw.shapeId)
790        .ok_or("debug shape referenced an unknown shape")?;
791    Ok(DebugShapeSnapshot {
792        shape_id,
793        shape_type,
794        geometry,
795    })
796}
797
798unsafe fn snapshot_child_shape(raw: ffi::b3ChildShape) -> SnapshotResult<DebugShapeGeometry> {
799    unsafe {
800        match ShapeType::from_raw(raw.type_).ok_or("compound child has unknown shape type")? {
801            ShapeType::Sphere => Ok(snapshot_sphere(raw.__bindgen_anon_1.sphere)),
802            ShapeType::Capsule => Ok(snapshot_capsule(raw.__bindgen_anon_1.capsule)),
803            ShapeType::Hull => snapshot_hull_ptr(raw.__bindgen_anon_1.hull),
804            ShapeType::Mesh => {
805                let mesh = raw.__bindgen_anon_1.mesh;
806                snapshot_mesh(&mesh)
807            }
808            ShapeType::Compound => Err("nested compound debug child is not supported by Box3D"),
809            ShapeType::HeightField => Err("height-field debug child is not supported by Box3D"),
810        }
811    }
812}
813
814unsafe fn snapshot_sphere_ptr(ptr: *const ffi::b3Sphere) -> SnapshotResult<DebugShapeGeometry> {
815    let sphere = unsafe { ptr.as_ref() }.ok_or("debug sphere pointer was null")?;
816    Ok(snapshot_sphere(*sphere))
817}
818
819fn snapshot_sphere(raw: ffi::b3Sphere) -> DebugShapeGeometry {
820    DebugShapeGeometry::Sphere {
821        center: Vec3::from_raw(raw.center),
822        radius: raw.radius,
823    }
824}
825
826unsafe fn snapshot_capsule_ptr(ptr: *const ffi::b3Capsule) -> SnapshotResult<DebugShapeGeometry> {
827    let capsule = unsafe { ptr.as_ref() }.ok_or("debug capsule pointer was null")?;
828    Ok(snapshot_capsule(*capsule))
829}
830
831fn snapshot_capsule(raw: ffi::b3Capsule) -> DebugShapeGeometry {
832    DebugShapeGeometry::Capsule {
833        center1: Vec3::from_raw(raw.center1),
834        center2: Vec3::from_raw(raw.center2),
835        radius: raw.radius,
836    }
837}
838
839unsafe fn snapshot_hull_ptr(ptr: *const ffi::b3HullData) -> SnapshotResult<DebugShapeGeometry> {
840    let hull = unsafe { ptr.as_ref() }.ok_or("debug hull pointer was null")?;
841    unsafe { snapshot_hull(hull) }
842}
843
844unsafe fn snapshot_hull(hull: &ffi::b3HullData) -> SnapshotResult<DebugShapeGeometry> {
845    let points = unsafe {
846        trailing_slice::<ffi::b3Vec3>(
847            hull as *const _ as *const u8,
848            hull.byteCount,
849            hull.pointOffset,
850            hull.vertexCount,
851        )?
852    }
853    .iter()
854    .copied()
855    .map(Vec3::from_raw)
856    .collect();
857    let edges = unsafe {
858        trailing_slice::<ffi::b3HullHalfEdge>(
859            hull as *const _ as *const u8,
860            hull.byteCount,
861            hull.edgeOffset,
862            hull.edgeCount,
863        )?
864    };
865    let raw_faces = unsafe {
866        trailing_slice::<ffi::b3HullFace>(
867            hull as *const _ as *const u8,
868            hull.byteCount,
869            hull.faceOffset,
870            hull.faceCount,
871        )?
872    };
873    let planes = unsafe {
874        trailing_slice::<ffi::b3Plane>(
875            hull as *const _ as *const u8,
876            hull.byteCount,
877            hull.planeOffset,
878            hull.faceCount,
879        )?
880    };
881
882    let mut faces = Vec::with_capacity(raw_faces.len());
883    for (face, plane) in raw_faces.iter().zip(planes) {
884        let start = face.edge as usize;
885        if start >= edges.len() {
886            return Err("debug hull face edge index was out of range");
887        }
888        let mut indices = Vec::new();
889        let mut edge_index = start;
890        for _ in 0..=edges.len() {
891            let edge = edges
892                .get(edge_index)
893                .ok_or("debug hull edge index was out of range")?;
894            indices.push(edge.origin as u32);
895            edge_index = edge.next as usize;
896            if edge_index == start {
897                break;
898            }
899        }
900        if edge_index != start {
901            return Err("debug hull face did not form a closed loop");
902        }
903        faces.push(DebugHullFace {
904            indices,
905            plane: Plane::from_raw(*plane),
906        });
907    }
908
909    Ok(DebugShapeGeometry::Hull {
910        aabb: Aabb::from_raw(hull.aabb),
911        points,
912        faces,
913    })
914}
915
916unsafe fn snapshot_mesh_ptr(ptr: *const ffi::b3Mesh) -> SnapshotResult<DebugShapeGeometry> {
917    let mesh = unsafe { ptr.as_ref() }.ok_or("debug mesh pointer was null")?;
918    unsafe { snapshot_mesh(mesh) }
919}
920
921unsafe fn snapshot_mesh(mesh: &ffi::b3Mesh) -> SnapshotResult<DebugShapeGeometry> {
922    let data = unsafe { mesh.data.as_ref() }.ok_or("debug mesh data pointer was null")?;
923    let raw_vertices = unsafe {
924        trailing_slice::<ffi::b3Vec3>(
925            data as *const _ as *const u8,
926            data.byteCount,
927            data.vertexOffset,
928            data.vertexCount,
929        )?
930    };
931    let raw_triangles = unsafe {
932        trailing_slice::<ffi::b3MeshTriangle>(
933            data as *const _ as *const u8,
934            data.byteCount,
935            data.triangleOffset,
936            data.triangleCount,
937        )?
938    };
939    let raw_material_indices = if data.materialOffset == 0 {
940        &[]
941    } else {
942        unsafe {
943            trailing_slice::<u8>(
944                data as *const _ as *const u8,
945                data.byteCount,
946                data.materialOffset,
947                data.triangleCount,
948            )?
949        }
950    };
951
952    let vertices: Vec<_> = raw_vertices.iter().copied().map(Vec3::from_raw).collect();
953    let mut triangles = Vec::with_capacity(raw_triangles.len());
954    for (index, triangle) in raw_triangles.iter().enumerate() {
955        let indices = [triangle.index1, triangle.index2, triangle.index3];
956        if indices
957            .iter()
958            .any(|index| *index < 0 || *index as usize >= vertices.len())
959        {
960            return Err("debug mesh triangle index was out of range");
961        }
962        triangles.push(DebugMeshTriangle {
963            indices: [indices[0] as u32, indices[1] as u32, indices[2] as u32],
964            material_index: raw_material_indices.get(index).copied(),
965        });
966    }
967
968    Ok(DebugShapeGeometry::Mesh {
969        mesh: DebugMesh {
970            bounds: Aabb::from_raw(data.bounds),
971            vertices,
972            triangles,
973            material_count: data.materialCount,
974        },
975        scale: Vec3::from_raw(mesh.scale),
976    })
977}
978
979unsafe fn snapshot_height_field_ptr(
980    ptr: *const ffi::b3HeightFieldData,
981) -> SnapshotResult<DebugShapeGeometry> {
982    let height_field = unsafe { ptr.as_ref() }.ok_or("debug height-field pointer was null")?;
983    unsafe { snapshot_height_field(height_field) }
984}
985
986unsafe fn snapshot_height_field(
987    height_field: &ffi::b3HeightFieldData,
988) -> SnapshotResult<DebugShapeGeometry> {
989    let sample_count = checked_grid_count(height_field.rowCount, height_field.columnCount)?;
990    let cell_count = checked_grid_count(height_field.rowCount - 1, height_field.columnCount - 1)?;
991    let triangle_count = cell_count
992        .checked_mul(2)
993        .ok_or("debug height-field triangle count overflowed")?;
994    let compressed_heights = unsafe {
995        trailing_slice::<u16>(
996            height_field as *const _ as *const u8,
997            height_field.byteCount,
998            height_field.heightsOffset,
999            sample_count as i32,
1000        )?
1001    };
1002    let material_indices = unsafe {
1003        trailing_slice::<u8>(
1004            height_field as *const _ as *const u8,
1005            height_field.byteCount,
1006            height_field.materialOffset,
1007            cell_count as i32,
1008        )?
1009    };
1010
1011    let mut vertices = Vec::with_capacity(sample_count);
1012    for row in 0..height_field.rowCount {
1013        for column in 0..height_field.columnCount {
1014            let index = (row * height_field.columnCount + column) as usize;
1015            let height = height_field.minHeight
1016                + height_field.heightScale * compressed_heights[index] as f32;
1017            vertices.push(Vec3::new(
1018                column as f32 * height_field.scale.x,
1019                height * height_field.scale.y,
1020                row as f32 * height_field.scale.z,
1021            ));
1022        }
1023    }
1024
1025    let mut triangles = Vec::with_capacity(triangle_count);
1026    for row in 0..height_field.rowCount - 1 {
1027        for column in 0..height_field.columnCount - 1 {
1028            let cell_index = (row * (height_field.columnCount - 1) + column) as usize;
1029            let material_index = material_indices
1030                .get(cell_index)
1031                .copied()
1032                .ok_or("debug height-field material index was out of range")?;
1033            if material_index == ffi::B3_HEIGHT_FIELD_HOLE as u8 {
1034                continue;
1035            }
1036            let index11 = (row * height_field.columnCount + column) as u32;
1037            let index12 = index11 + 1;
1038            let index21 = ((row + 1) * height_field.columnCount + column) as u32;
1039            let index22 = index21 + 1;
1040            let first = if height_field.clockwise {
1041                [index11, index12, index21]
1042            } else {
1043                [index11, index21, index12]
1044            };
1045            let second = if height_field.clockwise {
1046                [index22, index21, index12]
1047            } else {
1048                [index22, index12, index21]
1049            };
1050            triangles.push(DebugMeshTriangle {
1051                indices: first,
1052                material_index: Some(material_index),
1053            });
1054            triangles.push(DebugMeshTriangle {
1055                indices: second,
1056                material_index: Some(material_index),
1057            });
1058        }
1059    }
1060
1061    Ok(DebugShapeGeometry::HeightField {
1062        mesh: DebugMesh {
1063            bounds: Aabb::from_raw(height_field.aabb),
1064            vertices,
1065            triangles,
1066            material_count: 256,
1067        },
1068    })
1069}
1070
1071unsafe fn snapshot_compound_ptr(
1072    ptr: *const ffi::b3CompoundData,
1073) -> SnapshotResult<DebugShapeGeometry> {
1074    let compound = unsafe { ptr.as_ref() }.ok_or("debug compound pointer was null")?;
1075    let total = compound
1076        .capsuleCount
1077        .checked_add(compound.hullCount)
1078        .and_then(|count| count.checked_add(compound.meshCount))
1079        .and_then(|count| count.checked_add(compound.sphereCount))
1080        .ok_or("debug compound child count overflowed")?;
1081    if total < 0 {
1082        return Err("debug compound child count was negative");
1083    }
1084
1085    let mut children = Vec::with_capacity(total as usize);
1086    for index in 0..total {
1087        let child = unsafe { ffi::b3GetCompoundChild(compound, index) };
1088        children.push(DebugCompoundChild {
1089            transform: Transform::from_raw(child.transform),
1090            material_indices: child.materialIndices,
1091            geometry: unsafe { snapshot_child_shape(child)? },
1092        });
1093    }
1094    Ok(DebugShapeGeometry::Compound { children })
1095}
1096
1097fn checked_grid_count(rows: i32, columns: i32) -> SnapshotResult<usize> {
1098    if rows <= 0 || columns <= 0 {
1099        return Err("debug height-field dimensions were invalid");
1100    }
1101    (rows as usize)
1102        .checked_mul(columns as usize)
1103        .ok_or("debug height-field dimensions overflowed")
1104}
1105
1106unsafe fn trailing_slice<'a, T>(
1107    base: *const u8,
1108    byte_count: i32,
1109    offset: i32,
1110    count: i32,
1111) -> SnapshotResult<&'a [T]> {
1112    if base.is_null() || byte_count <= 0 || offset <= 0 || count < 0 {
1113        return Err("debug shape trailing storage was invalid");
1114    }
1115    let start = offset as usize;
1116    let len = count as usize;
1117    let bytes = len
1118        .checked_mul(mem::size_of::<T>())
1119        .ok_or("debug shape trailing storage length overflowed")?;
1120    let end = start
1121        .checked_add(bytes)
1122        .ok_or("debug shape trailing storage end overflowed")?;
1123    if end > byte_count as usize {
1124        return Err("debug shape trailing storage exceeded its allocation");
1125    }
1126    let ptr = unsafe { base.add(start) as *const T };
1127    Ok(unsafe { slice::from_raw_parts(ptr, len) })
1128}
1129
1130struct DebugDrawContext<'a> {
1131    drawer: &'a mut dyn DebugDraw,
1132    state: callback_state::LocalCallbackState,
1133}
1134
1135fn run_debug_draw_callback<R: Copy>(
1136    context: &mut DebugDrawContext<'_>,
1137    default: R,
1138    callback: impl FnOnce(&mut dyn DebugDraw) -> R,
1139) -> R {
1140    let DebugDrawContext { drawer, state } = context;
1141    state.invoke(default, || callback(*drawer))
1142}
1143
1144fn debug_shape_handle_from_user_shape(user_shape: *mut c_void) -> Option<DebugShapeHandle> {
1145    if user_shape.is_null() {
1146        None
1147    } else {
1148        Some(unsafe { (*(user_shape as *const DebugShapeResource)).handle })
1149    }
1150}
1151
1152fn apply_options(draw: &mut ffi::b3DebugDraw, options: DebugDrawOptions, context: *mut c_void) {
1153    draw.drawingBounds = options.drawing_bounds.into_raw();
1154    draw.forceScale = options.force_scale;
1155    draw.jointScale = options.joint_scale;
1156    draw.drawShapes = options.draw_shapes;
1157    draw.drawJoints = options.draw_joints;
1158    draw.drawJointExtras = options.draw_joint_extras;
1159    draw.drawBounds = options.draw_bounds;
1160    draw.drawMass = options.draw_mass;
1161    draw.drawSleep = options.draw_sleep;
1162    draw.drawBodyNames = options.draw_body_names;
1163    draw.drawContacts = options.draw_contacts;
1164    draw.drawAnchorA = options.draw_anchor_a;
1165    draw.drawGraphColors = options.draw_graph_colors;
1166    draw.drawContactFeatures = options.draw_contact_features;
1167    draw.drawContactNormals = options.draw_contact_normals;
1168    draw.drawContactForces = options.draw_contact_forces;
1169    draw.drawIslands = options.draw_islands;
1170    draw.context = context;
1171}
1172
1173pub(crate) struct CollectDebugDraw<'a> {
1174    commands: &'a mut Vec<DebugDrawCommand>,
1175    len: usize,
1176}
1177
1178impl<'a> CollectDebugDraw<'a> {
1179    pub(crate) fn new(commands: &'a mut Vec<DebugDrawCommand>) -> Self {
1180        Self { commands, len: 0 }
1181    }
1182
1183    pub(crate) fn finish(self) {
1184        self.commands.truncate(self.len);
1185    }
1186
1187    fn replace_or_push(&mut self, command: DebugDrawCommand) {
1188        if let Some(slot) = self.commands.get_mut(self.len) {
1189            *slot = command;
1190        } else {
1191            self.commands.push(command);
1192        }
1193        self.len += 1;
1194    }
1195}
1196
1197impl DebugDraw for CollectDebugDraw<'_> {
1198    fn draw_shape(
1199        &mut self,
1200        handle: Option<DebugShapeHandle>,
1201        transform: WorldTransform,
1202        color: HexColor,
1203    ) {
1204        self.replace_or_push(DebugDrawCommand::Shape {
1205            handle,
1206            transform,
1207            color,
1208        });
1209    }
1210
1211    fn draw_segment(&mut self, p1: Pos, p2: Pos, color: HexColor) {
1212        self.replace_or_push(DebugDrawCommand::Segment { p1, p2, color });
1213    }
1214
1215    fn draw_transform(&mut self, transform: WorldTransform) {
1216        self.replace_or_push(DebugDrawCommand::Transform(transform));
1217    }
1218
1219    fn draw_point(&mut self, position: Pos, size: f32, color: HexColor) {
1220        self.replace_or_push(DebugDrawCommand::Point {
1221            position,
1222            size,
1223            color,
1224        });
1225    }
1226
1227    fn draw_sphere(&mut self, center: Pos, radius: f32, color: HexColor, alpha: f32) {
1228        self.replace_or_push(DebugDrawCommand::Sphere {
1229            center,
1230            radius,
1231            color,
1232            alpha,
1233        });
1234    }
1235
1236    fn draw_capsule(&mut self, p1: Pos, p2: Pos, radius: f32, color: HexColor, alpha: f32) {
1237        self.replace_or_push(DebugDrawCommand::Capsule {
1238            p1,
1239            p2,
1240            radius,
1241            color,
1242            alpha,
1243        });
1244    }
1245
1246    fn draw_bounds(&mut self, aabb: Aabb, color: HexColor) {
1247        self.replace_or_push(DebugDrawCommand::Bounds { aabb, color });
1248    }
1249
1250    fn draw_box(&mut self, extents: Vec3, transform: WorldTransform, color: HexColor) {
1251        self.replace_or_push(DebugDrawCommand::Box {
1252            extents,
1253            transform,
1254            color,
1255        });
1256    }
1257
1258    fn draw_string(&mut self, position: Pos, text: &str, color: HexColor) {
1259        match self.commands.get_mut(self.len) {
1260            Some(DebugDrawCommand::String {
1261                position: stored_position,
1262                text: stored_text,
1263                color: stored_color,
1264            }) => {
1265                *stored_position = position;
1266                stored_text.clear();
1267                stored_text.push_str(text);
1268                *stored_color = color;
1269                self.len += 1;
1270            }
1271            _ => self.replace_or_push(DebugDrawCommand::String {
1272                position,
1273                text: text.to_owned(),
1274                color,
1275            }),
1276        }
1277    }
1278}
1279
1280unsafe extern "C" fn draw_shape(
1281    user_shape: *mut c_void,
1282    transform: ffi::b3WorldTransform,
1283    color: ffi::b3HexColor,
1284    context: *mut c_void,
1285) {
1286    if context.is_null() {
1287        return;
1288    }
1289    let context = unsafe { &mut *(context as *mut DebugDrawContext<'_>) };
1290    run_debug_draw_callback(context, (), |drawer| {
1291        drawer.draw_shape(
1292            debug_shape_handle_from_user_shape(user_shape),
1293            WorldTransform::from_raw(transform),
1294            HexColor::from_ffi(color),
1295        );
1296    });
1297}
1298
1299unsafe extern "C" fn draw_segment(
1300    p1: ffi::b3Pos,
1301    p2: ffi::b3Pos,
1302    color: ffi::b3HexColor,
1303    context: *mut c_void,
1304) {
1305    if context.is_null() {
1306        return;
1307    }
1308    let context = unsafe { &mut *(context as *mut DebugDrawContext<'_>) };
1309    run_debug_draw_callback(context, (), |drawer| {
1310        drawer.draw_segment(
1311            Pos::from_raw(p1),
1312            Pos::from_raw(p2),
1313            HexColor::from_ffi(color),
1314        );
1315    });
1316}
1317
1318unsafe extern "C" fn draw_transform(transform: ffi::b3WorldTransform, context: *mut c_void) {
1319    if context.is_null() {
1320        return;
1321    }
1322    let context = unsafe { &mut *(context as *mut DebugDrawContext<'_>) };
1323    run_debug_draw_callback(context, (), |drawer| {
1324        drawer.draw_transform(WorldTransform::from_raw(transform));
1325    });
1326}
1327
1328unsafe extern "C" fn draw_point(
1329    position: ffi::b3Pos,
1330    size: f32,
1331    color: ffi::b3HexColor,
1332    context: *mut c_void,
1333) {
1334    if context.is_null() {
1335        return;
1336    }
1337    let context = unsafe { &mut *(context as *mut DebugDrawContext<'_>) };
1338    run_debug_draw_callback(context, (), |drawer| {
1339        drawer.draw_point(Pos::from_raw(position), size, HexColor::from_ffi(color));
1340    });
1341}
1342
1343unsafe extern "C" fn draw_sphere(
1344    center: ffi::b3Pos,
1345    radius: f32,
1346    color: ffi::b3HexColor,
1347    alpha: f32,
1348    context: *mut c_void,
1349) {
1350    if context.is_null() {
1351        return;
1352    }
1353    let context = unsafe { &mut *(context as *mut DebugDrawContext<'_>) };
1354    run_debug_draw_callback(context, (), |drawer| {
1355        drawer.draw_sphere(
1356            Pos::from_raw(center),
1357            radius,
1358            HexColor::from_ffi(color),
1359            alpha,
1360        );
1361    });
1362}
1363
1364unsafe extern "C" fn draw_capsule(
1365    p1: ffi::b3Pos,
1366    p2: ffi::b3Pos,
1367    radius: f32,
1368    color: ffi::b3HexColor,
1369    alpha: f32,
1370    context: *mut c_void,
1371) {
1372    if context.is_null() {
1373        return;
1374    }
1375    let context = unsafe { &mut *(context as *mut DebugDrawContext<'_>) };
1376    run_debug_draw_callback(context, (), |drawer| {
1377        drawer.draw_capsule(
1378            Pos::from_raw(p1),
1379            Pos::from_raw(p2),
1380            radius,
1381            HexColor::from_ffi(color),
1382            alpha,
1383        );
1384    });
1385}
1386
1387unsafe extern "C" fn draw_bounds(aabb: ffi::b3AABB, color: ffi::b3HexColor, context: *mut c_void) {
1388    if context.is_null() {
1389        return;
1390    }
1391    let context = unsafe { &mut *(context as *mut DebugDrawContext<'_>) };
1392    run_debug_draw_callback(context, (), |drawer| {
1393        drawer.draw_bounds(Aabb::from_raw(aabb), HexColor::from_ffi(color));
1394    });
1395}
1396
1397unsafe extern "C" fn draw_box(
1398    extents: ffi::b3Vec3,
1399    transform: ffi::b3WorldTransform,
1400    color: ffi::b3HexColor,
1401    context: *mut c_void,
1402) {
1403    if context.is_null() {
1404        return;
1405    }
1406    let context = unsafe { &mut *(context as *mut DebugDrawContext<'_>) };
1407    run_debug_draw_callback(context, (), |drawer| {
1408        drawer.draw_box(
1409            Vec3::from_raw(extents),
1410            WorldTransform::from_raw(transform),
1411            HexColor::from_ffi(color),
1412        );
1413    });
1414}
1415
1416unsafe extern "C" fn draw_string(
1417    position: ffi::b3Pos,
1418    text: *const std::ffi::c_char,
1419    color: ffi::b3HexColor,
1420    context: *mut c_void,
1421) {
1422    if context.is_null() || text.is_null() {
1423        return;
1424    }
1425    let context = unsafe { &mut *(context as *mut DebugDrawContext<'_>) };
1426    run_debug_draw_callback(context, (), |drawer| {
1427        let text = unsafe { CStr::from_ptr(text) }.to_string_lossy();
1428        drawer.draw_string(Pos::from_raw(position), &text, HexColor::from_ffi(color));
1429    });
1430}
1431
1432pub(crate) fn with_debug_draw(
1433    drawer: &mut dyn DebugDraw,
1434    options: DebugDrawOptions,
1435    invoke: impl FnOnce(&mut ffi::b3DebugDraw) -> Result<()>,
1436) -> Result<()> {
1437    callback_state::check_not_in_callback()?;
1438    #[cfg(all(target_arch = "wasm32", boxddd_wasm_provider))]
1439    {
1440        let _ = (drawer, options, invoke);
1441        Err(Error::UnsupportedOnWasm)
1442    }
1443    #[cfg(not(all(target_arch = "wasm32", boxddd_wasm_provider)))]
1444    {
1445        with_debug_draw_context(drawer, options, invoke)
1446    }
1447}
1448
1449pub(crate) fn with_replay_debug_draw(
1450    drawer: &mut dyn DebugDraw,
1451    options: DebugDrawOptions,
1452    lease: &ReplayLease,
1453    invoke: impl FnOnce(&mut ffi::b3DebugDraw) -> Result<()>,
1454) -> Result<()> {
1455    callback_state::check_not_in_callback()?;
1456    #[cfg(all(target_arch = "wasm32", boxddd_wasm_provider))]
1457    {
1458        let _ = (drawer, options, lease, invoke);
1459        Err(Error::UnsupportedOnWasm)
1460    }
1461    #[cfg(not(all(target_arch = "wasm32", boxddd_wasm_provider)))]
1462    {
1463        let _call = lease.enter_call()?;
1464        with_debug_draw_context(drawer, options, invoke)
1465    }
1466}
1467
1468#[cfg(not(all(target_arch = "wasm32", boxddd_wasm_provider)))]
1469fn with_debug_draw_context(
1470    drawer: &mut dyn DebugDraw,
1471    options: DebugDrawOptions,
1472    invoke: impl FnOnce(&mut ffi::b3DebugDraw) -> Result<()>,
1473) -> Result<()> {
1474    validate_options(options)?;
1475    let owner_call_frame = callback_state::OwnerCallFrame::enter();
1476    let result = {
1477        let mut context = DebugDrawContext {
1478            drawer,
1479            state: callback_state::LocalCallbackState::new(),
1480        };
1481        let mut draw = unsafe { ffi::b3DefaultDebugDraw() };
1482        draw.DrawShapeFcn = Some(draw_shape);
1483        draw.DrawSegmentFcn = Some(draw_segment);
1484        draw.DrawTransformFcn = Some(draw_transform);
1485        draw.DrawPointFcn = Some(draw_point);
1486        draw.DrawSphereFcn = Some(draw_sphere);
1487        draw.DrawCapsuleFcn = Some(draw_capsule);
1488        draw.DrawBoundsFcn = Some(draw_bounds);
1489        draw.DrawBoxFcn = Some(draw_box);
1490        draw.DrawStringFcn = Some(draw_string);
1491        apply_options(&mut draw, options, &mut context as *mut _ as *mut c_void);
1492
1493        let native_result = invoke(&mut draw);
1494        let callback_result = context.state.drain();
1495        native_result.and(callback_result)
1496    };
1497    drop(owner_call_frame);
1498    result
1499}
1500
1501fn validate_options(options: DebugDrawOptions) -> Result<()> {
1502    validation::finite("debug_draw.force_scale", options.force_scale)?;
1503    validation::finite("debug_draw.joint_scale", options.joint_scale)?;
1504    options.drawing_bounds.validate()?;
1505    Ok(())
1506}
1507
1508impl World {
1509    /// Collects a lifecycle-aware, owned debug draw frame.
1510    ///
1511    /// Pending persistent-shape events are drained into the frame. Apply them in order before the
1512    /// commands so renderer caches contain every referenced [`DebugShapeHandle`]. Commands use
1513    /// world coordinates, while geometry in [`DebugShapeAsset`] remains local to the shape. Box3D
1514    /// traversal order is not part of this API's contract.
1515    ///
1516    /// Provider-mode WASM supports this method through the debug-draw frame bridge. The returned
1517    /// frame contains no native borrows and may be retained independently of the world, although
1518    /// its source [`ShapeId`] values can later become stale. [`DebugShapeHandle`] values remain
1519    /// scoped to that world's lifetime and must not key a cache shared with another world.
1520    pub fn debug_draw_frame(&mut self, options: DebugDrawOptions) -> Result<DebugDrawFrame> {
1521        let mut frame = DebugDrawFrame::default();
1522        self.debug_draw_frame_into(&mut frame, options)?;
1523        Ok(frame)
1524    }
1525
1526    /// Collects a lifecycle-aware debug draw frame into `out`.
1527    ///
1528    /// `out` is cleared first while retaining its allocations. This call consumes pending
1529    /// persistent-shape lifecycle events, so callers maintaining a renderer cache must process the
1530    /// returned `events` before `commands` rather than discarding the frame. On error, `out` may
1531    /// contain partial commands, events, or diagnostics collected before the failure.
1532    ///
1533    /// This is the reusable-buffer form of [`Self::debug_draw_frame`] and has the same coordinate,
1534    /// ownership, traversal, and provider semantics.
1535    pub fn debug_draw_frame_into(
1536        &mut self,
1537        out: &mut DebugDrawFrame,
1538        options: DebugDrawOptions,
1539    ) -> Result<()> {
1540        callback_state::check_not_in_callback()?;
1541        #[cfg(all(target_arch = "wasm32", boxddd_wasm_provider))]
1542        {
1543            out.clear();
1544            validate_options(options)?;
1545
1546            let token = self.state().provider_debug_shapes_token;
1547            if token == 0 {
1548                return Err(Error::UnsupportedOnWasm);
1549            }
1550
1551            let owner_call_frame = callback_state::OwnerCallFrame::enter();
1552            let frame_guard = ProviderDebugFrameGuard::new(token, &mut out.commands);
1553            let result = {
1554                let _call = self.enter_world_call()?;
1555                let mut draw = unsafe { ffi::b3DefaultDebugDraw() };
1556                unsafe { ffi::boxddd_provider_debug_init_draw(&mut draw, token) };
1557                apply_options(&mut draw, options, token as usize as *mut c_void);
1558                unsafe { ffi::b3World_Draw(self.raw(), &mut draw, options.mask_bits) };
1559                Ok(())
1560            };
1561            drop(frame_guard);
1562
1563            let result = result.and_then(|()| match take_provider_debug_error(token) {
1564                Some(error) => Err(error),
1565                None => Ok(()),
1566            });
1567            self.state().debug_shapes.drain_into(out);
1568            drop(owner_call_frame);
1569            result
1570        }
1571        #[cfg(not(all(target_arch = "wasm32", boxddd_wasm_provider)))]
1572        {
1573            out.clear();
1574            let mut collector = CollectDebugDraw::new(&mut out.commands);
1575            let result = self.debug_draw(&mut collector, options);
1576            collector.finish();
1577            self.state().debug_shapes.drain_into(out);
1578            result
1579        }
1580    }
1581
1582    /// Runs debug drawing synchronously with a custom sink.
1583    ///
1584    /// The sink is borrowed only for this call and receives callbacks in Box3D's unspecified world
1585    /// traversal order. Positions and transforms are in world coordinates. Safe API calls made
1586    /// reentrantly by the sink return [`Error::InCallback`]. On unwind-capable targets, the first
1587    /// sink panic is contained, later callbacks are suppressed, and this method returns
1588    /// [`Error::CallbackPanicked`]; `panic=abort` targets cannot contain it.
1589    ///
1590    /// This low-level path does not expose owned persistent-shape lifecycle assets. Prefer
1591    /// [`Self::debug_draw_frame`] for renderer integration. Provider-mode WASM cannot invoke a
1592    /// Rust-owned custom sink and returns [`Error::UnsupportedOnWasm`] before traversal.
1593    pub fn debug_draw(
1594        &mut self,
1595        drawer: &mut impl DebugDraw,
1596        options: DebugDrawOptions,
1597    ) -> Result<()> {
1598        let _call = self.enter_world_call()?;
1599        with_debug_draw(drawer, options, |draw| {
1600            unsafe { ffi::b3World_Draw(self.raw(), draw, options.mask_bits) };
1601            Ok(())
1602        })
1603    }
1604}
1605
1606#[cfg(all(test, not(all(target_arch = "wasm32", boxddd_wasm_provider))))]
1607mod tests {
1608    use super::*;
1609    use crate::{Error, Foundation, Sphere};
1610
1611    #[test]
1612    fn internal_debug_shape_panic_is_contained_and_poisons_the_world() {
1613        let foundation = Foundation::initialize_default().unwrap();
1614        let mut world = foundation.create_world(foundation.world_def()).unwrap();
1615        let body = world.create_body(foundation.body_def()).unwrap();
1616        world
1617            .create_sphere_shape(body, &foundation.shape_def(), &Sphere::new(Vec3::ZERO, 0.5))
1618            .unwrap();
1619
1620        world.state().debug_shapes.panic_next_native_callback();
1621        assert!(world.debug_draw_frame(DebugDrawOptions::default()).is_ok());
1622        assert_eq!(world.gravity().unwrap_err(), Error::OwnerPoisoned);
1623    }
1624}