Skip to main content

proof_engine/editor/
physics_debug.rs

1// physics_debug.rs — Physics visualizer for proof-engine editor
2// Draws: force fields, constraint axes, cloth mesh wireframe, rigid body AABBs,
3// soft body vertex normals, SDF collision shells, IK chain overlays,
4// particle velocity vectors, and contact point indicators.
5
6use glam::{Vec2, Vec3, Vec4, Mat4, Quat};
7use std::collections::HashMap;
8
9// ─── Primitive draw commands ─────────────────────────────────────────────────
10
11#[derive(Debug, Clone)]
12pub enum DebugPrim {
13    Line   { start: Vec3, end: Vec3, color: Vec4, width: f32 },
14    Arrow  { from: Vec3, to: Vec3, color: Vec4, head_size: f32 },
15    Sphere { center: Vec3, radius: f32, color: Vec4, filled: bool },
16    Box    { center: Vec3, half_extents: Vec3, rotation: Quat, color: Vec4, filled: bool },
17    Cylinder { base: Vec3, top: Vec3, radius: f32, color: Vec4 },
18    Cone   { apex: Vec3, base_center: Vec3, radius: f32, color: Vec4 },
19    Disc   { center: Vec3, normal: Vec3, radius: f32, color: Vec4 },
20    Grid   { center: Vec3, normal: Vec3, size: f32, divisions: u32, color: Vec4 },
21    Cross  { center: Vec3, size: f32, color: Vec4 },
22    Axes   { transform: Mat4, size: f32 },
23    Text   { pos: Vec3, text: String, color: Vec4, scale: f32 },
24    Quad   { corners: [Vec3; 4], color: Vec4, filled: bool },
25    Circle { center: Vec3, normal: Vec3, radius: f32, segments: u32, color: Vec4 },
26    Frustum { view_proj_inv: Mat4, color: Vec4 },
27    Bezier { p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, steps: u32, color: Vec4 },
28    Point  { pos: Vec3, size: f32, color: Vec4 },
29    Torus  { center: Vec3, normal: Vec3, major_r: f32, minor_r: f32, color: Vec4 },
30    Spring { start: Vec3, end: Vec3, coils: u32, radius: f32, color: Vec4 },
31}
32
33impl DebugPrim {
34    pub fn line(start: Vec3, end: Vec3, color: Vec4) -> Self {
35        Self::Line { start, end, color, width: 1.5 }
36    }
37    pub fn thin_line(start: Vec3, end: Vec3, color: Vec4) -> Self {
38        Self::Line { start, end, color, width: 1.0 }
39    }
40    pub fn thick_line(start: Vec3, end: Vec3, color: Vec4) -> Self {
41        Self::Line { start, end, color, width: 3.0 }
42    }
43    pub fn arrow(from: Vec3, to: Vec3, color: Vec4) -> Self {
44        Self::Arrow { from, to, color, head_size: 0.05 }
45    }
46    pub fn wire_sphere(center: Vec3, radius: f32, color: Vec4) -> Self {
47        Self::Sphere { center, radius, color, filled: false }
48    }
49    pub fn solid_sphere(center: Vec3, radius: f32, color: Vec4) -> Self {
50        Self::Sphere { center, radius, color, filled: true }
51    }
52    pub fn wire_box(center: Vec3, half: Vec3, rot: Quat, color: Vec4) -> Self {
53        Self::Box { center, half_extents: half, rotation: rot, color, filled: false }
54    }
55    pub fn aabb(min: Vec3, max: Vec3, color: Vec4) -> Self {
56        let center = (min + max) * 0.5;
57        let half   = (max - min) * 0.5;
58        Self::Box { center, half_extents: half, rotation: Quat::IDENTITY, color, filled: false }
59    }
60    pub fn axes(transform: Mat4) -> Self {
61        Self::Axes { transform, size: 0.2 }
62    }
63    pub fn point(pos: Vec3, color: Vec4) -> Self {
64        Self::Point { pos, size: 6.0, color }
65    }
66}
67
68/// Expand a DebugPrim into a flat list of line vertices for GPU upload.
69/// Returns pairs (start, end) as Vec3 tuples.
70impl DebugPrim {
71    pub fn to_lines(&self) -> Vec<(Vec3, Vec3, Vec4)> {
72        match self {
73            Self::Line { start, end, color, .. } =>
74                vec![(*start, *end, *color)],
75
76            Self::Arrow { from, to, color, head_size } => {
77                let dir = (*to - *from).normalize_or_zero();
78                let perp = dir.any_orthogonal_vector().normalize();
79                let perp2 = dir.cross(perp).normalize();
80                let head_base = *to - dir * (*head_size * 3.0);
81                let mut lines = vec![(*from, *to, *color)];
82                for i in 0..4u32 {
83                    let angle = i as f32 * std::f32::consts::FRAC_PI_2;
84                    let offset = perp * angle.cos() * *head_size + perp2 * angle.sin() * *head_size;
85                    lines.push((head_base + offset, *to, *color));
86                }
87                lines
88            }
89
90            Self::Sphere { center, radius, color, .. } => {
91                let mut lines = Vec::new();
92                let segs = 16u32;
93                for axis in 0..3 {
94                    let prev_angle = 0.0f32;
95                    let _ = prev_angle;
96                    for i in 0..segs {
97                        let a0 = i as f32 / segs as f32 * 2.0 * std::f32::consts::PI;
98                        let a1 = (i + 1) as f32 / segs as f32 * 2.0 * std::f32::consts::PI;
99                        let p0 = circle_point(*center, *radius, a0, axis);
100                        let p1 = circle_point(*center, *radius, a1, axis);
101                        lines.push((p0, p1, *color));
102                    }
103                }
104                lines
105            }
106
107            Self::Box { center, half_extents, rotation, color, .. } => {
108                let he = *half_extents;
109                let corners_local = [
110                    Vec3::new(-he.x,-he.y,-he.z), Vec3::new( he.x,-he.y,-he.z),
111                    Vec3::new( he.x, he.y,-he.z), Vec3::new(-he.x, he.y,-he.z),
112                    Vec3::new(-he.x,-he.y, he.z), Vec3::new( he.x,-he.y, he.z),
113                    Vec3::new( he.x, he.y, he.z), Vec3::new(-he.x, he.y, he.z),
114                ];
115                let c: Vec<Vec3> = corners_local.iter()
116                    .map(|&v| *center + rotation.mul_vec3(v))
117                    .collect();
118                let edges = [
119                    (0,1),(1,2),(2,3),(3,0),
120                    (4,5),(5,6),(6,7),(7,4),
121                    (0,4),(1,5),(2,6),(3,7),
122                ];
123                edges.iter().map(|&(a,b)| (c[a], c[b], *color)).collect()
124            }
125
126            Self::Cylinder { base, top, radius, color } => {
127                let mut lines = Vec::new();
128                let segs = 12u32;
129                let up = (*top - *base).normalize_or_zero();
130                let right = up.any_orthogonal_vector().normalize();
131                let fwd   = up.cross(right).normalize();
132                for i in 0..segs {
133                    let a0 = i as f32 / segs as f32 * 2.0 * std::f32::consts::PI;
134                    let a1 = (i + 1) as f32 / segs as f32 * 2.0 * std::f32::consts::PI;
135                    let r0 = right * a0.cos() * *radius + fwd * a0.sin() * *radius;
136                    let r1 = right * a1.cos() * *radius + fwd * a1.sin() * *radius;
137                    lines.push((*base + r0, *base + r1, *color));
138                    lines.push((*top  + r0, *top  + r1, *color));
139                    if i % 3 == 0 {
140                        lines.push((*base + r0, *top + r0, *color));
141                    }
142                }
143                lines
144            }
145
146            Self::Disc { center, normal, radius, color } => {
147                let mut lines = Vec::new();
148                let right = normal.any_orthogonal_vector().normalize();
149                let up    = normal.cross(right).normalize();
150                let segs  = 16u32;
151                for i in 0..segs {
152                    let a0 = i as f32 / segs as f32 * 2.0 * std::f32::consts::PI;
153                    let a1 = (i + 1) as f32 / segs as f32 * 2.0 * std::f32::consts::PI;
154                    let p0 = *center + right * a0.cos() * *radius + up * a0.sin() * *radius;
155                    let p1 = *center + right * a1.cos() * *radius + up * a1.sin() * *radius;
156                    lines.push((p0, p1, *color));
157                }
158                lines
159            }
160
161            Self::Circle { center, normal, radius, segments, color } => {
162                let mut lines = Vec::new();
163                let right = normal.any_orthogonal_vector().normalize();
164                let up    = normal.cross(right).normalize();
165                let s = *segments;
166                for i in 0..s {
167                    let a0 = i as f32 / s as f32 * 2.0 * std::f32::consts::PI;
168                    let a1 = (i + 1) as f32 / s as f32 * 2.0 * std::f32::consts::PI;
169                    let p0 = *center + right * a0.cos() * *radius + up * a0.sin() * *radius;
170                    let p1 = *center + right * a1.cos() * *radius + up * a1.sin() * *radius;
171                    lines.push((p0, p1, *color));
172                }
173                lines
174            }
175
176            Self::Axes { transform, size } => {
177                let o = transform.col(3).truncate();
178                let x = o + transform.col(0).truncate().normalize_or_zero() * *size;
179                let y = o + transform.col(1).truncate().normalize_or_zero() * *size;
180                let z = o + transform.col(2).truncate().normalize_or_zero() * *size;
181                vec![
182                    (o, x, Vec4::new(1.0, 0.2, 0.2, 1.0)),
183                    (o, y, Vec4::new(0.2, 1.0, 0.2, 1.0)),
184                    (o, z, Vec4::new(0.2, 0.4, 1.0, 1.0)),
185                ]
186            }
187
188            Self::Cross { center, size, color } => {
189                let h = *size * 0.5;
190                vec![
191                    (*center - Vec3::X*h, *center + Vec3::X*h, *color),
192                    (*center - Vec3::Y*h, *center + Vec3::Y*h, *color),
193                    (*center - Vec3::Z*h, *center + Vec3::Z*h, *color),
194                ]
195            }
196
197            Self::Grid { center, normal, size, divisions, color } => {
198                let mut lines = Vec::new();
199                let right = normal.any_orthogonal_vector().normalize();
200                let fwd   = normal.cross(right).normalize();
201                let half  = *size * 0.5;
202                let step  = *size / *divisions as f32;
203                for i in 0..=*divisions {
204                    let t = -half + i as f32 * step;
205                    lines.push((
206                        *center + right * t - fwd * half,
207                        *center + right * t + fwd * half,
208                        *color,
209                    ));
210                    lines.push((
211                        *center - right * half + fwd * t,
212                        *center + right * half + fwd * t,
213                        *color,
214                    ));
215                }
216                lines
217            }
218
219            Self::Bezier { p0, p1, p2, p3, steps, color } => {
220                let mut lines = Vec::new();
221                let mut prev = *p0;
222                for i in 1..=*steps {
223                    let t = i as f32 / *steps as f32;
224                    let u = 1.0 - t;
225                    let pt = *p0*(u*u*u) + *p1*(3.0*u*u*t) + *p2*(3.0*u*t*t) + *p3*(t*t*t);
226                    lines.push((prev, pt, *color));
227                    prev = pt;
228                }
229                lines
230            }
231
232            Self::Spring { start, end, coils, radius, color } => {
233                let mut lines = Vec::new();
234                let dir   = (*end - *start).normalize_or_zero();
235                let right = dir.any_orthogonal_vector().normalize();
236                let up    = dir.cross(right).normalize();
237                let total = (*end - *start).length();
238                let steps = *coils * 16;
239                let mut prev = *start;
240                for i in 1..=steps {
241                    let t = i as f32 / steps as f32;
242                    let along = t * total;
243                    let angle = i as f32 / 16.0 * 2.0 * std::f32::consts::PI;
244                    let pos = *start + dir * along
245                        + right * angle.cos() * *radius
246                        + up    * angle.sin() * *radius;
247                    lines.push((prev, pos, *color));
248                    prev = pos;
249                }
250                lines
251            }
252
253            Self::Torus { center, normal, major_r, minor_r, color } => {
254                let mut lines = Vec::new();
255                let right = normal.any_orthogonal_vector().normalize();
256                let up    = normal.cross(right).normalize();
257                let major_segs = 16u32;
258                let minor_segs = 8u32;
259                for i in 0..major_segs {
260                    let phi0 = i as f32 / major_segs as f32 * 2.0 * std::f32::consts::PI;
261                    let phi1 = (i+1) as f32 / major_segs as f32 * 2.0 * std::f32::consts::PI;
262                    let mc0 = *center + right * phi0.cos() * *major_r + up * phi0.sin() * *major_r;
263                    let mc1 = *center + right * phi1.cos() * *major_r + up * phi1.sin() * *major_r;
264                    // Draw minor circle
265                    let rad0 = (right * phi0.cos() + up * phi0.sin()).normalize();
266                    let _ = (mc0, mc1);
267                    for j in 0..minor_segs {
268                        let theta0 = j as f32 / minor_segs as f32 * 2.0 * std::f32::consts::PI;
269                        let theta1 = (j+1) as f32 / minor_segs as f32 * 2.0 * std::f32::consts::PI;
270                        let p0t = mc0 + rad0 * theta0.cos() * *minor_r + *normal * theta0.sin() * *minor_r;
271                        let p1t = mc0 + rad0 * theta1.cos() * *minor_r + *normal * theta1.sin() * *minor_r;
272                        lines.push((p0t, p1t, *color));
273                    }
274                }
275                lines
276            }
277
278            Self::Frustum { view_proj_inv, color } => {
279                let ndc_corners = [
280                    Vec4::new(-1.0,-1.0,-1.0,1.0), Vec4::new( 1.0,-1.0,-1.0,1.0),
281                    Vec4::new( 1.0, 1.0,-1.0,1.0), Vec4::new(-1.0, 1.0,-1.0,1.0),
282                    Vec4::new(-1.0,-1.0, 1.0,1.0), Vec4::new( 1.0,-1.0, 1.0,1.0),
283                    Vec4::new( 1.0, 1.0, 1.0,1.0), Vec4::new(-1.0, 1.0, 1.0,1.0),
284                ];
285                let ws: Vec<Vec3> = ndc_corners.iter().map(|&v| {
286                    let r = *view_proj_inv * v;
287                    r.truncate() / r.w
288                }).collect();
289                let edges = [(0,1),(1,2),(2,3),(3,0),(4,5),(5,6),(6,7),(7,4),(0,4),(1,5),(2,6),(3,7)];
290                edges.iter().map(|&(a,b)| (ws[a], ws[b], *color)).collect()
291            }
292
293            Self::Cone { apex, base_center, radius, color } => {
294                let mut lines = Vec::new();
295                let axis  = (*apex - *base_center).normalize_or_zero();
296                let right = axis.any_orthogonal_vector().normalize();
297                let fwd   = axis.cross(right).normalize();
298                let segs  = 8u32;
299                for i in 0..segs {
300                    let a = i as f32 / segs as f32 * 2.0 * std::f32::consts::PI;
301                    let a1 = (i+1) as f32 / segs as f32 * 2.0 * std::f32::consts::PI;
302                    let r0 = *base_center + right * a.cos() * *radius + fwd * a.sin() * *radius;
303                    let r1 = *base_center + right * a1.cos() * *radius + fwd * a1.sin() * *radius;
304                    lines.push((*apex, r0, *color));
305                    lines.push((r0, r1, *color));
306                }
307                lines
308            }
309
310            Self::Quad { corners, color, .. } => {
311                vec![
312                    (corners[0], corners[1], *color),
313                    (corners[1], corners[2], *color),
314                    (corners[2], corners[3], *color),
315                    (corners[3], corners[0], *color),
316                ]
317            }
318
319            Self::Point { pos, size, color } => {
320                let h = *size * 0.003;
321                vec![
322                    (*pos - Vec3::X*h, *pos + Vec3::X*h, *color),
323                    (*pos - Vec3::Y*h, *pos + Vec3::Y*h, *color),
324                    (*pos - Vec3::Z*h, *pos + Vec3::Z*h, *color),
325                ]
326            }
327
328            Self::Text { .. } => vec![],  // Text handled separately
329        }
330    }
331}
332
333fn circle_point(center: Vec3, radius: f32, angle: f32, axis: usize) -> Vec3 {
334    let (s, c) = angle.sin_cos();
335    match axis {
336        0 => center + Vec3::new(0.0, c * radius, s * radius),
337        1 => center + Vec3::new(c * radius, 0.0, s * radius),
338        _ => center + Vec3::new(c * radius, s * radius, 0.0),
339    }
340}
341
342// ─── Physics body types ───────────────────────────────────────────────────────
343
344#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
345pub struct PhysBodyId(pub u32);
346
347#[derive(Debug, Clone, Copy, PartialEq, Eq)]
348pub enum BodyType {
349    Static,
350    Kinematic,
351    Dynamic,
352    Cloth,
353    SoftBody,
354    Fluid,
355    SdfCollider,
356}
357
358#[derive(Debug, Clone)]
359pub struct PhysBodyState {
360    pub id: PhysBodyId,
361    pub body_type: BodyType,
362    pub position: Vec3,
363    pub rotation: Quat,
364    pub linear_velocity: Vec3,
365    pub angular_velocity: Vec3,
366    pub aabb_min: Vec3,
367    pub aabb_max: Vec3,
368    pub mass: f32,
369    pub asleep: bool,
370    pub active: bool,
371}
372
373impl PhysBodyState {
374    pub fn new(id: PhysBodyId) -> Self {
375        Self {
376            id,
377            body_type: BodyType::Dynamic,
378            position: Vec3::ZERO,
379            rotation: Quat::IDENTITY,
380            linear_velocity: Vec3::ZERO,
381            angular_velocity: Vec3::ZERO,
382            aabb_min: Vec3::splat(-0.5),
383            aabb_max: Vec3::splat( 0.5),
384            mass: 1.0,
385            asleep: false,
386            active: true,
387        }
388    }
389}
390
391// ─── Contact / constraint ─────────────────────────────────────────────────────
392
393#[derive(Debug, Clone)]
394pub struct ContactPoint {
395    pub position: Vec3,
396    pub normal: Vec3,
397    pub depth: f32,
398    pub body_a: PhysBodyId,
399    pub body_b: PhysBodyId,
400    pub friction_impulse: f32,
401    pub normal_impulse: f32,
402}
403
404#[derive(Debug, Clone)]
405pub struct ConstraintViz {
406    pub body_a: Option<PhysBodyId>,
407    pub body_b: Option<PhysBodyId>,
408    pub anchor_a: Vec3,
409    pub anchor_b: Vec3,
410    pub kind: ConstraintKind,
411    pub active: bool,
412    pub broken: bool,
413    pub strain: f32,
414}
415
416#[derive(Debug, Clone, Copy, PartialEq, Eq)]
417pub enum ConstraintKind {
418    Fixed,
419    Hinge,
420    BallSocket,
421    Slider,
422    Spring,
423    Distance,
424    Cone,
425    Gear,
426    Ik,
427}
428
429impl ConstraintKind {
430    pub fn color(&self) -> Vec4 {
431        match self {
432            Self::Fixed      => Vec4::new(0.8, 0.8, 0.8, 1.0),
433            Self::Hinge      => Vec4::new(1.0, 0.6, 0.1, 1.0),
434            Self::BallSocket => Vec4::new(0.3, 0.9, 0.5, 1.0),
435            Self::Slider     => Vec4::new(0.5, 0.5, 1.0, 1.0),
436            Self::Spring     => Vec4::new(1.0, 1.0, 0.2, 1.0),
437            Self::Distance   => Vec4::new(0.7, 0.3, 1.0, 1.0),
438            Self::Cone       => Vec4::new(1.0, 0.4, 0.4, 1.0),
439            Self::Gear       => Vec4::new(0.5, 0.8, 1.0, 1.0),
440            Self::Ik         => Vec4::new(0.2, 1.0, 1.0, 1.0),
441        }
442    }
443}
444
445// ─── Force field visualization ────────────────────────────────────────────────
446
447#[derive(Debug, Clone)]
448pub struct ForceFieldViz {
449    pub name: String,
450    pub position: Vec3,
451    pub radius: f32,
452    pub strength: f32,
453    pub field_type: ForceFieldType,
454    pub active: bool,
455}
456
457#[derive(Debug, Clone, Copy, PartialEq, Eq)]
458pub enum ForceFieldType {
459    Gravity,
460    Radial,
461    Directional,
462    Vortex,
463    Turbulence,
464    Attractor,
465    Repulsor,
466    Wind,
467    Explosion,
468    Magnetic,
469}
470
471impl ForceFieldType {
472    pub fn color(&self) -> Vec4 {
473        match self {
474            Self::Gravity     => Vec4::new(0.5, 0.5, 1.0, 0.7),
475            Self::Radial      => Vec4::new(1.0, 0.5, 0.0, 0.7),
476            Self::Directional => Vec4::new(0.2, 0.8, 0.2, 0.7),
477            Self::Vortex      => Vec4::new(0.9, 0.2, 0.9, 0.7),
478            Self::Turbulence  => Vec4::new(0.8, 0.8, 0.0, 0.7),
479            Self::Attractor   => Vec4::new(0.2, 0.8, 1.0, 0.7),
480            Self::Repulsor    => Vec4::new(1.0, 0.2, 0.2, 0.7),
481            Self::Wind        => Vec4::new(0.7, 0.9, 1.0, 0.7),
482            Self::Explosion   => Vec4::new(1.0, 0.6, 0.0, 0.9),
483            Self::Magnetic    => Vec4::new(0.6, 0.3, 1.0, 0.7),
484        }
485    }
486
487    pub fn sample_at(&self, relative_pos: Vec3, strength: f32, radius: f32) -> Vec3 {
488        let dist  = relative_pos.length();
489        if dist > radius { return Vec3::ZERO; }
490        let falloff = (1.0 - dist / radius).powi(2);
491        match self {
492            Self::Gravity     => Vec3::NEG_Y * strength * falloff,
493            Self::Radial      => relative_pos.normalize_or_zero() * strength * falloff,
494            Self::Directional => Vec3::Y * strength * falloff,
495            Self::Vortex      => {
496                let r = Vec3::new(relative_pos.z, 0.0, -relative_pos.x).normalize_or_zero();
497                r * strength * falloff
498            }
499            Self::Attractor   => -relative_pos.normalize_or_zero() * strength * falloff,
500            Self::Repulsor    =>  relative_pos.normalize_or_zero() * strength * falloff,
501            Self::Wind        => Vec3::X * strength * falloff,
502            Self::Turbulence  => Vec3::new(
503                Self::hash_noise(relative_pos, 0),
504                Self::hash_noise(relative_pos, 1),
505                Self::hash_noise(relative_pos, 2),
506            ) * strength * falloff,
507            Self::Explosion   => relative_pos.normalize_or_zero() * strength * falloff * 5.0,
508            Self::Magnetic    => {
509                let right = Vec3::X;
510                let tan = right.cross(relative_pos.normalize_or_zero()).normalize_or_zero();
511                tan * strength * falloff
512            }
513        }
514    }
515
516    fn hash_noise(p: Vec3, seed: u32) -> f32 {
517        let xi = ((p.x * 5.0 + seed as f32 * 31.0) as i32) as u32;
518        let yi = ((p.y * 5.0 + seed as f32 * 67.0) as i32) as u32;
519        let zi = ((p.z * 5.0 + seed as f32 * 113.0) as i32) as u32;
520        let h = xi.wrapping_mul(0x9e3779b9).wrapping_add(
521                yi.wrapping_mul(0x85ebca6b)).wrapping_add(
522                zi.wrapping_mul(0xc2b2ae35));
523        (h as f32 / u32::MAX as f32) * 2.0 - 1.0
524    }
525}
526
527// ─── Cloth mesh debug ─────────────────────────────────────────────────────────
528
529#[derive(Debug, Clone)]
530pub struct ClothMeshViz {
531    pub name: String,
532    pub vertices: Vec<Vec3>,
533    pub edges: Vec<(usize, usize)>,
534    pub pinned: Vec<bool>,
535    pub velocities: Vec<Vec3>,
536    pub show_velocities: bool,
537    pub show_normals: bool,
538    pub show_pinned: bool,
539    pub color: Vec4,
540    pub velocity_scale: f32,
541}
542
543impl ClothMeshViz {
544    pub fn new(name: String, rows: usize, cols: usize) -> Self {
545        let mut verts = Vec::new();
546        let mut edges = Vec::new();
547        let mut pinned = Vec::new();
548        // Build a simple grid mesh
549        for r in 0..rows {
550            for c in 0..cols {
551                let x = c as f32 / (cols-1).max(1) as f32 - 0.5;
552                let y = 0.0;
553                let z = r as f32 / (rows-1).max(1) as f32 - 0.5;
554                verts.push(Vec3::new(x, y, z));
555                pinned.push(r == 0);
556            }
557        }
558        // Horizontal
559        for r in 0..rows {
560            for c in 0..cols-1 {
561                edges.push((r*cols+c, r*cols+c+1));
562            }
563        }
564        // Vertical
565        for r in 0..rows-1 {
566            for c in 0..cols {
567                edges.push((r*cols+c, (r+1)*cols+c));
568            }
569        }
570        // Diagonal
571        for r in 0..rows-1 {
572            for c in 0..cols-1 {
573                edges.push((r*cols+c, (r+1)*cols+c+1));
574                edges.push((r*cols+c+1, (r+1)*cols+c));
575            }
576        }
577        let velocities = vec![Vec3::ZERO; verts.len()];
578        Self {
579            name,
580            vertices: verts,
581            edges,
582            pinned,
583            velocities,
584            show_velocities: false,
585            show_normals: false,
586            show_pinned: true,
587            color: Vec4::new(0.4, 0.7, 1.0, 0.6),
588            velocity_scale: 0.1,
589        }
590    }
591
592    pub fn build_primitives(&self) -> Vec<DebugPrim> {
593        let mut prims = Vec::new();
594        // Edge lines
595        for &(a, b) in &self.edges {
596            if a < self.vertices.len() && b < self.vertices.len() {
597                prims.push(DebugPrim::line(self.vertices[a], self.vertices[b], self.color));
598            }
599        }
600        // Pinned points
601        if self.show_pinned {
602            let pin_color = Vec4::new(1.0, 0.3, 0.3, 1.0);
603            for (i, &pinned) in self.pinned.iter().enumerate() {
604                if pinned && i < self.vertices.len() {
605                    prims.push(DebugPrim::Point {
606                        pos: self.vertices[i],
607                        size: 8.0,
608                        color: pin_color,
609                    });
610                }
611            }
612        }
613        // Velocity vectors
614        if self.show_velocities {
615            let vel_color = Vec4::new(0.2, 1.0, 0.2, 0.8);
616            for (i, &vel) in self.velocities.iter().enumerate() {
617                if i < self.vertices.len() && vel.length() > 0.001 {
618                    prims.push(DebugPrim::arrow(
619                        self.vertices[i],
620                        self.vertices[i] + vel * self.velocity_scale,
621                        vel_color,
622                    ));
623                }
624            }
625        }
626        prims
627    }
628}
629
630// ─── IK chain visualization ───────────────────────────────────────────────────
631
632#[derive(Debug, Clone)]
633pub struct IkChainViz {
634    pub name: String,
635    pub joints: Vec<Vec3>,
636    pub orientations: Vec<Quat>,
637    pub lengths: Vec<f32>,
638    pub target: Vec3,
639    pub target_reached: bool,
640    pub iterations: u32,
641    pub error: f32,
642}
643
644impl IkChainViz {
645    pub fn build_primitives(&self) -> Vec<DebugPrim> {
646        let mut prims = Vec::new();
647        let bone_color  = Vec4::new(0.9, 0.8, 0.2, 1.0);
648        let joint_color = Vec4::new(0.3, 0.9, 0.3, 1.0);
649        let target_color = if self.target_reached {
650            Vec4::new(0.0, 1.0, 0.0, 1.0)
651        } else {
652            Vec4::new(1.0, 0.4, 0.1, 1.0)
653        };
654
655        for i in 0..self.joints.len().saturating_sub(1) {
656            prims.push(DebugPrim::thick_line(self.joints[i], self.joints[i+1], bone_color));
657            prims.push(DebugPrim::wire_sphere(self.joints[i], 0.02, joint_color));
658            // Axes
659            let m = Mat4::from_rotation_translation(self.orientations[i], self.joints[i]);
660            prims.push(DebugPrim::axes(m));
661        }
662        if let Some(last) = self.joints.last() {
663            prims.push(DebugPrim::wire_sphere(*last, 0.02, joint_color));
664        }
665        // Target
666        prims.push(DebugPrim::Cross { center: self.target, size: 0.1, color: target_color });
667        prims.push(DebugPrim::wire_sphere(self.target, 0.03, target_color));
668        prims
669    }
670}
671
672// ─── Particle debug visualizer ───────────────────────────────────────────────
673
674#[derive(Debug, Clone)]
675pub struct ParticleDebugViz {
676    pub positions: Vec<Vec3>,
677    pub velocities: Vec<Vec3>,
678    pub life_normalized: Vec<f32>,
679    pub show_velocity: bool,
680    pub show_life_color: bool,
681    pub velocity_scale: f32,
682    pub point_size: f32,
683    pub max_shown: usize,
684}
685
686impl ParticleDebugViz {
687    pub fn new() -> Self {
688        Self {
689            positions: Vec::new(),
690            velocities: Vec::new(),
691            life_normalized: Vec::new(),
692            show_velocity: false,
693            show_life_color: true,
694            velocity_scale: 0.05,
695            point_size: 4.0,
696            max_shown: 10000,
697        }
698    }
699
700    pub fn build_primitives(&self) -> Vec<DebugPrim> {
701        let mut prims = Vec::new();
702        let count = self.positions.len().min(self.max_shown);
703        for i in 0..count {
704            let life = self.life_normalized.get(i).copied().unwrap_or(1.0);
705            let color = if self.show_life_color {
706                lerp_color(
707                    Vec4::new(1.0, 0.3, 0.1, 0.8),
708                    Vec4::new(0.1, 0.5, 1.0, 0.3),
709                    life,
710                )
711            } else {
712                Vec4::new(0.8, 0.8, 0.8, 0.6)
713            };
714            prims.push(DebugPrim::Point { pos: self.positions[i], size: self.point_size, color });
715            if self.show_velocity && i < self.velocities.len() {
716                let vel = self.velocities[i];
717                if vel.length() > 0.001 {
718                    prims.push(DebugPrim::line(
719                        self.positions[i],
720                        self.positions[i] + vel * self.velocity_scale,
721                        Vec4::new(0.4, 1.0, 0.4, 0.5),
722                    ));
723                }
724            }
725        }
726        prims
727    }
728}
729
730impl Default for ParticleDebugViz { fn default() -> Self { Self::new() } }
731
732fn lerp_color(a: Vec4, b: Vec4, t: f32) -> Vec4 {
733    a + (b - a) * t
734}
735
736// ─── Main physics debugger ───────────────────────────────────────────────────
737
738#[derive(Debug, Clone)]
739pub struct PhysicsDebugFlags {
740    pub show_bodies: bool,
741    pub show_aabbs: bool,
742    pub show_velocities: bool,
743    pub show_contacts: bool,
744    pub show_constraints: bool,
745    pub show_force_fields: bool,
746    pub show_cloth: bool,
747    pub show_ik_chains: bool,
748    pub show_particles: bool,
749    pub show_sleep_state: bool,
750    pub show_mass_centers: bool,
751    pub show_inertia_tensors: bool,
752    pub show_broadphase: bool,
753    pub velocity_scale: f32,
754    pub body_alpha: f32,
755}
756
757impl Default for PhysicsDebugFlags {
758    fn default() -> Self {
759        Self {
760            show_bodies: true,
761            show_aabbs: false,
762            show_velocities: false,
763            show_contacts: true,
764            show_constraints: true,
765            show_force_fields: true,
766            show_cloth: true,
767            show_ik_chains: true,
768            show_particles: false,
769            show_sleep_state: true,
770            show_mass_centers: false,
771            show_inertia_tensors: false,
772            show_broadphase: false,
773            velocity_scale: 0.1,
774            body_alpha: 0.5,
775        }
776    }
777}
778
779#[derive(Debug)]
780pub struct PhysicsDebugger {
781    pub flags: PhysicsDebugFlags,
782    pub bodies: HashMap<PhysBodyId, PhysBodyState>,
783    pub contacts: Vec<ContactPoint>,
784    pub constraints: Vec<ConstraintViz>,
785    pub force_fields: Vec<ForceFieldViz>,
786    pub cloth_meshes: Vec<ClothMeshViz>,
787    pub ik_chains: Vec<IkChainViz>,
788    pub particles: ParticleDebugViz,
789    pub stats: PhysicsStats,
790    prim_buffer: Vec<DebugPrim>,
791}
792
793#[derive(Debug, Default, Clone)]
794pub struct PhysicsStats {
795    pub active_bodies: u32,
796    pub sleeping_bodies: u32,
797    pub contact_count: u32,
798    pub constraint_count: u32,
799    pub step_time_ms: f32,
800    pub broadphase_pairs: u32,
801    pub substeps: u32,
802}
803
804impl PhysicsStats {
805    pub fn summary(&self) -> String {
806        format!(
807            "Bodies: {} active / {} sleeping | Contacts: {} | Constraints: {} | Step: {:.2}ms",
808            self.active_bodies, self.sleeping_bodies,
809            self.contact_count, self.constraint_count,
810            self.step_time_ms,
811        )
812    }
813}
814
815impl PhysicsDebugger {
816    pub fn new() -> Self {
817        Self {
818            flags: PhysicsDebugFlags::default(),
819            bodies: HashMap::new(),
820            contacts: Vec::new(),
821            constraints: Vec::new(),
822            force_fields: Vec::new(),
823            cloth_meshes: Vec::new(),
824            ik_chains: Vec::new(),
825            particles: ParticleDebugViz::new(),
826            stats: PhysicsStats::default(),
827            prim_buffer: Vec::new(),
828        }
829    }
830
831    pub fn register_body(&mut self, body: PhysBodyState) {
832        self.bodies.insert(body.id, body);
833    }
834
835    pub fn update_body(&mut self, id: PhysBodyId, pos: Vec3, rot: Quat, vel: Vec3) {
836        if let Some(b) = self.bodies.get_mut(&id) {
837            b.position = pos;
838            b.rotation = rot;
839            b.linear_velocity = vel;
840        }
841    }
842
843    pub fn set_contacts(&mut self, contacts: Vec<ContactPoint>) {
844        self.contacts = contacts;
845        self.stats.contact_count = self.contacts.len() as u32;
846    }
847
848    pub fn set_constraints(&mut self, constraints: Vec<ConstraintViz>) {
849        self.constraints = constraints;
850        self.stats.constraint_count = self.constraints.len() as u32;
851    }
852
853    pub fn add_force_field(&mut self, ff: ForceFieldViz) {
854        self.force_fields.push(ff);
855    }
856
857    pub fn add_cloth(&mut self, cloth: ClothMeshViz) {
858        self.cloth_meshes.push(cloth);
859    }
860
861    pub fn add_ik_chain(&mut self, chain: IkChainViz) {
862        self.ik_chains.push(chain);
863    }
864
865    /// Rebuild the debug primitive buffer
866    pub fn build_primitives(&mut self) {
867        self.prim_buffer.clear();
868        let flags = &self.flags;
869
870        // Bodies
871        if flags.show_bodies {
872            for body in self.bodies.values() {
873                if !body.active { continue; }
874                let col = if body.asleep && flags.show_sleep_state {
875                    Vec4::new(0.4, 0.4, 0.4, flags.body_alpha)
876                } else {
877                    match body.body_type {
878                        BodyType::Static    => Vec4::new(0.4, 0.4, 0.8, flags.body_alpha),
879                        BodyType::Kinematic => Vec4::new(0.8, 0.8, 0.2, flags.body_alpha),
880                        BodyType::Dynamic   => Vec4::new(0.3, 0.9, 0.3, flags.body_alpha),
881                        BodyType::Cloth     => Vec4::new(0.5, 0.7, 1.0, flags.body_alpha),
882                        BodyType::SoftBody  => Vec4::new(0.9, 0.5, 0.3, flags.body_alpha),
883                        BodyType::Fluid     => Vec4::new(0.2, 0.6, 1.0, flags.body_alpha),
884                        BodyType::SdfCollider => Vec4::new(1.0, 0.4, 0.8, flags.body_alpha),
885                    }
886                };
887                let m = Mat4::from_rotation_translation(body.rotation, body.position);
888                self.prim_buffer.push(DebugPrim::axes(m));
889                self.prim_buffer.push(DebugPrim::wire_sphere(body.position, 0.05, col));
890
891                if flags.show_mass_centers {
892                    self.prim_buffer.push(DebugPrim::Cross {
893                        center: body.position,
894                        size: 0.08,
895                        color: Vec4::new(1.0, 1.0, 0.0, 1.0),
896                    });
897                }
898            }
899        }
900
901        // AABBs
902        if flags.show_aabbs {
903            for body in self.bodies.values() {
904                let col = Vec4::new(0.6, 0.6, 0.6, 0.3);
905                self.prim_buffer.push(DebugPrim::aabb(body.aabb_min, body.aabb_max, col));
906            }
907        }
908
909        // Velocities
910        if flags.show_velocities {
911            for body in self.bodies.values() {
912                if body.linear_velocity.length() > 0.001 {
913                    self.prim_buffer.push(DebugPrim::arrow(
914                        body.position,
915                        body.position + body.linear_velocity * flags.velocity_scale,
916                        Vec4::new(0.1, 1.0, 0.3, 0.9),
917                    ));
918                }
919                if body.angular_velocity.length() > 0.001 {
920                    self.prim_buffer.push(DebugPrim::arrow(
921                        body.position,
922                        body.position + body.angular_velocity.normalize() * 0.1,
923                        Vec4::new(1.0, 0.6, 0.1, 0.9),
924                    ));
925                }
926            }
927        }
928
929        // Contacts
930        if flags.show_contacts {
931            for contact in &self.contacts {
932                let depth_color = lerp_color(
933                    Vec4::new(0.0, 1.0, 0.0, 1.0),
934                    Vec4::new(1.0, 0.0, 0.0, 1.0),
935                    (contact.depth * 10.0).clamp(0.0, 1.0),
936                );
937                self.prim_buffer.push(DebugPrim::point(contact.position, depth_color));
938                self.prim_buffer.push(DebugPrim::arrow(
939                    contact.position,
940                    contact.position + contact.normal * 0.1,
941                    Vec4::new(0.8, 0.2, 0.8, 0.9),
942                ));
943            }
944        }
945
946        // Constraints
947        if flags.show_constraints {
948            for c in &self.constraints {
949                let col = if c.broken {
950                    Vec4::new(1.0, 0.0, 0.0, 0.9)
951                } else if c.strain > 0.8 {
952                    lerp_color(Vec4::new(0.0,1.0,0.0,1.0), Vec4::new(1.0,0.0,0.0,1.0), c.strain)
953                } else {
954                    c.kind.color()
955                };
956                self.prim_buffer.push(DebugPrim::line(c.anchor_a, c.anchor_b, col));
957                self.prim_buffer.push(DebugPrim::wire_sphere(c.anchor_a, 0.02, col));
958                self.prim_buffer.push(DebugPrim::wire_sphere(c.anchor_b, 0.02, col));
959
960                match c.kind {
961                    ConstraintKind::Hinge => {
962                        let axis = (c.anchor_b - c.anchor_a).normalize_or_zero();
963                        let mid = (c.anchor_a + c.anchor_b) * 0.5;
964                        self.prim_buffer.push(DebugPrim::Disc {
965                            center: mid, normal: axis, radius: 0.05, color: col,
966                        });
967                    }
968                    ConstraintKind::BallSocket => {
969                        self.prim_buffer.push(DebugPrim::wire_sphere(c.anchor_a, 0.06, col));
970                    }
971                    ConstraintKind::Spring => {
972                        self.prim_buffer.push(DebugPrim::Spring {
973                            start: c.anchor_a, end: c.anchor_b,
974                            coils: 6, radius: 0.02, color: col,
975                        });
976                    }
977                    ConstraintKind::Ik => {
978                        self.prim_buffer.push(DebugPrim::Cross {
979                            center: c.anchor_b, size: 0.08, color: col,
980                        });
981                    }
982                    _ => {}
983                }
984            }
985        }
986
987        // Force fields
988        if flags.show_force_fields {
989            for ff in &self.force_fields {
990                if !ff.active { continue; }
991                let col = ff.field_type.color();
992                self.prim_buffer.push(DebugPrim::wire_sphere(ff.position, ff.radius, col));
993                // Sample field at grid points
994                let grid = 4i32;
995                for ix in -grid..=grid {
996                    for iz in -grid..=grid {
997                        let rel = Vec3::new(ix as f32, 0.0, iz as f32)
998                            / grid as f32 * ff.radius * 0.8;
999                        let force = ff.field_type.sample_at(rel, ff.strength, ff.radius);
1000                        if force.length() > 0.001 {
1001                            let from = ff.position + rel;
1002                            let scaled = force * 0.05;
1003                            self.prim_buffer.push(DebugPrim::arrow(from, from + scaled, col));
1004                        }
1005                    }
1006                }
1007            }
1008        }
1009
1010        // Cloth
1011        if flags.show_cloth {
1012            for cloth in &self.cloth_meshes {
1013                let prims = cloth.build_primitives();
1014                self.prim_buffer.extend(prims);
1015            }
1016        }
1017
1018        // IK chains
1019        if flags.show_ik_chains {
1020            for chain in &self.ik_chains {
1021                let prims = chain.build_primitives();
1022                self.prim_buffer.extend(prims);
1023            }
1024        }
1025
1026        // Particles
1027        if flags.show_particles {
1028            let prims = self.particles.build_primitives();
1029            self.prim_buffer.extend(prims);
1030        }
1031    }
1032
1033    /// Expand primitives into flat vertex buffer for GPU
1034    pub fn build_line_buffer(&self) -> Vec<(Vec3, Vec3, Vec4)> {
1035        self.prim_buffer.iter()
1036            .flat_map(|p| p.to_lines())
1037            .collect()
1038    }
1039
1040    pub fn primitives(&self) -> &[DebugPrim] {
1041        &self.prim_buffer
1042    }
1043
1044    pub fn clear(&mut self) {
1045        self.bodies.clear();
1046        self.contacts.clear();
1047        self.constraints.clear();
1048        self.force_fields.clear();
1049        self.cloth_meshes.clear();
1050        self.ik_chains.clear();
1051        self.prim_buffer.clear();
1052    }
1053}
1054
1055impl Default for PhysicsDebugger {
1056    fn default() -> Self { Self::new() }
1057}
1058
1059// ─── Tests ───────────────────────────────────────────────────────────────────
1060
1061#[cfg(test)]
1062mod tests {
1063    use super::*;
1064
1065    #[test]
1066    fn aabb_lines_count() {
1067        let p = DebugPrim::aabb(Vec3::splat(-1.0), Vec3::splat(1.0), Vec4::ONE);
1068        assert_eq!(p.to_lines().len(), 12);
1069    }
1070
1071    #[test]
1072    fn axes_emit_three_lines() {
1073        let p = DebugPrim::axes(Mat4::IDENTITY);
1074        assert_eq!(p.to_lines().len(), 3);
1075    }
1076
1077    #[test]
1078    fn force_field_attractor_points_inward() {
1079        let ff = ForceFieldType::Attractor;
1080        let rel = Vec3::new(1.0, 0.0, 0.0);
1081        let f = ff.sample_at(rel, 1.0, 2.0);
1082        assert!(f.x < 0.0, "attractor should pull x negative");
1083    }
1084
1085    #[test]
1086    fn cloth_grid_has_edges() {
1087        let c = ClothMeshViz::new("test".into(), 4, 4);
1088        assert!(!c.edges.is_empty());
1089    }
1090
1091    #[test]
1092    fn physics_debugger_build() {
1093        let mut dbg = PhysicsDebugger::new();
1094        let mut b = PhysBodyState::new(PhysBodyId(1));
1095        b.body_type = BodyType::Dynamic;
1096        dbg.register_body(b);
1097        dbg.build_primitives();
1098        assert!(!dbg.prim_buffer.is_empty());
1099    }
1100}