Skip to main content

proof_engine/editor/
physics_editor.rs

1#[allow(dead_code, unused_variables, unused_mut, unused_imports)]
2
3use glam::{Vec2, Vec3, Vec4, Quat, Mat4};
4use std::collections::{HashMap, VecDeque, HashSet, BTreeMap};
5
6// ============================================================
7// PHYSICAL CONSTANTS
8// ============================================================
9
10pub const GRAVITY: f32 = 9.80665;
11pub const PI: f32 = std::f32::consts::PI;
12pub const TWO_PI: f32 = 2.0 * PI;
13pub const HALF_PI: f32 = PI * 0.5;
14pub const DEG_TO_RAD: f32 = PI / 180.0;
15pub const RAD_TO_DEG: f32 = 180.0 / PI;
16pub const EPSILON: f32 = 1e-6;
17pub const SLEEP_LINEAR_THRESHOLD: f32 = 0.01;
18pub const SLEEP_ANGULAR_THRESHOLD: f32 = 0.01;
19pub const DEFAULT_FRICTION: f32 = 0.5;
20pub const DEFAULT_RESTITUTION: f32 = 0.3;
21pub const AIR_DENSITY: f32 = 1.225; // kg/m^3 at sea level
22pub const WATER_DENSITY: f32 = 1000.0;
23pub const STEEL_DENSITY: f32 = 7850.0;
24pub const WOOD_DENSITY: f32 = 700.0;
25pub const CONCRETE_DENSITY: f32 = 2400.0;
26pub const RUBBER_DENSITY: f32 = 1500.0;
27pub const GLASS_DENSITY: f32 = 2500.0;
28pub const ALUMINUM_DENSITY: f32 = 2700.0;
29pub const COPPER_DENSITY: f32 = 8960.0;
30pub const GOLD_DENSITY: f32 = 19300.0;
31pub const ICE_DENSITY: f32 = 917.0;
32pub const SAND_DENSITY: f32 = 1600.0;
33
34// ============================================================
35// INERTIA TENSOR COMPUTATION
36// ============================================================
37
38/// Compute inertia tensor for a solid box (principal moments)
39/// Ixx = (1/12)*m*(h^2 + d^2), Iyy = (1/12)*m*(w^2 + d^2), Izz = (1/12)*m*(w^2 + h^2)
40pub fn inertia_tensor_box(mass: f32, half_extents: Vec3) -> Mat4 {
41    let w = 2.0 * half_extents.x;
42    let h = 2.0 * half_extents.y;
43    let d = 2.0 * half_extents.z;
44    let ixx = (1.0 / 12.0) * mass * (h * h + d * d);
45    let iyy = (1.0 / 12.0) * mass * (w * w + d * d);
46    let izz = (1.0 / 12.0) * mass * (w * w + h * h);
47    Mat4::from_cols(
48        Vec4::new(ixx, 0.0, 0.0, 0.0),
49        Vec4::new(0.0, iyy, 0.0, 0.0),
50        Vec4::new(0.0, 0.0, izz, 0.0),
51        Vec4::new(0.0, 0.0, 0.0, 1.0),
52    )
53}
54
55/// Compute inertia tensor for a solid sphere
56/// I = (2/5)*m*r^2 (all principal moments equal)
57pub fn inertia_tensor_sphere(mass: f32, radius: f32) -> Mat4 {
58    let i = (2.0 / 5.0) * mass * radius * radius;
59    Mat4::from_cols(
60        Vec4::new(i, 0.0, 0.0, 0.0),
61        Vec4::new(0.0, i, 0.0, 0.0),
62        Vec4::new(0.0, 0.0, i, 0.0),
63        Vec4::new(0.0, 0.0, 0.0, 1.0),
64    )
65}
66
67/// Compute inertia tensor for a capsule (cylinder + two hemispheres)
68/// Capsule aligned along Y axis
69pub fn inertia_tensor_capsule(mass: f32, radius: f32, half_height: f32) -> Mat4 {
70    let h = 2.0 * half_height;
71    let r = radius;
72    // Volume of cylinder
73    let vol_cyl = PI * r * r * h;
74    // Volume of sphere (two hemispheres)
75    let vol_sph = (4.0 / 3.0) * PI * r * r * r;
76    let total_vol = vol_cyl + vol_sph;
77    let m_cyl = mass * vol_cyl / total_vol;
78    let m_sph = mass * vol_sph / total_vol;
79    // Cylinder inertia
80    let iyy_cyl = 0.5 * m_cyl * r * r;
81    let ixx_cyl = (1.0 / 12.0) * m_cyl * (3.0 * r * r + h * h);
82    // Sphere inertia + parallel axis theorem for hemisphere offsets
83    let i_sph_local = (2.0 / 5.0) * m_sph * r * r;
84    // Each hemisphere COM is at 3r/8 from flat face
85    let d = half_height + 3.0 * r / 8.0;
86    let ixx_sph = i_sph_local + m_sph * d * d;
87    let iyy_sph = i_sph_local;
88    let ixx = ixx_cyl + ixx_sph;
89    let iyy = iyy_cyl + iyy_sph;
90    let izz = ixx; // symmetry
91    Mat4::from_cols(
92        Vec4::new(ixx, 0.0, 0.0, 0.0),
93        Vec4::new(0.0, iyy, 0.0, 0.0),
94        Vec4::new(0.0, 0.0, izz, 0.0),
95        Vec4::new(0.0, 0.0, 0.0, 1.0),
96    )
97}
98
99/// Compute inertia tensor for a solid cylinder aligned along Y axis
100/// Ixx = Izz = (1/12)*m*(3*r^2 + h^2), Iyy = (1/2)*m*r^2
101pub fn inertia_tensor_cylinder(mass: f32, radius: f32, half_height: f32) -> Mat4 {
102    let h = 2.0 * half_height;
103    let r = radius;
104    let iyy = 0.5 * mass * r * r;
105    let ixx = (1.0 / 12.0) * mass * (3.0 * r * r + h * h);
106    let izz = ixx;
107    Mat4::from_cols(
108        Vec4::new(ixx, 0.0, 0.0, 0.0),
109        Vec4::new(0.0, iyy, 0.0, 0.0),
110        Vec4::new(0.0, 0.0, izz, 0.0),
111        Vec4::new(0.0, 0.0, 0.0, 1.0),
112    )
113}
114
115/// Compute inertia tensor for a solid cone aligned along Y axis
116/// Ixx = Izz = (3/80)*m*(4*r^2 + h^2), Iyy = (3/10)*m*r^2
117pub fn inertia_tensor_cone(mass: f32, radius: f32, height: f32) -> Mat4 {
118    let r = radius;
119    let h = height;
120    let iyy = (3.0 / 10.0) * mass * r * r;
121    let ixx = (3.0 / 80.0) * mass * (4.0 * r * r + h * h);
122    let izz = ixx;
123    Mat4::from_cols(
124        Vec4::new(ixx, 0.0, 0.0, 0.0),
125        Vec4::new(0.0, iyy, 0.0, 0.0),
126        Vec4::new(0.0, 0.0, izz, 0.0),
127        Vec4::new(0.0, 0.0, 0.0, 1.0),
128    )
129}
130
131/// Parallel axis theorem: shift inertia tensor by displacement d
132/// I_new = I_cm + m*(|d|^2*I3 - d*d^T)
133pub fn inertia_parallel_axis(i_cm: Mat4, mass: f32, displacement: Vec3) -> Mat4 {
134    let d = displacement;
135    let d2 = d.dot(d);
136    // Off-diagonal products
137    let dxx = d.x * d.x;
138    let dyy = d.y * d.y;
139    let dzz = d.z * d.z;
140    let dxy = d.x * d.y;
141    let dxz = d.x * d.z;
142    let dyz = d.y * d.z;
143    // Shift tensor (3x3 portion only)
144    let shift = Mat4::from_cols(
145        Vec4::new(mass * (d2 - dxx), -mass * dxy, -mass * dxz, 0.0),
146        Vec4::new(-mass * dxy, mass * (d2 - dyy), -mass * dyz, 0.0),
147        Vec4::new(-mass * dxz, -mass * dyz, mass * (d2 - dzz), 0.0),
148        Vec4::new(0.0, 0.0, 0.0, 0.0),
149    );
150    // Add matrices
151    let c0 = i_cm.col(0) + shift.col(0);
152    let c1 = i_cm.col(1) + shift.col(1);
153    let c2 = i_cm.col(2) + shift.col(2);
154    let c3 = i_cm.col(3);
155    Mat4::from_cols(c0, c1, c2, c3)
156}
157
158// ============================================================
159// RIGID BODY DATA
160// ============================================================
161
162#[derive(Debug, Clone)]
163pub struct RigidBodyInspector {
164    pub id: u64,
165    pub name: String,
166    pub mass: f32,
167    pub inertia_tensor: Mat4,
168    pub center_of_mass: Vec3,
169    pub linear_damping: f32,
170    pub angular_damping: f32,
171    pub linear_sleep_threshold: f32,
172    pub angular_sleep_threshold: f32,
173    pub is_kinematic: bool,
174    pub is_static: bool,
175    pub use_gravity: bool,
176    pub gravity_scale: f32,
177    pub collision_group: u32,
178    pub collision_mask: u32,
179    pub ccd_enabled: bool,
180    pub max_linear_velocity: f32,
181    pub max_angular_velocity: f32,
182    pub shape_type: RigidBodyShapeType,
183    pub shape_params: ShapeParameters,
184    pub position: Vec3,
185    pub orientation: Quat,
186    pub linear_velocity: Vec3,
187    pub angular_velocity: Vec3,
188    pub force_accumulator: Vec3,
189    pub torque_accumulator: Vec3,
190    pub sleeping: bool,
191    pub sleep_timer: f32,
192}
193
194#[derive(Debug, Clone, PartialEq)]
195pub enum RigidBodyShapeType {
196    Box,
197    Sphere,
198    Capsule,
199    Cylinder,
200    Cone,
201    ConvexHull,
202    TriangleMesh,
203    Heightfield,
204    Compound,
205}
206
207#[derive(Debug, Clone)]
208pub struct ShapeParameters {
209    pub half_extents: Vec3,
210    pub radius: f32,
211    pub half_height: f32,
212    pub height: f32,
213}
214
215impl Default for ShapeParameters {
216    fn default() -> Self {
217        Self {
218            half_extents: Vec3::splat(0.5),
219            radius: 0.5,
220            half_height: 1.0,
221            height: 2.0,
222        }
223    }
224}
225
226impl RigidBodyInspector {
227    pub fn new(id: u64, name: &str) -> Self {
228        Self {
229            id,
230            name: name.to_string(),
231            mass: 1.0,
232            inertia_tensor: Mat4::IDENTITY,
233            center_of_mass: Vec3::ZERO,
234            linear_damping: 0.01,
235            angular_damping: 0.05,
236            linear_sleep_threshold: SLEEP_LINEAR_THRESHOLD,
237            angular_sleep_threshold: SLEEP_ANGULAR_THRESHOLD,
238            is_kinematic: false,
239            is_static: false,
240            use_gravity: true,
241            gravity_scale: 1.0,
242            collision_group: 1,
243            collision_mask: 0xFFFF_FFFF,
244            ccd_enabled: false,
245            max_linear_velocity: 500.0,
246            max_angular_velocity: 50.0,
247            shape_type: RigidBodyShapeType::Box,
248            shape_params: ShapeParameters::default(),
249            position: Vec3::ZERO,
250            orientation: Quat::IDENTITY,
251            linear_velocity: Vec3::ZERO,
252            angular_velocity: Vec3::ZERO,
253            force_accumulator: Vec3::ZERO,
254            torque_accumulator: Vec3::ZERO,
255            sleeping: false,
256            sleep_timer: 0.0,
257        }
258    }
259
260    /// Recompute inertia tensor based on current shape type and parameters
261    pub fn recompute_inertia(&mut self) {
262        self.inertia_tensor = match self.shape_type {
263            RigidBodyShapeType::Box => {
264                inertia_tensor_box(self.mass, self.shape_params.half_extents)
265            }
266            RigidBodyShapeType::Sphere => {
267                inertia_tensor_sphere(self.mass, self.shape_params.radius)
268            }
269            RigidBodyShapeType::Capsule => {
270                inertia_tensor_capsule(self.mass, self.shape_params.radius, self.shape_params.half_height)
271            }
272            RigidBodyShapeType::Cylinder => {
273                inertia_tensor_cylinder(self.mass, self.shape_params.radius, self.shape_params.half_height)
274            }
275            RigidBodyShapeType::Cone => {
276                inertia_tensor_cone(self.mass, self.shape_params.radius, self.shape_params.height)
277            }
278            _ => Mat4::IDENTITY,
279        };
280    }
281
282    /// Integrate linear/angular velocity over dt using symplectic Euler
283    pub fn integrate(&mut self, dt: f32) {
284        if self.is_static || self.is_kinematic {
285            return;
286        }
287        if self.sleeping {
288            self.force_accumulator = Vec3::ZERO;
289            self.torque_accumulator = Vec3::ZERO;
290            return;
291        }
292        let inv_mass = if self.mass > EPSILON { 1.0 / self.mass } else { 0.0 };
293        let gravity_force = if self.use_gravity {
294            Vec3::new(0.0, -GRAVITY * self.gravity_scale * self.mass, 0.0)
295        } else {
296            Vec3::ZERO
297        };
298        let total_force = self.force_accumulator + gravity_force;
299        // Linear integration
300        let lin_accel = total_force * inv_mass;
301        self.linear_velocity += lin_accel * dt;
302        // Linear damping (exponential decay)
303        let lin_damp_factor = (1.0 - self.linear_damping * dt).max(0.0);
304        self.linear_velocity *= lin_damp_factor;
305        // Clamp linear velocity
306        let lv_len = self.linear_velocity.length();
307        if lv_len > self.max_linear_velocity {
308            self.linear_velocity *= self.max_linear_velocity / lv_len;
309        }
310        self.position += self.linear_velocity * dt;
311        // Angular integration — simple Euler in body space
312        let inv_inertia_diag = Vec3::new(
313            if self.inertia_tensor.col(0).x > EPSILON { 1.0 / self.inertia_tensor.col(0).x } else { 0.0 },
314            if self.inertia_tensor.col(1).y > EPSILON { 1.0 / self.inertia_tensor.col(1).y } else { 0.0 },
315            if self.inertia_tensor.col(2).z > EPSILON { 1.0 / self.inertia_tensor.col(2).z } else { 0.0 },
316        );
317        let ang_accel = Vec3::new(
318            self.torque_accumulator.x * inv_inertia_diag.x,
319            self.torque_accumulator.y * inv_inertia_diag.y,
320            self.torque_accumulator.z * inv_inertia_diag.z,
321        );
322        self.angular_velocity += ang_accel * dt;
323        let ang_damp_factor = (1.0 - self.angular_damping * dt).max(0.0);
324        self.angular_velocity *= ang_damp_factor;
325        let av_len = self.angular_velocity.length();
326        if av_len > self.max_angular_velocity {
327            self.angular_velocity *= self.max_angular_velocity / av_len;
328        }
329        // Update orientation quaternion: dq/dt = 0.5 * omega_quat * q
330        let omega = self.angular_velocity;
331        let dq = Quat::from_xyzw(omega.x * 0.5, omega.y * 0.5, omega.z * 0.5, 0.0);
332        let q = self.orientation;
333        // Quaternion multiplication dq * q gives delta rotation
334        let nq = Quat::from_xyzw(
335            dq.w * q.x + dq.x * q.w + dq.y * q.z - dq.z * q.y,
336            dq.w * q.y - dq.x * q.z + dq.y * q.w + dq.z * q.x,
337            dq.w * q.z + dq.x * q.y - dq.y * q.x + dq.z * q.w,
338            dq.w * q.w - dq.x * q.x - dq.y * q.y - dq.z * q.z,
339        );
340        self.orientation = Quat::from_xyzw(
341            q.x + nq.x * dt,
342            q.y + nq.y * dt,
343            q.z + nq.z * dt,
344            q.w + nq.w * dt,
345        ).normalize();
346        // Check sleep
347        let v2 = self.linear_velocity.length_squared();
348        let w2 = self.angular_velocity.length_squared();
349        let lt = self.linear_sleep_threshold * self.linear_sleep_threshold;
350        let at = self.angular_sleep_threshold * self.angular_sleep_threshold;
351        if v2 < lt && w2 < at {
352            self.sleep_timer += dt;
353            if self.sleep_timer > 0.5 {
354                self.sleeping = true;
355            }
356        } else {
357            self.sleep_timer = 0.0;
358            self.sleeping = false;
359        }
360        // Clear accumulators
361        self.force_accumulator = Vec3::ZERO;
362        self.torque_accumulator = Vec3::ZERO;
363    }
364
365    pub fn apply_force(&mut self, force: Vec3) {
366        self.force_accumulator += force;
367    }
368
369    pub fn apply_torque(&mut self, torque: Vec3) {
370        self.torque_accumulator += torque;
371    }
372
373    pub fn apply_force_at_point(&mut self, force: Vec3, world_point: Vec3) {
374        self.force_accumulator += force;
375        let r = world_point - (self.position + self.orientation * self.center_of_mass);
376        self.torque_accumulator += r.cross(force);
377    }
378
379    pub fn wake_up(&mut self) {
380        self.sleeping = false;
381        self.sleep_timer = 0.0;
382    }
383}
384
385// ============================================================
386// CONSTRAINT TYPES — 15+ with Jacobian helpers
387// ============================================================
388
389#[derive(Debug, Clone)]
390pub struct JacobianRow {
391    /// Linear component for body A
392    pub j_lin_a: Vec3,
393    /// Angular component for body A
394    pub j_ang_a: Vec3,
395    /// Linear component for body B
396    pub j_lin_b: Vec3,
397    /// Angular component for body B
398    pub j_ang_b: Vec3,
399    /// Right-hand side bias (Baumgarte stabilization)
400    pub bias: f32,
401    /// Effective mass (diagonal element of J * M^-1 * J^T)
402    pub effective_mass: f32,
403    /// Accumulated impulse (warm starting)
404    pub lambda: f32,
405    /// Lower limit on lambda
406    pub lambda_min: f32,
407    /// Upper limit on lambda
408    pub lambda_max: f32,
409}
410
411impl JacobianRow {
412    pub fn new() -> Self {
413        Self {
414            j_lin_a: Vec3::ZERO,
415            j_ang_a: Vec3::ZERO,
416            j_lin_b: Vec3::ZERO,
417            j_ang_b: Vec3::ZERO,
418            bias: 0.0,
419            effective_mass: 1.0,
420            lambda: 0.0,
421            lambda_min: f32::NEG_INFINITY,
422            lambda_max: f32::INFINITY,
423        }
424    }
425
426    /// Compute effective mass given inverse masses and inverse inertia tensors
427    pub fn compute_effective_mass(&mut self,
428        inv_mass_a: f32, inv_inertia_a: Vec3,
429        inv_mass_b: f32, inv_inertia_b: Vec3) {
430        let ka = inv_mass_a * self.j_lin_a.dot(self.j_lin_a)
431            + (inv_inertia_a * self.j_ang_a).dot(self.j_ang_a);
432        let kb = inv_mass_b * self.j_lin_b.dot(self.j_lin_b)
433            + (inv_inertia_b * self.j_ang_b).dot(self.j_ang_b);
434        let k = ka + kb;
435        self.effective_mass = if k.abs() > EPSILON { 1.0 / k } else { 0.0 };
436    }
437
438    /// Solve one PGS iteration; returns delta lambda
439    pub fn solve_velocity(&mut self,
440        vel_a: Vec3, omega_a: Vec3,
441        vel_b: Vec3, omega_b: Vec3) -> f32 {
442        let jv = self.j_lin_a.dot(vel_a)
443            + self.j_ang_a.dot(omega_a)
444            + self.j_lin_b.dot(vel_b)
445            + self.j_ang_b.dot(omega_b);
446        let delta = self.effective_mass * (-jv - self.bias);
447        let old_lambda = self.lambda;
448        self.lambda = (self.lambda + delta).clamp(self.lambda_min, self.lambda_max);
449        self.lambda - old_lambda
450    }
451}
452
453// ---- Fixed Constraint ----
454
455#[derive(Debug, Clone)]
456pub struct FixedConstraint {
457    pub body_a: u64,
458    pub body_b: u64,
459    pub anchor_a: Vec3,
460    pub anchor_b: Vec3,
461    pub initial_rotation_ab: Quat,
462    pub breaking_force: f32,
463    pub breaking_torque: f32,
464    pub rows: Vec<JacobianRow>,
465}
466
467impl FixedConstraint {
468    pub fn new(body_a: u64, body_b: u64, anchor_a: Vec3, anchor_b: Vec3) -> Self {
469        let mut rows = Vec::new();
470        for _ in 0..6 { rows.push(JacobianRow::new()); }
471        Self {
472            body_a, body_b, anchor_a, anchor_b,
473            initial_rotation_ab: Quat::IDENTITY,
474            breaking_force: f32::INFINITY,
475            breaking_torque: f32::INFINITY,
476            rows,
477        }
478    }
479
480    pub fn build_jacobian(
481        &mut self,
482        pos_a: Vec3, rot_a: Quat,
483        pos_b: Vec3, rot_b: Quat,
484        baumgarte: f32, dt: f32,
485    ) {
486        let ra = rot_a * self.anchor_a;
487        let rb = rot_b * self.anchor_b;
488        let world_a = pos_a + ra;
489        let world_b = pos_b + rb;
490        let err = world_b - world_a;
491        let bias_factor = baumgarte / dt;
492        // 3 linear rows
493        let axes = [Vec3::X, Vec3::Y, Vec3::Z];
494        for (i, &ax) in axes.iter().enumerate() {
495            let row = &mut self.rows[i];
496            row.j_lin_a = -ax;
497            row.j_ang_a = -ra.cross(ax);
498            row.j_lin_b = ax;
499            row.j_ang_b = rb.cross(ax);
500            row.bias = bias_factor * err.dot(ax);
501        }
502        // 3 angular rows (lock rotation difference)
503        let rot_diff = rot_b * self.initial_rotation_ab.inverse() * rot_a.inverse();
504        let (axis_err, angle_err) = quat_to_axis_angle(rot_diff);
505        let ang_err = axis_err * angle_err;
506        for (i, &ax) in axes.iter().enumerate() {
507            let row = &mut self.rows[3 + i];
508            row.j_lin_a = Vec3::ZERO;
509            row.j_ang_a = -ax;
510            row.j_lin_b = Vec3::ZERO;
511            row.j_ang_b = ax;
512            row.bias = bias_factor * ang_err.dot(ax);
513        }
514    }
515}
516
517// ---- Hinge Constraint ----
518
519#[derive(Debug, Clone)]
520pub struct HingeConstraint {
521    pub body_a: u64,
522    pub body_b: u64,
523    pub anchor_a: Vec3,
524    pub anchor_b: Vec3,
525    pub axis_a: Vec3, // hinge axis in body A local space
526    pub axis_b: Vec3,
527    pub lower_limit: f32,
528    pub upper_limit: f32,
529    pub enable_limits: bool,
530    pub motor_enabled: bool,
531    pub motor_target_velocity: f32,
532    pub motor_max_impulse: f32,
533    pub rows: Vec<JacobianRow>,
534}
535
536impl HingeConstraint {
537    pub fn new(body_a: u64, body_b: u64, anchor: Vec3, axis: Vec3) -> Self {
538        let mut rows = Vec::new();
539        for _ in 0..7 { rows.push(JacobianRow::new()); }
540        Self {
541            body_a, body_b,
542            anchor_a: anchor,
543            anchor_b: anchor,
544            axis_a: axis.normalize(),
545            axis_b: axis.normalize(),
546            lower_limit: -PI,
547            upper_limit: PI,
548            enable_limits: false,
549            motor_enabled: false,
550            motor_target_velocity: 0.0,
551            motor_max_impulse: 10.0,
552            rows,
553        }
554    }
555
556    pub fn build_jacobian(
557        &mut self,
558        pos_a: Vec3, rot_a: Quat,
559        pos_b: Vec3, rot_b: Quat,
560        baumgarte: f32, dt: f32,
561    ) {
562        let ra = rot_a * self.anchor_a;
563        let rb = rot_b * self.anchor_b;
564        let world_a = pos_a + ra;
565        let world_b = pos_b + rb;
566        let err = world_b - world_a;
567        let bias_factor = baumgarte / dt;
568        // 3 linear rows (ball-socket part)
569        let axes = [Vec3::X, Vec3::Y, Vec3::Z];
570        for (i, &ax) in axes.iter().enumerate() {
571            let row = &mut self.rows[i];
572            row.j_lin_a = -ax;
573            row.j_ang_a = -(ra.cross(ax));
574            row.j_lin_b = ax;
575            row.j_ang_b = rb.cross(ax);
576            row.bias = bias_factor * err.dot(ax);
577        }
578        // 2 angular rows to constrain perpendicular rotations
579        let hinge_world_a = rot_a * self.axis_a;
580        let hinge_world_b = rot_b * self.axis_b;
581        // Build two vectors perpendicular to hinge axis
582        let perp1 = perpendicular_to(hinge_world_a);
583        let perp2 = hinge_world_a.cross(perp1).normalize();
584        let ang_err_vec = hinge_world_a.cross(hinge_world_b);
585        for (i, &perp) in [perp1, perp2].iter().enumerate() {
586            let row = &mut self.rows[3 + i];
587            row.j_lin_a = Vec3::ZERO;
588            row.j_ang_a = -perp;
589            row.j_lin_b = Vec3::ZERO;
590            row.j_ang_b = perp;
591            row.bias = bias_factor * ang_err_vec.dot(perp);
592        }
593        // Motor row
594        if self.motor_enabled {
595            let row = &mut self.rows[5];
596            row.j_lin_a = Vec3::ZERO;
597            row.j_ang_a = -hinge_world_a;
598            row.j_lin_b = Vec3::ZERO;
599            row.j_ang_b = hinge_world_a;
600            row.bias = -self.motor_target_velocity;
601            row.lambda_min = -self.motor_max_impulse;
602            row.lambda_max = self.motor_max_impulse;
603        }
604        // Limit row
605        if self.enable_limits {
606            let angle = compute_hinge_angle(rot_a, rot_b, &self.axis_a, &self.axis_b);
607            let row = &mut self.rows[6];
608            row.j_lin_a = Vec3::ZERO;
609            row.j_ang_a = -hinge_world_a;
610            row.j_lin_b = Vec3::ZERO;
611            row.j_ang_b = hinge_world_a;
612            if angle < self.lower_limit {
613                row.bias = bias_factor * (angle - self.lower_limit);
614                row.lambda_min = 0.0;
615                row.lambda_max = f32::INFINITY;
616            } else if angle > self.upper_limit {
617                row.bias = bias_factor * (angle - self.upper_limit);
618                row.lambda_min = f32::NEG_INFINITY;
619                row.lambda_max = 0.0;
620            } else {
621                row.lambda_min = 0.0;
622                row.lambda_max = 0.0;
623            }
624        }
625    }
626
627    pub fn get_current_angle(&self, rot_a: Quat, rot_b: Quat) -> f32 {
628        compute_hinge_angle(rot_a, rot_b, &self.axis_a, &self.axis_b)
629    }
630}
631
632fn compute_hinge_angle(rot_a: Quat, rot_b: Quat, axis_a: &Vec3, axis_b: &Vec3) -> f32 {
633    let wa = rot_a * *axis_a;
634    let wb = rot_b * *axis_b;
635    let perp = perpendicular_to(wa);
636    let ref_vec = rot_a * perp;
637    let cur_vec = wb - wb.dot(wa) * wa;
638    let cur_len = cur_vec.length();
639    if cur_len < EPSILON { return 0.0; }
640    let cur_norm = cur_vec / cur_len;
641    let cos_a = ref_vec.dot(cur_norm).clamp(-1.0, 1.0);
642    let sin_a = wa.dot(ref_vec.cross(cur_norm));
643    sin_a.atan2(cos_a)
644}
645
646// ---- Slider Constraint (Prismatic) ----
647
648#[derive(Debug, Clone)]
649pub struct SliderConstraint {
650    pub body_a: u64,
651    pub body_b: u64,
652    pub anchor_a: Vec3,
653    pub anchor_b: Vec3,
654    pub slide_axis_a: Vec3,
655    pub lower_limit: f32,
656    pub upper_limit: f32,
657    pub enable_limits: bool,
658    pub motor_enabled: bool,
659    pub motor_target_velocity: f32,
660    pub motor_max_force: f32,
661    pub rows: Vec<JacobianRow>,
662}
663
664impl SliderConstraint {
665    pub fn new(body_a: u64, body_b: u64, anchor_a: Vec3, slide_axis: Vec3) -> Self {
666        let mut rows = Vec::new();
667        for _ in 0..6 { rows.push(JacobianRow::new()); }
668        Self {
669            body_a, body_b,
670            anchor_a,
671            anchor_b: anchor_a,
672            slide_axis_a: slide_axis.normalize(),
673            lower_limit: -1.0,
674            upper_limit: 1.0,
675            enable_limits: false,
676            motor_enabled: false,
677            motor_target_velocity: 0.0,
678            motor_max_force: 100.0,
679            rows,
680        }
681    }
682
683    pub fn build_jacobian(
684        &mut self,
685        pos_a: Vec3, rot_a: Quat,
686        pos_b: Vec3, rot_b: Quat,
687        baumgarte: f32, dt: f32,
688    ) {
689        let slide_world = rot_a * self.slide_axis_a;
690        let perp1 = perpendicular_to(slide_world);
691        let perp2 = slide_world.cross(perp1).normalize();
692        let ra = rot_a * self.anchor_a;
693        let rb = rot_b * self.anchor_b;
694        let diff = (pos_b + rb) - (pos_a + ra);
695        let bias_factor = baumgarte / dt;
696        // 2 perpendicular translation rows
697        for (i, &perp) in [perp1, perp2].iter().enumerate() {
698            let row = &mut self.rows[i];
699            row.j_lin_a = -perp;
700            row.j_ang_a = -(ra.cross(perp));
701            row.j_lin_b = perp;
702            row.j_ang_b = rb.cross(perp);
703            row.bias = bias_factor * diff.dot(perp);
704        }
705        // 3 angular lock rows
706        let axes = [Vec3::X, Vec3::Y, Vec3::Z];
707        let rot_diff = rot_b * rot_a.inverse();
708        let (axis_err, angle_err) = quat_to_axis_angle(rot_diff);
709        let ang_err = axis_err * angle_err;
710        for (i, &ax) in axes.iter().enumerate() {
711            let row = &mut self.rows[2 + i];
712            row.j_lin_a = Vec3::ZERO;
713            row.j_ang_a = -ax;
714            row.j_lin_b = Vec3::ZERO;
715            row.j_ang_b = ax;
716            row.bias = bias_factor * ang_err.dot(ax);
717        }
718        // Limit / motor on slide axis handled by caller
719    }
720
721    pub fn current_position(&self, pos_a: Vec3, rot_a: Quat, pos_b: Vec3, rot_b: Quat) -> f32 {
722        let slide_world = rot_a * self.slide_axis_a;
723        let ra = rot_a * self.anchor_a;
724        let rb = rot_b * self.anchor_b;
725        let diff = (pos_b + rb) - (pos_a + ra);
726        diff.dot(slide_world)
727    }
728}
729
730// ---- Ball-Socket Constraint ----
731
732#[derive(Debug, Clone)]
733pub struct BallSocketConstraint {
734    pub body_a: u64,
735    pub body_b: u64,
736    pub pivot_a: Vec3,
737    pub pivot_b: Vec3,
738    pub rows: Vec<JacobianRow>,
739}
740
741impl BallSocketConstraint {
742    pub fn new(body_a: u64, body_b: u64, pivot_a: Vec3, pivot_b: Vec3) -> Self {
743        let mut rows = Vec::new();
744        for _ in 0..3 { rows.push(JacobianRow::new()); }
745        Self { body_a, body_b, pivot_a, pivot_b, rows }
746    }
747
748    pub fn build_jacobian(
749        &mut self,
750        pos_a: Vec3, rot_a: Quat,
751        pos_b: Vec3, rot_b: Quat,
752        baumgarte: f32, dt: f32,
753    ) {
754        let ra = rot_a * self.pivot_a;
755        let rb = rot_b * self.pivot_b;
756        let err = (pos_b + rb) - (pos_a + ra);
757        let bf = baumgarte / dt;
758        let axes = [Vec3::X, Vec3::Y, Vec3::Z];
759        for (i, &ax) in axes.iter().enumerate() {
760            let row = &mut self.rows[i];
761            row.j_lin_a = -ax;
762            row.j_ang_a = -(ra.cross(ax));
763            row.j_lin_b = ax;
764            row.j_ang_b = rb.cross(ax);
765            row.bias = bf * err.dot(ax);
766        }
767    }
768}
769
770// ---- Cone-Twist Constraint ----
771
772#[derive(Debug, Clone)]
773pub struct ConeTwistConstraint {
774    pub body_a: u64,
775    pub body_b: u64,
776    pub pivot_a: Vec3,
777    pub pivot_b: Vec3,
778    pub axis_a: Vec3,
779    pub axis_b: Vec3,
780    pub swing_span1: f32,
781    pub swing_span2: f32,
782    pub twist_span: f32,
783    pub softness: f32,
784    pub bias_factor: f32,
785    pub relaxation_factor: f32,
786    pub rows: Vec<JacobianRow>,
787}
788
789impl ConeTwistConstraint {
790    pub fn new(body_a: u64, body_b: u64, pivot: Vec3, axis: Vec3) -> Self {
791        let mut rows = Vec::new();
792        for _ in 0..6 { rows.push(JacobianRow::new()); }
793        Self {
794            body_a, body_b,
795            pivot_a: pivot, pivot_b: pivot,
796            axis_a: axis.normalize(), axis_b: axis.normalize(),
797            swing_span1: HALF_PI,
798            swing_span2: HALF_PI,
799            twist_span: PI,
800            softness: 1.0,
801            bias_factor: 0.3,
802            relaxation_factor: 1.0,
803            rows,
804        }
805    }
806
807    pub fn build_jacobian(
808        &mut self,
809        pos_a: Vec3, rot_a: Quat,
810        pos_b: Vec3, rot_b: Quat,
811        baumgarte: f32, dt: f32,
812    ) {
813        let ra = rot_a * self.pivot_a;
814        let rb = rot_b * self.pivot_b;
815        let err = (pos_b + rb) - (pos_a + ra);
816        let bf = baumgarte / dt;
817        // Ball-socket part
818        let axes = [Vec3::X, Vec3::Y, Vec3::Z];
819        for (i, &ax) in axes.iter().enumerate() {
820            let row = &mut self.rows[i];
821            row.j_lin_a = -ax;
822            row.j_ang_a = -(ra.cross(ax));
823            row.j_lin_b = ax;
824            row.j_ang_b = rb.cross(ax);
825            row.bias = bf * err.dot(ax);
826        }
827        // Cone and twist limit rows
828        let axis_world_a = rot_a * self.axis_a;
829        let axis_world_b = rot_b * self.axis_b;
830        // Compute swing and twist decomposition
831        let (swing_q, twist_q) = decompose_swing_twist(
832            rot_b * rot_a.inverse(), axis_world_a
833        );
834        let (swing_axis, swing_angle) = quat_to_axis_angle(swing_q);
835        let (twist_axis, twist_angle) = quat_to_axis_angle(twist_q);
836        // Swing limit row
837        {
838            let row = &mut self.rows[3];
839            row.j_lin_a = Vec3::ZERO;
840            row.j_lin_b = Vec3::ZERO;
841            let perp = perpendicular_to(axis_world_a);
842            row.j_ang_a = -perp;
843            row.j_ang_b = perp;
844            let limit = self.swing_span1.max(EPSILON);
845            if swing_angle > limit {
846                row.bias = bf * (swing_angle - limit);
847                row.lambda_min = f32::NEG_INFINITY;
848                row.lambda_max = 0.0;
849            }
850        }
851        // Twist limit row
852        {
853            let row = &mut self.rows[4];
854            row.j_lin_a = Vec3::ZERO;
855            row.j_lin_b = Vec3::ZERO;
856            row.j_ang_a = -axis_world_a;
857            row.j_ang_b = axis_world_a;
858            let limit = self.twist_span.max(EPSILON);
859            if twist_angle.abs() > limit {
860                let sign = twist_angle.signum();
861                row.bias = bf * (twist_angle - sign * limit);
862                if sign > 0.0 {
863                    row.lambda_min = f32::NEG_INFINITY;
864                    row.lambda_max = 0.0;
865                } else {
866                    row.lambda_min = 0.0;
867                    row.lambda_max = f32::INFINITY;
868                }
869            }
870        }
871    }
872}
873
874// ---- Generic 6DOF Constraint ----
875
876#[derive(Debug, Clone)]
877pub struct Generic6DOFConstraint {
878    pub body_a: u64,
879    pub body_b: u64,
880    pub frame_a: Mat4, // local frame in body A
881    pub frame_b: Mat4, // local frame in body B
882    pub linear_lower: Vec3,
883    pub linear_upper: Vec3,
884    pub angular_lower: Vec3,
885    pub angular_upper: Vec3,
886    pub linear_enabled: [bool; 3],
887    pub angular_enabled: [bool; 3],
888    pub spring_stiffness: [f32; 6],
889    pub spring_damping: [f32; 6],
890    pub spring_enabled: [bool; 6],
891    pub rows: Vec<JacobianRow>,
892}
893
894impl Generic6DOFConstraint {
895    pub fn new(body_a: u64, body_b: u64, frame_a: Mat4, frame_b: Mat4) -> Self {
896        let mut rows = Vec::new();
897        for _ in 0..6 { rows.push(JacobianRow::new()); }
898        Self {
899            body_a, body_b,
900            frame_a, frame_b,
901            linear_lower: Vec3::ZERO,
902            linear_upper: Vec3::ZERO,
903            angular_lower: Vec3::splat(-PI),
904            angular_upper: Vec3::splat(PI),
905            linear_enabled: [true; 3],
906            angular_enabled: [true; 3],
907            spring_stiffness: [0.0; 6],
908            spring_damping: [0.0; 6],
909            spring_enabled: [false; 6],
910            rows,
911        }
912    }
913
914    pub fn set_linear_free(&mut self) {
915        self.linear_lower = Vec3::splat(f32::NEG_INFINITY);
916        self.linear_upper = Vec3::splat(f32::INFINITY);
917    }
918
919    pub fn set_angular_locked(&mut self) {
920        self.angular_lower = Vec3::ZERO;
921        self.angular_upper = Vec3::ZERO;
922    }
923
924    pub fn build_jacobian(
925        &mut self,
926        pos_a: Vec3, rot_a: Quat,
927        pos_b: Vec3, rot_b: Quat,
928        baumgarte: f32, dt: f32,
929    ) {
930        let bf = baumgarte / dt;
931        // Extract world frames
932        let r_a = Mat4::from_quat(rot_a);
933        let ax_ax = Vec3::new(r_a.col(0).x, r_a.col(0).y, r_a.col(0).z);
934        let ax_ay = Vec3::new(r_a.col(1).x, r_a.col(1).y, r_a.col(1).z);
935        let ax_az = Vec3::new(r_a.col(2).x, r_a.col(2).y, r_a.col(2).z);
936        let pivot_a = Vec3::new(self.frame_a.col(3).x, self.frame_a.col(3).y, self.frame_a.col(3).z);
937        let pivot_b = Vec3::new(self.frame_b.col(3).x, self.frame_b.col(3).y, self.frame_b.col(3).z);
938        let ra = rot_a * pivot_a;
939        let rb = rot_b * pivot_b;
940        let diff = (pos_b + rb) - (pos_a + ra);
941        let lin_axes = [ax_ax, ax_ay, ax_az];
942        for (i, &ax) in lin_axes.iter().enumerate() {
943            let row = &mut self.rows[i];
944            row.j_lin_a = -ax;
945            row.j_ang_a = -(ra.cross(ax));
946            row.j_lin_b = ax;
947            row.j_ang_b = rb.cross(ax);
948            let dist = diff.dot(ax);
949            let lo = match i { 0 => self.linear_lower.x, 1 => self.linear_lower.y, _ => self.linear_lower.z };
950            let hi = match i { 0 => self.linear_upper.x, 1 => self.linear_upper.y, _ => self.linear_upper.z };
951            if self.linear_enabled[i] {
952                if lo == hi {
953                    row.bias = bf * (dist - lo);
954                } else if dist < lo {
955                    row.bias = bf * (dist - lo);
956                    row.lambda_min = 0.0;
957                    row.lambda_max = f32::INFINITY;
958                } else if dist > hi {
959                    row.bias = bf * (dist - hi);
960                    row.lambda_min = f32::NEG_INFINITY;
961                    row.lambda_max = 0.0;
962                } else {
963                    row.lambda_min = 0.0;
964                    row.lambda_max = 0.0;
965                }
966            }
967        }
968        // Angular 3 rows
969        let rot_diff = rot_b * rot_a.inverse();
970        let (axis_err, angle_err) = quat_to_axis_angle(rot_diff);
971        let ang_err = axis_err * angle_err;
972        for (i, &ax) in lin_axes.iter().enumerate() {
973            let row = &mut self.rows[3 + i];
974            row.j_lin_a = Vec3::ZERO;
975            row.j_ang_a = -ax;
976            row.j_lin_b = Vec3::ZERO;
977            row.j_ang_b = ax;
978            row.bias = bf * ang_err.dot(ax);
979        }
980    }
981}
982
983// ---- Spring Constraint ----
984
985#[derive(Debug, Clone)]
986pub struct SpringConstraint {
987    pub body_a: u64,
988    pub body_b: u64,
989    pub anchor_a: Vec3,
990    pub anchor_b: Vec3,
991    pub rest_length: f32,
992    pub stiffness: f32,
993    pub damping: f32,
994    pub min_length: f32,
995    pub max_length: f32,
996    pub row: JacobianRow,
997}
998
999impl SpringConstraint {
1000    pub fn new(body_a: u64, body_b: u64, anchor_a: Vec3, anchor_b: Vec3,
1001               rest_length: f32, stiffness: f32, damping: f32) -> Self {
1002        Self {
1003            body_a, body_b, anchor_a, anchor_b,
1004            rest_length, stiffness, damping,
1005            min_length: 0.0,
1006            max_length: f32::INFINITY,
1007            row: JacobianRow::new(),
1008        }
1009    }
1010
1011    pub fn build_jacobian(
1012        &mut self,
1013        pos_a: Vec3, rot_a: Quat, vel_a: Vec3, omega_a: Vec3,
1014        pos_b: Vec3, rot_b: Quat, vel_b: Vec3, omega_b: Vec3,
1015        dt: f32,
1016    ) {
1017        let ra = rot_a * self.anchor_a;
1018        let rb = rot_b * self.anchor_b;
1019        let wa = pos_a + ra;
1020        let wb = pos_b + rb;
1021        let delta = wb - wa;
1022        let dist = delta.length();
1023        if dist < EPSILON { return; }
1024        let n = delta / dist;
1025        let extension = dist - self.rest_length;
1026        // Spring force: F = -k*x - d*v_rel_along_n
1027        let v_a_pt = vel_a + omega_a.cross(ra);
1028        let v_b_pt = vel_b + omega_b.cross(rb);
1029        let v_rel = (v_b_pt - v_a_pt).dot(n);
1030        let spring_force = -self.stiffness * extension - self.damping * v_rel;
1031        let row = &mut self.row;
1032        row.j_lin_a = -n;
1033        row.j_ang_a = -(ra.cross(n));
1034        row.j_lin_b = n;
1035        row.j_ang_b = rb.cross(n);
1036        // Convert spring force to position constraint bias
1037        row.bias = spring_force / (self.stiffness.max(EPSILON) * dt);
1038        row.lambda_min = if dist < self.min_length { 0.0 } else { f32::NEG_INFINITY };
1039        row.lambda_max = if dist > self.max_length { 0.0 } else { f32::INFINITY };
1040    }
1041}
1042
1043// ---- Gear Constraint ----
1044
1045#[derive(Debug, Clone)]
1046pub struct GearConstraint {
1047    pub body_a: u64,
1048    pub body_b: u64,
1049    pub axis_a: Vec3,
1050    pub axis_b: Vec3,
1051    pub ratio: f32,      // gear ratio (omega_b = ratio * omega_a)
1052    pub row: JacobianRow,
1053}
1054
1055impl GearConstraint {
1056    pub fn new(body_a: u64, body_b: u64, axis_a: Vec3, axis_b: Vec3, ratio: f32) -> Self {
1057        Self { body_a, body_b, axis_a: axis_a.normalize(), axis_b: axis_b.normalize(), ratio, row: JacobianRow::new() }
1058    }
1059
1060    pub fn build_jacobian(&mut self, rot_a: Quat, rot_b: Quat) {
1061        let wa = rot_a * self.axis_a;
1062        let wb = rot_b * self.axis_b;
1063        let row = &mut self.row;
1064        row.j_lin_a = Vec3::ZERO;
1065        row.j_ang_a = wa;
1066        row.j_lin_b = Vec3::ZERO;
1067        row.j_ang_b = wb * (-self.ratio);
1068        row.bias = 0.0;
1069        row.lambda_min = f32::NEG_INFINITY;
1070        row.lambda_max = f32::INFINITY;
1071    }
1072}
1073
1074// ---- Rack-and-Pinion Constraint ----
1075
1076#[derive(Debug, Clone)]
1077pub struct RackAndPinionConstraint {
1078    pub body_pinion: u64,   // rotating gear
1079    pub body_rack: u64,     // translating rack
1080    pub pinion_axis: Vec3,  // rotation axis of pinion
1081    pub rack_axis: Vec3,    // translation axis of rack
1082    pub pitch_radius: f32,  // pinion pitch radius
1083    pub row: JacobianRow,
1084}
1085
1086impl RackAndPinionConstraint {
1087    pub fn new(body_pinion: u64, body_rack: u64, pinion_axis: Vec3, rack_axis: Vec3, pitch_radius: f32) -> Self {
1088        Self {
1089            body_pinion, body_rack,
1090            pinion_axis: pinion_axis.normalize(),
1091            rack_axis: rack_axis.normalize(),
1092            pitch_radius,
1093            row: JacobianRow::new(),
1094        }
1095    }
1096
1097    pub fn build_jacobian(&mut self, rot_pinion: Quat, rot_rack: Quat) {
1098        let wa = rot_pinion * self.pinion_axis;
1099        let trans_ax = rot_rack * self.rack_axis;
1100        // v_rack = pitch_radius * omega_pinion
1101        let row = &mut self.row;
1102        row.j_lin_a = Vec3::ZERO;
1103        row.j_ang_a = wa * self.pitch_radius;
1104        row.j_lin_b = trans_ax * (-1.0);
1105        row.j_ang_b = Vec3::ZERO;
1106        row.bias = 0.0;
1107    }
1108}
1109
1110// ---- Pulley Constraint ----
1111
1112#[derive(Debug, Clone)]
1113pub struct PulleyConstraint {
1114    pub body_a: u64,
1115    pub body_b: u64,
1116    pub anchor_a: Vec3,         // attachment on body A
1117    pub anchor_b: Vec3,         // attachment on body B
1118    pub fixed_point_a: Vec3,    // fixed pulley wheel A world pos
1119    pub fixed_point_b: Vec3,    // fixed pulley wheel B world pos
1120    pub ratio: f32,             // pulley ratio
1121    pub total_length: f32,      // total rope length
1122    pub row: JacobianRow,
1123}
1124
1125impl PulleyConstraint {
1126    pub fn new(body_a: u64, body_b: u64,
1127               anchor_a: Vec3, anchor_b: Vec3,
1128               fixed_a: Vec3, fixed_b: Vec3,
1129               ratio: f32, total_length: f32) -> Self {
1130        Self {
1131            body_a, body_b, anchor_a, anchor_b,
1132            fixed_point_a: fixed_a, fixed_point_b: fixed_b,
1133            ratio, total_length,
1134            row: JacobianRow::new(),
1135        }
1136    }
1137
1138    pub fn build_jacobian(
1139        &mut self,
1140        pos_a: Vec3, rot_a: Quat,
1141        pos_b: Vec3, rot_b: Quat,
1142        baumgarte: f32, dt: f32,
1143    ) {
1144        let ra = rot_a * self.anchor_a;
1145        let rb = rot_b * self.anchor_b;
1146        let wa = pos_a + ra;
1147        let wb = pos_b + rb;
1148        let dir_a = self.fixed_point_a - wa;
1149        let dist_a = dir_a.length();
1150        let dir_b = self.fixed_point_b - wb;
1151        let dist_b = dir_b.length();
1152        let n_a = if dist_a > EPSILON { dir_a / dist_a } else { Vec3::Y };
1153        let n_b = if dist_b > EPSILON { dir_b / dist_b } else { Vec3::Y };
1154        let constraint_err = dist_a + self.ratio * dist_b - self.total_length;
1155        let row = &mut self.row;
1156        row.j_lin_a = -n_a;
1157        row.j_ang_a = -(ra.cross(n_a));
1158        row.j_lin_b = -n_b * self.ratio;
1159        row.j_ang_b = -(rb.cross(n_b)) * self.ratio;
1160        row.bias = (baumgarte / dt) * constraint_err;
1161        row.lambda_min = 0.0; // rope can only pull
1162        row.lambda_max = f32::INFINITY;
1163    }
1164}
1165
1166// ---- Motor Constraint ----
1167
1168#[derive(Debug, Clone)]
1169pub struct MotorConstraint {
1170    pub body_a: u64,
1171    pub body_b: u64,
1172    pub axis: Vec3,
1173    pub target_velocity: f32,
1174    pub max_torque: f32,
1175    pub servo_enabled: bool,
1176    pub target_angle: f32,
1177    pub servo_stiffness: f32,
1178    pub row: JacobianRow,
1179}
1180
1181impl MotorConstraint {
1182    pub fn new(body_a: u64, body_b: u64, axis: Vec3) -> Self {
1183        Self {
1184            body_a, body_b,
1185            axis: axis.normalize(),
1186            target_velocity: 0.0,
1187            max_torque: 100.0,
1188            servo_enabled: false,
1189            target_angle: 0.0,
1190            servo_stiffness: 10.0,
1191            row: JacobianRow::new(),
1192        }
1193    }
1194
1195    pub fn build_jacobian(&mut self, rot_a: Quat, rot_b: Quat, current_angle: f32, dt: f32) {
1196        let wa = rot_a * self.axis;
1197        let row = &mut self.row;
1198        row.j_lin_a = Vec3::ZERO;
1199        row.j_ang_a = -wa;
1200        row.j_lin_b = Vec3::ZERO;
1201        row.j_ang_b = wa;
1202        if self.servo_enabled {
1203            let angle_err = self.target_angle - current_angle;
1204            row.bias = -self.target_velocity - self.servo_stiffness * angle_err * dt;
1205        } else {
1206            row.bias = -self.target_velocity;
1207        }
1208        row.lambda_min = -self.max_torque * dt;
1209        row.lambda_max = self.max_torque * dt;
1210    }
1211}
1212
1213// ---- Limit Constraint (generic 1-DOF with lo/hi) ----
1214
1215#[derive(Debug, Clone)]
1216pub struct LimitConstraint {
1217    pub body_a: u64,
1218    pub body_b: u64,
1219    pub axis: Vec3,
1220    pub linear: bool, // true = linear, false = angular
1221    pub lower: f32,
1222    pub upper: f32,
1223    pub anchor_a: Vec3,
1224    pub anchor_b: Vec3,
1225    pub restitution: f32,
1226    pub row: JacobianRow,
1227}
1228
1229impl LimitConstraint {
1230    pub fn new(body_a: u64, body_b: u64, axis: Vec3, lower: f32, upper: f32, linear: bool) -> Self {
1231        Self {
1232            body_a, body_b,
1233            axis: axis.normalize(),
1234            linear, lower, upper,
1235            anchor_a: Vec3::ZERO, anchor_b: Vec3::ZERO,
1236            restitution: 0.0,
1237            row: JacobianRow::new(),
1238        }
1239    }
1240
1241    pub fn build_jacobian(
1242        &mut self,
1243        pos_a: Vec3, rot_a: Quat, vel_a: Vec3, omega_a: Vec3,
1244        pos_b: Vec3, rot_b: Quat, vel_b: Vec3, omega_b: Vec3,
1245        baumgarte: f32, dt: f32,
1246    ) {
1247        let bf = baumgarte / dt;
1248        if self.linear {
1249            let wa_ax = rot_a * self.axis;
1250            let ra = rot_a * self.anchor_a;
1251            let rb = rot_b * self.anchor_b;
1252            let diff = (pos_b + rb) - (pos_a + ra);
1253            let pos = diff.dot(wa_ax);
1254            self.row.j_lin_a = -wa_ax;
1255            self.row.j_ang_a = -(ra.cross(wa_ax));
1256            self.row.j_lin_b = wa_ax;
1257            self.row.j_ang_b = rb.cross(wa_ax);
1258            if pos < self.lower {
1259                self.row.bias = bf * (pos - self.lower);
1260                let rel_vel = self.row.j_lin_a.dot(vel_a) + self.row.j_lin_b.dot(vel_b);
1261                if rel_vel < 0.0 { self.row.bias += self.restitution * rel_vel; }
1262                self.row.lambda_min = 0.0;
1263                self.row.lambda_max = f32::INFINITY;
1264            } else if pos > self.upper {
1265                self.row.bias = bf * (pos - self.upper);
1266                self.row.lambda_min = f32::NEG_INFINITY;
1267                self.row.lambda_max = 0.0;
1268            }
1269        } else {
1270            let wa = rot_a * self.axis;
1271            self.row.j_lin_a = Vec3::ZERO;
1272            self.row.j_ang_a = -wa;
1273            self.row.j_lin_b = Vec3::ZERO;
1274            self.row.j_ang_b = wa;
1275            // Angular position would be computed by caller
1276        }
1277    }
1278}
1279
1280// ---- Distance Constraint ----
1281
1282#[derive(Debug, Clone)]
1283pub struct DistanceConstraint {
1284    pub body_a: u64,
1285    pub body_b: u64,
1286    pub anchor_a: Vec3,
1287    pub anchor_b: Vec3,
1288    pub min_distance: f32,
1289    pub max_distance: f32,
1290    pub row: JacobianRow,
1291}
1292
1293impl DistanceConstraint {
1294    pub fn new(body_a: u64, body_b: u64, anchor_a: Vec3, anchor_b: Vec3, dist: f32) -> Self {
1295        Self {
1296            body_a, body_b, anchor_a, anchor_b,
1297            min_distance: dist, max_distance: dist,
1298            row: JacobianRow::new(),
1299        }
1300    }
1301
1302    pub fn build_jacobian(
1303        &mut self,
1304        pos_a: Vec3, rot_a: Quat,
1305        pos_b: Vec3, rot_b: Quat,
1306        baumgarte: f32, dt: f32,
1307    ) {
1308        let ra = rot_a * self.anchor_a;
1309        let rb = rot_b * self.anchor_b;
1310        let wa = pos_a + ra;
1311        let wb = pos_b + rb;
1312        let delta = wb - wa;
1313        let dist = delta.length();
1314        if dist < EPSILON { return; }
1315        let n = delta / dist;
1316        let bf = baumgarte / dt;
1317        self.row.j_lin_a = -n;
1318        self.row.j_ang_a = -(ra.cross(n));
1319        self.row.j_lin_b = n;
1320        self.row.j_ang_b = rb.cross(n);
1321        if dist < self.min_distance {
1322            self.row.bias = bf * (dist - self.min_distance);
1323            self.row.lambda_min = 0.0;
1324            self.row.lambda_max = f32::INFINITY;
1325        } else if dist > self.max_distance {
1326            self.row.bias = bf * (dist - self.max_distance);
1327            self.row.lambda_min = f32::NEG_INFINITY;
1328            self.row.lambda_max = 0.0;
1329        } else {
1330            self.row.lambda_min = f32::NEG_INFINITY;
1331            self.row.lambda_max = f32::INFINITY;
1332        }
1333    }
1334}
1335
1336// ---- Point-to-Point Constraint (alias of BallSocket with extra params) ----
1337
1338#[derive(Debug, Clone)]
1339pub struct PointToPointConstraint {
1340    pub body_a: u64,
1341    pub body_b: u64,
1342    pub pivot_a: Vec3,
1343    pub pivot_b: Vec3,
1344    pub tau: f32,   // softness factor (0 = rigid, 1 = very soft)
1345    pub damping: f32,
1346    pub impulse_clamp: f32,
1347    pub rows: Vec<JacobianRow>,
1348}
1349
1350impl PointToPointConstraint {
1351    pub fn new(body_a: u64, body_b: u64, pivot: Vec3) -> Self {
1352        let mut rows = Vec::new();
1353        for _ in 0..3 { rows.push(JacobianRow::new()); }
1354        Self {
1355            body_a, body_b,
1356            pivot_a: pivot, pivot_b: pivot,
1357            tau: 0.3, damping: 1.0,
1358            impulse_clamp: 0.0,
1359            rows,
1360        }
1361    }
1362
1363    pub fn build_jacobian(
1364        &mut self,
1365        pos_a: Vec3, rot_a: Quat, vel_a: Vec3, omega_a: Vec3,
1366        pos_b: Vec3, rot_b: Quat, vel_b: Vec3, omega_b: Vec3,
1367        dt: f32,
1368    ) {
1369        let ra = rot_a * self.pivot_a;
1370        let rb = rot_b * self.pivot_b;
1371        let err = (pos_b + rb) - (pos_a + ra);
1372        let axes = [Vec3::X, Vec3::Y, Vec3::Z];
1373        for (i, &ax) in axes.iter().enumerate() {
1374            let row = &mut self.rows[i];
1375            row.j_lin_a = -ax;
1376            row.j_ang_a = -(ra.cross(ax));
1377            row.j_lin_b = ax;
1378            row.j_ang_b = rb.cross(ax);
1379            // Soft constraint: bias = tau/dt * error + damping * rel_vel
1380            let v_a_pt = vel_a + omega_a.cross(ra);
1381            let v_b_pt = vel_b + omega_b.cross(rb);
1382            let rel_v = (v_b_pt - v_a_pt).dot(ax);
1383            row.bias = (self.tau / dt) * err.dot(ax) + self.damping * rel_v;
1384            if self.impulse_clamp > 0.0 {
1385                row.lambda_min = -self.impulse_clamp;
1386                row.lambda_max = self.impulse_clamp;
1387            }
1388        }
1389    }
1390}
1391
1392// ---- Angular Constraint ----
1393
1394#[derive(Debug, Clone)]
1395pub struct AngularConstraint {
1396    pub body_a: u64,
1397    pub body_b: u64,
1398    pub axis: Vec3,
1399    pub target_angle: f32,
1400    pub stiffness: f32,
1401    pub damping: f32,
1402    pub row: JacobianRow,
1403}
1404
1405impl AngularConstraint {
1406    pub fn new(body_a: u64, body_b: u64, axis: Vec3) -> Self {
1407        Self {
1408            body_a, body_b,
1409            axis: axis.normalize(),
1410            target_angle: 0.0,
1411            stiffness: 100.0,
1412            damping: 10.0,
1413            row: JacobianRow::new(),
1414        }
1415    }
1416
1417    pub fn build_jacobian(
1418        &mut self,
1419        rot_a: Quat, omega_a: Vec3,
1420        rot_b: Quat, omega_b: Vec3,
1421        current_angle: f32, dt: f32,
1422    ) {
1423        let wa = rot_a * self.axis;
1424        let angle_err = self.target_angle - current_angle;
1425        let rel_omega = (omega_b - omega_a).dot(wa);
1426        let row = &mut self.row;
1427        row.j_lin_a = Vec3::ZERO;
1428        row.j_ang_a = -wa;
1429        row.j_lin_b = Vec3::ZERO;
1430        row.j_ang_b = wa;
1431        row.bias = self.stiffness * angle_err * dt - self.damping * rel_omega;
1432    }
1433}
1434
1435// ---- Weld Constraint (alias of Fixed with no breaking) ----
1436#[derive(Debug, Clone)]
1437pub struct WeldConstraint {
1438    pub inner: FixedConstraint,
1439    pub allow_rotation: bool,
1440}
1441
1442impl WeldConstraint {
1443    pub fn new(body_a: u64, body_b: u64, anchor_a: Vec3, anchor_b: Vec3) -> Self {
1444        let inner = FixedConstraint::new(body_a, body_b, anchor_a, anchor_b);
1445        Self { inner, allow_rotation: false }
1446    }
1447}
1448
1449// ============================================================
1450// CONSTRAINT ENUM for editor storage
1451// ============================================================
1452
1453#[derive(Debug, Clone)]
1454pub enum Constraint {
1455    Fixed(FixedConstraint),
1456    Hinge(HingeConstraint),
1457    Slider(SliderConstraint),
1458    BallSocket(BallSocketConstraint),
1459    ConeTwist(ConeTwistConstraint),
1460    Generic6DOF(Generic6DOFConstraint),
1461    Spring(SpringConstraint),
1462    Gear(GearConstraint),
1463    RackAndPinion(RackAndPinionConstraint),
1464    Pulley(PulleyConstraint),
1465    Motor(MotorConstraint),
1466    Limit(LimitConstraint),
1467    Distance(DistanceConstraint),
1468    PointToPoint(PointToPointConstraint),
1469    Angular(AngularConstraint),
1470    Weld(WeldConstraint),
1471}
1472
1473impl Constraint {
1474    pub fn name(&self) -> &'static str {
1475        match self {
1476            Constraint::Fixed(_) => "Fixed",
1477            Constraint::Hinge(_) => "Hinge",
1478            Constraint::Slider(_) => "Slider",
1479            Constraint::BallSocket(_) => "BallSocket",
1480            Constraint::ConeTwist(_) => "ConeTwist",
1481            Constraint::Generic6DOF(_) => "Generic6DOF",
1482            Constraint::Spring(_) => "Spring",
1483            Constraint::Gear(_) => "Gear",
1484            Constraint::RackAndPinion(_) => "RackAndPinion",
1485            Constraint::Pulley(_) => "Pulley",
1486            Constraint::Motor(_) => "Motor",
1487            Constraint::Limit(_) => "Limit",
1488            Constraint::Distance(_) => "Distance",
1489            Constraint::PointToPoint(_) => "PointToPoint",
1490            Constraint::Angular(_) => "Angular",
1491            Constraint::Weld(_) => "Weld",
1492        }
1493    }
1494}
1495
1496// ============================================================
1497// JOINT EDITOR — visual positioning & axis/limit arc drawing
1498// ============================================================
1499
1500#[derive(Debug, Clone)]
1501pub struct JointVisualizer {
1502    pub constraint_id: u64,
1503    pub pivot_world: Vec3,
1504    pub axis_world: Vec3,
1505    pub axis_color: Vec4,
1506    pub arc_segments: u32,
1507    pub arc_radius: f32,
1508    pub show_limits: bool,
1509    pub show_drive: bool,
1510    pub selected: bool,
1511}
1512
1513#[derive(Debug, Clone)]
1514pub struct ArcPoint {
1515    pub pos: Vec3,
1516    pub t: f32,     // parameter [0,1]
1517}
1518
1519impl JointVisualizer {
1520    pub fn new(constraint_id: u64, pivot: Vec3, axis: Vec3) -> Self {
1521        Self {
1522            constraint_id,
1523            pivot_world: pivot,
1524            axis_world: axis.normalize(),
1525            axis_color: Vec4::new(1.0, 1.0, 0.0, 1.0),
1526            arc_segments: 32,
1527            arc_radius: 0.2,
1528            show_limits: true,
1529            show_drive: true,
1530            selected: false,
1531        }
1532    }
1533
1534    /// Generate arc points for a hinge limit arc in 3D space
1535    pub fn compute_limit_arc(&self, lower: f32, upper: f32) -> Vec<ArcPoint> {
1536        let mut points = Vec::new();
1537        let n = self.arc_segments as usize;
1538        if n == 0 { return points; }
1539        // Build local frame: perp1, perp2, axis
1540        let perp1 = perpendicular_to(self.axis_world);
1541        let perp2 = self.axis_world.cross(perp1).normalize();
1542        let span = upper - lower;
1543        let step = span / n as f32;
1544        for i in 0..=n {
1545            let angle = lower + step * i as f32;
1546            let t = i as f32 / n as f32;
1547            let c = angle.cos();
1548            let s = angle.sin();
1549            let local = perp1 * c + perp2 * s;
1550            let pos = self.pivot_world + local * self.arc_radius;
1551            points.push(ArcPoint { pos, t });
1552        }
1553        points
1554    }
1555
1556    /// Generate cone surface sample points
1557    pub fn compute_cone_surface(&self, half_angle: f32) -> Vec<Vec3> {
1558        let mut pts = Vec::new();
1559        let n = self.arc_segments as usize;
1560        let perp1 = perpendicular_to(self.axis_world);
1561        let perp2 = self.axis_world.cross(perp1).normalize();
1562        let tip = self.pivot_world;
1563        let r = self.arc_radius * half_angle.tan();
1564        let apex = tip + self.axis_world * self.arc_radius;
1565        for i in 0..n {
1566            let angle = TWO_PI * i as f32 / n as f32;
1567            let base = apex + (perp1 * angle.cos() + perp2 * angle.sin()) * r;
1568            pts.push(tip);
1569            pts.push(base);
1570            let angle_next = TWO_PI * (i + 1) as f32 / n as f32;
1571            let base_next = apex + (perp1 * angle_next.cos() + perp2 * angle_next.sin()) * r;
1572            pts.push(base_next);
1573        }
1574        pts
1575    }
1576
1577    /// Axis line endpoints
1578    pub fn axis_line(&self, len: f32) -> (Vec3, Vec3) {
1579        let start = self.pivot_world - self.axis_world * len * 0.5;
1580        let end = self.pivot_world + self.axis_world * len * 0.5;
1581        (start, end)
1582    }
1583
1584    /// Generate drive target indicator
1585    pub fn drive_indicator(&self, target_angle: f32) -> Vec3 {
1586        let perp1 = perpendicular_to(self.axis_world);
1587        let perp2 = self.axis_world.cross(perp1).normalize();
1588        let c = target_angle.cos();
1589        let s = target_angle.sin();
1590        self.pivot_world + (perp1 * c + perp2 * s) * self.arc_radius
1591    }
1592}
1593
1594#[derive(Debug, Clone)]
1595pub struct JointEditor {
1596    pub visualizers: HashMap<u64, JointVisualizer>,
1597    pub selected_joint: Option<u64>,
1598    pub next_id: u64,
1599    pub gizmo_mode: JointGizmoMode,
1600    pub snap_angle_deg: f32,
1601    pub snap_position: f32,
1602}
1603
1604#[derive(Debug, Clone, PartialEq)]
1605pub enum JointGizmoMode {
1606    Translate,
1607    Rotate,
1608    Scale,
1609}
1610
1611impl JointEditor {
1612    pub fn new() -> Self {
1613        Self {
1614            visualizers: HashMap::new(),
1615            selected_joint: None,
1616            next_id: 1,
1617            gizmo_mode: JointGizmoMode::Translate,
1618            snap_angle_deg: 5.0,
1619            snap_position: 0.1,
1620        }
1621    }
1622
1623    pub fn add_joint_visualizer(&mut self, pivot: Vec3, axis: Vec3) -> u64 {
1624        let id = self.next_id;
1625        self.next_id += 1;
1626        self.visualizers.insert(id, JointVisualizer::new(id, pivot, axis));
1627        id
1628    }
1629
1630    pub fn set_selected(&mut self, id: u64) {
1631        if let Some(old) = self.selected_joint {
1632            if let Some(v) = self.visualizers.get_mut(&old) {
1633                v.selected = false;
1634            }
1635        }
1636        self.selected_joint = Some(id);
1637        if let Some(v) = self.visualizers.get_mut(&id) {
1638            v.selected = true;
1639        }
1640    }
1641
1642    pub fn move_joint(&mut self, id: u64, delta: Vec3) {
1643        if let Some(v) = self.visualizers.get_mut(&id) {
1644            let snap = self.snap_position;
1645            let snapped = Vec3::new(
1646                snap_value(v.pivot_world.x + delta.x, snap),
1647                snap_value(v.pivot_world.y + delta.y, snap),
1648                snap_value(v.pivot_world.z + delta.z, snap),
1649            );
1650            v.pivot_world = snapped;
1651        }
1652    }
1653
1654    pub fn rotate_axis(&mut self, id: u64, rotation: Quat) {
1655        if let Some(v) = self.visualizers.get_mut(&id) {
1656            v.axis_world = (rotation * v.axis_world).normalize();
1657        }
1658    }
1659
1660    pub fn generate_all_debug_lines(&self) -> Vec<(Vec3, Vec3, Vec4)> {
1661        let mut lines = Vec::new();
1662        for (_, vis) in &self.visualizers {
1663            let (a, b) = vis.axis_line(0.5);
1664            lines.push((a, b, vis.axis_color));
1665        }
1666        lines
1667    }
1668}
1669
1670// ============================================================
1671// CLOTH SIMULATION PARAMETERS
1672// ============================================================
1673
1674#[derive(Debug, Clone)]
1675pub struct ClothParticle {
1676    pub position: Vec3,
1677    pub prev_position: Vec3,
1678    pub velocity: Vec3,
1679    pub mass: f32,
1680    pub inv_mass: f32,
1681    pub pinned: bool,
1682    pub normal: Vec3,
1683    pub tex_coord: Vec2,
1684}
1685
1686impl ClothParticle {
1687    pub fn new(pos: Vec3, mass: f32) -> Self {
1688        Self {
1689            position: pos,
1690            prev_position: pos,
1691            velocity: Vec3::ZERO,
1692            mass,
1693            inv_mass: if mass > EPSILON { 1.0 / mass } else { 0.0 },
1694            pinned: false,
1695            normal: Vec3::Y,
1696            tex_coord: Vec2::ZERO,
1697        }
1698    }
1699}
1700
1701#[derive(Debug, Clone)]
1702pub struct ClothSpring {
1703    pub particle_a: usize,
1704    pub particle_b: usize,
1705    pub rest_length: f32,
1706    pub stiffness: f32,
1707    pub damping: f32,
1708    pub spring_type: ClothSpringType,
1709    pub torn: bool,
1710    pub tear_threshold: f32,
1711}
1712
1713#[derive(Debug, Clone, PartialEq)]
1714pub enum ClothSpringType {
1715    Stretch,
1716    Shear,
1717    Bend,
1718}
1719
1720#[derive(Debug, Clone)]
1721pub struct ClothSimulation {
1722    pub particles: Vec<ClothParticle>,
1723    pub springs: Vec<ClothSpring>,
1724    pub grid_width: usize,
1725    pub grid_height: usize,
1726    pub total_mass: f32,
1727    pub stretch_stiffness: f32,
1728    pub shear_stiffness: f32,
1729    pub bend_stiffness: f32,
1730    pub stretch_damping: f32,
1731    pub shear_damping: f32,
1732    pub bend_damping: f32,
1733    pub gravity: Vec3,
1734    pub wind_velocity: Vec3,
1735    pub wind_turbulence: f32,
1736    pub drag_coefficient: f32,
1737    pub thickness: f32,
1738    pub self_collision_enabled: bool,
1739    pub self_collision_radius: f32,
1740    pub tearing_enabled: bool,
1741    pub global_tear_threshold: f32,
1742    pub pinned_vertices: HashSet<usize>,
1743    pub iterations: u32,
1744}
1745
1746impl ClothSimulation {
1747    pub fn new(width: usize, height: usize, cell_size: f32, mass_per_particle: f32) -> Self {
1748        let mut particles = Vec::new();
1749        let mut springs = Vec::new();
1750        // Create particle grid
1751        for j in 0..height {
1752            for i in 0..width {
1753                let pos = Vec3::new(i as f32 * cell_size, 0.0, j as f32 * cell_size);
1754                let mut p = ClothParticle::new(pos, mass_per_particle);
1755                p.tex_coord = Vec2::new(i as f32 / (width - 1) as f32, j as f32 / (height - 1) as f32);
1756                particles.push(p);
1757            }
1758        }
1759        let idx = |i: usize, j: usize| j * width + i;
1760        // Structural (stretch) springs — horizontal and vertical
1761        for j in 0..height {
1762            for i in 0..width {
1763                if i + 1 < width {
1764                    let dist = cell_size;
1765                    springs.push(ClothSpring {
1766                        particle_a: idx(i, j),
1767                        particle_b: idx(i + 1, j),
1768                        rest_length: dist,
1769                        stiffness: 1000.0,
1770                        damping: 0.1,
1771                        spring_type: ClothSpringType::Stretch,
1772                        torn: false,
1773                        tear_threshold: 2.0 * dist,
1774                    });
1775                }
1776                if j + 1 < height {
1777                    let dist = cell_size;
1778                    springs.push(ClothSpring {
1779                        particle_a: idx(i, j),
1780                        particle_b: idx(i, j + 1),
1781                        rest_length: dist,
1782                        stiffness: 1000.0,
1783                        damping: 0.1,
1784                        spring_type: ClothSpringType::Stretch,
1785                        torn: false,
1786                        tear_threshold: 2.0 * dist,
1787                    });
1788                }
1789            }
1790        }
1791        // Shear springs — diagonals
1792        for j in 0..height - 1 {
1793            for i in 0..width - 1 {
1794                let diag = cell_size * 2.0_f32.sqrt();
1795                springs.push(ClothSpring {
1796                    particle_a: idx(i, j),
1797                    particle_b: idx(i + 1, j + 1),
1798                    rest_length: diag,
1799                    stiffness: 500.0,
1800                    damping: 0.05,
1801                    spring_type: ClothSpringType::Shear,
1802                    torn: false,
1803                    tear_threshold: 3.0 * diag,
1804                });
1805                springs.push(ClothSpring {
1806                    particle_a: idx(i + 1, j),
1807                    particle_b: idx(i, j + 1),
1808                    rest_length: diag,
1809                    stiffness: 500.0,
1810                    damping: 0.05,
1811                    spring_type: ClothSpringType::Shear,
1812                    torn: false,
1813                    tear_threshold: 3.0 * diag,
1814                });
1815            }
1816        }
1817        // Bend springs — skip one particle
1818        for j in 0..height {
1819            for i in 0..width {
1820                if i + 2 < width {
1821                    let dist = 2.0 * cell_size;
1822                    springs.push(ClothSpring {
1823                        particle_a: idx(i, j),
1824                        particle_b: idx(i + 2, j),
1825                        rest_length: dist,
1826                        stiffness: 100.0,
1827                        damping: 0.01,
1828                        spring_type: ClothSpringType::Bend,
1829                        torn: false,
1830                        tear_threshold: 4.0 * dist,
1831                    });
1832                }
1833                if j + 2 < height {
1834                    let dist = 2.0 * cell_size;
1835                    springs.push(ClothSpring {
1836                        particle_a: idx(i, j),
1837                        particle_b: idx(i, j + 2),
1838                        rest_length: dist,
1839                        stiffness: 100.0,
1840                        damping: 0.01,
1841                        spring_type: ClothSpringType::Bend,
1842                        torn: false,
1843                        tear_threshold: 4.0 * dist,
1844                    });
1845                }
1846            }
1847        }
1848        let total_mass = mass_per_particle * (width * height) as f32;
1849        Self {
1850            particles, springs,
1851            grid_width: width, grid_height: height,
1852            total_mass,
1853            stretch_stiffness: 1000.0,
1854            shear_stiffness: 500.0,
1855            bend_stiffness: 100.0,
1856            stretch_damping: 0.1,
1857            shear_damping: 0.05,
1858            bend_damping: 0.01,
1859            gravity: Vec3::new(0.0, -GRAVITY, 0.0),
1860            wind_velocity: Vec3::ZERO,
1861            wind_turbulence: 0.1,
1862            drag_coefficient: 0.05,
1863            thickness: 0.001,
1864            self_collision_enabled: true,
1865            self_collision_radius: 0.01,
1866            tearing_enabled: false,
1867            global_tear_threshold: 3.0,
1868            pinned_vertices: HashSet::new(),
1869            iterations: 10,
1870        }
1871    }
1872
1873    pub fn pin_vertex(&mut self, idx: usize) {
1874        if idx < self.particles.len() {
1875            self.particles[idx].pinned = true;
1876            self.particles[idx].inv_mass = 0.0;
1877            self.pinned_vertices.insert(idx);
1878        }
1879    }
1880
1881    pub fn unpin_vertex(&mut self, idx: usize) {
1882        if idx < self.particles.len() {
1883            self.particles[idx].pinned = false;
1884            if self.particles[idx].mass > EPSILON {
1885                self.particles[idx].inv_mass = 1.0 / self.particles[idx].mass;
1886            }
1887            self.pinned_vertices.remove(&idx);
1888        }
1889    }
1890
1891    /// Compute wind force on a triangle given its vertices
1892    pub fn wind_force_on_triangle(&self, p0: Vec3, p1: Vec3, p2: Vec3, time: f32) -> Vec3 {
1893        let edge1 = p1 - p0;
1894        let edge2 = p2 - p0;
1895        let normal = edge1.cross(edge2);
1896        let area = normal.length() * 0.5;
1897        if area < EPSILON { return Vec3::ZERO; }
1898        let n = normal / (2.0 * area); // unit normal
1899        // Turbulence using simple hash-based noise
1900        let turb = Vec3::new(
1901            (time * 2.3 + p0.x).sin() * self.wind_turbulence,
1902            (time * 1.7 + p0.y).cos() * self.wind_turbulence * 0.5,
1903            (time * 3.1 + p0.z).sin() * self.wind_turbulence,
1904        );
1905        let effective_wind = self.wind_velocity + turb;
1906        let relative_wind = effective_wind; // ignore particle velocity for now
1907        let wind_dot_n = relative_wind.dot(n);
1908        // Force proportional to area * (v·n) * n (aerodynamic pressure)
1909        let rho_half = AIR_DENSITY * 0.5;
1910        let force = n * (rho_half * wind_dot_n * wind_dot_n.abs() * area);
1911        force
1912    }
1913
1914    /// Verlet integrate all particles
1915    pub fn integrate(&mut self, dt: f32, time: f32) {
1916        let dt2 = dt * dt;
1917        for p in &mut self.particles {
1918            if p.pinned { continue; }
1919            // Save old position
1920            let old_pos = p.position;
1921            // Gravity force per unit mass (since Verlet: x += v*dt + a*dt^2)
1922            let acc = self.gravity;
1923            // Wind drag (rough approximation)
1924            let drag = -p.velocity * self.drag_coefficient;
1925            let total_acc = acc + drag * p.inv_mass;
1926            // Verlet step
1927            let new_pos = p.position * 2.0 - p.prev_position + total_acc * dt2;
1928            p.prev_position = old_pos;
1929            p.position = new_pos;
1930            p.velocity = (p.position - p.prev_position) / dt;
1931        }
1932    }
1933
1934    /// Solve spring constraints (Gauss-Seidel)
1935    pub fn solve_springs(&mut self, dt: f32) {
1936        for _ in 0..self.iterations {
1937            for spring_idx in 0..self.springs.len() {
1938                if self.springs[spring_idx].torn { continue; }
1939                let a = self.springs[spring_idx].particle_a;
1940                let b = self.springs[spring_idx].particle_b;
1941                let rest = self.springs[spring_idx].rest_length;
1942                let stiff = self.springs[spring_idx].stiffness;
1943                let damp = self.springs[spring_idx].damping;
1944                let tear = self.springs[spring_idx].tear_threshold;
1945                let pa = self.particles[a].position;
1946                let pb = self.particles[b].position;
1947                let ia = self.particles[a].inv_mass;
1948                let ib = self.particles[b].inv_mass;
1949                let sum_inv = ia + ib;
1950                if sum_inv < EPSILON { continue; }
1951                let delta = pb - pa;
1952                let dist = delta.length();
1953                if dist < EPSILON { continue; }
1954                let stretch = (dist - rest) / dist;
1955                // Tearing
1956                if self.tearing_enabled && dist > tear {
1957                    self.springs[spring_idx].torn = true;
1958                    continue;
1959                }
1960                let correction = delta * stretch;
1961                let w_a = ia / sum_inv;
1962                let w_b = ib / sum_inv;
1963                self.particles[a].position += correction * w_a;
1964                self.particles[b].position -= correction * w_b;
1965            }
1966        }
1967    }
1968
1969    /// Compute vertex normals from mesh connectivity
1970    pub fn compute_normals(&mut self) {
1971        for p in &mut self.particles {
1972            p.normal = Vec3::ZERO;
1973        }
1974        let w = self.grid_width;
1975        let h = self.grid_height;
1976        let idx = |i: usize, j: usize| j * w + i;
1977        for j in 0..h - 1 {
1978            for i in 0..w - 1 {
1979                let i00 = idx(i, j);
1980                let i10 = idx(i + 1, j);
1981                let i01 = idx(i, j + 1);
1982                let i11 = idx(i + 1, j + 1);
1983                let p00 = self.particles[i00].position;
1984                let p10 = self.particles[i10].position;
1985                let p01 = self.particles[i01].position;
1986                let p11 = self.particles[i11].position;
1987                // Triangle 1: 00, 10, 01
1988                let n1 = (p10 - p00).cross(p01 - p00);
1989                self.particles[i00].normal += n1;
1990                self.particles[i10].normal += n1;
1991                self.particles[i01].normal += n1;
1992                // Triangle 2: 10, 11, 01
1993                let n2 = (p11 - p10).cross(p01 - p10);
1994                self.particles[i10].normal += n2;
1995                self.particles[i11].normal += n2;
1996                self.particles[i01].normal += n2;
1997            }
1998        }
1999        for p in &mut self.particles {
2000            let l = p.normal.length();
2001            if l > EPSILON { p.normal /= l; }
2002        }
2003    }
2004}
2005
2006// ============================================================
2007// FLUID SIMULATION — SPH (Smoothed Particle Hydrodynamics)
2008// ============================================================
2009
2010pub const SPH_POLY6_COEFF: f32 = 315.0 / (64.0 * PI);
2011pub const SPH_SPIKY_COEFF: f32 = 15.0 / PI;
2012pub const SPH_VISC_COEFF: f32 = 15.0 / (2.0 * PI);
2013
2014/// Poly6 kernel W(r,h) = (315/64πh^9)*(h^2-r^2)^3 for |r|<=h
2015pub fn sph_kernel_poly6(r_sq: f32, h: f32) -> f32 {
2016    let h2 = h * h;
2017    if r_sq > h2 { return 0.0; }
2018    let diff = h2 - r_sq;
2019    SPH_POLY6_COEFF / h.powi(9) * diff * diff * diff
2020}
2021
2022/// Gradient of Poly6 kernel
2023pub fn sph_kernel_poly6_grad(r: Vec3, r_sq: f32, h: f32) -> Vec3 {
2024    let h2 = h * h;
2025    if r_sq > h2 { return Vec3::ZERO; }
2026    let diff = h2 - r_sq;
2027    let coeff = -6.0 * SPH_POLY6_COEFF / h.powi(9) * diff * diff;
2028    r * coeff
2029}
2030
2031/// Laplacian of Poly6 kernel
2032pub fn sph_kernel_poly6_lap(r_sq: f32, h: f32) -> f32 {
2033    let h2 = h * h;
2034    if r_sq > h2 { return 0.0; }
2035    let diff = h2 - r_sq;
2036    -6.0 * SPH_POLY6_COEFF / h.powi(9) * diff * (3.0 * h2 - 7.0 * r_sq)
2037}
2038
2039/// Spiky kernel gradient (for pressure) — W_spiky = (15/πh^6)*(h-|r|)^3
2040pub fn sph_kernel_spiky_grad(r: Vec3, r_len: f32, h: f32) -> Vec3 {
2041    if r_len > h || r_len < EPSILON { return Vec3::ZERO; }
2042    let diff = h - r_len;
2043    let coeff = -3.0 * SPH_SPIKY_COEFF / h.powi(6) * diff * diff / r_len;
2044    r * coeff
2045}
2046
2047/// Viscosity kernel laplacian: W_lap = (15/(2πh^3))*(-(r^3/2h^3) + r^2/h^2 + h/(2r) - 1)
2048pub fn sph_kernel_viscosity_lap(r_len: f32, h: f32) -> f32 {
2049    if r_len > h { return 0.0; }
2050    (45.0 / (PI * h.powi(6))) * (h - r_len)
2051}
2052
2053#[derive(Debug, Clone)]
2054pub struct SphParticle {
2055    pub position: Vec3,
2056    pub velocity: Vec3,
2057    pub force: Vec3,
2058    pub density: f32,
2059    pub pressure: f32,
2060    pub mass: f32,
2061}
2062
2063impl SphParticle {
2064    pub fn new(pos: Vec3, mass: f32) -> Self {
2065        Self {
2066            position: pos,
2067            velocity: Vec3::ZERO,
2068            force: Vec3::ZERO,
2069            density: 0.0,
2070            pressure: 0.0,
2071            mass,
2072        }
2073    }
2074}
2075
2076#[derive(Debug, Clone)]
2077pub struct FluidSimulation {
2078    pub particles: Vec<SphParticle>,
2079    pub kernel_radius: f32,
2080    pub rest_density: f32,
2081    pub viscosity: f32,
2082    pub pressure_stiffness: f32,
2083    pub surface_tension: f32,
2084    pub gravity: Vec3,
2085    pub restitution: f32,
2086    pub particle_radius: f32,
2087    pub neighbor_grid: HashMap<(i32, i32, i32), Vec<usize>>,
2088    pub domain_min: Vec3,
2089    pub domain_max: Vec3,
2090    pub time_scale: f32,
2091    pub max_velocity: f32,
2092    pub color_field_threshold: f32,
2093}
2094
2095impl FluidSimulation {
2096    pub fn new(kernel_radius: f32, rest_density: f32) -> Self {
2097        Self {
2098            particles: Vec::new(),
2099            kernel_radius,
2100            rest_density,
2101            viscosity: 0.1,
2102            pressure_stiffness: 200.0,
2103            surface_tension: 0.0728,
2104            gravity: Vec3::new(0.0, -GRAVITY, 0.0),
2105            restitution: 0.0,
2106            particle_radius: kernel_radius * 0.5,
2107            neighbor_grid: HashMap::new(),
2108            domain_min: Vec3::splat(-10.0),
2109            domain_max: Vec3::splat(10.0),
2110            time_scale: 1.0,
2111            max_velocity: 50.0,
2112            color_field_threshold: 0.6,
2113        }
2114    }
2115
2116    pub fn spawn_block(&mut self, min: Vec3, max: Vec3, spacing: f32, mass: f32) {
2117        let h = spacing;
2118        let mut x = min.x;
2119        while x <= max.x {
2120            let mut y = min.y;
2121            while y <= max.y {
2122                let mut z = min.z;
2123                while z <= max.z {
2124                    self.particles.push(SphParticle::new(Vec3::new(x, y, z), mass));
2125                    z += h;
2126                }
2127                y += h;
2128            }
2129            x += h;
2130        }
2131    }
2132
2133    fn cell_key(&self, pos: Vec3) -> (i32, i32, i32) {
2134        let h = self.kernel_radius;
2135        (
2136            (pos.x / h).floor() as i32,
2137            (pos.y / h).floor() as i32,
2138            (pos.z / h).floor() as i32,
2139        )
2140    }
2141
2142    pub fn build_neighbor_grid(&mut self) {
2143        self.neighbor_grid.clear();
2144        for (i, p) in self.particles.iter().enumerate() {
2145            let key = self.cell_key(p.position);
2146            self.neighbor_grid.entry(key).or_default().push(i);
2147        }
2148    }
2149
2150    pub fn get_neighbors(&self, pos: Vec3) -> Vec<usize> {
2151        let mut result = Vec::new();
2152        let (cx, cy, cz) = self.cell_key(pos);
2153        for dx in -1..=1 {
2154            for dy in -1..=1 {
2155                for dz in -1..=1 {
2156                    if let Some(list) = self.neighbor_grid.get(&(cx + dx, cy + dy, cz + dz)) {
2157                        result.extend_from_slice(list);
2158                    }
2159                }
2160            }
2161        }
2162        result
2163    }
2164
2165    /// Compute density for all particles
2166    pub fn compute_density(&mut self) {
2167        let h = self.kernel_radius;
2168        let n = self.particles.len();
2169        let positions: Vec<Vec3> = self.particles.iter().map(|p| p.position).collect();
2170        let masses: Vec<f32> = self.particles.iter().map(|p| p.mass).collect();
2171        for i in 0..n {
2172            let mut rho = 0.0f32;
2173            let neighbors = self.get_neighbors(positions[i]);
2174            for j in neighbors {
2175                let r = positions[i] - positions[j];
2176                let r_sq = r.length_squared();
2177                rho += masses[j] * sph_kernel_poly6(r_sq, h);
2178            }
2179            self.particles[i].density = rho.max(self.rest_density * 0.001);
2180        }
2181    }
2182
2183    /// Compute pressure from density: P = k*(rho - rho_0)
2184    pub fn compute_pressure(&mut self) {
2185        for p in &mut self.particles {
2186            p.pressure = self.pressure_stiffness * ((p.density / self.rest_density) - 1.0).max(0.0);
2187        }
2188    }
2189
2190    /// Compute pressure, viscosity, and surface tension forces
2191    pub fn compute_forces(&mut self) {
2192        let h = self.kernel_radius;
2193        let n = self.particles.len();
2194        let positions: Vec<Vec3> = self.particles.iter().map(|p| p.position).collect();
2195        let velocities: Vec<Vec3> = self.particles.iter().map(|p| p.velocity).collect();
2196        let masses: Vec<f32> = self.particles.iter().map(|p| p.mass).collect();
2197        let densities: Vec<f32> = self.particles.iter().map(|p| p.density).collect();
2198        let pressures: Vec<f32> = self.particles.iter().map(|p| p.pressure).collect();
2199        for i in 0..n {
2200            let mut f_pressure = Vec3::ZERO;
2201            let mut f_viscosity = Vec3::ZERO;
2202            let mut f_surface = Vec3::ZERO;
2203            let mut color_grad = Vec3::ZERO;
2204            let mut color_lap = 0.0f32;
2205            let neighbors = self.get_neighbors(positions[i]);
2206            for j in neighbors {
2207                if i == j { continue; }
2208                let r = positions[i] - positions[j];
2209                let r_len = r.length();
2210                let r_sq = r_len * r_len;
2211                if r_sq >= h * h { continue; }
2212                let m_j = masses[j];
2213                let rho_j = densities[j];
2214                if rho_j < EPSILON { continue; }
2215                // Pressure force: -sum_j m_j * (P_i/rho_i^2 + P_j/rho_j^2) * grad_W_spiky
2216                let p_term = pressures[i] / (densities[i] * densities[i])
2217                    + pressures[j] / (rho_j * rho_j);
2218                let grad_spiky = sph_kernel_spiky_grad(r, r_len, h);
2219                f_pressure -= m_j * p_term * grad_spiky;
2220                // Viscosity force: mu * sum_j m_j * (v_j - v_i)/rho_j * lap_W_visc
2221                let lap_visc = sph_kernel_viscosity_lap(r_len, h);
2222                f_viscosity += (velocities[j] - velocities[i]) * (m_j / rho_j * lap_visc);
2223                // Surface tension (color field)
2224                let grad_poly6 = sph_kernel_poly6_grad(r, r_sq, h);
2225                let lap_poly6 = sph_kernel_poly6_lap(r_sq, h);
2226                color_grad += grad_poly6 * (m_j / rho_j);
2227                color_lap += lap_poly6 * (m_j / rho_j);
2228            }
2229            let rho_i = densities[i];
2230            f_pressure *= rho_i;
2231            f_viscosity *= self.viscosity;
2232            let color_grad_len = color_grad.length();
2233            if color_grad_len > self.color_field_threshold {
2234                f_surface = -self.surface_tension * color_lap / color_grad_len * color_grad;
2235            }
2236            let gravity_force = self.gravity * masses[i];
2237            self.particles[i].force = f_pressure + f_viscosity + f_surface + gravity_force;
2238        }
2239    }
2240
2241    /// Integrate SPH particles using Euler
2242    pub fn integrate(&mut self, dt: f32) {
2243        let dt_scaled = dt * self.time_scale;
2244        for p in &mut self.particles {
2245            if p.density < EPSILON { continue; }
2246            let acc = p.force / p.density;
2247            p.velocity += acc * dt_scaled;
2248            // Clamp
2249            let v_len = p.velocity.length();
2250            if v_len > self.max_velocity {
2251                p.velocity *= self.max_velocity / v_len;
2252            }
2253            p.position += p.velocity * dt_scaled;
2254            // Simple domain boundary bounce
2255            let r = self.particle_radius;
2256            for dim in 0..3 {
2257                let lo = match dim { 0 => self.domain_min.x, 1 => self.domain_min.y, _ => self.domain_min.z };
2258                let hi = match dim { 0 => self.domain_max.x, 1 => self.domain_max.y, _ => self.domain_max.z };
2259                let pos_val = match dim { 0 => &mut p.position.x, 1 => &mut p.position.y, _ => &mut p.position.z };
2260                let vel_val = match dim { 0 => &mut p.velocity.x, 1 => &mut p.velocity.y, _ => &mut p.velocity.z };
2261                if *pos_val < lo + r {
2262                    *pos_val = lo + r;
2263                    *vel_val = vel_val.abs() * self.restitution;
2264                } else if *pos_val > hi - r {
2265                    *pos_val = hi - r;
2266                    *vel_val = -vel_val.abs() * self.restitution;
2267                }
2268            }
2269        }
2270    }
2271
2272    /// Full simulation step
2273    pub fn step(&mut self, dt: f32) {
2274        self.build_neighbor_grid();
2275        self.compute_density();
2276        self.compute_pressure();
2277        self.compute_forces();
2278        self.integrate(dt);
2279    }
2280}
2281
2282// ============================================================
2283// DESTRUCTION / FRACTURE SYSTEM
2284// ============================================================
2285
2286#[derive(Debug, Clone)]
2287pub struct VoronoiCell {
2288    pub seed: Vec3,
2289    pub vertices: Vec<Vec3>,
2290    pub mass: f32,
2291    pub volume: f32,
2292    pub intact: bool,
2293    pub stress: f32,
2294    pub velocity: Vec3,
2295    pub angular_velocity: Vec3,
2296}
2297
2298impl VoronoiCell {
2299    pub fn new(seed: Vec3) -> Self {
2300        Self {
2301            seed,
2302            vertices: Vec::new(),
2303            mass: 0.0,
2304            volume: 0.0,
2305            intact: true,
2306            stress: 0.0,
2307            velocity: Vec3::ZERO,
2308            angular_velocity: Vec3::ZERO,
2309        }
2310    }
2311
2312    /// Compute volume from convex hull vertices (divergence theorem approximation)
2313    pub fn compute_volume_from_vertices(&mut self) {
2314        if self.vertices.len() < 4 { self.volume = 0.0; return; }
2315        // Use centroid-based tetrahedra decomposition
2316        let centroid = self.vertices.iter().fold(Vec3::ZERO, |a, &v| a + v) / self.vertices.len() as f32;
2317        let mut vol = 0.0f32;
2318        let n = self.vertices.len();
2319        for i in 0..n {
2320            let a = self.vertices[i];
2321            let b = self.vertices[(i + 1) % n];
2322            let c = centroid;
2323            let d = Vec3::new(centroid.x, centroid.y + 0.01, centroid.z);
2324            let tet_vol = (b - a).cross(c - a).dot(d - a).abs() / 6.0;
2325            vol += tet_vol;
2326        }
2327        self.volume = vol;
2328    }
2329}
2330
2331#[derive(Debug, Clone)]
2332pub struct VoronoiFracture {
2333    pub cells: Vec<VoronoiCell>,
2334    pub bounds_min: Vec3,
2335    pub bounds_max: Vec3,
2336    pub num_cells: usize,
2337    pub material_strength: f32,     // Pa
2338    pub toughness: f32,             // J/m^2
2339    pub density: f32,
2340    pub cracks: Vec<(usize, usize)>, // cell pairs that are cracked
2341    pub debris_particles: Vec<DebrisParticle>,
2342    pub fractured: bool,
2343}
2344
2345#[derive(Debug, Clone)]
2346pub struct DebrisParticle {
2347    pub position: Vec3,
2348    pub velocity: Vec3,
2349    pub angular_velocity: Vec3,
2350    pub mass: f32,
2351    pub lifetime: f32,
2352    pub scale: f32,
2353}
2354
2355/// Simple LCG random number generator for reproducible fracture
2356pub struct FractureRng {
2357    state: u64,
2358}
2359impl FractureRng {
2360    pub fn new(seed: u64) -> Self { Self { state: seed } }
2361    pub fn next_f32(&mut self) -> f32 {
2362        self.state = self.state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
2363        let bits = ((self.state >> 33) as u32) | 0x3F80_0000;
2364        f32::from_bits(bits) - 1.0
2365    }
2366    pub fn next_range(&mut self, lo: f32, hi: f32) -> f32 {
2367        lo + self.next_f32() * (hi - lo)
2368    }
2369}
2370
2371impl VoronoiFracture {
2372    pub fn new(bounds_min: Vec3, bounds_max: Vec3, num_cells: usize, seed: u64) -> Self {
2373        let mut rng = FractureRng::new(seed);
2374        let mut cells = Vec::new();
2375        for _ in 0..num_cells {
2376            let seed_pt = Vec3::new(
2377                rng.next_range(bounds_min.x, bounds_max.x),
2378                rng.next_range(bounds_min.y, bounds_max.y),
2379                rng.next_range(bounds_min.z, bounds_max.z),
2380            );
2381            cells.push(VoronoiCell::new(seed_pt));
2382        }
2383        Self {
2384            cells, bounds_min, bounds_max, num_cells,
2385            material_strength: 1e7,
2386            toughness: 100.0,
2387            density: CONCRETE_DENSITY,
2388            cracks: Vec::new(),
2389            debris_particles: Vec::new(),
2390            fractured: false,
2391        }
2392    }
2393
2394    /// For each point, find the nearest Voronoi seed (Lloyd-style cell assignment)
2395    pub fn find_cell(&self, point: Vec3) -> usize {
2396        let mut best = 0;
2397        let mut best_dist = f32::INFINITY;
2398        for (i, cell) in self.cells.iter().enumerate() {
2399            let d = (point - cell.seed).length_squared();
2400            if d < best_dist {
2401                best_dist = d;
2402                best = i;
2403            }
2404        }
2405        best
2406    }
2407
2408    /// Accumulate stress on a cell given an impulse force
2409    pub fn accumulate_stress(&mut self, cell_idx: usize, force_magnitude: f32, contact_area: f32) {
2410        if cell_idx >= self.cells.len() { return; }
2411        let stress_increment = if contact_area > EPSILON { force_magnitude / contact_area } else { 0.0 };
2412        self.cells[cell_idx].stress += stress_increment;
2413    }
2414
2415    /// Check if any cells exceed strength threshold and initiate fracture
2416    pub fn check_fracture(&mut self) -> Vec<usize> {
2417        let mut fractured_cells = Vec::new();
2418        for (i, cell) in self.cells.iter_mut().enumerate() {
2419            if cell.intact && cell.stress > self.material_strength {
2420                cell.intact = false;
2421                fractured_cells.push(i);
2422            }
2423        }
2424        fractured_cells
2425    }
2426
2427    /// Propagate crack from a fractured cell to neighbors
2428    pub fn propagate_cracks(&mut self, initial_cells: &[usize]) {
2429        let mut queue: VecDeque<usize> = initial_cells.iter().copied().collect();
2430        let mut visited: HashSet<usize> = initial_cells.iter().copied().collect();
2431        while let Some(cell_idx) = queue.pop_front() {
2432            // Find neighbors (cells within 2x the average cell spacing)
2433            let seed = self.cells[cell_idx].seed;
2434            let avg_spacing = (self.bounds_max - self.bounds_min).length() / (self.num_cells as f32).cbrt();
2435            let crack_radius = avg_spacing * 1.5;
2436            let stress_here = self.cells[cell_idx].stress;
2437            for j in 0..self.cells.len() {
2438                if visited.contains(&j) { continue; }
2439                let dist = (self.cells[j].seed - seed).length();
2440                if dist < crack_radius {
2441                    // Stress transfer (linear fall-off with distance)
2442                    let transfer = stress_here * (1.0 - dist / crack_radius) * 0.5;
2443                    self.cells[j].stress += transfer;
2444                    self.cracks.push((cell_idx, j));
2445                    if self.cells[j].stress > self.material_strength {
2446                        self.cells[j].intact = false;
2447                        visited.insert(j);
2448                        queue.push_back(j);
2449                    }
2450                }
2451            }
2452        }
2453        self.fractured = true;
2454    }
2455
2456    /// Spawn debris from fractured cells
2457    pub fn spawn_debris(&mut self, impact_velocity: Vec3, rng: &mut FractureRng) {
2458        for cell in &self.cells {
2459            if !cell.intact {
2460                let vel = impact_velocity + Vec3::new(
2461                    rng.next_range(-2.0, 2.0),
2462                    rng.next_range(1.0, 5.0),
2463                    rng.next_range(-2.0, 2.0),
2464                );
2465                let ang_vel = Vec3::new(
2466                    rng.next_range(-10.0, 10.0),
2467                    rng.next_range(-10.0, 10.0),
2468                    rng.next_range(-10.0, 10.0),
2469                );
2470                self.debris_particles.push(DebrisParticle {
2471                    position: cell.seed,
2472                    velocity: vel,
2473                    angular_velocity: ang_vel,
2474                    mass: cell.mass.max(0.001),
2475                    lifetime: rng.next_range(2.0, 8.0),
2476                    scale: rng.next_range(0.05, 0.3),
2477                });
2478            }
2479        }
2480    }
2481
2482    /// Integrate debris particles
2483    pub fn integrate_debris(&mut self, dt: f32) {
2484        let gravity = Vec3::new(0.0, -GRAVITY, 0.0);
2485        self.debris_particles.retain_mut(|d| {
2486            d.velocity += gravity * dt;
2487            d.position += d.velocity * dt;
2488            // Simple floor bounce
2489            if d.position.y < 0.0 {
2490                d.position.y = 0.0;
2491                d.velocity.y = d.velocity.y.abs() * 0.4;
2492                d.velocity.x *= 0.8;
2493                d.velocity.z *= 0.8;
2494            }
2495            d.lifetime -= dt;
2496            d.lifetime > 0.0
2497        });
2498    }
2499
2500    /// Check if two cells are still connected (no crack between them)
2501    pub fn are_connected(&self, a: usize, b: usize) -> bool {
2502        !self.cracks.contains(&(a, b)) && !self.cracks.contains(&(b, a))
2503    }
2504}
2505
2506// ============================================================
2507// RAGDOLL EDITOR
2508// ============================================================
2509
2510#[derive(Debug, Clone)]
2511pub struct RagdollBone {
2512    pub name: String,
2513    pub body_id: u64,
2514    pub bone_index: u32,
2515    pub local_offset: Vec3,
2516    pub local_rotation: Quat,
2517    pub mass: f32,
2518    pub shape_type: RigidBodyShapeType,
2519    pub shape_params: ShapeParameters,
2520    pub muscle_tone: f32,      // 0 = limp, 1 = fully tensed
2521    pub blend_weight: f32,     // blend between ragdoll and animation pose
2522}
2523
2524#[derive(Debug, Clone)]
2525pub struct RagdollJoint {
2526    pub parent_bone: String,
2527    pub child_bone: String,
2528    pub constraint_id: u64,
2529    pub constraint_type: String,
2530    pub swing_limit: f32,
2531    pub twist_limit: f32,
2532    pub stiffness: f32,
2533    pub damping: f32,
2534}
2535
2536#[derive(Debug, Clone)]
2537pub struct RagdollEditor {
2538    pub bones: HashMap<String, RagdollBone>,
2539    pub joints: Vec<RagdollJoint>,
2540    pub blend_mode: RagdollBlendMode,
2541    pub animation_blend: f32,   // 0 = full ragdoll, 1 = full animation
2542    pub blend_time: f32,
2543    pub current_blend_timer: f32,
2544    pub active: bool,
2545    pub total_mass: f32,
2546    pub com_world: Vec3,
2547    pub next_body_id: u64,
2548}
2549
2550#[derive(Debug, Clone, PartialEq)]
2551pub enum RagdollBlendMode {
2552    FullRagdoll,
2553    FullAnimation,
2554    BlendedPhysics,
2555    KinematicDriven,
2556}
2557
2558impl RagdollEditor {
2559    pub fn new() -> Self {
2560        Self {
2561            bones: HashMap::new(),
2562            joints: Vec::new(),
2563            blend_mode: RagdollBlendMode::FullRagdoll,
2564            animation_blend: 0.0,
2565            blend_time: 0.3,
2566            current_blend_timer: 0.0,
2567            active: false,
2568            total_mass: 0.0,
2569            com_world: Vec3::ZERO,
2570            next_body_id: 1000,
2571        }
2572    }
2573
2574    pub fn build_humanoid_skeleton(&mut self) {
2575        // --- Torso/Spine ---
2576        self.add_bone("pelvis", 0, Vec3::ZERO, Quat::IDENTITY, 10.0, RigidBodyShapeType::Box,
2577            ShapeParameters { half_extents: Vec3::new(0.15, 0.1, 0.08), ..Default::default() });
2578        self.add_bone("spine", 1, Vec3::new(0.0, 0.2, 0.0), Quat::IDENTITY, 8.0, RigidBodyShapeType::Capsule,
2579            ShapeParameters { radius: 0.08, half_height: 0.1, ..Default::default() });
2580        self.add_bone("chest", 2, Vec3::new(0.0, 0.4, 0.0), Quat::IDENTITY, 12.0, RigidBodyShapeType::Box,
2581            ShapeParameters { half_extents: Vec3::new(0.18, 0.12, 0.09), ..Default::default() });
2582        self.add_bone("neck", 3, Vec3::new(0.0, 0.55, 0.0), Quat::IDENTITY, 1.5, RigidBodyShapeType::Capsule,
2583            ShapeParameters { radius: 0.04, half_height: 0.04, ..Default::default() });
2584        self.add_bone("head", 4, Vec3::new(0.0, 0.65, 0.0), Quat::IDENTITY, 5.0, RigidBodyShapeType::Sphere,
2585            ShapeParameters { radius: 0.12, ..Default::default() });
2586        // --- Left arm ---
2587        self.add_bone("l_shoulder", 5, Vec3::new(0.22, 0.5, 0.0), Quat::IDENTITY, 2.0, RigidBodyShapeType::Sphere,
2588            ShapeParameters { radius: 0.04, ..Default::default() });
2589        self.add_bone("l_upper_arm", 6, Vec3::new(0.3, 0.45, 0.0), Quat::IDENTITY, 2.5, RigidBodyShapeType::Capsule,
2590            ShapeParameters { radius: 0.04, half_height: 0.15, ..Default::default() });
2591        self.add_bone("l_forearm", 7, Vec3::new(0.45, 0.3, 0.0), Quat::IDENTITY, 1.5, RigidBodyShapeType::Capsule,
2592            ShapeParameters { radius: 0.03, half_height: 0.12, ..Default::default() });
2593        self.add_bone("l_hand", 8, Vec3::new(0.55, 0.18, 0.0), Quat::IDENTITY, 0.5, RigidBodyShapeType::Box,
2594            ShapeParameters { half_extents: Vec3::new(0.04, 0.03, 0.06), ..Default::default() });
2595        // --- Right arm ---
2596        self.add_bone("r_shoulder", 9, Vec3::new(-0.22, 0.5, 0.0), Quat::IDENTITY, 2.0, RigidBodyShapeType::Sphere,
2597            ShapeParameters { radius: 0.04, ..Default::default() });
2598        self.add_bone("r_upper_arm", 10, Vec3::new(-0.3, 0.45, 0.0), Quat::IDENTITY, 2.5, RigidBodyShapeType::Capsule,
2599            ShapeParameters { radius: 0.04, half_height: 0.15, ..Default::default() });
2600        self.add_bone("r_forearm", 11, Vec3::new(-0.45, 0.3, 0.0), Quat::IDENTITY, 1.5, RigidBodyShapeType::Capsule,
2601            ShapeParameters { radius: 0.03, half_height: 0.12, ..Default::default() });
2602        self.add_bone("r_hand", 12, Vec3::new(-0.55, 0.18, 0.0), Quat::IDENTITY, 0.5, RigidBodyShapeType::Box,
2603            ShapeParameters { half_extents: Vec3::new(0.04, 0.03, 0.06), ..Default::default() });
2604        // --- Left leg ---
2605        self.add_bone("l_thigh", 13, Vec3::new(0.1, -0.1, 0.0), Quat::IDENTITY, 7.0, RigidBodyShapeType::Capsule,
2606            ShapeParameters { radius: 0.06, half_height: 0.22, ..Default::default() });
2607        self.add_bone("l_shin", 14, Vec3::new(0.1, -0.5, 0.0), Quat::IDENTITY, 4.0, RigidBodyShapeType::Capsule,
2608            ShapeParameters { radius: 0.04, half_height: 0.2, ..Default::default() });
2609        self.add_bone("l_foot", 15, Vec3::new(0.1, -0.85, 0.04), Quat::IDENTITY, 1.0, RigidBodyShapeType::Box,
2610            ShapeParameters { half_extents: Vec3::new(0.04, 0.03, 0.1), ..Default::default() });
2611        // --- Right leg ---
2612        self.add_bone("r_thigh", 16, Vec3::new(-0.1, -0.1, 0.0), Quat::IDENTITY, 7.0, RigidBodyShapeType::Capsule,
2613            ShapeParameters { radius: 0.06, half_height: 0.22, ..Default::default() });
2614        self.add_bone("r_shin", 17, Vec3::new(-0.1, -0.5, 0.0), Quat::IDENTITY, 4.0, RigidBodyShapeType::Capsule,
2615            ShapeParameters { radius: 0.04, half_height: 0.2, ..Default::default() });
2616        self.add_bone("r_foot", 18, Vec3::new(-0.1, -0.85, 0.04), Quat::IDENTITY, 1.0, RigidBodyShapeType::Box,
2617            ShapeParameters { half_extents: Vec3::new(0.04, 0.03, 0.1), ..Default::default() });
2618        // Build joints
2619        self.build_spine_chain();
2620        self.build_arm_chain("l");
2621        self.build_arm_chain("r");
2622        self.build_leg_chain("l");
2623        self.build_leg_chain("r");
2624        self.compute_total_mass();
2625    }
2626
2627    fn add_bone(&mut self, name: &str, bone_index: u32, offset: Vec3, rot: Quat,
2628                mass: f32, shape: RigidBodyShapeType, params: ShapeParameters) {
2629        let id = self.next_body_id;
2630        self.next_body_id += 1;
2631        self.bones.insert(name.to_string(), RagdollBone {
2632            name: name.to_string(),
2633            body_id: id,
2634            bone_index,
2635            local_offset: offset,
2636            local_rotation: rot,
2637            mass,
2638            shape_type: shape,
2639            shape_params: params,
2640            muscle_tone: 0.0,
2641            blend_weight: 1.0,
2642        });
2643    }
2644
2645    fn build_spine_chain(&mut self) {
2646        self.add_joint("pelvis", "spine", 45.0_f32.to_radians(), 30.0_f32.to_radians(), 100.0, 10.0);
2647        self.add_joint("spine", "chest", 30.0_f32.to_radians(), 20.0_f32.to_radians(), 150.0, 15.0);
2648        self.add_joint("chest", "neck", 40.0_f32.to_radians(), 30.0_f32.to_radians(), 80.0, 8.0);
2649        self.add_joint("neck", "head", 50.0_f32.to_radians(), 60.0_f32.to_radians(), 60.0, 6.0);
2650    }
2651
2652    fn build_arm_chain(&mut self, side: &str) {
2653        let s = side;
2654        let shoulder = format!("{}_shoulder", s);
2655        let upper_arm = format!("{}_upper_arm", s);
2656        let forearm = format!("{}_forearm", s);
2657        let hand = format!("{}_hand", s);
2658        let chest_key = "chest";
2659        self.add_joint(chest_key, &shoulder, 60.0_f32.to_radians(), 45.0_f32.to_radians(), 120.0, 12.0);
2660        self.add_joint(&shoulder, &upper_arm, 90.0_f32.to_radians(), 60.0_f32.to_radians(), 100.0, 10.0);
2661        self.add_joint(&upper_arm, &forearm, 120.0_f32.to_radians(), 5.0_f32.to_radians(), 80.0, 8.0);
2662        self.add_joint(&forearm, &hand, 70.0_f32.to_radians(), 30.0_f32.to_radians(), 40.0, 4.0);
2663    }
2664
2665    fn build_leg_chain(&mut self, side: &str) {
2666        let s = side;
2667        let thigh = format!("{}_thigh", s);
2668        let shin = format!("{}_shin", s);
2669        let foot = format!("{}_foot", s);
2670        self.add_joint("pelvis", &thigh, 80.0_f32.to_radians(), 45.0_f32.to_radians(), 200.0, 20.0);
2671        self.add_joint(&thigh, &shin, 130.0_f32.to_radians(), 5.0_f32.to_radians(), 160.0, 16.0);
2672        self.add_joint(&shin, &foot, 50.0_f32.to_radians(), 20.0_f32.to_radians(), 60.0, 6.0);
2673    }
2674
2675    fn add_joint(&mut self, parent: &str, child: &str, swing: f32, twist: f32, stiffness: f32, damping: f32) {
2676        let id = self.joints.len() as u64 + 2000;
2677        self.joints.push(RagdollJoint {
2678            parent_bone: parent.to_string(),
2679            child_bone: child.to_string(),
2680            constraint_id: id,
2681            constraint_type: "ConeTwist".to_string(),
2682            swing_limit: swing,
2683            twist_limit: twist,
2684            stiffness,
2685            damping,
2686        });
2687    }
2688
2689    pub fn compute_total_mass(&mut self) {
2690        self.total_mass = self.bones.values().map(|b| b.mass).sum();
2691    }
2692
2693    /// Blend bone transforms between ragdoll physics and animation
2694    /// Returns blended world transform for each bone
2695    pub fn compute_blended_transforms(&self,
2696        physics_poses: &HashMap<String, (Vec3, Quat)>,
2697        animation_poses: &HashMap<String, (Vec3, Quat)>,
2698    ) -> HashMap<String, (Vec3, Quat)> {
2699        let mut result = HashMap::new();
2700        let blend = self.animation_blend;
2701        for (name, bone) in &self.bones {
2702            let phys = physics_poses.get(name).copied().unwrap_or((Vec3::ZERO, Quat::IDENTITY));
2703            let anim = animation_poses.get(name).copied().unwrap_or((Vec3::ZERO, Quat::IDENTITY));
2704            let w = blend * bone.blend_weight;
2705            let pos = phys.0.lerp(anim.0, w);
2706            let rot = phys.1.slerp(anim.1, w);
2707            result.insert(name.clone(), (pos, rot));
2708        }
2709        result
2710    }
2711
2712    /// Compute muscle spring torque for a joint given current angle
2713    pub fn muscle_torque(&self, joint: &RagdollJoint, bone_name: &str, current_angle: f32, target_angle: f32) -> f32 {
2714        let bone = match self.bones.get(bone_name) {
2715            Some(b) => b,
2716            None => return 0.0,
2717        };
2718        let tone = bone.muscle_tone;
2719        let error = target_angle - current_angle;
2720        tone * joint.stiffness * error
2721    }
2722
2723    pub fn set_blend_mode(&mut self, mode: RagdollBlendMode, blend_time: f32) {
2724        self.blend_mode = mode.clone();
2725        self.blend_time = blend_time;
2726        self.current_blend_timer = 0.0;
2727        match mode {
2728            RagdollBlendMode::FullRagdoll => self.animation_blend = 0.0,
2729            RagdollBlendMode::FullAnimation => self.animation_blend = 1.0,
2730            _ => {}
2731        }
2732    }
2733
2734    pub fn update_blend(&mut self, dt: f32) {
2735        self.current_blend_timer = (self.current_blend_timer + dt).min(self.blend_time);
2736        let t = if self.blend_time > EPSILON { self.current_blend_timer / self.blend_time } else { 1.0 };
2737        match self.blend_mode {
2738            RagdollBlendMode::BlendedPhysics => {
2739                self.animation_blend = smoothstep(0.0, 1.0, t) * 0.5;
2740            }
2741            RagdollBlendMode::FullAnimation => {
2742                self.animation_blend = smoothstep(0.0, 1.0, t);
2743            }
2744            RagdollBlendMode::FullRagdoll => {
2745                self.animation_blend = 1.0 - smoothstep(0.0, 1.0, t);
2746            }
2747            _ => {}
2748        }
2749    }
2750}
2751
2752// ============================================================
2753// VEHICLE PHYSICS
2754// ============================================================
2755
2756/// Pacejka Magic Formula tire model
2757/// F = D * sin(C * atan(B*alpha - E*(B*alpha - atan(B*alpha))))
2758pub fn pacejka_magic_formula(slip_angle_rad: f32, b: f32, c: f32, d: f32, e: f32) -> f32 {
2759    let ba = b * slip_angle_rad;
2760    let inner = ba - e * (ba - ba.atan());
2761    d * (c * inner.atan()).sin()
2762}
2763
2764/// Pacejka coefficients for typical dry asphalt
2765pub fn pacejka_dry_asphalt() -> (f32, f32, f32, f32) {
2766    // B=10, C=1.9, D=1.0, E=0.97 (lateral force coefficients)
2767    (10.0, 1.9, 1.0, 0.97)
2768}
2769
2770/// Pacejka coefficients for wet road
2771pub fn pacejka_wet_road() -> (f32, f32, f32, f32) {
2772    (7.0, 1.7, 0.7, 0.9)
2773}
2774
2775/// Pacejka coefficients for ice
2776pub fn pacejka_ice() -> (f32, f32, f32, f32) {
2777    (4.0, 1.5, 0.2, 0.8)
2778}
2779
2780/// Compute Ackermann steering angles for inner and outer wheel
2781/// L = wheelbase, W = track width, delta = steering angle (outer)
2782pub fn ackermann_steering(wheelbase: f32, track_width: f32, steer_angle: f32) -> (f32, f32) {
2783    // Outer wheel angle = steer_angle (input)
2784    // Inner wheel angle = atan(L / (L/tan(delta) - W/2))
2785    let l = wheelbase;
2786    let w = track_width;
2787    let delta = steer_angle;
2788    if delta.abs() < EPSILON {
2789        return (0.0, 0.0);
2790    }
2791    let r_outer = l / delta.tan();
2792    let r_inner = r_outer - w;
2793    let inner_angle = (l / r_inner).atan();
2794    (delta, inner_angle)
2795}
2796
2797#[derive(Debug, Clone)]
2798pub struct WheelState {
2799    pub position_local: Vec3,   // Attachment point in vehicle local space
2800    pub radius: f32,
2801    pub width: f32,
2802    pub suspension_rest_length: f32,
2803    pub suspension_max_travel: f32,
2804    pub suspension_stiffness: f32,
2805    pub suspension_damping: f32,
2806    pub current_compression: f32,
2807    pub suspension_force: f32,
2808    pub contact_point: Vec3,
2809    pub contact_normal: Vec3,
2810    pub is_grounded: bool,
2811    pub slip_angle: f32,
2812    pub slip_ratio: f32,
2813    pub lateral_force: f32,
2814    pub longitudinal_force: f32,
2815    pub steer_angle: f32,
2816    pub angular_velocity: f32,  // rad/s
2817    pub brake_torque: f32,
2818    pub drive_torque: f32,
2819    pub friction_limit: f32,
2820    pub rolling_resistance: f32,
2821    pub camber_angle: f32,
2822    pub road_surface: SurfaceType,
2823}
2824
2825impl WheelState {
2826    pub fn new(position_local: Vec3, radius: f32) -> Self {
2827        Self {
2828            position_local,
2829            radius,
2830            width: 0.2,
2831            suspension_rest_length: 0.3,
2832            suspension_max_travel: 0.15,
2833            suspension_stiffness: 25000.0,
2834            suspension_damping: 2000.0,
2835            current_compression: 0.0,
2836            suspension_force: 0.0,
2837            contact_point: Vec3::ZERO,
2838            contact_normal: Vec3::Y,
2839            is_grounded: false,
2840            slip_angle: 0.0,
2841            slip_ratio: 0.0,
2842            lateral_force: 0.0,
2843            longitudinal_force: 0.0,
2844            steer_angle: 0.0,
2845            angular_velocity: 0.0,
2846            brake_torque: 0.0,
2847            drive_torque: 0.0,
2848            friction_limit: 1.0,
2849            rolling_resistance: 0.015,
2850            camber_angle: 0.0,
2851            road_surface: SurfaceType::Asphalt,
2852        }
2853    }
2854
2855    /// Compute suspension force using spring-damper model
2856    /// F_susp = k * compression + c * compression_velocity
2857    pub fn compute_suspension_force(&mut self, compression_velocity: f32) -> f32 {
2858        let spring_force = self.suspension_stiffness * self.current_compression;
2859        let damper_force = self.suspension_damping * compression_velocity;
2860        let force = (spring_force + damper_force).max(0.0);
2861        self.suspension_force = force;
2862        force
2863    }
2864
2865    /// Compute tire forces using Pacejka model
2866    pub fn compute_tire_forces(&mut self, normal_force: f32, surface_friction: f32) {
2867        if !self.is_grounded || normal_force < EPSILON {
2868            self.lateral_force = 0.0;
2869            self.longitudinal_force = 0.0;
2870            return;
2871        }
2872        let (b, c, d, e) = match self.road_surface {
2873            SurfaceType::Asphalt => pacejka_dry_asphalt(),
2874            SurfaceType::WetAsphalt | SurfaceType::Mud => pacejka_wet_road(),
2875            SurfaceType::Ice | SurfaceType::Snow => pacejka_ice(),
2876            _ => pacejka_dry_asphalt(),
2877        };
2878        // Scale D by normal force and surface friction
2879        let d_scaled = d * normal_force * surface_friction;
2880        // Lateral force from slip angle
2881        self.lateral_force = pacejka_magic_formula(self.slip_angle, b, c, d_scaled, e);
2882        // Longitudinal force from slip ratio (using same formula)
2883        let long_slip = self.slip_ratio;
2884        self.longitudinal_force = pacejka_magic_formula(long_slip, b * 1.2, c * 1.1, d_scaled, e);
2885        // Friction circle: combine longitudinal and lateral
2886        let combined = (self.lateral_force * self.lateral_force + self.longitudinal_force * self.longitudinal_force).sqrt();
2887        let limit = d_scaled * self.friction_limit;
2888        if combined > limit && combined > EPSILON {
2889            let scale = limit / combined;
2890            self.lateral_force *= scale;
2891            self.longitudinal_force *= scale;
2892        }
2893    }
2894
2895    /// Compute slip angle from wheel velocity in local frame
2896    pub fn compute_slip_angle(&mut self, wheel_vel_local: Vec3) {
2897        let vx = wheel_vel_local.x;
2898        let vy = wheel_vel_local.z; // lateral in vehicle z
2899        if vx.abs() < 0.5 {
2900            self.slip_angle = 0.0;
2901            return;
2902        }
2903        self.slip_angle = (vy / vx.abs()).atan();
2904    }
2905
2906    /// Compute longitudinal slip ratio
2907    pub fn compute_slip_ratio(&mut self, vehicle_speed: f32) {
2908        let wheel_speed = self.angular_velocity * self.radius;
2909        let v_ref = vehicle_speed.abs().max(0.1);
2910        self.slip_ratio = (wheel_speed - vehicle_speed) / v_ref;
2911        self.slip_ratio = self.slip_ratio.clamp(-1.0, 1.0);
2912    }
2913}
2914
2915#[derive(Debug, Clone)]
2916pub struct EngineTorqueCurve {
2917    /// RPM values
2918    pub rpm_points: Vec<f32>,
2919    /// Torque values in Nm at each RPM point
2920    pub torque_points: Vec<f32>,
2921    pub idle_rpm: f32,
2922    pub max_rpm: f32,
2923    pub redline_rpm: f32,
2924    pub current_rpm: f32,
2925    pub inertia: f32,
2926}
2927
2928impl EngineTorqueCurve {
2929    pub fn petrol_sport() -> Self {
2930        // Typical sport petrol engine: peak torque ~300Nm at 4500rpm
2931        let rpm = vec![0.0, 1000.0, 2000.0, 3000.0, 4000.0, 4500.0, 5500.0, 6500.0, 7000.0, 7500.0];
2932        let torque = vec![0.0, 180.0, 250.0, 280.0, 295.0, 300.0, 290.0, 260.0, 220.0, 0.0];
2933        Self {
2934            rpm_points: rpm,
2935            torque_points: torque,
2936            idle_rpm: 800.0,
2937            max_rpm: 7500.0,
2938            redline_rpm: 7000.0,
2939            current_rpm: 800.0,
2940            inertia: 0.15,
2941        }
2942    }
2943
2944    pub fn diesel_truck() -> Self {
2945        let rpm = vec![0.0, 800.0, 1200.0, 1600.0, 2000.0, 2500.0, 3000.0, 3500.0, 4000.0];
2946        let torque = vec![0.0, 400.0, 800.0, 1000.0, 1100.0, 1100.0, 950.0, 700.0, 0.0];
2947        Self {
2948            rpm_points: rpm,
2949            torque_points: torque,
2950            idle_rpm: 600.0,
2951            max_rpm: 4000.0,
2952            redline_rpm: 3500.0,
2953            current_rpm: 600.0,
2954            inertia: 0.5,
2955        }
2956    }
2957
2958    /// Evaluate torque at given RPM using linear interpolation
2959    pub fn evaluate(&self, rpm: f32) -> f32 {
2960        if self.rpm_points.is_empty() { return 0.0; }
2961        let rpm = rpm.clamp(0.0, *self.rpm_points.last().unwrap());
2962        for i in 0..self.rpm_points.len() - 1 {
2963            if rpm >= self.rpm_points[i] && rpm <= self.rpm_points[i + 1] {
2964                let t = (rpm - self.rpm_points[i]) / (self.rpm_points[i + 1] - self.rpm_points[i] + EPSILON);
2965                return self.torque_points[i] + t * (self.torque_points[i + 1] - self.torque_points[i]);
2966            }
2967        }
2968        0.0
2969    }
2970
2971    /// Compute engine RPM from wheel angular velocity through gearbox
2972    pub fn update_rpm(&mut self, wheel_ang_vel: f32, gear_ratio: f32, final_drive: f32) {
2973        let new_rpm = (wheel_ang_vel * gear_ratio * final_drive * 60.0 / TWO_PI).abs();
2974        self.current_rpm = new_rpm.clamp(self.idle_rpm, self.max_rpm);
2975    }
2976}
2977
2978#[derive(Debug, Clone)]
2979pub struct GearBox {
2980    pub gear_ratios: Vec<f32>,   // index 0 = reverse, 1 = 1st, 2 = 2nd...
2981    pub current_gear: i32,       // -1 = reverse, 0 = neutral, 1..n = forward
2982    pub final_drive_ratio: f32,
2983    pub auto_shift: bool,
2984    pub shift_up_rpm: f32,
2985    pub shift_down_rpm: f32,
2986    pub shift_time: f32,
2987    pub shifting_timer: f32,
2988    pub efficiency: f32,
2989}
2990
2991impl GearBox {
2992    pub fn new() -> Self {
2993        Self {
2994            gear_ratios: vec![-3.5, 3.82, 2.36, 1.68, 1.31, 1.00, 0.78],
2995            current_gear: 1,
2996            final_drive_ratio: 3.73,
2997            auto_shift: false,
2998            shift_up_rpm: 6000.0,
2999            shift_down_rpm: 2500.0,
3000            shift_time: 0.2,
3001            shifting_timer: 0.0,
3002            efficiency: 0.92,
3003        }
3004    }
3005
3006    pub fn current_ratio(&self) -> f32 {
3007        if self.current_gear < 0 {
3008            self.gear_ratios[0]
3009        } else if self.current_gear == 0 {
3010            0.0
3011        } else {
3012            let idx = (self.current_gear as usize).min(self.gear_ratios.len() - 1);
3013            self.gear_ratios[idx]
3014        }
3015    }
3016
3017    pub fn output_torque(&self, engine_torque: f32) -> f32 {
3018        engine_torque * self.current_ratio() * self.final_drive_ratio * self.efficiency
3019    }
3020
3021    pub fn auto_shift_logic(&mut self, current_rpm: f32) {
3022        if !self.auto_shift || self.shifting_timer > 0.0 { return; }
3023        if current_rpm > self.shift_up_rpm && self.current_gear < (self.gear_ratios.len() as i32 - 1) {
3024            self.current_gear += 1;
3025            self.shifting_timer = self.shift_time;
3026        } else if current_rpm < self.shift_down_rpm && self.current_gear > 1 {
3027            self.current_gear -= 1;
3028            self.shifting_timer = self.shift_time;
3029        }
3030    }
3031
3032    pub fn update(&mut self, dt: f32) {
3033        if self.shifting_timer > 0.0 {
3034            self.shifting_timer = (self.shifting_timer - dt).max(0.0);
3035        }
3036    }
3037}
3038
3039#[derive(Debug, Clone, PartialEq)]
3040pub enum DifferentialType {
3041    Open,
3042    Locked,
3043    LimitedSlip { preload_torque: f32, ramp_angle_accel: f32, ramp_angle_decel: f32 },
3044    Torsen { worm_gear_ratio: f32 },
3045    Electronic { target_diff: f32, max_transfer: f32 },
3046}
3047
3048#[derive(Debug, Clone)]
3049pub struct Differential {
3050    pub diff_type: DifferentialType,
3051    pub left_torque: f32,
3052    pub right_torque: f32,
3053    pub left_rpm: f32,
3054    pub right_rpm: f32,
3055}
3056
3057impl Differential {
3058    pub fn new(diff_type: DifferentialType) -> Self {
3059        Self { diff_type, left_torque: 0.0, right_torque: 0.0, left_rpm: 0.0, right_rpm: 0.0 }
3060    }
3061
3062    /// Distribute torque between two wheels based on differential type
3063    pub fn distribute(&mut self, input_torque: f32) {
3064        match &self.diff_type {
3065            DifferentialType::Open => {
3066                // Open diff: equal torque distribution
3067                self.left_torque = input_torque * 0.5;
3068                self.right_torque = input_torque * 0.5;
3069            }
3070            DifferentialType::Locked => {
3071                // Locked: equal torque, equal speed forced by constraint
3072                self.left_torque = input_torque * 0.5;
3073                self.right_torque = input_torque * 0.5;
3074            }
3075            DifferentialType::LimitedSlip { preload_torque, ramp_angle_accel, ramp_angle_decel } => {
3076                let speed_diff = (self.left_rpm - self.right_rpm).abs();
3077                let lock_torque = *preload_torque + speed_diff * ramp_angle_accel.tan();
3078                let base = input_torque * 0.5;
3079                let transfer = lock_torque.min(base.abs());
3080                if self.left_rpm > self.right_rpm {
3081                    self.left_torque = base - transfer;
3082                    self.right_torque = base + transfer;
3083                } else {
3084                    self.left_torque = base + transfer;
3085                    self.right_torque = base - transfer;
3086                }
3087            }
3088            DifferentialType::Torsen { worm_gear_ratio } => {
3089                // Torsen: speed-sensitive, based on worm gear ratio
3090                let speed_diff = (self.left_rpm - self.right_rpm).abs();
3091                let torque_ratio = 1.0 + speed_diff * worm_gear_ratio;
3092                let t = input_torque;
3093                self.left_torque = t / (1.0 + 1.0 / torque_ratio);
3094                self.right_torque = t / (1.0 + torque_ratio);
3095            }
3096            DifferentialType::Electronic { target_diff, max_transfer } => {
3097                let speed_diff = self.left_rpm - self.right_rpm;
3098                let transfer = (speed_diff * target_diff).clamp(-*max_transfer, *max_transfer);
3099                let base = input_torque * 0.5;
3100                self.left_torque = base + transfer;
3101                self.right_torque = base - transfer;
3102            }
3103        }
3104    }
3105}
3106
3107#[derive(Debug, Clone)]
3108pub struct VehiclePhysics {
3109    pub body_id: u64,
3110    pub wheels: Vec<WheelState>,
3111    pub engine: EngineTorqueCurve,
3112    pub gearbox: GearBox,
3113    pub front_diff: Differential,
3114    pub rear_diff: Differential,
3115    pub center_diff: Option<Differential>,
3116    pub wheelbase: f32,
3117    pub track_width_front: f32,
3118    pub track_width_rear: f32,
3119    pub center_of_mass: Vec3,
3120    pub mass: f32,
3121    pub aerodynamic_drag: f32,   // Cd * A
3122    pub downforce_coefficient: f32,
3123    pub brake_bias_front: f32,
3124    pub abs_enabled: bool,
3125    pub tcs_enabled: bool,
3126    pub esc_enabled: bool,
3127    pub throttle: f32,
3128    pub brake: f32,
3129    pub steering: f32,
3130    pub handbrake: bool,
3131}
3132
3133impl VehiclePhysics {
3134    pub fn new(mass: f32, wheelbase: f32, track_width: f32) -> Self {
3135        // 4 wheels: FL, FR, RL, RR
3136        let hw = track_width * 0.5;
3137        let hl = wheelbase * 0.5;
3138        let wheels = vec![
3139            WheelState::new(Vec3::new(hw, 0.0, hl), 0.33),   // FL
3140            WheelState::new(Vec3::new(-hw, 0.0, hl), 0.33),  // FR
3141            WheelState::new(Vec3::new(hw, 0.0, -hl), 0.33),  // RL
3142            WheelState::new(Vec3::new(-hw, 0.0, -hl), 0.33), // RR
3143        ];
3144        Self {
3145            body_id: 0,
3146            wheels,
3147            engine: EngineTorqueCurve::petrol_sport(),
3148            gearbox: GearBox::new(),
3149            front_diff: Differential::new(DifferentialType::Open),
3150            rear_diff: Differential::new(DifferentialType::LimitedSlip {
3151                preload_torque: 50.0, ramp_angle_accel: 30.0_f32.to_radians(), ramp_angle_decel: 50.0_f32.to_radians()
3152            }),
3153            center_diff: None,
3154            wheelbase,
3155            track_width_front: track_width,
3156            track_width_rear: track_width,
3157            center_of_mass: Vec3::new(0.0, 0.3, 0.0),
3158            mass,
3159            aerodynamic_drag: 0.3 * 2.2, // Cd=0.3, A=2.2m^2
3160            downforce_coefficient: 0.0,
3161            brake_bias_front: 0.6,
3162            abs_enabled: true,
3163            tcs_enabled: true,
3164            esc_enabled: false,
3165            throttle: 0.0,
3166            brake: 0.0,
3167            steering: 0.0,
3168            handbrake: false,
3169        }
3170    }
3171
3172    pub fn apply_steering(&mut self) {
3173        let max_steer = 35.0_f32.to_radians();
3174        let steer_rad = self.steering * max_steer;
3175        let (delta_out, delta_in) = ackermann_steering(self.wheelbase, self.track_width_front, steer_rad);
3176        // Apply to FL and FR (indices 0, 1)
3177        if steer_rad > 0.0 {
3178            self.wheels[0].steer_angle = delta_out;
3179            self.wheels[1].steer_angle = delta_in;
3180        } else {
3181            self.wheels[0].steer_angle = delta_in;
3182            self.wheels[1].steer_angle = delta_out;
3183        }
3184    }
3185
3186    /// Compute total aerodynamic drag force given vehicle speed (m/s)
3187    pub fn aerodynamic_drag_force(&self, speed: f32) -> f32 {
3188        0.5 * AIR_DENSITY * self.aerodynamic_drag * speed * speed
3189    }
3190
3191    /// Compute downforce at given speed
3192    pub fn downforce(&self, speed: f32) -> f32 {
3193        0.5 * AIR_DENSITY * self.downforce_coefficient * speed * speed
3194    }
3195
3196    /// ABS: prevent wheel lock-up
3197    pub fn abs_update(&mut self, vehicle_speed: f32) {
3198        if !self.abs_enabled { return; }
3199        for wheel in &mut self.wheels {
3200            if !wheel.is_grounded { continue; }
3201            // If wheel is locking (slip ratio < -0.2), reduce brake torque
3202            if wheel.slip_ratio < -0.2 {
3203                wheel.brake_torque *= 0.7;
3204            }
3205        }
3206    }
3207
3208    /// TCS: prevent wheel spin
3209    pub fn tcs_update(&mut self) {
3210        if !self.tcs_enabled { return; }
3211        for wheel in &mut self.wheels {
3212            if !wheel.is_grounded { continue; }
3213            if wheel.slip_ratio > 0.2 {
3214                wheel.drive_torque *= 0.7;
3215            }
3216        }
3217    }
3218
3219    pub fn update(&mut self, dt: f32, vehicle_speed: f32) {
3220        // Engine
3221        let wheel_ang_vel = if vehicle_speed.abs() > 0.1 { vehicle_speed / self.wheels[2].radius } else { 0.0 };
3222        self.engine.update_rpm(wheel_ang_vel, self.gearbox.current_ratio().abs(), self.gearbox.final_drive_ratio);
3223        self.gearbox.auto_shift_logic(self.engine.current_rpm);
3224        self.gearbox.update(dt);
3225        let engine_torque = self.engine.evaluate(self.engine.current_rpm) * self.throttle;
3226        let output_torque = self.gearbox.output_torque(engine_torque);
3227        // Distribute to rear wheels
3228        self.rear_diff.left_rpm = self.wheels[2].angular_velocity * 60.0 / TWO_PI;
3229        self.rear_diff.right_rpm = self.wheels[3].angular_velocity * 60.0 / TWO_PI;
3230        self.rear_diff.distribute(output_torque);
3231        self.wheels[2].drive_torque = self.rear_diff.left_torque;
3232        self.wheels[3].drive_torque = self.rear_diff.right_torque;
3233        // Braking
3234        let total_brake_torque = self.brake * 5000.0;
3235        let front_brake = total_brake_torque * self.brake_bias_front;
3236        let rear_brake = total_brake_torque * (1.0 - self.brake_bias_front);
3237        self.wheels[0].brake_torque = front_brake * 0.5;
3238        self.wheels[1].brake_torque = front_brake * 0.5;
3239        let rear_brake_actual = if self.handbrake { total_brake_torque } else { rear_brake * 0.5 };
3240        self.wheels[2].brake_torque = rear_brake_actual;
3241        self.wheels[3].brake_torque = rear_brake_actual;
3242        // Steering
3243        self.apply_steering();
3244        // Tire model
3245        let wheel_load = self.mass * GRAVITY / 4.0; // Simplified equal distribution
3246        for wheel in &mut self.wheels {
3247            if wheel.is_grounded {
3248                let surf_friction = surface_friction_coefficient(wheel.road_surface.clone(), false);
3249                wheel.compute_slip_ratio(vehicle_speed);
3250                wheel.compute_tire_forces(wheel.suspension_force.max(wheel_load), surf_friction);
3251            }
3252        }
3253        self.abs_update(vehicle_speed);
3254        self.tcs_update();
3255    }
3256}
3257
3258// ============================================================
3259// COLLISION SHAPE EDITOR
3260// ============================================================
3261
3262#[derive(Debug, Clone)]
3263pub struct Aabb {
3264    pub min: Vec3,
3265    pub max: Vec3,
3266}
3267
3268impl Aabb {
3269    pub fn new(min: Vec3, max: Vec3) -> Self { Self { min, max } }
3270
3271    pub fn from_points(points: &[Vec3]) -> Self {
3272        let mut min = Vec3::splat(f32::INFINITY);
3273        let mut max = Vec3::splat(f32::NEG_INFINITY);
3274        for &p in points {
3275            min = min.min(p);
3276            max = max.max(p);
3277        }
3278        Self { min, max }
3279    }
3280
3281    pub fn center(&self) -> Vec3 { (self.min + self.max) * 0.5 }
3282    pub fn half_extents(&self) -> Vec3 { (self.max - self.min) * 0.5 }
3283    pub fn surface_area(&self) -> f32 {
3284        let e = self.max - self.min;
3285        2.0 * (e.x * e.y + e.y * e.z + e.z * e.x)
3286    }
3287    pub fn volume(&self) -> f32 {
3288        let e = self.max - self.min;
3289        (e.x * e.y * e.z).max(0.0)
3290    }
3291    pub fn contains_point(&self, p: Vec3) -> bool {
3292        p.x >= self.min.x && p.x <= self.max.x &&
3293        p.y >= self.min.y && p.y <= self.max.y &&
3294        p.z >= self.min.z && p.z <= self.max.z
3295    }
3296    pub fn intersects(&self, other: &Aabb) -> bool {
3297        self.max.x >= other.min.x && self.min.x <= other.max.x &&
3298        self.max.y >= other.min.y && self.min.y <= other.max.y &&
3299        self.max.z >= other.min.z && self.min.z <= other.max.z
3300    }
3301    pub fn expand(&mut self, point: Vec3) {
3302        self.min = self.min.min(point);
3303        self.max = self.max.max(point);
3304    }
3305    pub fn merged(&self, other: &Aabb) -> Aabb {
3306        Aabb::new(self.min.min(other.min), self.max.max(other.max))
3307    }
3308}
3309
3310#[derive(Debug, Clone)]
3311pub struct Obb {
3312    pub center: Vec3,
3313    pub axes: [Vec3; 3],   // orthonormal local axes
3314    pub half_extents: Vec3,
3315}
3316
3317impl Obb {
3318    pub fn new(center: Vec3, axes: [Vec3; 3], half_extents: Vec3) -> Self {
3319        Self { center, axes, half_extents }
3320    }
3321
3322    /// Project OBB onto axis n, return [min, max] interval
3323    pub fn project(&self, n: Vec3) -> (f32, f32) {
3324        let c = self.center.dot(n);
3325        let r = self.half_extents.x * self.axes[0].dot(n).abs()
3326            + self.half_extents.y * self.axes[1].dot(n).abs()
3327            + self.half_extents.z * self.axes[2].dot(n).abs();
3328        (c - r, c + r)
3329    }
3330
3331    /// SAT-based OBB vs OBB overlap test
3332    pub fn overlaps(&self, other: &Obb) -> bool {
3333        let test_axes: Vec<Vec3> = {
3334            let mut v = Vec::new();
3335            for &a in &self.axes { v.push(a); }
3336            for &b in &other.axes { v.push(b); }
3337            for &a in &self.axes {
3338                for &b in &other.axes {
3339                    let cross = a.cross(b);
3340                    if cross.length_squared() > EPSILON * EPSILON {
3341                        v.push(cross.normalize());
3342                    }
3343                }
3344            }
3345            v
3346        };
3347        for axis in test_axes {
3348            let (a_min, a_max) = self.project(axis);
3349            let (b_min, b_max) = other.project(axis);
3350            if a_max < b_min || b_max < a_min { return false; }
3351        }
3352        true
3353    }
3354
3355    pub fn to_aabb(&self) -> Aabb {
3356        let mut corners = Vec::new();
3357        for sx in [-1.0, 1.0] {
3358            for sy in [-1.0, 1.0] {
3359                for sz in [-1.0, 1.0] {
3360                    let pt = self.center
3361                        + self.axes[0] * (self.half_extents.x * sx)
3362                        + self.axes[1] * (self.half_extents.y * sy)
3363                        + self.axes[2] * (self.half_extents.z * sz);
3364                    corners.push(pt);
3365                }
3366            }
3367        }
3368        Aabb::from_points(&corners)
3369    }
3370}
3371
3372/// Fit an AABB to a set of triangles
3373pub fn fit_aabb_to_triangles(triangles: &[(Vec3, Vec3, Vec3)]) -> Aabb {
3374    let mut min = Vec3::splat(f32::INFINITY);
3375    let mut max = Vec3::splat(f32::NEG_INFINITY);
3376    for &(a, b, c) in triangles {
3377        min = min.min(a).min(b).min(c);
3378        max = max.max(a).max(b).max(c);
3379    }
3380    Aabb::new(min, max)
3381}
3382
3383/// Fit an OBB to a point cloud using PCA approximation
3384/// Finds principal axes from covariance matrix (3x3 symmetric Jacobi)
3385pub fn fit_obb_pca(points: &[Vec3]) -> Obb {
3386    if points.is_empty() { return Obb::new(Vec3::ZERO, [Vec3::X, Vec3::Y, Vec3::Z], Vec3::splat(0.5)); }
3387    let n = points.len() as f32;
3388    let mean = points.iter().fold(Vec3::ZERO, |a, &b| a + b) / n;
3389    // Build covariance matrix
3390    let mut cov = [[0.0f32; 3]; 3];
3391    for &p in points {
3392        let d = p - mean;
3393        let dv = [d.x, d.y, d.z];
3394        for i in 0..3 {
3395            for j in 0..3 {
3396                cov[i][j] += dv[i] * dv[j];
3397            }
3398        }
3399    }
3400    for i in 0..3 { for j in 0..3 { cov[i][j] /= n; } }
3401    // Jacobi eigendecomposition (symmetric 3x3)
3402    let (eigenvalues, eigenvectors) = jacobi_eigen_3x3(cov);
3403    // Sort by eigenvalue descending
3404    let mut order = [0usize, 1, 2];
3405    order.sort_by(|&a, &b| eigenvalues[b].partial_cmp(&eigenvalues[a]).unwrap_or(std::cmp::Ordering::Equal));
3406    let ax0 = eigenvectors[order[0]];
3407    let ax1 = eigenvectors[order[1]];
3408    let ax2 = ax0.cross(ax1).normalize(); // ensure right-handed
3409    // Project points onto axes to find extents
3410    let mut mins = [f32::INFINITY; 3];
3411    let mut maxs = [f32::NEG_INFINITY; 3];
3412    let axes_arr = [ax0, ax1, ax2];
3413    for &p in points {
3414        for k in 0..3 {
3415            let proj = (p - mean).dot(axes_arr[k]);
3416            if proj < mins[k] { mins[k] = proj; }
3417            if proj > maxs[k] { maxs[k] = proj; }
3418        }
3419    }
3420    let he = Vec3::new(
3421        (maxs[0] - mins[0]) * 0.5,
3422        (maxs[1] - mins[1]) * 0.5,
3423        (maxs[2] - mins[2]) * 0.5,
3424    );
3425    let center = mean + ax0 * (mins[0] + maxs[0]) * 0.5
3426        + ax1 * (mins[1] + maxs[1]) * 0.5
3427        + ax2 * (mins[2] + maxs[2]) * 0.5;
3428    Obb::new(center, [ax0, ax1, ax2], he)
3429}
3430
3431/// Simple Jacobi eigendecomposition for symmetric 3x3 matrix
3432/// Returns (eigenvalues, eigenvectors as Vec3)
3433pub fn jacobi_eigen_3x3(mut a: [[f32; 3]; 3]) -> ([f32; 3], [Vec3; 3]) {
3434    let mut v = [[0.0f32; 3]; 3];
3435    for i in 0..3 { v[i][i] = 1.0; }
3436    for _ in 0..50 {
3437        // Find largest off-diagonal element
3438        let mut max_val = 0.0f32;
3439        let (mut p, mut q) = (0, 1);
3440        for i in 0..3 {
3441            for j in i + 1..3 {
3442                if a[i][j].abs() > max_val {
3443                    max_val = a[i][j].abs();
3444                    p = i; q = j;
3445                }
3446            }
3447        }
3448        if max_val < 1e-7 { break; }
3449        // Compute rotation angle
3450        let theta = if (a[q][q] - a[p][p]).abs() < EPSILON {
3451            HALF_PI * 0.25
3452        } else {
3453            0.5 * (2.0 * a[p][q] / (a[q][q] - a[p][p])).atan()
3454        };
3455        let c = theta.cos();
3456        let s = theta.sin();
3457        // Apply Jacobi rotation
3458        let app = a[p][p]; let aqq = a[q][q]; let apq = a[p][q];
3459        a[p][p] = c * c * app - 2.0 * s * c * apq + s * s * aqq;
3460        a[q][q] = s * s * app + 2.0 * s * c * apq + c * c * aqq;
3461        a[p][q] = 0.0; a[q][p] = 0.0;
3462        for k in 0..3 {
3463            if k != p && k != q {
3464                let aip = a[k][p]; let aiq = a[k][q];
3465                a[k][p] = c * aip - s * aiq;
3466                a[p][k] = a[k][p];
3467                a[k][q] = s * aip + c * aiq;
3468                a[q][k] = a[k][q];
3469            }
3470        }
3471        for k in 0..3 {
3472            let vkp = v[k][p]; let vkq = v[k][q];
3473            v[k][p] = c * vkp - s * vkq;
3474            v[k][q] = s * vkp + c * vkq;
3475        }
3476    }
3477    let eigenvalues = [a[0][0], a[1][1], a[2][2]];
3478    let ev0 = Vec3::new(v[0][0], v[1][0], v[2][0]).normalize();
3479    let ev1 = Vec3::new(v[0][1], v[1][1], v[2][1]).normalize();
3480    let ev2 = Vec3::new(v[0][2], v[1][2], v[2][2]).normalize();
3481    (eigenvalues, [ev0, ev1, ev2])
3482}
3483
3484/// Compute convex hull (gift wrapping / Jarvis march) in 2D projection
3485/// Then extrude in 3D for approximate convex hull vertices
3486pub fn compute_convex_hull_2d(points: &[Vec2]) -> Vec<Vec2> {
3487    if points.len() < 3 { return points.to_vec(); }
3488    let mut hull = Vec::new();
3489    // Find leftmost point
3490    let start = points.iter().enumerate().min_by(|(_, a), (_, b)| {
3491        a.x.partial_cmp(&b.x).unwrap_or(std::cmp::Ordering::Equal)
3492    }).map(|(i, _)| i).unwrap_or(0);
3493    let mut current = start;
3494    loop {
3495        hull.push(points[current]);
3496        let mut next = (current + 1) % points.len();
3497        for i in 0..points.len() {
3498            let cross = cross2d(points[next] - points[current], points[i] - points[current]);
3499            if cross < 0.0 { next = i; }
3500        }
3501        current = next;
3502        if current == start { break; }
3503        if hull.len() > points.len() { break; } // safety
3504    }
3505    hull
3506}
3507
3508fn cross2d(a: Vec2, b: Vec2) -> f32 { a.x * b.y - a.y * b.x }
3509
3510#[derive(Debug, Clone)]
3511pub struct HeightfieldShape {
3512    pub width: usize,
3513    pub depth: usize,
3514    pub heights: Vec<f32>,
3515    pub scale: Vec3,
3516    pub min_height: f32,
3517    pub max_height: f32,
3518    pub up_axis: u8, // 0=X, 1=Y, 2=Z
3519}
3520
3521impl HeightfieldShape {
3522    pub fn new(width: usize, depth: usize, scale: Vec3) -> Self {
3523        let heights = vec![0.0; width * depth];
3524        Self {
3525            width, depth, heights,
3526            scale,
3527            min_height: 0.0, max_height: 0.0,
3528            up_axis: 1,
3529        }
3530    }
3531
3532    pub fn set_height(&mut self, x: usize, z: usize, h: f32) {
3533        if x < self.width && z < self.depth {
3534            self.heights[z * self.width + x] = h;
3535            if h < self.min_height { self.min_height = h; }
3536            if h > self.max_height { self.max_height = h; }
3537        }
3538    }
3539
3540    pub fn get_height(&self, x: usize, z: usize) -> f32 {
3541        if x < self.width && z < self.depth {
3542            self.heights[z * self.width + x]
3543        } else {
3544            0.0
3545        }
3546    }
3547
3548    pub fn height_at_world(&self, world_x: f32, world_z: f32) -> f32 {
3549        let lx = world_x / self.scale.x;
3550        let lz = world_z / self.scale.z;
3551        let ix = lx as usize;
3552        let iz = lz as usize;
3553        if ix + 1 >= self.width || iz + 1 >= self.depth { return 0.0; }
3554        let tx = lx - ix as f32;
3555        let tz = lz - iz as f32;
3556        let h00 = self.get_height(ix, iz);
3557        let h10 = self.get_height(ix + 1, iz);
3558        let h01 = self.get_height(ix, iz + 1);
3559        let h11 = self.get_height(ix + 1, iz + 1);
3560        // Bilinear interpolation
3561        let h0 = h00 * (1.0 - tx) + h10 * tx;
3562        let h1 = h01 * (1.0 - tx) + h11 * tx;
3563        (h0 * (1.0 - tz) + h1 * tz) * self.scale.y
3564    }
3565
3566    pub fn compute_aabb(&self) -> Aabb {
3567        Aabb::new(
3568            Vec3::ZERO,
3569            Vec3::new(
3570                (self.width - 1) as f32 * self.scale.x,
3571                self.max_height * self.scale.y,
3572                (self.depth - 1) as f32 * self.scale.z,
3573            ),
3574        )
3575    }
3576}
3577
3578#[derive(Debug, Clone)]
3579pub struct CollisionShapeEditor {
3580    pub shape_type: CollisionShapeType,
3581    pub box_half_extents: Vec3,
3582    pub sphere_radius: f32,
3583    pub capsule_radius: f32,
3584    pub capsule_half_height: f32,
3585    pub cylinder_radius: f32,
3586    pub cylinder_half_height: f32,
3587    pub convex_hull_points: Vec<Vec3>,
3588    pub triangle_mesh: Vec<(Vec3, Vec3, Vec3)>,
3589    pub heightfield: Option<HeightfieldShape>,
3590    pub aabb: Aabb,
3591    pub obb: Option<Obb>,
3592    pub mass_properties_dirty: bool,
3593    pub volume: f32,
3594    pub surface_area: f32,
3595    pub margin: f32,        // collision margin (GJK/EPA)
3596    pub local_transform: Mat4,
3597}
3598
3599#[derive(Debug, Clone, PartialEq)]
3600pub enum CollisionShapeType {
3601    Box,
3602    Sphere,
3603    Capsule,
3604    Cylinder,
3605    ConvexHull,
3606    TriangleMesh,
3607    Heightfield,
3608    Compound,
3609}
3610
3611impl CollisionShapeEditor {
3612    pub fn new() -> Self {
3613        Self {
3614            shape_type: CollisionShapeType::Box,
3615            box_half_extents: Vec3::splat(0.5),
3616            sphere_radius: 0.5,
3617            capsule_radius: 0.25,
3618            capsule_half_height: 0.5,
3619            cylinder_radius: 0.25,
3620            cylinder_half_height: 0.5,
3621            convex_hull_points: Vec::new(),
3622            triangle_mesh: Vec::new(),
3623            heightfield: None,
3624            aabb: Aabb::new(Vec3::splat(-0.5), Vec3::splat(0.5)),
3625            obb: None,
3626            mass_properties_dirty: true,
3627            volume: 0.0,
3628            surface_area: 0.0,
3629            margin: 0.01,
3630            local_transform: Mat4::IDENTITY,
3631        }
3632    }
3633
3634    pub fn compute_aabb(&mut self) {
3635        self.aabb = match self.shape_type {
3636            CollisionShapeType::Box => {
3637                let he = self.box_half_extents;
3638                Aabb::new(-he, he)
3639            }
3640            CollisionShapeType::Sphere => {
3641                let r = self.sphere_radius;
3642                Aabb::new(Vec3::splat(-r), Vec3::splat(r))
3643            }
3644            CollisionShapeType::Capsule => {
3645                let r = self.capsule_radius;
3646                let hh = self.capsule_half_height;
3647                Aabb::new(Vec3::new(-r, -(hh + r), -r), Vec3::new(r, hh + r, r))
3648            }
3649            CollisionShapeType::Cylinder => {
3650                let r = self.cylinder_radius;
3651                let hh = self.cylinder_half_height;
3652                Aabb::new(Vec3::new(-r, -hh, -r), Vec3::new(r, hh, r))
3653            }
3654            CollisionShapeType::ConvexHull | CollisionShapeType::TriangleMesh => {
3655                Aabb::from_points(&self.convex_hull_points)
3656            }
3657            CollisionShapeType::Heightfield => {
3658                if let Some(ref hf) = self.heightfield {
3659                    hf.compute_aabb()
3660                } else {
3661                    Aabb::new(Vec3::ZERO, Vec3::ZERO)
3662                }
3663            }
3664            CollisionShapeType::Compound => self.aabb.clone(),
3665        };
3666    }
3667
3668    pub fn compute_volume(&mut self) {
3669        self.volume = match self.shape_type {
3670            CollisionShapeType::Box => {
3671                let e = self.box_half_extents * 2.0;
3672                e.x * e.y * e.z
3673            }
3674            CollisionShapeType::Sphere => {
3675                (4.0 / 3.0) * PI * self.sphere_radius.powi(3)
3676            }
3677            CollisionShapeType::Capsule => {
3678                let r = self.capsule_radius;
3679                let h = self.capsule_half_height * 2.0;
3680                PI * r * r * h + (4.0 / 3.0) * PI * r * r * r
3681            }
3682            CollisionShapeType::Cylinder => {
3683                let r = self.cylinder_radius;
3684                let h = self.cylinder_half_height * 2.0;
3685                PI * r * r * h
3686            }
3687            CollisionShapeType::ConvexHull => {
3688                // Approximate volume from AABB (convex hull is always <= AABB volume)
3689                self.aabb.volume() * 0.7
3690            }
3691            _ => self.aabb.volume(),
3692        };
3693    }
3694
3695    pub fn compute_surface_area(&mut self) {
3696        self.surface_area = match self.shape_type {
3697            CollisionShapeType::Box => {
3698                let e = self.box_half_extents * 2.0;
3699                2.0 * (e.x * e.y + e.y * e.z + e.z * e.x)
3700            }
3701            CollisionShapeType::Sphere => {
3702                4.0 * PI * self.sphere_radius * self.sphere_radius
3703            }
3704            CollisionShapeType::Capsule => {
3705                let r = self.capsule_radius;
3706                let h = self.capsule_half_height * 2.0;
3707                TWO_PI * r * (h + 2.0 * r)
3708            }
3709            CollisionShapeType::Cylinder => {
3710                let r = self.cylinder_radius;
3711                let h = self.cylinder_half_height * 2.0;
3712                TWO_PI * r * (h + r)
3713            }
3714            _ => self.aabb.surface_area(),
3715        };
3716    }
3717
3718    pub fn fit_to_point_cloud(&mut self, points: &[Vec3]) {
3719        self.convex_hull_points = points.to_vec();
3720        self.obb = Some(fit_obb_pca(points));
3721        self.compute_aabb();
3722    }
3723
3724    pub fn generate_box_wireframe(&self) -> Vec<(Vec3, Vec3)> {
3725        let he = match self.shape_type {
3726            CollisionShapeType::Box => self.box_half_extents,
3727            _ => self.aabb.half_extents(),
3728        };
3729        let c = Vec3::ZERO;
3730        let corners = [
3731            Vec3::new(-he.x, -he.y, -he.z), Vec3::new(he.x, -he.y, -he.z),
3732            Vec3::new(he.x, he.y, -he.z),   Vec3::new(-he.x, he.y, -he.z),
3733            Vec3::new(-he.x, -he.y, he.z),  Vec3::new(he.x, -he.y, he.z),
3734            Vec3::new(he.x, he.y, he.z),    Vec3::new(-he.x, he.y, he.z),
3735        ];
3736        let edges = [
3737            (0,1),(1,2),(2,3),(3,0), // bottom
3738            (4,5),(5,6),(6,7),(7,4), // top
3739            (0,4),(1,5),(2,6),(3,7), // verticals
3740        ];
3741        edges.iter().map(|&(a, b)| (corners[a], corners[b])).collect()
3742    }
3743
3744    pub fn generate_sphere_wireframe(&self, segments: usize) -> Vec<(Vec3, Vec3)> {
3745        let r = self.sphere_radius;
3746        let mut lines = Vec::new();
3747        for plane in 0..3 {
3748            let mut prev = Vec3::ZERO;
3749            for i in 0..=segments {
3750                let t = TWO_PI * i as f32 / segments as f32;
3751                let c = t.cos() * r;
3752                let s = t.sin() * r;
3753                let pt = match plane {
3754                    0 => Vec3::new(c, s, 0.0),
3755                    1 => Vec3::new(c, 0.0, s),
3756                    _ => Vec3::new(0.0, c, s),
3757                };
3758                if i > 0 { lines.push((prev, pt)); }
3759                prev = pt;
3760            }
3761        }
3762        lines
3763    }
3764}
3765
3766// ============================================================
3767// PHYSICS MATERIAL LIBRARY
3768// ============================================================
3769
3770#[derive(Debug, Clone, PartialEq, Eq, Hash)]
3771pub enum SurfaceType {
3772    Asphalt,
3773    WetAsphalt,
3774    Concrete,
3775    WetConcrete,
3776    Gravel,
3777    Dirt,
3778    Mud,
3779    Sand,
3780    Snow,
3781    Ice,
3782    Grass,
3783    WetGrass,
3784    Rubber,
3785    Metal,
3786    WetMetal,
3787    Wood,
3788    WetWood,
3789    Stone,
3790    WetStone,
3791    Carpet,
3792    Fabric,
3793    Leather,
3794    Glass,
3795    WetGlass,
3796    Ceramic,
3797    Marble,
3798    Granite,
3799    Foam,
3800    Cork,
3801    Plastic,
3802    HardPlastic,
3803    SoftPlastic,
3804    ToughRubber,
3805    Silicon,
3806    Wax,
3807    Oil,
3808    WaterSurface,
3809    DeepWater,
3810    MudThick,
3811    Clay,
3812    Chalk,
3813    Coal,
3814    Sandstone,
3815    Limestone,
3816    Brick,
3817    WetBrick,
3818    TarmacRoad,
3819    DirtRoad,
3820    SnowRoad,
3821    Forest,
3822    RockFace,
3823    Custom,
3824}
3825
3826#[derive(Debug, Clone)]
3827pub struct PhysicsMaterial {
3828    pub name: String,
3829    pub surface_type: SurfaceType,
3830    pub static_friction: f32,
3831    pub dynamic_friction: f32,
3832    pub rolling_friction: f32,
3833    pub restitution: f32,        // coefficient of restitution (bounciness)
3834    pub density: f32,            // kg/m^3
3835    pub young_modulus: f32,      // Pa
3836    pub poisson_ratio: f32,
3837    pub hardness: f32,           // Vickers hardness (HV)
3838    pub thermal_conductivity: f32, // W/(m·K)
3839    pub sound_damping: f32,      // audio parameter
3840}
3841
3842impl PhysicsMaterial {
3843    pub fn new(name: &str, surface_type: SurfaceType,
3844               sf: f32, df: f32, rf: f32, rest: f32, density: f32) -> Self {
3845        Self {
3846            name: name.to_string(),
3847            surface_type,
3848            static_friction: sf,
3849            dynamic_friction: df,
3850            rolling_friction: rf,
3851            restitution: rest,
3852            density,
3853            young_modulus: 1e9,
3854            poisson_ratio: 0.3,
3855            hardness: 100.0,
3856            thermal_conductivity: 1.0,
3857            sound_damping: 0.5,
3858        }
3859    }
3860}
3861
3862pub fn surface_friction_coefficient(surface: SurfaceType, wet: bool) -> f32 {
3863    match (surface, wet) {
3864        (SurfaceType::Asphalt, false) => 0.8,
3865        (SurfaceType::Asphalt, true) | (SurfaceType::WetAsphalt, _) => 0.5,
3866        (SurfaceType::Concrete, false) => 0.75,
3867        (SurfaceType::Concrete, true) | (SurfaceType::WetConcrete, _) => 0.45,
3868        (SurfaceType::Gravel, _) => 0.6,
3869        (SurfaceType::Dirt, false) => 0.55,
3870        (SurfaceType::Dirt, true) => 0.4,
3871        (SurfaceType::Mud, _) | (SurfaceType::MudThick, _) => 0.3,
3872        (SurfaceType::Sand, _) => 0.45,
3873        (SurfaceType::Snow, _) => 0.2,
3874        (SurfaceType::Ice, _) => 0.05,
3875        (SurfaceType::Grass, false) => 0.5,
3876        (SurfaceType::Grass, true) | (SurfaceType::WetGrass, _) => 0.35,
3877        (SurfaceType::Rubber, _) | (SurfaceType::ToughRubber, _) => 0.9,
3878        (SurfaceType::Metal, false) => 0.4,
3879        (SurfaceType::Metal, true) | (SurfaceType::WetMetal, _) => 0.2,
3880        (SurfaceType::Wood, false) => 0.4,
3881        (SurfaceType::Wood, true) | (SurfaceType::WetWood, _) => 0.25,
3882        (SurfaceType::Stone, false) | (SurfaceType::Granite, _) | (SurfaceType::Marble, _) => 0.6,
3883        (SurfaceType::Stone, true) | (SurfaceType::WetStone, _) => 0.35,
3884        (SurfaceType::Carpet, _) => 0.7,
3885        (SurfaceType::Fabric, _) => 0.6,
3886        (SurfaceType::Leather, _) => 0.5,
3887        (SurfaceType::Glass, false) => 0.4,
3888        (SurfaceType::Glass, true) | (SurfaceType::WetGlass, _) => 0.1,
3889        (SurfaceType::Ceramic, _) => 0.55,
3890        (SurfaceType::Foam, _) => 0.55,
3891        (SurfaceType::Cork, _) => 0.7,
3892        (SurfaceType::Plastic, _) | (SurfaceType::SoftPlastic, _) => 0.35,
3893        (SurfaceType::HardPlastic, _) => 0.3,
3894        (SurfaceType::Silicon, _) => 0.8,
3895        (SurfaceType::Wax, _) => 0.05,
3896        (SurfaceType::Oil, _) => 0.03,
3897        (SurfaceType::WaterSurface, _) | (SurfaceType::DeepWater, _) => 0.01,
3898        (SurfaceType::Clay, _) => 0.45,
3899        (SurfaceType::Chalk, _) => 0.65,
3900        (SurfaceType::Coal, _) => 0.4,
3901        (SurfaceType::Sandstone, _) => 0.55,
3902        (SurfaceType::Limestone, _) => 0.6,
3903        (SurfaceType::Brick, false) => 0.65,
3904        (SurfaceType::Brick, true) | (SurfaceType::WetBrick, _) => 0.4,
3905        (SurfaceType::TarmacRoad, _) => 0.75,
3906        (SurfaceType::DirtRoad, _) => 0.5,
3907        (SurfaceType::SnowRoad, _) => 0.2,
3908        (SurfaceType::Forest, _) => 0.5,
3909        (SurfaceType::RockFace, _) => 0.65,
3910        _ => DEFAULT_FRICTION,
3911    }
3912}
3913
3914pub fn build_material_library() -> HashMap<String, PhysicsMaterial> {
3915    let mut lib = HashMap::new();
3916    let add = |lib: &mut HashMap<String, PhysicsMaterial>, m: PhysicsMaterial| {
3917        lib.insert(m.name.clone(), m);
3918    };
3919    // Static, dynamic, rolling, restitution, density
3920    add(&mut lib, PhysicsMaterial::new("Asphalt", SurfaceType::Asphalt, 0.9, 0.8, 0.02, 0.1, 2300.0));
3921    add(&mut lib, PhysicsMaterial::new("Wet Asphalt", SurfaceType::WetAsphalt, 0.55, 0.5, 0.03, 0.1, 2300.0));
3922    add(&mut lib, PhysicsMaterial::new("Concrete", SurfaceType::Concrete, 0.8, 0.75, 0.02, 0.15, CONCRETE_DENSITY));
3923    add(&mut lib, PhysicsMaterial::new("Wet Concrete", SurfaceType::WetConcrete, 0.5, 0.45, 0.025, 0.15, CONCRETE_DENSITY));
3924    add(&mut lib, PhysicsMaterial::new("Gravel", SurfaceType::Gravel, 0.65, 0.6, 0.04, 0.2, 1650.0));
3925    add(&mut lib, PhysicsMaterial::new("Dirt", SurfaceType::Dirt, 0.6, 0.55, 0.04, 0.2, 1600.0));
3926    add(&mut lib, PhysicsMaterial::new("Mud", SurfaceType::Mud, 0.35, 0.3, 0.05, 0.05, 1800.0));
3927    add(&mut lib, PhysicsMaterial::new("Sand", SurfaceType::Sand, 0.5, 0.45, 0.06, 0.1, SAND_DENSITY));
3928    add(&mut lib, PhysicsMaterial::new("Snow", SurfaceType::Snow, 0.25, 0.2, 0.04, 0.05, 200.0));
3929    add(&mut lib, PhysicsMaterial::new("Ice", SurfaceType::Ice, 0.07, 0.05, 0.01, 0.02, ICE_DENSITY));
3930    add(&mut lib, PhysicsMaterial::new("Grass", SurfaceType::Grass, 0.55, 0.5, 0.04, 0.2, 1200.0));
3931    add(&mut lib, PhysicsMaterial::new("Wet Grass", SurfaceType::WetGrass, 0.4, 0.35, 0.05, 0.1, 1200.0));
3932    add(&mut lib, PhysicsMaterial::new("Rubber", SurfaceType::Rubber, 1.0, 0.9, 0.015, 0.8, RUBBER_DENSITY));
3933    add(&mut lib, PhysicsMaterial::new("Metal", SurfaceType::Metal, 0.45, 0.4, 0.01, 0.3, STEEL_DENSITY));
3934    add(&mut lib, PhysicsMaterial::new("Wet Metal", SurfaceType::WetMetal, 0.25, 0.2, 0.01, 0.3, STEEL_DENSITY));
3935    add(&mut lib, PhysicsMaterial::new("Steel", SurfaceType::Metal, 0.5, 0.45, 0.01, 0.35, STEEL_DENSITY));
3936    add(&mut lib, PhysicsMaterial::new("Aluminum", SurfaceType::Metal, 0.42, 0.38, 0.01, 0.3, ALUMINUM_DENSITY));
3937    add(&mut lib, PhysicsMaterial::new("Copper", SurfaceType::Metal, 0.48, 0.44, 0.01, 0.3, COPPER_DENSITY));
3938    add(&mut lib, PhysicsMaterial::new("Gold", SurfaceType::Metal, 0.35, 0.3, 0.01, 0.3, GOLD_DENSITY));
3939    add(&mut lib, PhysicsMaterial::new("Wood", SurfaceType::Wood, 0.45, 0.4, 0.02, 0.35, WOOD_DENSITY));
3940    add(&mut lib, PhysicsMaterial::new("Wet Wood", SurfaceType::WetWood, 0.3, 0.25, 0.025, 0.35, WOOD_DENSITY));
3941    add(&mut lib, PhysicsMaterial::new("Stone", SurfaceType::Stone, 0.65, 0.6, 0.015, 0.2, 2600.0));
3942    add(&mut lib, PhysicsMaterial::new("Granite", SurfaceType::Granite, 0.68, 0.62, 0.015, 0.18, 2700.0));
3943    add(&mut lib, PhysicsMaterial::new("Marble", SurfaceType::Marble, 0.55, 0.5, 0.015, 0.25, 2700.0));
3944    add(&mut lib, PhysicsMaterial::new("Glass", SurfaceType::Glass, 0.45, 0.4, 0.01, 0.65, GLASS_DENSITY));
3945    add(&mut lib, PhysicsMaterial::new("Wet Glass", SurfaceType::WetGlass, 0.15, 0.1, 0.005, 0.65, GLASS_DENSITY));
3946    add(&mut lib, PhysicsMaterial::new("Ceramic", SurfaceType::Ceramic, 0.6, 0.55, 0.015, 0.4, 2400.0));
3947    add(&mut lib, PhysicsMaterial::new("Carpet", SurfaceType::Carpet, 0.75, 0.7, 0.03, 0.05, 300.0));
3948    add(&mut lib, PhysicsMaterial::new("Fabric", SurfaceType::Fabric, 0.65, 0.6, 0.03, 0.1, 200.0));
3949    add(&mut lib, PhysicsMaterial::new("Leather", SurfaceType::Leather, 0.55, 0.5, 0.02, 0.3, 900.0));
3950    add(&mut lib, PhysicsMaterial::new("Foam", SurfaceType::Foam, 0.6, 0.55, 0.03, 0.05, 30.0));
3951    add(&mut lib, PhysicsMaterial::new("Cork", SurfaceType::Cork, 0.75, 0.7, 0.03, 0.5, 200.0));
3952    add(&mut lib, PhysicsMaterial::new("Plastic", SurfaceType::Plastic, 0.4, 0.35, 0.01, 0.4, 950.0));
3953    add(&mut lib, PhysicsMaterial::new("Hard Plastic", SurfaceType::HardPlastic, 0.35, 0.3, 0.01, 0.45, 1100.0));
3954    add(&mut lib, PhysicsMaterial::new("Soft Plastic", SurfaceType::SoftPlastic, 0.55, 0.5, 0.015, 0.2, 900.0));
3955    add(&mut lib, PhysicsMaterial::new("Silicone", SurfaceType::Silicon, 0.85, 0.8, 0.015, 0.5, 1100.0));
3956    add(&mut lib, PhysicsMaterial::new("Wax", SurfaceType::Wax, 0.07, 0.05, 0.005, 0.15, 900.0));
3957    add(&mut lib, PhysicsMaterial::new("Brick", SurfaceType::Brick, 0.7, 0.65, 0.02, 0.15, 1800.0));
3958    add(&mut lib, PhysicsMaterial::new("Wet Brick", SurfaceType::WetBrick, 0.45, 0.4, 0.025, 0.1, 1800.0));
3959    add(&mut lib, PhysicsMaterial::new("Clay", SurfaceType::Clay, 0.5, 0.45, 0.04, 0.1, 1500.0));
3960    add(&mut lib, PhysicsMaterial::new("Chalk", SurfaceType::Chalk, 0.7, 0.65, 0.02, 0.1, 2000.0));
3961    add(&mut lib, PhysicsMaterial::new("Coal", SurfaceType::Coal, 0.45, 0.4, 0.02, 0.2, 1400.0));
3962    add(&mut lib, PhysicsMaterial::new("Sandstone", SurfaceType::Sandstone, 0.6, 0.55, 0.02, 0.15, 2200.0));
3963    add(&mut lib, PhysicsMaterial::new("Limestone", SurfaceType::Limestone, 0.65, 0.6, 0.015, 0.15, 2300.0));
3964    add(&mut lib, PhysicsMaterial::new("Water Surface", SurfaceType::WaterSurface, 0.02, 0.01, 0.001, 0.0, WATER_DENSITY));
3965    add(&mut lib, PhysicsMaterial::new("Thick Mud", SurfaceType::MudThick, 0.4, 0.35, 0.06, 0.02, 2000.0));
3966    add(&mut lib, PhysicsMaterial::new("Tarmac Road", SurfaceType::TarmacRoad, 0.8, 0.75, 0.02, 0.1, 2300.0));
3967    add(&mut lib, PhysicsMaterial::new("Dirt Road", SurfaceType::DirtRoad, 0.55, 0.5, 0.04, 0.15, 1600.0));
3968    add(&mut lib, PhysicsMaterial::new("Snow Road", SurfaceType::SnowRoad, 0.25, 0.2, 0.03, 0.05, 300.0));
3969    lib
3970}
3971
3972// ============================================================
3973// CONTACT VISUALIZER
3974// ============================================================
3975
3976#[derive(Debug, Clone)]
3977pub struct ContactPoint {
3978    pub world_position: Vec3,
3979    pub world_normal: Vec3,
3980    pub penetration_depth: f32,
3981    pub impulse: Vec3,
3982    pub normal_impulse: f32,
3983    pub tangent_impulse1: f32,
3984    pub tangent_impulse2: f32,
3985    pub body_a: u64,
3986    pub body_b: u64,
3987    pub lifetime: f32,  // frames remaining
3988    pub warm_impulse: f32,
3989}
3990
3991impl ContactPoint {
3992    pub fn new(pos: Vec3, normal: Vec3, depth: f32, body_a: u64, body_b: u64) -> Self {
3993        Self {
3994            world_position: pos,
3995            world_normal: normal,
3996            penetration_depth: depth,
3997            impulse: Vec3::ZERO,
3998            normal_impulse: 0.0,
3999            tangent_impulse1: 0.0,
4000            tangent_impulse2: 0.0,
4001            body_a, body_b,
4002            lifetime: 3.0,
4003            warm_impulse: 0.0,
4004        }
4005    }
4006}
4007
4008#[derive(Debug, Clone)]
4009pub struct ContactVisualizer {
4010    pub contacts: Vec<ContactPoint>,
4011    pub show_normals: bool,
4012    pub show_impulses: bool,
4013    pub show_penetration: bool,
4014    pub normal_length: f32,
4015    pub impulse_scale: f32,
4016    pub contact_sphere_radius: f32,
4017    pub color_by_depth: bool,
4018    pub color_no_contact: Vec4,
4019    pub color_shallow: Vec4,
4020    pub color_deep: Vec4,
4021    pub max_display_contacts: usize,
4022}
4023
4024impl ContactVisualizer {
4025    pub fn new() -> Self {
4026        Self {
4027            contacts: Vec::new(),
4028            show_normals: true,
4029            show_impulses: true,
4030            show_penetration: true,
4031            normal_length: 0.1,
4032            impulse_scale: 0.001,
4033            contact_sphere_radius: 0.02,
4034            color_by_depth: true,
4035            color_no_contact: Vec4::new(0.0, 1.0, 0.0, 1.0),
4036            color_shallow: Vec4::new(1.0, 1.0, 0.0, 1.0),
4037            color_deep: Vec4::new(1.0, 0.0, 0.0, 1.0),
4038            max_display_contacts: 256,
4039        }
4040    }
4041
4042    pub fn add_contact(&mut self, contact: ContactPoint) {
4043        if self.contacts.len() < self.max_display_contacts {
4044            self.contacts.push(contact);
4045        }
4046    }
4047
4048    pub fn update(&mut self, dt: f32) {
4049        for c in &mut self.contacts {
4050            c.lifetime -= dt * 60.0;
4051        }
4052        self.contacts.retain(|c| c.lifetime > 0.0);
4053    }
4054
4055    pub fn clear(&mut self) {
4056        self.contacts.clear();
4057    }
4058
4059    /// Compute color for a contact point based on penetration depth
4060    pub fn depth_color(&self, depth: f32, max_depth: f32) -> Vec4 {
4061        if !self.color_by_depth {
4062            return self.color_shallow;
4063        }
4064        let t = (depth / max_depth.max(EPSILON)).clamp(0.0, 1.0);
4065        Vec4::new(
4066            self.color_shallow.x + (self.color_deep.x - self.color_shallow.x) * t,
4067            self.color_shallow.y + (self.color_deep.y - self.color_shallow.y) * t,
4068            self.color_shallow.z + (self.color_deep.z - self.color_shallow.z) * t,
4069            1.0,
4070        )
4071    }
4072
4073    /// Generate debug lines for all contacts
4074    pub fn generate_debug_lines(&self) -> Vec<(Vec3, Vec3, Vec4)> {
4075        let mut lines = Vec::new();
4076        let max_depth = self.contacts.iter().map(|c| c.penetration_depth).fold(0.0f32, f32::max).max(0.001);
4077        for contact in &self.contacts {
4078            let alpha = (contact.lifetime / 3.0).clamp(0.0, 1.0);
4079            let mut col = self.depth_color(contact.penetration_depth, max_depth);
4080            col.w = alpha;
4081            if self.show_normals {
4082                let end = contact.world_position + contact.world_normal * self.normal_length;
4083                lines.push((contact.world_position, end, col));
4084            }
4085            if self.show_impulses && contact.normal_impulse > EPSILON {
4086                let imp_end = contact.world_position + contact.world_normal * contact.normal_impulse * self.impulse_scale;
4087                lines.push((contact.world_position, imp_end, Vec4::new(0.0, 0.5, 1.0, alpha)));
4088            }
4089            if self.show_penetration {
4090                let pen_end = contact.world_position - contact.world_normal * contact.penetration_depth;
4091                lines.push((contact.world_position, pen_end, Vec4::new(1.0, 0.5, 0.0, alpha)));
4092            }
4093        }
4094        lines
4095    }
4096
4097    /// Generate sphere positions for contact point indicators
4098    pub fn contact_sphere_positions(&self) -> Vec<(Vec3, Vec4, f32)> {
4099        let max_depth = self.contacts.iter().map(|c| c.penetration_depth).fold(0.001f32, f32::max);
4100        self.contacts.iter().map(|c| {
4101            let alpha = (c.lifetime / 3.0).clamp(0.0, 1.0);
4102            let mut col = self.depth_color(c.penetration_depth, max_depth);
4103            col.w = alpha;
4104            (c.world_position, col, self.contact_sphere_radius)
4105        }).collect()
4106    }
4107
4108    pub fn stats(&self) -> ContactStats {
4109        let total = self.contacts.len();
4110        let max_depth = self.contacts.iter().map(|c| c.penetration_depth).fold(0.0f32, f32::max);
4111        let avg_depth = if total > 0 {
4112            self.contacts.iter().map(|c| c.penetration_depth).sum::<f32>() / total as f32
4113        } else { 0.0 };
4114        let total_impulse = self.contacts.iter().map(|c| c.normal_impulse).sum::<f32>();
4115        ContactStats { total, max_depth, avg_depth, total_impulse }
4116    }
4117}
4118
4119#[derive(Debug, Clone)]
4120pub struct ContactStats {
4121    pub total: usize,
4122    pub max_depth: f32,
4123    pub avg_depth: f32,
4124    pub total_impulse: f32,
4125}
4126
4127// ============================================================
4128// BROAD PHASE — Sweep and Prune (SAP)
4129// ============================================================
4130
4131#[derive(Debug, Clone)]
4132pub struct SapEndpoint {
4133    pub value: f32,
4134    pub is_min: bool,
4135    pub body_id: u64,
4136}
4137
4138#[derive(Debug, Clone)]
4139pub struct BroadPhase {
4140    pub x_endpoints: Vec<SapEndpoint>,
4141    pub y_endpoints: Vec<SapEndpoint>,
4142    pub z_endpoints: Vec<SapEndpoint>,
4143    pub active_pairs: HashSet<(u64, u64)>,
4144    pub aabbs: HashMap<u64, Aabb>,
4145}
4146
4147impl BroadPhase {
4148    pub fn new() -> Self {
4149        Self {
4150            x_endpoints: Vec::new(),
4151            y_endpoints: Vec::new(),
4152            z_endpoints: Vec::new(),
4153            active_pairs: HashSet::new(),
4154            aabbs: HashMap::new(),
4155        }
4156    }
4157
4158    pub fn update_aabb(&mut self, body_id: u64, aabb: Aabb) {
4159        self.aabbs.insert(body_id, aabb);
4160    }
4161
4162    pub fn compute_overlapping_pairs(&self) -> Vec<(u64, u64)> {
4163        let mut result = Vec::new();
4164        let bodies: Vec<u64> = self.aabbs.keys().copied().collect();
4165        let n = bodies.len();
4166        for i in 0..n {
4167            for j in i + 1..n {
4168                let a = bodies[i]; let b = bodies[j];
4169                if let (Some(aa), Some(ab)) = (self.aabbs.get(&a), self.aabbs.get(&b)) {
4170                    if aa.intersects(ab) {
4171                        let pair = if a < b { (a, b) } else { (b, a) };
4172                        result.push(pair);
4173                    }
4174                }
4175            }
4176        }
4177        result
4178    }
4179}
4180
4181// ============================================================
4182// NARROW PHASE — GJK / EPA helpers
4183// ============================================================
4184
4185/// GJK support function for a convex shape (sphere)
4186pub fn support_sphere(center: Vec3, radius: f32, dir: Vec3) -> Vec3 {
4187    let d = dir.normalize();
4188    center + d * radius
4189}
4190
4191/// GJK support function for a box
4192pub fn support_box(half_extents: Vec3, dir: Vec3) -> Vec3 {
4193    Vec3::new(
4194        half_extents.x * dir.x.signum(),
4195        half_extents.y * dir.y.signum(),
4196        half_extents.z * dir.z.signum(),
4197    )
4198}
4199
4200/// Minkowski difference support function
4201pub fn support_minkowski(
4202    pa: Vec3, ha: Vec3,
4203    pb: Vec3, hb: Vec3,
4204    dir: Vec3,
4205) -> Vec3 {
4206    let sa = support_box(ha, dir) + pa;
4207    let sb = support_box(hb, -dir) + pb;
4208    sa - sb
4209}
4210
4211/// Dot of two vectors — for clarity
4212fn dot(a: Vec3, b: Vec3) -> f32 { a.dot(b) }
4213
4214/// Simple GJK (line simplex and triangle tests inline)
4215pub fn gjk_intersect_box_box(
4216    pos_a: Vec3, half_a: Vec3,
4217    pos_b: Vec3, half_b: Vec3,
4218) -> bool {
4219    // Separating Axis Theorem for two OBBs (axis-aligned in this simplified version)
4220    let diff = pos_b - pos_a;
4221    let axes = [Vec3::X, Vec3::Y, Vec3::Z];
4222    for &ax in &axes {
4223        let proj_a = half_a.dot(ax.abs());
4224        let proj_b = half_b.dot(ax.abs());
4225        let dist = diff.dot(ax).abs();
4226        if dist > proj_a + proj_b { return false; }
4227    }
4228    true
4229}
4230
4231/// EPA (Expanding Polytope Algorithm) — compute penetration depth and normal
4232/// Simplified version for sphere vs sphere
4233pub fn epa_sphere_sphere(
4234    center_a: Vec3, radius_a: f32,
4235    center_b: Vec3, radius_b: f32,
4236) -> Option<(Vec3, f32)> {
4237    let diff = center_b - center_a;
4238    let dist = diff.length();
4239    let overlap = radius_a + radius_b - dist;
4240    if overlap < 0.0 { return None; }
4241    let normal = if dist > EPSILON { diff / dist } else { Vec3::Y };
4242    Some((normal, overlap))
4243}
4244
4245// ============================================================
4246// PHYSICS WORLD EDITOR — integrating everything
4247// ============================================================
4248
4249#[derive(Debug, Clone)]
4250pub struct PhysicsWorldSettings {
4251    pub gravity: Vec3,
4252    pub default_linear_damping: f32,
4253    pub default_angular_damping: f32,
4254    pub default_restitution: f32,
4255    pub default_friction: f32,
4256    pub solver_iterations: u32,
4257    pub solver_velocity_iterations: u32,
4258    pub baumgarte_factor: f32,
4259    pub sleep_enabled: bool,
4260    pub fixed_timestep: f32,
4261    pub max_substeps: u32,
4262    pub broadphase_type: BroadphaseType,
4263    pub warm_starting: bool,
4264    pub continuous_collision: bool,
4265    pub split_impulses: bool,
4266}
4267
4268#[derive(Debug, Clone, PartialEq)]
4269pub enum BroadphaseType {
4270    BruteForce,
4271    SweepAndPrune,
4272    DynamicAabbTree,
4273}
4274
4275impl Default for PhysicsWorldSettings {
4276    fn default() -> Self {
4277        Self {
4278            gravity: Vec3::new(0.0, -GRAVITY, 0.0),
4279            default_linear_damping: 0.01,
4280            default_angular_damping: 0.05,
4281            default_restitution: DEFAULT_RESTITUTION,
4282            default_friction: DEFAULT_FRICTION,
4283            solver_iterations: 10,
4284            solver_velocity_iterations: 10,
4285            baumgarte_factor: 0.2,
4286            sleep_enabled: true,
4287            fixed_timestep: 1.0 / 60.0,
4288            max_substeps: 4,
4289            broadphase_type: BroadphaseType::SweepAndPrune,
4290            warm_starting: true,
4291            continuous_collision: false,
4292            split_impulses: true,
4293        }
4294    }
4295}
4296
4297// ============================================================
4298// PHYSICS DEBUG OVERLAY
4299// ============================================================
4300
4301#[derive(Debug, Clone)]
4302pub struct PhysicsDebugOverlay {
4303    pub show_colliders: bool,
4304    pub show_contacts: bool,
4305    pub show_joints: bool,
4306    pub show_velocities: bool,
4307    pub show_forces: bool,
4308    pub show_center_of_mass: bool,
4309    pub show_inertia: bool,
4310    pub show_sleep_state: bool,
4311    pub show_broadphase: bool,
4312    pub collider_color: Vec4,
4313    pub contact_color: Vec4,
4314    pub velocity_color: Vec4,
4315    pub force_color: Vec4,
4316    pub com_color: Vec4,
4317    pub sleeping_color: Vec4,
4318    pub velocity_scale: f32,
4319    pub force_scale: f32,
4320    pub line_width: f32,
4321}
4322
4323impl Default for PhysicsDebugOverlay {
4324    fn default() -> Self {
4325        Self {
4326            show_colliders: true,
4327            show_contacts: true,
4328            show_joints: true,
4329            show_velocities: false,
4330            show_forces: false,
4331            show_center_of_mass: true,
4332            show_inertia: false,
4333            show_sleep_state: true,
4334            show_broadphase: false,
4335            collider_color: Vec4::new(0.0, 1.0, 0.0, 0.7),
4336            contact_color: Vec4::new(1.0, 0.0, 0.0, 1.0),
4337            velocity_color: Vec4::new(1.0, 1.0, 0.0, 1.0),
4338            force_color: Vec4::new(1.0, 0.5, 0.0, 1.0),
4339            com_color: Vec4::new(0.0, 0.5, 1.0, 1.0),
4340            sleeping_color: Vec4::new(0.5, 0.5, 0.5, 0.5),
4341            velocity_scale: 0.1,
4342            force_scale: 0.0001,
4343            line_width: 1.0,
4344        }
4345    }
4346}
4347
4348impl PhysicsDebugOverlay {
4349    pub fn generate_body_debug_lines(&self, body: &RigidBodyInspector) -> Vec<(Vec3, Vec3, Vec4)> {
4350        let mut lines = Vec::new();
4351        let col = if body.sleeping { self.sleeping_color } else { self.collider_color };
4352        // Velocity arrow
4353        if self.show_velocities && !body.is_static {
4354            let v_end = body.position + body.linear_velocity * self.velocity_scale;
4355            lines.push((body.position, v_end, self.velocity_color));
4356        }
4357        // Center of mass indicator (small cross)
4358        if self.show_center_of_mass {
4359            let com = body.position + body.orientation * body.center_of_mass;
4360            let d = 0.05;
4361            lines.push((com - Vec3::X * d, com + Vec3::X * d, self.com_color));
4362            lines.push((com - Vec3::Y * d, com + Vec3::Y * d, self.com_color));
4363            lines.push((com - Vec3::Z * d, com + Vec3::Z * d, self.com_color));
4364        }
4365        lines
4366    }
4367}
4368
4369// ============================================================
4370// MAIN PHYSICS EDITOR STRUCT
4371// ============================================================
4372
4373#[derive(Debug)]
4374pub struct PhysicsEditor {
4375    pub rigid_bodies: HashMap<u64, RigidBodyInspector>,
4376    pub constraints: HashMap<u64, Constraint>,
4377    pub joint_editor: JointEditor,
4378    pub cloth_sims: HashMap<u64, ClothSimulation>,
4379    pub fluid_sims: HashMap<u64, FluidSimulation>,
4380    pub fracture_systems: HashMap<u64, VoronoiFracture>,
4381    pub ragdoll_editor: RagdollEditor,
4382    pub vehicles: HashMap<u64, VehiclePhysics>,
4383    pub collision_shapes: HashMap<u64, CollisionShapeEditor>,
4384    pub material_library: HashMap<String, PhysicsMaterial>,
4385    pub contact_visualizer: ContactVisualizer,
4386    pub broad_phase: BroadPhase,
4387    pub world_settings: PhysicsWorldSettings,
4388    pub debug_overlay: PhysicsDebugOverlay,
4389    pub next_id: u64,
4390    pub selected_body: Option<u64>,
4391    pub selected_constraint: Option<u64>,
4392    pub simulation_running: bool,
4393    pub accumulated_time: f32,
4394    pub simulation_time: f32,
4395    pub step_count: u64,
4396    pub history: VecDeque<PhysicsSnapshot>,
4397    pub max_history: usize,
4398    pub undo_stack: VecDeque<PhysicsEditorCommand>,
4399    pub redo_stack: VecDeque<PhysicsEditorCommand>,
4400}
4401
4402#[derive(Debug, Clone)]
4403pub struct PhysicsSnapshot {
4404    pub time: f32,
4405    pub body_states: HashMap<u64, (Vec3, Quat, Vec3, Vec3)>,
4406}
4407
4408#[derive(Debug, Clone)]
4409pub enum PhysicsEditorCommand {
4410    AddBody { id: u64 },
4411    RemoveBody { id: u64, body: RigidBodyInspector },
4412    MoveBody { id: u64, old_pos: Vec3, new_pos: Vec3 },
4413    ChangeProperty { id: u64, property: String, old_value: f32, new_value: f32 },
4414    AddConstraint { id: u64 },
4415    RemoveConstraint { id: u64, constraint: Constraint },
4416}
4417
4418impl PhysicsEditor {
4419    pub fn new() -> Self {
4420        Self {
4421            rigid_bodies: HashMap::new(),
4422            constraints: HashMap::new(),
4423            joint_editor: JointEditor::new(),
4424            cloth_sims: HashMap::new(),
4425            fluid_sims: HashMap::new(),
4426            fracture_systems: HashMap::new(),
4427            ragdoll_editor: RagdollEditor::new(),
4428            vehicles: HashMap::new(),
4429            collision_shapes: HashMap::new(),
4430            material_library: build_material_library(),
4431            contact_visualizer: ContactVisualizer::new(),
4432            broad_phase: BroadPhase::new(),
4433            world_settings: PhysicsWorldSettings::default(),
4434            debug_overlay: PhysicsDebugOverlay::default(),
4435            next_id: 1,
4436            selected_body: None,
4437            selected_constraint: None,
4438            simulation_running: false,
4439            accumulated_time: 0.0,
4440            simulation_time: 0.0,
4441            step_count: 0,
4442            history: VecDeque::new(),
4443            max_history: 60,
4444            undo_stack: VecDeque::new(),
4445            redo_stack: VecDeque::new(),
4446        }
4447    }
4448
4449    pub fn alloc_id(&mut self) -> u64 {
4450        let id = self.next_id;
4451        self.next_id += 1;
4452        id
4453    }
4454
4455    pub fn add_rigid_body(&mut self, name: &str) -> u64 {
4456        let id = self.alloc_id();
4457        let body = RigidBodyInspector::new(id, name);
4458        self.rigid_bodies.insert(id, body);
4459        self.undo_stack.push_back(PhysicsEditorCommand::AddBody { id });
4460        id
4461    }
4462
4463    pub fn remove_rigid_body(&mut self, id: u64) {
4464        if let Some(body) = self.rigid_bodies.remove(&id) {
4465            self.undo_stack.push_back(PhysicsEditorCommand::RemoveBody { id, body });
4466        }
4467    }
4468
4469    pub fn add_constraint(&mut self, constraint: Constraint) -> u64 {
4470        let id = self.alloc_id();
4471        self.constraints.insert(id, constraint);
4472        id
4473    }
4474
4475    pub fn add_cloth(&mut self, w: usize, h: usize, cell_size: f32) -> u64 {
4476        let id = self.alloc_id();
4477        self.cloth_sims.insert(id, ClothSimulation::new(w, h, cell_size, 0.01));
4478        id
4479    }
4480
4481    pub fn add_fluid(&mut self, kernel_radius: f32, rest_density: f32) -> u64 {
4482        let id = self.alloc_id();
4483        self.fluid_sims.insert(id, FluidSimulation::new(kernel_radius, rest_density));
4484        id
4485    }
4486
4487    pub fn add_fracture_system(&mut self, bounds_min: Vec3, bounds_max: Vec3, num_cells: usize, seed: u64) -> u64 {
4488        let id = self.alloc_id();
4489        self.fracture_systems.insert(id, VoronoiFracture::new(bounds_min, bounds_max, num_cells, seed));
4490        id
4491    }
4492
4493    pub fn add_vehicle(&mut self, mass: f32, wheelbase: f32, track: f32) -> u64 {
4494        let id = self.alloc_id();
4495        let mut vehicle = VehiclePhysics::new(mass, wheelbase, track);
4496        vehicle.body_id = id;
4497        self.vehicles.insert(id, vehicle);
4498        id
4499    }
4500
4501    pub fn add_collision_shape(&mut self, shape_type: CollisionShapeType) -> u64 {
4502        let id = self.alloc_id();
4503        let mut shape = CollisionShapeEditor::new();
4504        shape.shape_type = shape_type;
4505        shape.compute_aabb();
4506        shape.compute_volume();
4507        shape.compute_surface_area();
4508        self.collision_shapes.insert(id, shape);
4509        id
4510    }
4511
4512    /// Update all broad-phase AABBs
4513    pub fn update_broadphase(&mut self) {
4514        for (id, body) in &self.rigid_bodies {
4515            if let Some(shape) = self.collision_shapes.get(id) {
4516                let aabb = shape.aabb.clone();
4517                // Transform AABB by body position/orientation (conservative)
4518                let center = body.position;
4519                let he = aabb.half_extents();
4520                let transformed = Aabb::new(center - he, center + he);
4521                self.broad_phase.update_aabb(*id, transformed);
4522            }
4523        }
4524    }
4525
4526    /// Detect overlapping pairs
4527    pub fn broad_phase_query(&self) -> Vec<(u64, u64)> {
4528        self.broad_phase.compute_overlapping_pairs()
4529    }
4530
4531    /// Step the physics simulation
4532    pub fn step(&mut self, dt: f32) {
4533        if !self.simulation_running { return; }
4534        self.accumulated_time += dt;
4535        let fixed_dt = self.world_settings.fixed_timestep;
4536        let mut steps = 0u32;
4537        while self.accumulated_time >= fixed_dt && steps < self.world_settings.max_substeps {
4538            self.fixed_step(fixed_dt);
4539            self.accumulated_time -= fixed_dt;
4540            steps += 1;
4541        }
4542    }
4543
4544    fn fixed_step(&mut self, dt: f32) {
4545        // Integrate rigid bodies
4546        let ids: Vec<u64> = self.rigid_bodies.keys().copied().collect();
4547        for id in &ids {
4548            if let Some(body) = self.rigid_bodies.get_mut(id) {
4549                body.integrate(dt);
4550            }
4551        }
4552        // Cloth simulation
4553        let cloth_ids: Vec<u64> = self.cloth_sims.keys().copied().collect();
4554        for id in &cloth_ids {
4555            if let Some(cloth) = self.cloth_sims.get_mut(&id) {
4556                cloth.integrate(dt, self.simulation_time);
4557                cloth.solve_springs(dt);
4558                cloth.compute_normals();
4559            }
4560        }
4561        // Fluid simulation
4562        let fluid_ids: Vec<u64> = self.fluid_sims.keys().copied().collect();
4563        for id in &fluid_ids {
4564            if let Some(fluid) = self.fluid_sims.get_mut(&id) {
4565                fluid.step(dt);
4566            }
4567        }
4568        // Debris
4569        let fracture_ids: Vec<u64> = self.fracture_systems.keys().copied().collect();
4570        for id in &fracture_ids {
4571            if let Some(frac) = self.fracture_systems.get_mut(&id) {
4572                frac.integrate_debris(dt);
4573            }
4574        }
4575        // Contact visualizer update
4576        self.contact_visualizer.update(dt);
4577        self.simulation_time += dt;
4578        self.step_count += 1;
4579        // Snapshot
4580        if self.step_count % 6 == 0 {
4581            self.record_snapshot();
4582        }
4583    }
4584
4585    fn record_snapshot(&mut self) {
4586        let mut states = HashMap::new();
4587        for (id, body) in &self.rigid_bodies {
4588            states.insert(*id, (body.position, body.orientation, body.linear_velocity, body.angular_velocity));
4589        }
4590        let snap = PhysicsSnapshot { time: self.simulation_time, body_states: states };
4591        if self.history.len() >= self.max_history {
4592            self.history.pop_front();
4593        }
4594        self.history.push_back(snap);
4595    }
4596
4597    pub fn play(&mut self) { self.simulation_running = true; }
4598    pub fn pause(&mut self) { self.simulation_running = false; }
4599
4600    pub fn reset(&mut self) {
4601        self.simulation_running = false;
4602        self.simulation_time = 0.0;
4603        self.step_count = 0;
4604        self.accumulated_time = 0.0;
4605        self.contact_visualizer.clear();
4606        self.history.clear();
4607        // Reset body states
4608        for body in self.rigid_bodies.values_mut() {
4609            body.linear_velocity = Vec3::ZERO;
4610            body.angular_velocity = Vec3::ZERO;
4611            body.force_accumulator = Vec3::ZERO;
4612            body.torque_accumulator = Vec3::ZERO;
4613            body.sleeping = false;
4614            body.sleep_timer = 0.0;
4615        }
4616    }
4617
4618    /// Get body info string for UI display
4619    pub fn body_info_string(&self, id: u64) -> String {
4620        if let Some(body) = self.rigid_bodies.get(&id) {
4621            format!(
4622                "Body '{}' | Mass: {:.3}kg | Pos: ({:.3},{:.3},{:.3}) | V: {:.3}m/s | Sleep: {}",
4623                body.name,
4624                body.mass,
4625                body.position.x, body.position.y, body.position.z,
4626                body.linear_velocity.length(),
4627                body.sleeping,
4628            )
4629        } else {
4630            "No body selected".to_string()
4631        }
4632    }
4633
4634    /// Create a standard scene with floor + stacked boxes
4635    pub fn create_demo_scene(&mut self) {
4636        // Floor (static)
4637        let floor_id = self.add_rigid_body("Floor");
4638        if let Some(body) = self.rigid_bodies.get_mut(&floor_id) {
4639            body.is_static = true;
4640            body.shape_type = RigidBodyShapeType::Box;
4641            body.shape_params.half_extents = Vec3::new(10.0, 0.1, 10.0);
4642            body.position = Vec3::new(0.0, -0.1, 0.0);
4643            body.recompute_inertia();
4644        }
4645        // Stack of boxes
4646        for i in 0..5 {
4647            let box_id = self.add_rigid_body(&format!("Box_{}", i));
4648            if let Some(body) = self.rigid_bodies.get_mut(&box_id) {
4649                body.mass = 1.0;
4650                body.shape_type = RigidBodyShapeType::Box;
4651                body.shape_params.half_extents = Vec3::splat(0.25);
4652                body.position = Vec3::new(0.0, 0.5 + i as f32 * 0.6, 0.0);
4653                body.recompute_inertia();
4654            }
4655        }
4656        // A sphere
4657        let sphere_id = self.add_rigid_body("Sphere");
4658        if let Some(body) = self.rigid_bodies.get_mut(&sphere_id) {
4659            body.mass = 2.0;
4660            body.shape_type = RigidBodyShapeType::Sphere;
4661            body.shape_params.radius = 0.3;
4662            body.position = Vec3::new(2.0, 1.5, 0.0);
4663            body.linear_velocity = Vec3::new(-3.0, 0.0, 0.0);
4664            body.recompute_inertia();
4665        }
4666    }
4667
4668    /// Raycast against all rigid bodies (simplified AABB test)
4669    pub fn raycast(&self, ray_origin: Vec3, ray_dir: Vec3, max_dist: f32) -> Option<(u64, f32, Vec3)> {
4670        let mut closest: Option<(u64, f32, Vec3)> = None;
4671        let rd = ray_dir.normalize();
4672        for (id, _body) in &self.rigid_bodies {
4673            if let Some(shape) = self.collision_shapes.get(id) {
4674                if let Some(t) = ray_aabb_intersect(ray_origin, rd, &shape.aabb) {
4675                    if t < max_dist {
4676                        if closest.is_none() || t < closest.as_ref().unwrap().1 {
4677                            let hit_pt = ray_origin + rd * t;
4678                            closest = Some((*id, t, hit_pt));
4679                        }
4680                    }
4681                }
4682            }
4683        }
4684        closest
4685    }
4686
4687    pub fn select_body_at_ray(&mut self, ray_origin: Vec3, ray_dir: Vec3) -> Option<u64> {
4688        if let Some((id, _, _)) = self.raycast(ray_origin, ray_dir, 1000.0) {
4689            self.selected_body = Some(id);
4690            Some(id)
4691        } else {
4692            self.selected_body = None;
4693            None
4694        }
4695    }
4696
4697    pub fn generate_all_debug_lines(&self) -> Vec<(Vec3, Vec3, Vec4)> {
4698        let mut lines = Vec::new();
4699        // Body lines
4700        if self.debug_overlay.show_colliders || self.debug_overlay.show_velocities {
4701            for body in self.rigid_bodies.values() {
4702                let mut body_lines = self.debug_overlay.generate_body_debug_lines(body);
4703                lines.append(&mut body_lines);
4704            }
4705        }
4706        // Contact lines
4707        if self.debug_overlay.show_contacts {
4708            let mut contact_lines = self.contact_visualizer.generate_debug_lines();
4709            lines.append(&mut contact_lines);
4710        }
4711        // Joint lines
4712        if self.debug_overlay.show_joints {
4713            let mut joint_lines = self.joint_editor.generate_all_debug_lines();
4714            lines.append(&mut joint_lines);
4715        }
4716        lines
4717    }
4718
4719    /// Export physics world to a simple text description
4720    pub fn export_description(&self) -> String {
4721        let mut out = String::new();
4722        out.push_str(&format!("PhysicsWorld: {} bodies, {} constraints\n",
4723            self.rigid_bodies.len(), self.constraints.len()));
4724        for (id, body) in &self.rigid_bodies {
4725            out.push_str(&format!("  Body[{}] '{}' mass={:.2} pos=({:.2},{:.2},{:.2})\n",
4726                id, body.name, body.mass, body.position.x, body.position.y, body.position.z));
4727        }
4728        for (id, c) in &self.constraints {
4729            out.push_str(&format!("  Constraint[{}] type={}\n", id, c.name()));
4730        }
4731        out
4732    }
4733
4734    /// Force-based explosion at a point
4735    pub fn apply_explosion(&mut self, center: Vec3, force: f32, radius: f32) {
4736        for body in self.rigid_bodies.values_mut() {
4737            if body.is_static { continue; }
4738            let dir = body.position - center;
4739            let dist = dir.length();
4740            if dist < radius && dist > EPSILON {
4741                let falloff = 1.0 - (dist / radius);
4742                let impulse = (dir / dist) * force * falloff;
4743                body.apply_force_at_point(impulse, body.position);
4744                body.wake_up();
4745            }
4746        }
4747    }
4748}
4749
4750// ============================================================
4751// UTILITY FUNCTIONS
4752// ============================================================
4753
4754pub fn perpendicular_to(v: Vec3) -> Vec3 {
4755    let n = v.normalize();
4756    let candidate = if n.x.abs() < 0.9 { Vec3::X } else { Vec3::Y };
4757    n.cross(candidate).normalize()
4758}
4759
4760pub fn quat_to_axis_angle(q: Quat) -> (Vec3, f32) {
4761    let w = q.w.clamp(-1.0, 1.0);
4762    let angle = 2.0 * w.acos();
4763    let s = (1.0 - w * w).sqrt();
4764    let axis = if s > EPSILON {
4765        Vec3::new(q.x / s, q.y / s, q.z / s)
4766    } else {
4767        Vec3::X
4768    };
4769    (axis, angle)
4770}
4771
4772pub fn decompose_swing_twist(q: Quat, twist_axis: Vec3) -> (Quat, Quat) {
4773    let proj = Vec3::new(q.x, q.y, q.z).dot(twist_axis) * twist_axis;
4774    let twist = Quat::from_xyzw(proj.x, proj.y, proj.z, q.w).normalize();
4775    let swing = q * twist.inverse();
4776    (swing, twist)
4777}
4778
4779pub fn smoothstep(edge0: f32, edge1: f32, x: f32) -> f32 {
4780    let t = ((x - edge0) / (edge1 - edge0 + EPSILON)).clamp(0.0, 1.0);
4781    t * t * (3.0 - 2.0 * t)
4782}
4783
4784pub fn snap_value(v: f32, snap: f32) -> f32 {
4785    if snap < EPSILON { return v; }
4786    (v / snap).round() * snap
4787}
4788
4789pub fn ray_aabb_intersect(origin: Vec3, dir: Vec3, aabb: &Aabb) -> Option<f32> {
4790    let inv_dir = Vec3::new(
4791        if dir.x.abs() > EPSILON { 1.0 / dir.x } else { f32::INFINITY },
4792        if dir.y.abs() > EPSILON { 1.0 / dir.y } else { f32::INFINITY },
4793        if dir.z.abs() > EPSILON { 1.0 / dir.z } else { f32::INFINITY },
4794    );
4795    let t1 = (aabb.min - origin) * inv_dir;
4796    let t2 = (aabb.max - origin) * inv_dir;
4797    let t_min = t1.min(t2);
4798    let t_max = t1.max(t2);
4799    let t_enter = t_min.x.max(t_min.y).max(t_min.z);
4800    let t_exit = t_max.x.min(t_max.y).min(t_max.z);
4801    if t_enter <= t_exit && t_exit >= 0.0 {
4802        Some(t_enter.max(0.0))
4803    } else {
4804        None
4805    }
4806}
4807
4808/// Compute the angular impulse needed to align two coordinate frames
4809pub fn angular_correction_impulse(
4810    rot_a: Quat, rot_b: Quat,
4811    inv_inertia_a: Vec3, inv_inertia_b: Vec3,
4812    baumgarte: f32, dt: f32,
4813) -> Vec3 {
4814    let rot_diff = rot_b * rot_a.inverse();
4815    let (axis, angle) = quat_to_axis_angle(rot_diff);
4816    let error = axis * angle * baumgarte / dt;
4817    let inv_sum = inv_inertia_a + inv_inertia_b;
4818    Vec3::new(
4819        if inv_sum.x > EPSILON { error.x / inv_sum.x } else { 0.0 },
4820        if inv_sum.y > EPSILON { error.y / inv_sum.y } else { 0.0 },
4821        if inv_sum.z > EPSILON { error.z / inv_sum.z } else { 0.0 },
4822    )
4823}
4824
4825/// Compute the center of mass of a compound body
4826pub fn compute_compound_com(masses: &[f32], positions: &[Vec3]) -> Vec3 {
4827    let total_mass: f32 = masses.iter().sum();
4828    if total_mass < EPSILON { return Vec3::ZERO; }
4829    let weighted: Vec3 = masses.iter().zip(positions.iter())
4830        .map(|(&m, &p)| p * m)
4831        .fold(Vec3::ZERO, |a, b| a + b);
4832    weighted / total_mass
4833}
4834
4835/// Compute combined inertia for a compound body using parallel axis theorem
4836pub fn compute_compound_inertia(
4837    masses: &[f32],
4838    inertias: &[Mat4],
4839    positions: &[Vec3],
4840    com: Vec3,
4841) -> Mat4 {
4842    let mut result = Mat4::ZERO;
4843    for i in 0..masses.len() {
4844        let shifted = inertia_parallel_axis(inertias[i], masses[i], positions[i] - com);
4845        // Add matrices (3x3 part only)
4846        for col in 0..4 {
4847            let rc = result.col(col);
4848            let sc = shifted.col(col);
4849            // Mat4 doesn't impl AddAssign directly, reconstruct
4850            let _ = (rc, sc); // suppressed
4851        }
4852        // Manual element-wise add
4853        let rc0 = result.col(0) + shifted.col(0);
4854        let rc1 = result.col(1) + shifted.col(1);
4855        let rc2 = result.col(2) + shifted.col(2);
4856        let rc3 = result.col(3) + shifted.col(3);
4857        result = Mat4::from_cols(rc0, rc1, rc2, rc3);
4858    }
4859    result
4860}
4861
4862/// Solve a 1D position constraint (Baumgarte): return position correction
4863pub fn baumgarte_position_correction(error: f32, effective_mass: f32, baumgarte: f32, dt: f32) -> f32 {
4864    -baumgarte / dt * error * effective_mass
4865}
4866
4867/// Compute relative velocity of two points on two rigid bodies
4868pub fn relative_velocity(
4869    vel_a: Vec3, omega_a: Vec3, r_a: Vec3,
4870    vel_b: Vec3, omega_b: Vec3, r_b: Vec3,
4871) -> Vec3 {
4872    let v_a = vel_a + omega_a.cross(r_a);
4873    let v_b = vel_b + omega_b.cross(r_b);
4874    v_b - v_a
4875}
4876
4877/// Restitution-based rebound velocity
4878pub fn restitution_velocity(v_rel_normal: f32, restitution: f32) -> f32 {
4879    if v_rel_normal >= 0.0 { return 0.0; }
4880    -(1.0 + restitution) * v_rel_normal
4881}
4882
4883/// Compute friction impulse (Coulomb friction cone)
4884pub fn friction_impulse(
4885    j_normal: f32, friction_coeff: f32,
4886    tangent_impulse: f32,
4887) -> f32 {
4888    tangent_impulse.clamp(-friction_coeff * j_normal, friction_coeff * j_normal)
4889}
4890
4891/// Compute angular velocity from rotation delta over dt
4892pub fn angular_velocity_from_delta_quat(q_prev: Quat, q_curr: Quat, dt: f32) -> Vec3 {
4893    let dq = q_curr * q_prev.inverse();
4894    let (axis, angle) = quat_to_axis_angle(dq);
4895    axis * (angle / dt)
4896}
4897
4898/// Convert angular velocity vector to quaternion derivative
4899pub fn omega_to_quat_deriv(omega: Vec3, q: Quat) -> Quat {
4900    let ox = omega.x;
4901    let oy = omega.y;
4902    let oz = omega.z;
4903    Quat::from_xyzw(
4904        0.5 * (oy * q.z - oz * q.y + ox * q.w),
4905        0.5 * (oz * q.x - ox * q.z + oy * q.w),
4906        0.5 * (ox * q.y - oy * q.x + oz * q.w),
4907        0.5 * (-ox * q.x - oy * q.y - oz * q.z),
4908    )
4909}
4910
4911/// Cross product matrix (skew symmetric) for angular dynamics
4912pub fn cross_matrix(v: Vec3) -> [[f32; 3]; 3] {
4913    [
4914        [0.0, -v.z, v.y],
4915        [v.z, 0.0, -v.x],
4916        [-v.y, v.x, 0.0],
4917    ]
4918}
4919
4920/// Multiply 3x3 matrix by vector
4921pub fn mat3_mul_vec(m: [[f32; 3]; 3], v: Vec3) -> Vec3 {
4922    Vec3::new(
4923        m[0][0]*v.x + m[0][1]*v.y + m[0][2]*v.z,
4924        m[1][0]*v.x + m[1][1]*v.y + m[1][2]*v.z,
4925        m[2][0]*v.x + m[2][1]*v.y + m[2][2]*v.z,
4926    )
4927}
4928
4929/// Transpose 3x3 matrix
4930pub fn mat3_transpose(m: [[f32; 3]; 3]) -> [[f32; 3]; 3] {
4931    [
4932        [m[0][0], m[1][0], m[2][0]],
4933        [m[0][1], m[1][1], m[2][1]],
4934        [m[0][2], m[1][2], m[2][2]],
4935    ]
4936}
4937
4938/// Multiply two 3x3 matrices
4939pub fn mat3_mul(a: [[f32; 3]; 3], b: [[f32; 3]; 3]) -> [[f32; 3]; 3] {
4940    let mut c = [[0.0f32; 3]; 3];
4941    for i in 0..3 {
4942        for j in 0..3 {
4943            for k in 0..3 {
4944                c[i][j] += a[i][k] * b[k][j];
4945            }
4946        }
4947    }
4948    c
4949}
4950
4951/// 3x3 determinant
4952pub fn mat3_det(m: [[f32; 3]; 3]) -> f32 {
4953    m[0][0]*(m[1][1]*m[2][2]-m[1][2]*m[2][1])
4954    - m[0][1]*(m[1][0]*m[2][2]-m[1][2]*m[2][0])
4955    + m[0][2]*(m[1][0]*m[2][1]-m[1][1]*m[2][0])
4956}
4957
4958/// 3x3 matrix inverse
4959pub fn mat3_inverse(m: [[f32; 3]; 3]) -> Option<[[f32; 3]; 3]> {
4960    let det = mat3_det(m);
4961    if det.abs() < EPSILON { return None; }
4962    let inv_det = 1.0 / det;
4963    let result = [
4964        [
4965            (m[1][1]*m[2][2]-m[1][2]*m[2][1])*inv_det,
4966            (m[0][2]*m[2][1]-m[0][1]*m[2][2])*inv_det,
4967            (m[0][1]*m[1][2]-m[0][2]*m[1][1])*inv_det,
4968        ],
4969        [
4970            (m[1][2]*m[2][0]-m[1][0]*m[2][2])*inv_det,
4971            (m[0][0]*m[2][2]-m[0][2]*m[2][0])*inv_det,
4972            (m[0][2]*m[1][0]-m[0][0]*m[1][2])*inv_det,
4973        ],
4974        [
4975            (m[1][0]*m[2][1]-m[1][1]*m[2][0])*inv_det,
4976            (m[0][1]*m[2][0]-m[0][0]*m[2][1])*inv_det,
4977            (m[0][0]*m[1][1]-m[0][1]*m[1][0])*inv_det,
4978        ],
4979    ];
4980    Some(result)
4981}
4982
4983/// Gram-Schmidt orthonormalization of three vectors
4984pub fn gram_schmidt(v0: Vec3, v1: Vec3, v2: Vec3) -> (Vec3, Vec3, Vec3) {
4985    let u0 = v0.normalize();
4986    let u1 = (v1 - v1.dot(u0) * u0).normalize();
4987    let u2 = (v2 - v2.dot(u0) * u0 - v2.dot(u1) * u1).normalize();
4988    (u0, u1, u2)
4989}
4990
4991// ============================================================
4992// ADDITIONAL PHYSICS FORMULAS AND TABLES
4993// ============================================================
4994
4995/// Young's modulus table (Pa)
4996pub fn young_modulus(surface: SurfaceType) -> f32 {
4997    match surface {
4998        SurfaceType::Metal => 210e9,
4999        SurfaceType::Glass => 70e9,
5000        SurfaceType::Concrete | SurfaceType::WetConcrete => 30e9,
5001        SurfaceType::Wood | SurfaceType::WetWood => 10e9,
5002        SurfaceType::Rubber | SurfaceType::ToughRubber => 0.05e9,
5003        SurfaceType::Foam => 0.001e9,
5004        SurfaceType::Plastic | SurfaceType::HardPlastic => 3e9,
5005        SurfaceType::Granite => 50e9,
5006        SurfaceType::Marble => 50e9,
5007        SurfaceType::Ceramic => 100e9,
5008        SurfaceType::Ice => 9e9,
5009        SurfaceType::Leather => 0.1e9,
5010        SurfaceType::Asphalt | SurfaceType::TarmacRoad => 5e9,
5011        _ => 1e9,
5012    }
5013}
5014
5015/// Poisson's ratio table
5016pub fn poisson_ratio(surface: SurfaceType) -> f32 {
5017    match surface {
5018        SurfaceType::Metal => 0.3,
5019        SurfaceType::Glass => 0.22,
5020        SurfaceType::Rubber | SurfaceType::ToughRubber => 0.48,
5021        SurfaceType::Concrete => 0.2,
5022        SurfaceType::Wood => 0.35,
5023        SurfaceType::Foam => 0.3,
5024        SurfaceType::Plastic | SurfaceType::HardPlastic => 0.35,
5025        SurfaceType::Ceramic => 0.25,
5026        SurfaceType::Ice => 0.33,
5027        _ => 0.3,
5028    }
5029}
5030
5031/// Coefficient of restitution between two materials
5032pub fn restitution_between(a: SurfaceType, b: SurfaceType) -> f32 {
5033    let ra = material_restitution(a);
5034    let rb = material_restitution(b);
5035    (ra * rb).sqrt() // geometric mean
5036}
5037
5038pub fn material_restitution(s: SurfaceType) -> f32 {
5039    match s {
5040        SurfaceType::Rubber | SurfaceType::ToughRubber => 0.9,
5041        SurfaceType::Metal => 0.4,
5042        SurfaceType::Glass => 0.7,
5043        SurfaceType::Wood => 0.4,
5044        SurfaceType::Concrete => 0.2,
5045        SurfaceType::Ice => 0.1,
5046        SurfaceType::Foam => 0.05,
5047        SurfaceType::Mud | SurfaceType::MudThick => 0.02,
5048        SurfaceType::Sand => 0.1,
5049        SurfaceType::Carpet => 0.05,
5050        SurfaceType::Cork => 0.6,
5051        SurfaceType::Asphalt | SurfaceType::TarmacRoad => 0.15,
5052        SurfaceType::Marble | SurfaceType::Granite => 0.6,
5053        SurfaceType::Ceramic => 0.5,
5054        SurfaceType::Plastic | SurfaceType::HardPlastic => 0.4,
5055        _ => DEFAULT_RESTITUTION,
5056    }
5057}
5058
5059/// Compute terminal velocity for an object falling through air
5060/// v_t = sqrt(2*m*g / (rho * Cd * A))
5061pub fn terminal_velocity(mass: f32, drag_coeff: f32, cross_section_area: f32) -> f32 {
5062    (2.0 * mass * GRAVITY / (AIR_DENSITY * drag_coeff * cross_section_area)).sqrt()
5063}
5064
5065/// Compute buoyancy force on a submerged object
5066/// F_b = rho_fluid * V_submerged * g
5067pub fn buoyancy_force(fluid_density: f32, submerged_volume: f32) -> f32 {
5068    fluid_density * submerged_volume * GRAVITY
5069}
5070
5071/// Drag force: F_d = 0.5 * rho * v^2 * Cd * A
5072pub fn drag_force(fluid_density: f32, velocity: f32, drag_coeff: f32, area: f32) -> f32 {
5073    0.5 * fluid_density * velocity * velocity * drag_coeff * area
5074}
5075
5076/// Compute impact force from collision (impulse-momentum)
5077/// F = m * delta_v / delta_t
5078pub fn impact_force(mass: f32, delta_velocity: f32, contact_time: f32) -> f32 {
5079    mass * delta_velocity / contact_time.max(EPSILON)
5080}
5081
5082/// Kinetic energy of a rigid body
5083pub fn kinetic_energy(mass: f32, vel: Vec3, inertia_diag: Vec3, omega: Vec3) -> f32 {
5084    let linear = 0.5 * mass * vel.length_squared();
5085    let angular = 0.5 * (inertia_diag.x * omega.x * omega.x
5086        + inertia_diag.y * omega.y * omega.y
5087        + inertia_diag.z * omega.z * omega.z);
5088    linear + angular
5089}
5090
5091/// Potential energy (gravitational)
5092pub fn potential_energy(mass: f32, height: f32) -> f32 {
5093    mass * GRAVITY * height
5094}
5095
5096/// Spring potential energy
5097pub fn spring_potential_energy(k: f32, extension: f32) -> f32 {
5098    0.5 * k * extension * extension
5099}
5100
5101/// Approximate collision time from material properties (Hertz contact)
5102/// t_contact ~ 2.87 * (m / (E_eff * v_rel))^(2/5)
5103pub fn hertz_contact_time(mass: f32, eff_young: f32, rel_velocity: f32, radius: f32) -> f32 {
5104    let v = rel_velocity.abs().max(EPSILON);
5105    let factor = (mass / (eff_young * radius.sqrt() * v)).powf(0.4);
5106    2.87 * factor
5107}
5108
5109/// Effective Young's modulus for two materials in contact (Hertz)
5110/// 1/E_eff = (1-nu_a^2)/E_a + (1-nu_b^2)/E_b
5111pub fn effective_young_modulus(e_a: f32, nu_a: f32, e_b: f32, nu_b: f32) -> f32 {
5112    let inv = (1.0 - nu_a * nu_a) / e_a + (1.0 - nu_b * nu_b) / e_b;
5113    1.0 / inv.max(EPSILON)
5114}
5115
5116/// Hertz contact force: F = (4/3) * E_eff * sqrt(R_eff) * delta^(3/2)
5117pub fn hertz_contact_force(eff_young: f32, eff_radius: f32, penetration: f32) -> f32 {
5118    (4.0 / 3.0) * eff_young * eff_radius.sqrt() * penetration.powf(1.5)
5119}
5120
5121/// Effective radius for two spheres in contact
5122/// 1/R_eff = 1/R_a + 1/R_b
5123pub fn effective_radius(r_a: f32, r_b: f32) -> f32 {
5124    (r_a * r_b) / (r_a + r_b + EPSILON)
5125}
5126
5127// ============================================================
5128// TORQUE & FORCE ANALYSIS
5129// ============================================================
5130
5131#[derive(Debug, Clone)]
5132pub struct ForceAnalyzer {
5133    pub forces: Vec<(Vec3, Vec3, Vec3)>,  // (origin, direction*magnitude, color)
5134    pub show_resultant: bool,
5135    pub scale: f32,
5136}
5137
5138impl ForceAnalyzer {
5139    pub fn new() -> Self {
5140        Self { forces: Vec::new(), show_resultant: true, scale: 0.01 }
5141    }
5142
5143    pub fn add_force(&mut self, origin: Vec3, force: Vec3, color: Vec3) {
5144        self.forces.push((origin, force * self.scale, Vec3::new(color.x, color.y, color.z)));
5145    }
5146
5147    pub fn resultant(&self) -> Vec3 {
5148        self.forces.iter().fold(Vec3::ZERO, |acc, (_, f, _)| acc + *f)
5149    }
5150
5151    pub fn resultant_torque(&self, about: Vec3) -> Vec3 {
5152        self.forces.iter().fold(Vec3::ZERO, |acc, (origin, force, _)| {
5153            let r = *origin - about;
5154            acc + r.cross(*force)
5155        })
5156    }
5157
5158    pub fn clear(&mut self) { self.forces.clear(); }
5159
5160    pub fn generate_lines(&self) -> Vec<(Vec3, Vec3, Vec4)> {
5161        let mut lines: Vec<(Vec3, Vec3, Vec4)> = self.forces.iter().map(|(orig, f, col)| {
5162            (*orig, *orig + *f, Vec4::new(col.x, col.y, col.z, 1.0))
5163        }).collect();
5164        if self.show_resultant {
5165            let com = Vec3::ZERO;
5166            let res = self.resultant();
5167            lines.push((com, com + res, Vec4::new(1.0, 1.0, 1.0, 1.0)));
5168        }
5169        lines
5170    }
5171}
5172
5173// ============================================================
5174// MESH TOOLS FOR PHYSICS
5175// ============================================================
5176
5177/// Compute signed volume of a triangle mesh (divergence theorem)
5178pub fn mesh_signed_volume(triangles: &[(Vec3, Vec3, Vec3)]) -> f32 {
5179    let mut vol = 0.0f32;
5180    for &(a, b, c) in triangles {
5181        vol += a.dot(b.cross(c)) / 6.0;
5182    }
5183    vol
5184}
5185
5186/// Compute mesh surface area
5187pub fn mesh_surface_area(triangles: &[(Vec3, Vec3, Vec3)]) -> f32 {
5188    triangles.iter().map(|&(a, b, c)| {
5189        (b - a).cross(c - a).length() * 0.5
5190    }).sum()
5191}
5192
5193/// Compute mesh center of mass (uniform density)
5194pub fn mesh_center_of_mass(triangles: &[(Vec3, Vec3, Vec3)]) -> Vec3 {
5195    let mut weighted_sum = Vec3::ZERO;
5196    let mut total_vol = 0.0f32;
5197    for &(a, b, c) in triangles {
5198        let tet_vol = a.dot(b.cross(c)) / 6.0;
5199        let tet_com = (a + b + c) / 4.0; // (0 + a + b + c) / 4
5200        weighted_sum += tet_com * tet_vol;
5201        total_vol += tet_vol;
5202    }
5203    if total_vol.abs() > EPSILON { weighted_sum / total_vol } else { Vec3::ZERO }
5204}
5205
5206/// Compute mesh inertia tensor (uniform density, about origin)
5207pub fn mesh_inertia_tensor(triangles: &[(Vec3, Vec3, Vec3)], density: f32) -> [[f32; 3]; 3] {
5208    let mut i = [[0.0f32; 3]; 3];
5209    for &(a, b, c) in triangles {
5210        let vol = a.dot(b.cross(c)) / 6.0;
5211        let pts = [a, b, c, Vec3::ZERO]; // tetrahedron with origin
5212        // Covariance contribution
5213        let mut cov = [[0.0f32; 3]; 3];
5214        for p in &pts {
5215            let pv = [p.x, p.y, p.z];
5216            for ii in 0..3 {
5217                for jj in 0..3 {
5218                    cov[ii][jj] += pv[ii] * pv[jj];
5219                }
5220            }
5221        }
5222        let scale = density * vol * 0.1; // simplified
5223        let trace = cov[0][0] + cov[1][1] + cov[2][2];
5224        for ii in 0..3 {
5225            for jj in 0..3 {
5226                let delta = if ii == jj { trace } else { 0.0 };
5227                i[ii][jj] += scale * (delta - cov[ii][jj]);
5228            }
5229        }
5230    }
5231    i
5232}
5233
5234// ============================================================
5235// CONSTRAINT SOLVER (PGS — Projected Gauss-Seidel)
5236// ============================================================
5237
5238pub struct ConstraintSolver {
5239    pub iterations: u32,
5240    pub baumgarte: f32,
5241    pub slop: f32,         // penetration slop (allowance before correction)
5242    pub warm_starting: bool,
5243}
5244
5245impl ConstraintSolver {
5246    pub fn new() -> Self {
5247        Self { iterations: 10, baumgarte: 0.2, slop: 0.001, warm_starting: true }
5248    }
5249
5250    /// Solve a single JacobianRow given body masses
5251    pub fn solve_row(
5252        &self, row: &mut JacobianRow,
5253        vel_a: &mut Vec3, omega_a: &mut Vec3, inv_mass_a: f32, inv_inertia_a: Vec3,
5254        vel_b: &mut Vec3, omega_b: &mut Vec3, inv_mass_b: f32, inv_inertia_b: Vec3,
5255    ) {
5256        let delta = row.solve_velocity(*vel_a, *omega_a, *vel_b, *omega_b);
5257        *vel_a += row.j_lin_a * (delta * inv_mass_a);
5258        *omega_a += row.j_ang_a * delta * inv_inertia_a;
5259        *vel_b += row.j_lin_b * (delta * inv_mass_b);
5260        *omega_b += row.j_ang_b * delta * inv_inertia_b;
5261    }
5262
5263    /// Multi-iteration PGS for a set of rows (all against single pair)
5264    pub fn solve_rows(
5265        &self, rows: &mut Vec<JacobianRow>,
5266        vel_a: &mut Vec3, omega_a: &mut Vec3, inv_mass_a: f32, inv_inertia_a: Vec3,
5267        vel_b: &mut Vec3, omega_b: &mut Vec3, inv_mass_b: f32, inv_inertia_b: Vec3,
5268    ) {
5269        for _ in 0..self.iterations {
5270            for row in rows.iter_mut() {
5271                self.solve_row(row,
5272                    vel_a, omega_a, inv_mass_a, inv_inertia_a,
5273                    vel_b, omega_b, inv_mass_b, inv_inertia_b);
5274            }
5275        }
5276    }
5277
5278    /// Setup and warm-start rows before solving
5279    pub fn warm_start(
5280        &self, rows: &[JacobianRow],
5281        vel_a: &mut Vec3, omega_a: &mut Vec3, inv_mass_a: f32, inv_inertia_a: Vec3,
5282        vel_b: &mut Vec3, omega_b: &mut Vec3, inv_mass_b: f32, inv_inertia_b: Vec3,
5283    ) {
5284        if !self.warm_starting { return; }
5285        for row in rows {
5286            let lambda = row.lambda;
5287            *vel_a += row.j_lin_a * (lambda * inv_mass_a);
5288            *omega_a += row.j_ang_a * lambda * inv_inertia_a;
5289            *vel_b += row.j_lin_b * (lambda * inv_mass_b);
5290            *omega_b += row.j_ang_b * lambda * inv_inertia_b;
5291        }
5292    }
5293}
5294
5295// ============================================================
5296// STABILITY ANALYSIS TOOLS
5297// ============================================================
5298
5299pub struct StabilityAnalyzer {
5300    pub energy_history: VecDeque<f32>,
5301    pub momentum_history: VecDeque<Vec3>,
5302    pub angular_momentum_history: VecDeque<Vec3>,
5303    pub history_size: usize,
5304}
5305
5306impl StabilityAnalyzer {
5307    pub fn new(history_size: usize) -> Self {
5308        Self {
5309            energy_history: VecDeque::with_capacity(history_size),
5310            momentum_history: VecDeque::with_capacity(history_size),
5311            angular_momentum_history: VecDeque::with_capacity(history_size),
5312            history_size,
5313        }
5314    }
5315
5316    pub fn record(&mut self, bodies: &HashMap<u64, RigidBodyInspector>) {
5317        let mut total_ke = 0.0f32;
5318        let mut total_mom = Vec3::ZERO;
5319        let mut total_ang = Vec3::ZERO;
5320        for body in bodies.values() {
5321            if body.is_static { continue; }
5322            let inertia_diag = Vec3::new(
5323                body.inertia_tensor.col(0).x,
5324                body.inertia_tensor.col(1).y,
5325                body.inertia_tensor.col(2).z,
5326            );
5327            total_ke += kinetic_energy(body.mass, body.linear_velocity, inertia_diag, body.angular_velocity);
5328            total_ke += potential_energy(body.mass, body.position.y);
5329            total_mom += body.linear_velocity * body.mass;
5330            let r = body.position;
5331            total_ang += r.cross(body.linear_velocity * body.mass);
5332        }
5333        if self.energy_history.len() >= self.history_size { self.energy_history.pop_front(); }
5334        if self.momentum_history.len() >= self.history_size { self.momentum_history.pop_front(); }
5335        if self.angular_momentum_history.len() >= self.history_size { self.angular_momentum_history.pop_front(); }
5336        self.energy_history.push_back(total_ke);
5337        self.momentum_history.push_back(total_mom);
5338        self.angular_momentum_history.push_back(total_ang);
5339    }
5340
5341    pub fn energy_variance(&self) -> f32 {
5342        let n = self.energy_history.len();
5343        if n < 2 { return 0.0; }
5344        let mean = self.energy_history.iter().sum::<f32>() / n as f32;
5345        let var = self.energy_history.iter().map(|e| (e - mean) * (e - mean)).sum::<f32>() / n as f32;
5346        var
5347    }
5348
5349    pub fn is_numerically_stable(&self) -> bool {
5350        let var = self.energy_variance();
5351        let mean = if self.energy_history.is_empty() { 1.0 } else {
5352            self.energy_history.iter().sum::<f32>() / self.energy_history.len() as f32
5353        };
5354        // Stable if variance is < 1% of mean energy
5355        mean.abs() < EPSILON || var / mean.abs() < 0.01
5356    }
5357}
5358
5359// ============================================================
5360// PICKING / DRAGGING TOOLS
5361// ============================================================
5362
5363#[derive(Debug, Clone)]
5364pub struct PhysicsPicker {
5365    pub active: bool,
5366    pub body_id: u64,
5367    pub pick_point_local: Vec3,
5368    pub pick_point_world: Vec3,
5369    pub target_point: Vec3,
5370    pub spring_stiffness: f32,
5371    pub spring_damping: f32,
5372    pub max_force: f32,
5373    pub mouse_ray_origin: Vec3,
5374    pub mouse_ray_dir: Vec3,
5375    pub pick_distance: f32,
5376}
5377
5378impl PhysicsPicker {
5379    pub fn new() -> Self {
5380        Self {
5381            active: false,
5382            body_id: 0,
5383            pick_point_local: Vec3::ZERO,
5384            pick_point_world: Vec3::ZERO,
5385            target_point: Vec3::ZERO,
5386            spring_stiffness: 300.0,
5387            spring_damping: 20.0,
5388            max_force: 500.0,
5389            mouse_ray_origin: Vec3::ZERO,
5390            mouse_ray_dir: Vec3::Z,
5391            pick_distance: 5.0,
5392        }
5393    }
5394
5395    pub fn begin_pick(&mut self, body_id: u64, local_pt: Vec3, world_pt: Vec3, dist: f32) {
5396        self.active = true;
5397        self.body_id = body_id;
5398        self.pick_point_local = local_pt;
5399        self.pick_point_world = world_pt;
5400        self.target_point = world_pt;
5401        self.pick_distance = dist;
5402    }
5403
5404    pub fn end_pick(&mut self) {
5405        self.active = false;
5406    }
5407
5408    pub fn update_target(&mut self, new_ray_origin: Vec3, new_ray_dir: Vec3) {
5409        self.mouse_ray_origin = new_ray_origin;
5410        self.mouse_ray_dir = new_ray_dir;
5411        self.target_point = new_ray_origin + new_ray_dir * self.pick_distance;
5412    }
5413
5414    /// Compute force to apply to the body to drag the pick point to target
5415    pub fn compute_force(&self, body: &RigidBodyInspector) -> Vec3 {
5416        if !self.active { return Vec3::ZERO; }
5417        let current_world_pt = body.position + body.orientation * self.pick_point_local;
5418        let error = self.target_point - current_world_pt;
5419        let vel_at_pt = body.linear_velocity
5420            + body.angular_velocity.cross(body.orientation * self.pick_point_local);
5421        let force = error * self.spring_stiffness - vel_at_pt * self.spring_damping;
5422        let f_len = force.length();
5423        if f_len > self.max_force { force * (self.max_force / f_len) } else { force }
5424    }
5425}
5426
5427// ============================================================
5428// SIMULATION REPLAY
5429// ============================================================
5430
5431#[derive(Debug)]
5432pub struct SimulationReplay {
5433    pub snapshots: Vec<PhysicsSnapshot>,
5434    pub current_frame: usize,
5435    pub playback_speed: f32,
5436    pub looping: bool,
5437    pub playing: bool,
5438    pub time_accumulator: f32,
5439}
5440
5441impl SimulationReplay {
5442    pub fn new() -> Self {
5443        Self {
5444            snapshots: Vec::new(),
5445            current_frame: 0,
5446            playback_speed: 1.0,
5447            looping: false,
5448            playing: false,
5449            time_accumulator: 0.0,
5450        }
5451    }
5452
5453    pub fn record_snapshot(&mut self, snap: PhysicsSnapshot) {
5454        self.snapshots.push(snap);
5455    }
5456
5457    pub fn play(&mut self) { self.playing = true; }
5458    pub fn pause(&mut self) { self.playing = false; }
5459    pub fn stop(&mut self) { self.playing = false; self.current_frame = 0; }
5460
5461    pub fn advance(&mut self, dt: f32) -> Option<&PhysicsSnapshot> {
5462        if !self.playing || self.snapshots.is_empty() { return None; }
5463        self.time_accumulator += dt * self.playback_speed;
5464        // Assume ~60fps snapshots
5465        let frame_dt = 1.0 / 60.0;
5466        while self.time_accumulator >= frame_dt {
5467            self.current_frame += 1;
5468            self.time_accumulator -= frame_dt;
5469        }
5470        if self.current_frame >= self.snapshots.len() {
5471            if self.looping {
5472                self.current_frame = 0;
5473            } else {
5474                self.current_frame = self.snapshots.len().saturating_sub(1);
5475                self.playing = false;
5476            }
5477        }
5478        self.snapshots.get(self.current_frame)
5479    }
5480
5481    pub fn seek_to_time(&mut self, target_time: f32) {
5482        for (i, snap) in self.snapshots.iter().enumerate() {
5483            if snap.time >= target_time {
5484                self.current_frame = i;
5485                return;
5486            }
5487        }
5488        self.current_frame = self.snapshots.len().saturating_sub(1);
5489    }
5490
5491    pub fn duration(&self) -> f32 {
5492        self.snapshots.last().map(|s| s.time).unwrap_or(0.0)
5493    }
5494}
5495
5496// ============================================================
5497// FLUID PARAMETERS PANEL
5498// ============================================================
5499
5500#[derive(Debug, Clone)]
5501pub struct FluidParamsPanel {
5502    pub kernel_radius: f32,
5503    pub rest_density: f32,
5504    pub viscosity: f32,
5505    pub pressure_stiffness: f32,
5506    pub surface_tension: f32,
5507    pub gravity_scale: f32,
5508    pub time_scale: f32,
5509    pub max_particles: usize,
5510    pub particle_mass: f32,
5511    pub domain_size: Vec3,
5512    pub domain_offset: Vec3,
5513    pub boundary_restitution: f32,
5514    pub display_radius_scale: f32,
5515    pub color_by_velocity: bool,
5516    pub color_low_vel: Vec4,
5517    pub color_high_vel: Vec4,
5518    pub color_velocity_scale: f32,
5519}
5520
5521impl Default for FluidParamsPanel {
5522    fn default() -> Self {
5523        Self {
5524            kernel_radius: 0.15,
5525            rest_density: 1000.0,
5526            viscosity: 0.1,
5527            pressure_stiffness: 200.0,
5528            surface_tension: 0.0728,
5529            gravity_scale: 1.0,
5530            time_scale: 1.0,
5531            max_particles: 4096,
5532            particle_mass: 0.02,
5533            domain_size: Vec3::splat(5.0),
5534            domain_offset: Vec3::ZERO,
5535            boundary_restitution: 0.1,
5536            display_radius_scale: 1.5,
5537            color_by_velocity: true,
5538            color_low_vel: Vec4::new(0.0, 0.2, 0.8, 0.8),
5539            color_high_vel: Vec4::new(0.8, 0.8, 1.0, 0.9),
5540            color_velocity_scale: 5.0,
5541        }
5542    }
5543}
5544
5545impl FluidParamsPanel {
5546    pub fn apply_to_sim(&self, sim: &mut FluidSimulation) {
5547        sim.kernel_radius = self.kernel_radius;
5548        sim.rest_density = self.rest_density;
5549        sim.viscosity = self.viscosity;
5550        sim.pressure_stiffness = self.pressure_stiffness;
5551        sim.surface_tension = self.surface_tension;
5552        sim.gravity = Vec3::new(0.0, -GRAVITY * self.gravity_scale, 0.0);
5553        sim.time_scale = self.time_scale;
5554        sim.restitution = self.boundary_restitution;
5555        let half = self.domain_size * 0.5;
5556        sim.domain_min = self.domain_offset - half;
5557        sim.domain_max = self.domain_offset + half;
5558    }
5559
5560    pub fn particle_color(&self, velocity: Vec3) -> Vec4 {
5561        if !self.color_by_velocity {
5562            return self.color_low_vel;
5563        }
5564        let t = (velocity.length() / self.color_velocity_scale).clamp(0.0, 1.0);
5565        Vec4::new(
5566            self.color_low_vel.x + (self.color_high_vel.x - self.color_low_vel.x) * t,
5567            self.color_low_vel.y + (self.color_high_vel.y - self.color_low_vel.y) * t,
5568            self.color_low_vel.z + (self.color_high_vel.z - self.color_low_vel.z) * t,
5569            self.color_low_vel.w + (self.color_high_vel.w - self.color_low_vel.w) * t,
5570        )
5571    }
5572}
5573
5574// ============================================================
5575// DESTRUCTION PANEL
5576// ============================================================
5577
5578#[derive(Debug, Clone)]
5579pub struct DestructionPanel {
5580    pub num_fragments: usize,
5581    pub seed: u64,
5582    pub material_strength: f32,
5583    pub fracture_on_impact: bool,
5584    pub impact_threshold: f32,
5585    pub propagation_speed: f32,
5586    pub debris_lifetime: f32,
5587    pub debris_gravity_scale: f32,
5588    pub debris_air_resistance: f32,
5589    pub sound_on_fracture: bool,
5590    pub particle_effect_on_fracture: bool,
5591    pub fragment_density: f32,
5592    pub auto_remove_small_debris: bool,
5593    pub min_debris_mass: f32,
5594}
5595
5596impl Default for DestructionPanel {
5597    fn default() -> Self {
5598        Self {
5599            num_fragments: 20,
5600            seed: 42,
5601            material_strength: 1e6,
5602            fracture_on_impact: true,
5603            impact_threshold: 1000.0,
5604            propagation_speed: 5000.0,
5605            debris_lifetime: 5.0,
5606            debris_gravity_scale: 1.0,
5607            debris_air_resistance: 0.1,
5608            sound_on_fracture: true,
5609            particle_effect_on_fracture: true,
5610            fragment_density: CONCRETE_DENSITY,
5611            auto_remove_small_debris: true,
5612            min_debris_mass: 0.01,
5613        }
5614    }
5615}
5616
5617// ============================================================
5618// CLOTH PARAMS PANEL
5619// ============================================================
5620
5621#[derive(Debug, Clone)]
5622pub struct ClothParamsPanel {
5623    pub grid_width: usize,
5624    pub grid_height: usize,
5625    pub cell_size: f32,
5626    pub total_mass: f32,
5627    pub stretch_stiffness: f32,
5628    pub shear_stiffness: f32,
5629    pub bend_stiffness: f32,
5630    pub gravity_scale: f32,
5631    pub wind_speed: f32,
5632    pub wind_direction: Vec3,
5633    pub wind_turbulence: f32,
5634    pub drag: f32,
5635    pub self_collision: bool,
5636    pub tearing: bool,
5637    pub tear_threshold: f32,
5638    pub iterations: u32,
5639    pub thickness: f32,
5640    pub visualize_springs: bool,
5641    pub spring_color_stretch: Vec4,
5642    pub spring_color_shear: Vec4,
5643    pub spring_color_bend: Vec4,
5644    pub visualize_normals: bool,
5645}
5646
5647impl Default for ClothParamsPanel {
5648    fn default() -> Self {
5649        Self {
5650            grid_width: 20,
5651            grid_height: 20,
5652            cell_size: 0.1,
5653            total_mass: 0.5,
5654            stretch_stiffness: 1000.0,
5655            shear_stiffness: 500.0,
5656            bend_stiffness: 100.0,
5657            gravity_scale: 1.0,
5658            wind_speed: 0.0,
5659            wind_direction: Vec3::new(1.0, 0.0, 0.0),
5660            wind_turbulence: 0.1,
5661            drag: 0.05,
5662            self_collision: true,
5663            tearing: false,
5664            tear_threshold: 3.0,
5665            iterations: 10,
5666            thickness: 0.001,
5667            visualize_springs: false,
5668            spring_color_stretch: Vec4::new(0.2, 0.8, 0.2, 1.0),
5669            spring_color_shear: Vec4::new(0.8, 0.8, 0.2, 1.0),
5670            spring_color_bend: Vec4::new(0.8, 0.2, 0.2, 1.0),
5671            visualize_normals: false,
5672        }
5673    }
5674}
5675
5676impl ClothParamsPanel {
5677    pub fn apply_to_sim(&self, sim: &mut ClothSimulation) {
5678        sim.stretch_stiffness = self.stretch_stiffness;
5679        sim.shear_stiffness = self.shear_stiffness;
5680        sim.bend_stiffness = self.bend_stiffness;
5681        sim.gravity = Vec3::new(0.0, -GRAVITY * self.gravity_scale, 0.0);
5682        sim.wind_velocity = self.wind_direction.normalize_or_zero() * self.wind_speed;
5683        sim.wind_turbulence = self.wind_turbulence;
5684        sim.drag_coefficient = self.drag;
5685        sim.self_collision_enabled = self.self_collision;
5686        sim.tearing_enabled = self.tearing;
5687        sim.global_tear_threshold = self.tear_threshold;
5688        sim.iterations = self.iterations;
5689        sim.thickness = self.thickness;
5690        // Update spring constants for all springs
5691        for spring in &mut sim.springs {
5692            match spring.spring_type {
5693                ClothSpringType::Stretch => {
5694                    spring.stiffness = self.stretch_stiffness;
5695                    if self.tearing {
5696                        spring.tear_threshold = spring.rest_length * self.tear_threshold;
5697                    }
5698                }
5699                ClothSpringType::Shear => spring.stiffness = self.shear_stiffness,
5700                ClothSpringType::Bend => spring.stiffness = self.bend_stiffness,
5701            }
5702        }
5703    }
5704}
5705
5706// ============================================================
5707// VEHICLE PARAMS PANEL
5708// ============================================================
5709
5710#[derive(Debug, Clone)]
5711pub struct VehicleParamsPanel {
5712    pub mass: f32,
5713    pub wheelbase: f32,
5714    pub track_width: f32,
5715    pub com_height: f32,
5716    pub engine_type: EngineType,
5717    pub max_steering_angle: f32,
5718    pub brake_force_total: f32,
5719    pub brake_bias_front: f32,
5720    pub abs: bool,
5721    pub tcs: bool,
5722    pub esc: bool,
5723    pub diff_type_front: String,
5724    pub diff_type_rear: String,
5725    pub aero_drag: f32,
5726    pub downforce: f32,
5727    pub show_suspension: bool,
5728    pub show_tire_forces: bool,
5729    pub show_rpm: bool,
5730}
5731
5732#[derive(Debug, Clone, PartialEq)]
5733pub enum EngineType {
5734    PetrolSport,
5735    PetrolTurbo,
5736    DieselTruck,
5737    Electric,
5738    Custom,
5739}
5740
5741impl Default for VehicleParamsPanel {
5742    fn default() -> Self {
5743        Self {
5744            mass: 1500.0,
5745            wheelbase: 2.7,
5746            track_width: 1.6,
5747            com_height: 0.5,
5748            engine_type: EngineType::PetrolSport,
5749            max_steering_angle: 35.0,
5750            brake_force_total: 8000.0,
5751            brake_bias_front: 0.6,
5752            abs: true,
5753            tcs: true,
5754            esc: false,
5755            diff_type_front: "Open".to_string(),
5756            diff_type_rear: "LSD".to_string(),
5757            aero_drag: 0.6,
5758            downforce: 0.0,
5759            show_suspension: true,
5760            show_tire_forces: false,
5761            show_rpm: true,
5762        }
5763    }
5764}
5765
5766// ============================================================
5767// MATERIAL EDITOR PANEL
5768// ============================================================
5769
5770#[derive(Debug, Clone)]
5771pub struct MaterialEditorPanel {
5772    pub selected_material: Option<String>,
5773    pub preview_sphere_radius: f32,
5774    pub drop_height: f32,
5775    pub floor_material: String,
5776    pub show_bounce_preview: bool,
5777    pub filter_text: String,
5778    pub sort_by: MaterialSortBy,
5779}
5780
5781#[derive(Debug, Clone, PartialEq)]
5782pub enum MaterialSortBy {
5783    Name,
5784    Friction,
5785    Restitution,
5786    Density,
5787}
5788
5789impl MaterialEditorPanel {
5790    pub fn new() -> Self {
5791        Self {
5792            selected_material: None,
5793            preview_sphere_radius: 0.1,
5794            drop_height: 2.0,
5795            floor_material: "Concrete".to_string(),
5796            show_bounce_preview: true,
5797            filter_text: String::new(),
5798            sort_by: MaterialSortBy::Name,
5799        }
5800    }
5801
5802    /// Simulate a sphere drop and compute bounce height
5803    pub fn compute_bounce_height(&self, restitution: f32) -> f32 {
5804        // h_n = e^2 * h_0
5805        self.drop_height * restitution * restitution
5806    }
5807
5808    /// Filter materials by name substring
5809    pub fn filtered_materials<'a>(&'a self, library: &'a HashMap<String, PhysicsMaterial>) -> Vec<&'a PhysicsMaterial> {
5810        let mut mats: Vec<&PhysicsMaterial> = library.values()
5811            .filter(|m| m.name.to_lowercase().contains(&self.filter_text.to_lowercase()))
5812            .collect();
5813        match self.sort_by {
5814            MaterialSortBy::Name => mats.sort_by(|a, b| a.name.cmp(&b.name)),
5815            MaterialSortBy::Friction => mats.sort_by(|a, b| b.static_friction.partial_cmp(&a.static_friction).unwrap_or(std::cmp::Ordering::Equal)),
5816            MaterialSortBy::Restitution => mats.sort_by(|a, b| b.restitution.partial_cmp(&a.restitution).unwrap_or(std::cmp::Ordering::Equal)),
5817            MaterialSortBy::Density => mats.sort_by(|a, b| b.density.partial_cmp(&a.density).unwrap_or(std::cmp::Ordering::Equal)),
5818        }
5819        mats
5820    }
5821}
5822
5823// ============================================================
5824// RAGDOLL PANEL
5825// ============================================================
5826
5827#[derive(Debug, Clone)]
5828pub struct RagdollPanel {
5829    pub selected_bone: Option<String>,
5830    pub muscle_tone_global: f32,
5831    pub blend_to_animation: bool,
5832    pub blend_time: f32,
5833    pub show_bone_bodies: bool,
5834    pub show_joint_axes: bool,
5835    pub show_muscle_forces: bool,
5836    pub impact_threshold: f32,
5837    pub collapse_on_impact: bool,
5838    pub get_up_enabled: bool,
5839    pub get_up_threshold_velocity: f32,
5840    pub get_up_blend_time: f32,
5841}
5842
5843impl Default for RagdollPanel {
5844    fn default() -> Self {
5845        Self {
5846            selected_bone: None,
5847            muscle_tone_global: 0.0,
5848            blend_to_animation: false,
5849            blend_time: 0.5,
5850            show_bone_bodies: true,
5851            show_joint_axes: true,
5852            show_muscle_forces: false,
5853            impact_threshold: 10.0,
5854            collapse_on_impact: true,
5855            get_up_enabled: true,
5856            get_up_threshold_velocity: 0.5,
5857            get_up_blend_time: 1.0,
5858        }
5859    }
5860}
5861
5862// ============================================================
5863// DEBUG COUNTERS
5864// ============================================================
5865
5866#[derive(Debug, Clone, Default)]
5867pub struct PhysicsStats {
5868    pub active_bodies: usize,
5869    pub sleeping_bodies: usize,
5870    pub static_bodies: usize,
5871    pub active_constraints: usize,
5872    pub contact_pairs: usize,
5873    pub cloth_particles: usize,
5874    pub fluid_particles: usize,
5875    pub debris_particles: usize,
5876    pub solver_iterations_used: u32,
5877    pub step_time_us: u64,
5878    pub broadphase_pairs: usize,
5879    pub narrowphase_contacts: usize,
5880}
5881
5882impl PhysicsStats {
5883    pub fn gather(editor: &PhysicsEditor) -> Self {
5884        let mut stats = PhysicsStats::default();
5885        for body in editor.rigid_bodies.values() {
5886            if body.is_static { stats.static_bodies += 1; }
5887            else if body.sleeping { stats.sleeping_bodies += 1; }
5888            else { stats.active_bodies += 1; }
5889        }
5890        stats.active_constraints = editor.constraints.len();
5891        stats.contact_pairs = editor.contact_visualizer.contacts.len();
5892        for cloth in editor.cloth_sims.values() {
5893            stats.cloth_particles += cloth.particles.len();
5894        }
5895        for fluid in editor.fluid_sims.values() {
5896            stats.fluid_particles += fluid.particles.len();
5897        }
5898        for frac in editor.fracture_systems.values() {
5899            stats.debris_particles += frac.debris_particles.len();
5900        }
5901        stats.solver_iterations_used = editor.world_settings.solver_iterations;
5902        stats
5903    }
5904
5905    pub fn summary(&self) -> String {
5906        format!(
5907            "Bodies: {} active / {} sleeping / {} static | Constraints: {} | Contacts: {} | SPH: {} | Cloth: {}",
5908            self.active_bodies, self.sleeping_bodies, self.static_bodies,
5909            self.active_constraints,
5910            self.contact_pairs,
5911            self.fluid_particles,
5912            self.cloth_particles,
5913        )
5914    }
5915}
5916
5917// ============================================================
5918// DYNAMIC AABB TREE (Bounding Volume Hierarchy)
5919// ============================================================
5920
5921#[derive(Debug, Clone)]
5922pub struct BvhNode {
5923    pub aabb: Aabb,
5924    pub body_id: Option<u64>,
5925    pub left: Option<usize>,
5926    pub right: Option<usize>,
5927    pub parent: Option<usize>,
5928    pub height: i32,
5929}
5930
5931impl BvhNode {
5932    pub fn new_leaf(aabb: Aabb, body_id: u64) -> Self {
5933        Self { aabb, body_id: Some(body_id), left: None, right: None, parent: None, height: 0 }
5934    }
5935    pub fn is_leaf(&self) -> bool { self.left.is_none() }
5936}
5937
5938pub struct DynamicAabbTree {
5939    pub nodes: Vec<BvhNode>,
5940    pub root: Option<usize>,
5941    pub free_list: Vec<usize>,
5942}
5943
5944impl DynamicAabbTree {
5945    pub fn new() -> Self {
5946        Self { nodes: Vec::new(), root: None, free_list: Vec::new() }
5947    }
5948
5949    pub fn alloc_node(&mut self) -> usize {
5950        if let Some(idx) = self.free_list.pop() { return idx; }
5951        let idx = self.nodes.len();
5952        self.nodes.push(BvhNode {
5953            aabb: Aabb::new(Vec3::ZERO, Vec3::ZERO),
5954            body_id: None,
5955            left: None,
5956            right: None,
5957            parent: None,
5958            height: 0,
5959        });
5960        idx
5961    }
5962
5963    pub fn insert(&mut self, body_id: u64, aabb: Aabb) {
5964        let leaf = self.alloc_node();
5965        self.nodes[leaf] = BvhNode::new_leaf(aabb, body_id);
5966        if self.root.is_none() {
5967            self.root = Some(leaf);
5968            return;
5969        }
5970        // Find best sibling
5971        let sibling = self.find_best_sibling(leaf);
5972        let old_parent = self.nodes[sibling].parent;
5973        let new_parent = self.alloc_node();
5974        let merged_aabb = self.nodes[sibling].aabb.merged(&self.nodes[leaf].aabb);
5975        self.nodes[new_parent].aabb = merged_aabb;
5976        self.nodes[new_parent].parent = old_parent;
5977        self.nodes[new_parent].left = Some(sibling);
5978        self.nodes[new_parent].right = Some(leaf);
5979        self.nodes[sibling].parent = Some(new_parent);
5980        self.nodes[leaf].parent = Some(new_parent);
5981        if let Some(op) = old_parent {
5982            if self.nodes[op].left == Some(sibling) {
5983                self.nodes[op].left = Some(new_parent);
5984            } else {
5985                self.nodes[op].right = Some(new_parent);
5986            }
5987        } else {
5988            self.root = Some(new_parent);
5989        }
5990        // Refit ancestors
5991        let mut current = Some(new_parent);
5992        while let Some(idx) = current {
5993            if let (Some(l), Some(r)) = (self.nodes[idx].left, self.nodes[idx].right) {
5994                let la = self.nodes[l].aabb.clone();
5995                let ra = self.nodes[r].aabb.clone();
5996                self.nodes[idx].aabb = la.merged(&ra);
5997                self.nodes[idx].height = 1 + self.nodes[l].height.max(self.nodes[r].height);
5998            }
5999            current = self.nodes[idx].parent;
6000        }
6001    }
6002
6003    fn find_best_sibling(&self, leaf: usize) -> usize {
6004        // Simplified: just return root
6005        self.root.unwrap_or(leaf)
6006    }
6007
6008    pub fn query_aabb(&self, query: &Aabb) -> Vec<u64> {
6009        let mut result = Vec::new();
6010        let mut stack = Vec::new();
6011        if let Some(root) = self.root { stack.push(root); }
6012        while let Some(idx) = stack.pop() {
6013            let node = &self.nodes[idx];
6014            if !node.aabb.intersects(query) { continue; }
6015            if node.is_leaf() {
6016                if let Some(id) = node.body_id { result.push(id); }
6017            } else {
6018                if let Some(l) = node.left { stack.push(l); }
6019                if let Some(r) = node.right { stack.push(r); }
6020            }
6021        }
6022        result
6023    }
6024}
6025
6026// ============================================================
6027// FINAL INTEGRATION TEST / SMOKE TEST FUNCTION
6028// ============================================================
6029
6030pub fn run_physics_editor_smoke_test() -> bool {
6031    let mut editor = PhysicsEditor::new();
6032    // Test rigid body creation
6033    let id = editor.add_rigid_body("TestBox");
6034    if let Some(body) = editor.rigid_bodies.get_mut(&id) {
6035        body.mass = 5.0;
6036        body.shape_type = RigidBodyShapeType::Box;
6037        body.shape_params.half_extents = Vec3::new(1.0, 0.5, 0.5);
6038        body.recompute_inertia();
6039    }
6040    // Test cloth
6041    let cloth_id = editor.add_cloth(10, 10, 0.1);
6042    if let Some(cloth) = editor.cloth_sims.get_mut(&cloth_id) {
6043        cloth.pin_vertex(0);
6044        cloth.pin_vertex(9);
6045    }
6046    // Test fluid
6047    let fluid_id = editor.add_fluid(0.1, 1000.0);
6048    if let Some(fluid) = editor.fluid_sims.get_mut(&fluid_id) {
6049        fluid.spawn_block(Vec3::ZERO, Vec3::splat(0.5), 0.1, 0.02);
6050    }
6051    // Test vehicle
6052    let _veh_id = editor.add_vehicle(1500.0, 2.7, 1.6);
6053    // Test fracture
6054    let frac_id = editor.add_fracture_system(Vec3::ZERO, Vec3::ONE, 10, 1234);
6055    if let Some(frac) = editor.fracture_systems.get_mut(&frac_id) {
6056        frac.material_strength = 1e6;
6057        frac.accumulate_stress(0, 2e6, 0.01);
6058        let fractured = frac.check_fracture();
6059        let frac_ref = editor.fracture_systems.get_mut(&frac_id).unwrap();
6060        if !fractured.is_empty() {
6061            let f2 = fractured.clone();
6062            frac_ref.propagate_cracks(&f2);
6063        }
6064    }
6065    // Test inertia tensor computations
6066    let i_box = inertia_tensor_box(1.0, Vec3::splat(0.5));
6067    let i_sphere = inertia_tensor_sphere(1.0, 0.5);
6068    let i_capsule = inertia_tensor_capsule(1.0, 0.25, 0.5);
6069    let _i_cylinder = inertia_tensor_cylinder(1.0, 0.25, 0.5);
6070    assert!(i_box.col(0).x > 0.0);
6071    assert!(i_sphere.col(0).x > 0.0);
6072    assert!(i_capsule.col(0).x > 0.0);
6073    // Test Pacejka
6074    let (b, c, d, e) = pacejka_dry_asphalt();
6075    let f = pacejka_magic_formula(5.0_f32.to_radians(), b, c, d, e);
6076    assert!(f.abs() > 0.0);
6077    // Test SPH kernels
6078    let r = Vec3::new(0.05, 0.0, 0.0);
6079    let h = 0.1f32;
6080    let k = sph_kernel_poly6(r.length_squared(), h);
6081    assert!(k > 0.0);
6082    // Test AABB
6083    let aabb = Aabb::from_points(&[Vec3::ZERO, Vec3::ONE]);
6084    assert!(aabb.contains_point(Vec3::splat(0.5)));
6085    // Test OBB PCA
6086    let pts: Vec<Vec3> = (0..20).map(|i| Vec3::new(i as f32 * 0.1, 0.0, 0.0)).collect();
6087    let obb = fit_obb_pca(&pts);
6088    assert!(obb.half_extents.length() > 0.0);
6089    // Test Ackermann
6090    let (outer, inner) = ackermann_steering(2.7, 1.6, 0.3);
6091    assert!(inner.abs() > outer.abs()); // inner wheel turns sharper
6092    // Test material library
6093    let lib = build_material_library();
6094    assert!(lib.len() >= 40);
6095    // Test BVH
6096    let mut bvh = DynamicAabbTree::new();
6097    bvh.insert(1, Aabb::new(Vec3::ZERO, Vec3::ONE));
6098    bvh.insert(2, Aabb::new(Vec3::new(0.5, 0.5, 0.5), Vec3::new(1.5, 1.5, 1.5)));
6099    let hits = bvh.query_aabb(&Aabb::new(Vec3::splat(0.4), Vec3::splat(0.6)));
6100    assert!(!hits.is_empty());
6101    // Test stability analyzer
6102    let mut stability = StabilityAnalyzer::new(60);
6103    stability.record(&editor.rigid_bodies);
6104    // Test ragdoll
6105    editor.ragdoll_editor.build_humanoid_skeleton();
6106    assert!(editor.ragdoll_editor.bones.len() > 10);
6107    assert!(editor.ragdoll_editor.joints.len() > 5);
6108    // Simulate a few steps
6109    editor.play();
6110    editor.step(1.0 / 60.0);
6111    editor.step(1.0 / 60.0);
6112    editor.step(1.0 / 60.0);
6113    true
6114}
6115
6116// ============================================================
6117// EDITOR PANEL LAYOUT DESCRIPTOR
6118// ============================================================
6119
6120#[derive(Debug, Clone)]
6121pub struct PhysicsEditorLayout {
6122    pub show_rigid_body_panel: bool,
6123    pub show_constraint_panel: bool,
6124    pub show_joint_editor: bool,
6125    pub show_cloth_panel: bool,
6126    pub show_fluid_panel: bool,
6127    pub show_destruction_panel: bool,
6128    pub show_ragdoll_panel: bool,
6129    pub show_vehicle_panel: bool,
6130    pub show_collision_shape_panel: bool,
6131    pub show_material_panel: bool,
6132    pub show_contact_panel: bool,
6133    pub show_stats: bool,
6134    pub show_world_settings: bool,
6135    pub panel_width: f32,
6136    pub panel_height: f32,
6137    pub selected_tab: PhysicsTab,
6138}
6139
6140#[derive(Debug, Clone, PartialEq)]
6141pub enum PhysicsTab {
6142    RigidBodies,
6143    Constraints,
6144    Cloth,
6145    Fluid,
6146    Destruction,
6147    Ragdoll,
6148    Vehicles,
6149    Shapes,
6150    Materials,
6151    Contacts,
6152    Settings,
6153}
6154
6155impl Default for PhysicsEditorLayout {
6156    fn default() -> Self {
6157        Self {
6158            show_rigid_body_panel: true,
6159            show_constraint_panel: true,
6160            show_joint_editor: true,
6161            show_cloth_panel: false,
6162            show_fluid_panel: false,
6163            show_destruction_panel: false,
6164            show_ragdoll_panel: false,
6165            show_vehicle_panel: false,
6166            show_collision_shape_panel: true,
6167            show_material_panel: true,
6168            show_contact_panel: true,
6169            show_stats: true,
6170            show_world_settings: true,
6171            panel_width: 320.0,
6172            panel_height: 600.0,
6173            selected_tab: PhysicsTab::RigidBodies,
6174        }
6175    }
6176}
6177
6178impl PhysicsEditorLayout {
6179    pub fn tab_label(&self, tab: &PhysicsTab) -> &'static str {
6180        match tab {
6181            PhysicsTab::RigidBodies => "Rigid Bodies",
6182            PhysicsTab::Constraints => "Constraints",
6183            PhysicsTab::Cloth => "Cloth",
6184            PhysicsTab::Fluid => "Fluid",
6185            PhysicsTab::Destruction => "Destruction",
6186            PhysicsTab::Ragdoll => "Ragdoll",
6187            PhysicsTab::Vehicles => "Vehicles",
6188            PhysicsTab::Shapes => "Shapes",
6189            PhysicsTab::Materials => "Materials",
6190            PhysicsTab::Contacts => "Contacts",
6191            PhysicsTab::Settings => "Settings",
6192        }
6193    }
6194}
6195
6196// ============================================================
6197// WIND FIELD
6198// ============================================================
6199
6200#[derive(Debug, Clone)]
6201pub struct WindField {
6202    pub base_velocity: Vec3,
6203    pub gust_strength: f32,
6204    pub gust_frequency: f32,
6205    pub gust_duration: f32,
6206    pub turbulence_scale: f32,
6207    pub turbulence_frequency: f32,
6208    pub enabled: bool,
6209    pub altitude_gradient: f32, // velocity increase per meter of altitude
6210}
6211
6212impl WindField {
6213    pub fn new(base: Vec3) -> Self {
6214        Self {
6215            base_velocity: base,
6216            gust_strength: 5.0,
6217            gust_frequency: 0.2,
6218            gust_duration: 2.0,
6219            turbulence_scale: 1.0,
6220            turbulence_frequency: 0.5,
6221            enabled: true,
6222            altitude_gradient: 0.1,
6223        }
6224    }
6225
6226    pub fn velocity_at(&self, position: Vec3, time: f32) -> Vec3 {
6227        if !self.enabled { return Vec3::ZERO; }
6228        let mut v = self.base_velocity;
6229        // Altitude gradient
6230        let alt_factor = 1.0 + position.y * self.altitude_gradient;
6231        v *= alt_factor;
6232        // Gust (simple sinusoidal)
6233        let gust_phase = time * self.gust_frequency * TWO_PI;
6234        let gust = self.base_velocity.normalize_or_zero() * (gust_phase.sin() * 0.5 + 0.5) * self.gust_strength;
6235        v += gust;
6236        // Turbulence
6237        let tx = (position.x * 0.3 + time * self.turbulence_frequency).sin();
6238        let ty = (position.y * 0.4 + time * self.turbulence_frequency * 1.3).cos();
6239        let tz = (position.z * 0.25 + time * self.turbulence_frequency * 0.7).sin();
6240        v += Vec3::new(tx, ty * 0.3, tz) * self.turbulence_scale;
6241        v
6242    }
6243
6244    pub fn force_on_particle(&self, position: Vec3, time: f32, mass: f32, drag_area: f32) -> Vec3 {
6245        let wind = self.velocity_at(position, time);
6246        let drag = drag_force(AIR_DENSITY, wind.length(), 1.0, drag_area);
6247        wind.normalize_or_zero() * drag
6248    }
6249}
6250
6251// ============================================================
6252// HEIGHTFIELD COLLISION DETECTION
6253// ============================================================
6254
6255pub fn heightfield_closest_point(hf: &HeightfieldShape, query: Vec3) -> Vec3 {
6256    let h = hf.height_at_world(query.x, query.z);
6257    Vec3::new(query.x.clamp(0.0, (hf.width - 1) as f32 * hf.scale.x),
6258              h,
6259              query.z.clamp(0.0, (hf.depth - 1) as f32 * hf.scale.z))
6260}
6261
6262pub fn sphere_heightfield_contact(center: Vec3, radius: f32, hf: &HeightfieldShape) -> Option<(Vec3, Vec3, f32)> {
6263    let closest = heightfield_closest_point(hf, center);
6264    let h = hf.height_at_world(center.x, center.z);
6265    let ground_y = h;
6266    let depth = radius - (center.y - ground_y);
6267    if depth > 0.0 {
6268        let contact_pt = Vec3::new(center.x, ground_y, center.z);
6269        Some((contact_pt, Vec3::Y, depth))
6270    } else {
6271        None
6272    }
6273}
6274
6275// ============================================================
6276// RAG TO POSITION BODY
6277// ============================================================
6278
6279pub fn transform_rigid_body(body: &mut RigidBodyInspector, translation: Vec3, rotation: Quat) {
6280    body.position = translation;
6281    body.orientation = rotation;
6282    if !body.sleeping { body.wake_up(); }
6283}
6284
6285pub fn set_rigid_body_velocity(body: &mut RigidBodyInspector, linear: Vec3, angular: Vec3) {
6286    body.linear_velocity = linear;
6287    body.angular_velocity = angular;
6288    body.wake_up();
6289}
6290
6291// ============================================================
6292// EDITOR GIZMO HELPERS
6293// ============================================================
6294
6295pub fn world_to_screen(world_pos: Vec3, view_proj: Mat4, screen_width: f32, screen_height: f32) -> Vec2 {
6296    let clip = view_proj * Vec4::new(world_pos.x, world_pos.y, world_pos.z, 1.0);
6297    if clip.w.abs() < EPSILON { return Vec2::ZERO; }
6298    let ndc = Vec3::new(clip.x / clip.w, clip.y / clip.w, clip.z / clip.w);
6299    Vec2::new(
6300        (ndc.x * 0.5 + 0.5) * screen_width,
6301        (0.5 - ndc.y * 0.5) * screen_height,
6302    )
6303}
6304
6305pub fn screen_to_world_ray(
6306    screen_x: f32, screen_y: f32,
6307    screen_width: f32, screen_height: f32,
6308    inv_view_proj: Mat4,
6309) -> (Vec3, Vec3) {
6310    let ndc_x = (screen_x / screen_width) * 2.0 - 1.0;
6311    let ndc_y = 1.0 - (screen_y / screen_height) * 2.0;
6312    let near = inv_view_proj * Vec4::new(ndc_x, ndc_y, -1.0, 1.0);
6313    let far  = inv_view_proj * Vec4::new(ndc_x, ndc_y,  1.0, 1.0);
6314    let origin = Vec3::new(near.x / near.w, near.y / near.w, near.z / near.w);
6315    let far_pt = Vec3::new(far.x / far.w, far.y / far.w, far.z / far.w);
6316    let direction = (far_pt - origin).normalize();
6317    (origin, direction)
6318}
6319
6320// ============================================================
6321// IMPULSE RESOLUTION
6322// ============================================================
6323
6324pub fn resolve_collision_impulse(
6325    body_a: &mut RigidBodyInspector,
6326    body_b: &mut RigidBodyInspector,
6327    contact: &ContactPoint,
6328    restitution: f32,
6329) {
6330    let ra = contact.world_position - body_a.position;
6331    let rb = contact.world_position - body_b.position;
6332    let v_a = body_a.linear_velocity + body_a.angular_velocity.cross(ra);
6333    let v_b = body_b.linear_velocity + body_b.angular_velocity.cross(rb);
6334    let v_rel = v_b - v_a;
6335    let v_rel_n = v_rel.dot(contact.world_normal);
6336    if v_rel_n > 0.0 { return; } // separating
6337    let inv_ma = if body_a.is_static { 0.0 } else { 1.0 / body_a.mass };
6338    let inv_mb = if body_b.is_static { 0.0 } else { 1.0 / body_b.mass };
6339    let i_a = Vec3::new(
6340        body_a.inertia_tensor.col(0).x,
6341        body_a.inertia_tensor.col(1).y,
6342        body_a.inertia_tensor.col(2).z,
6343    );
6344    let i_b = Vec3::new(
6345        body_b.inertia_tensor.col(0).x,
6346        body_b.inertia_tensor.col(1).y,
6347        body_b.inertia_tensor.col(2).z,
6348    );
6349    let inv_ia = Vec3::new(
6350        if i_a.x > EPSILON { 1.0 / i_a.x } else { 0.0 },
6351        if i_a.y > EPSILON { 1.0 / i_a.y } else { 0.0 },
6352        if i_a.z > EPSILON { 1.0 / i_a.z } else { 0.0 },
6353    );
6354    let inv_ib = Vec3::new(
6355        if i_b.x > EPSILON { 1.0 / i_b.x } else { 0.0 },
6356        if i_b.y > EPSILON { 1.0 / i_b.y } else { 0.0 },
6357        if i_b.z > EPSILON { 1.0 / i_b.z } else { 0.0 },
6358    );
6359    let n = contact.world_normal;
6360    let ra_cross_n = ra.cross(n);
6361    let rb_cross_n = rb.cross(n);
6362    let denom = inv_ma + inv_mb
6363        + ra_cross_n.dot(inv_ia * ra_cross_n)
6364        + rb_cross_n.dot(inv_ib * rb_cross_n);
6365    if denom < EPSILON { return; }
6366    let j = -(1.0 + restitution) * v_rel_n / denom;
6367    let impulse = n * j;
6368    if !body_a.is_static {
6369        body_a.linear_velocity -= impulse * inv_ma;
6370        body_a.angular_velocity -= inv_ia * ra.cross(impulse);
6371        body_a.wake_up();
6372    }
6373    if !body_b.is_static {
6374        body_b.linear_velocity += impulse * inv_mb;
6375        body_b.angular_velocity += inv_ib * rb.cross(impulse);
6376        body_b.wake_up();
6377    }
6378}
6379
6380// ============================================================
6381// TRIGGER VOLUMES
6382// ============================================================
6383
6384#[derive(Debug, Clone)]
6385pub struct TriggerVolume {
6386    pub id: u64,
6387    pub name: String,
6388    pub aabb: Aabb,
6389    pub bodies_inside: HashSet<u64>,
6390    pub on_enter_events: Vec<String>,
6391    pub on_exit_events: Vec<String>,
6392    pub active: bool,
6393}
6394
6395impl TriggerVolume {
6396    pub fn new(id: u64, name: &str, aabb: Aabb) -> Self {
6397        Self {
6398            id, name: name.to_string(), aabb,
6399            bodies_inside: HashSet::new(),
6400            on_enter_events: Vec::new(),
6401            on_exit_events: Vec::new(),
6402            active: true,
6403        }
6404    }
6405
6406    pub fn update(&mut self, bodies: &HashMap<u64, RigidBodyInspector>) -> (Vec<u64>, Vec<u64>) {
6407        let mut entered = Vec::new();
6408        let mut exited = Vec::new();
6409        let mut now_inside = HashSet::new();
6410        for (id, body) in bodies {
6411            if self.aabb.contains_point(body.position) {
6412                now_inside.insert(*id);
6413                if !self.bodies_inside.contains(id) {
6414                    entered.push(*id);
6415                }
6416            }
6417        }
6418        for id in &self.bodies_inside {
6419            if !now_inside.contains(id) {
6420                exited.push(*id);
6421            }
6422        }
6423        self.bodies_inside = now_inside;
6424        (entered, exited)
6425    }
6426}
6427
6428// ============================================================
6429// JOINT DRIVE PARAMETERS
6430// ============================================================
6431
6432#[derive(Debug, Clone)]
6433pub struct JointDrive {
6434    pub target_position: f32,
6435    pub target_velocity: f32,
6436    pub stiffness: f32,
6437    pub damping: f32,
6438    pub max_force: f32,
6439    pub force_mode: DriveForceMode,
6440    pub enabled: bool,
6441}
6442
6443#[derive(Debug, Clone, PartialEq)]
6444pub enum DriveForceMode { Force, Acceleration }
6445
6446impl JointDrive {
6447    pub fn new(stiffness: f32, damping: f32, max_force: f32) -> Self {
6448        Self { target_position: 0.0, target_velocity: 0.0, stiffness, damping, max_force, force_mode: DriveForceMode::Force, enabled: true }
6449    }
6450    pub fn compute_force(&self, current_pos: f32, current_vel: f32) -> f32 {
6451        if !self.enabled { return 0.0; }
6452        let f = self.stiffness * (self.target_position - current_pos) + self.damping * (self.target_velocity - current_vel);
6453        f.clamp(-self.max_force, self.max_force)
6454    }
6455    pub fn compute_torque(&self, current_angle: f32, current_ang_vel: f32) -> f32 { self.compute_force(current_angle, current_ang_vel) }
6456}
6457
6458#[derive(Debug, Clone)]
6459pub struct DOF6Drive {
6460    pub x_linear: JointDrive,
6461    pub y_linear: JointDrive,
6462    pub z_linear: JointDrive,
6463    pub x_angular: JointDrive,
6464    pub y_angular: JointDrive,
6465    pub z_angular: JointDrive,
6466}
6467impl DOF6Drive {
6468    pub fn new() -> Self {
6469        let d = || JointDrive::new(0.0, 0.0, f32::INFINITY);
6470        Self { x_linear: d(), y_linear: d(), z_linear: d(), x_angular: d(), y_angular: d(), z_angular: d() }
6471    }
6472    pub fn compute_linear_forces(&self, cur: Vec3, vel: Vec3) -> Vec3 {
6473        Vec3::new(self.x_linear.compute_force(cur.x, vel.x), self.y_linear.compute_force(cur.y, vel.y), self.z_linear.compute_force(cur.z, vel.z))
6474    }
6475    pub fn compute_angular_torques(&self, cur: Vec3, omega: Vec3) -> Vec3 {
6476        Vec3::new(self.x_angular.compute_torque(cur.x, omega.x), self.y_angular.compute_torque(cur.y, omega.y), self.z_angular.compute_torque(cur.z, omega.z))
6477    }
6478}
6479
6480// ============================================================
6481// PNEUMATIC SPRING MODEL
6482// ============================================================
6483
6484#[derive(Debug, Clone)]
6485pub struct PneumaticSpring {
6486    pub natural_length: f32,
6487    pub area: f32,
6488    pub initial_pressure: f32,
6489    pub polytropic_index: f32,
6490    pub initial_volume: f32,
6491    pub damping: f32,
6492}
6493impl PneumaticSpring {
6494    pub fn new(natural_length: f32, area: f32, pressure: f32) -> Self {
6495        Self { natural_length, area, initial_pressure: pressure, polytropic_index: 1.4, initial_volume: area * natural_length, damping: 500.0 }
6496    }
6497    pub fn force(&self, current_length: f32, velocity: f32) -> f32 {
6498        let current_volume = self.area * current_length.max(EPSILON);
6499        let ratio = (self.initial_volume / current_volume).powf(self.polytropic_index);
6500        let gauge_pressure = self.initial_pressure * ratio - self.initial_pressure;
6501        gauge_pressure * self.area - self.damping * velocity
6502    }
6503}
6504
6505// ============================================================
6506// CONTACT MANIFOLD MANAGEMENT
6507// ============================================================
6508
6509#[derive(Debug, Clone)]
6510pub struct ContactManifold {
6511    pub body_a: u64,
6512    pub body_b: u64,
6513    pub points: Vec<ContactPoint>,
6514    pub normal: Vec3,
6515    pub friction: f32,
6516    pub restitution: f32,
6517    pub age: u32,
6518    pub persistent_threshold: f32,
6519}
6520impl ContactManifold {
6521    pub fn new(body_a: u64, body_b: u64, friction: f32, restitution: f32) -> Self {
6522        Self { body_a, body_b, points: Vec::new(), normal: Vec3::Y, friction, restitution, age: 0, persistent_threshold: 0.02 }
6523    }
6524    pub fn add_point(&mut self, pt: ContactPoint) {
6525        for existing in &mut self.points {
6526            if (existing.world_position - pt.world_position).length_squared() < self.persistent_threshold * self.persistent_threshold {
6527                existing.warm_impulse = existing.normal_impulse;
6528                existing.normal_impulse = pt.normal_impulse;
6529                existing.world_position = pt.world_position;
6530                existing.penetration_depth = pt.penetration_depth;
6531                return;
6532            }
6533        }
6534        if self.points.len() < 4 { self.points.push(pt); }
6535        else {
6536            let min_idx = self.points.iter().enumerate()
6537                .min_by(|(_, a), (_, b)| a.penetration_depth.partial_cmp(&b.penetration_depth).unwrap_or(std::cmp::Ordering::Equal))
6538                .map(|(i, _)| i).unwrap_or(0);
6539            if pt.penetration_depth > self.points[min_idx].penetration_depth { self.points[min_idx] = pt; }
6540        }
6541    }
6542    pub fn purge_invalid(&mut self) {
6543        self.points.retain(|p| p.penetration_depth > -0.02);
6544        self.age += 1;
6545    }
6546    pub fn total_impulse(&self) -> f32 { self.points.iter().map(|p| p.normal_impulse).sum() }
6547}
6548
6549// ============================================================
6550// SOFT BODY
6551// ============================================================
6552
6553#[derive(Debug, Clone)]
6554pub struct SoftBodyNode {
6555    pub position: Vec3,
6556    pub velocity: Vec3,
6557    pub force: Vec3,
6558    pub mass: f32,
6559    pub inv_mass: f32,
6560    pub fixed: bool,
6561}
6562
6563#[derive(Debug, Clone)]
6564pub struct SoftBodySpring {
6565    pub node_a: usize,
6566    pub node_b: usize,
6567    pub rest_length: f32,
6568    pub stiffness: f32,
6569    pub damping: f32,
6570}
6571
6572#[derive(Debug, Clone)]
6573pub struct SoftBody {
6574    pub nodes: Vec<SoftBodyNode>,
6575    pub springs: Vec<SoftBodySpring>,
6576    pub rest_volume: f32,
6577    pub volume_stiffness: f32,
6578    pub pressure_stiffness: f32,
6579    pub density: f32,
6580    pub total_mass: f32,
6581}
6582
6583impl SoftBody {
6584    pub fn new_sphere(radius: f32, resolution: usize, mass: f32, density: f32) -> Self {
6585        let mut nodes = Vec::new();
6586        let mut springs = Vec::new();
6587        let stacks = resolution;
6588        let slices = resolution * 2;
6589        for i in 0..=stacks {
6590            let phi = PI * i as f32 / stacks as f32;
6591            for j in 0..slices {
6592                let theta = TWO_PI * j as f32 / slices as f32;
6593                let pos = Vec3::new(radius * phi.sin() * theta.cos(), radius * phi.cos(), radius * phi.sin() * theta.sin());
6594                let node_mass = mass / ((stacks + 1) * slices) as f32;
6595                nodes.push(SoftBodyNode { position: pos, velocity: Vec3::ZERO, force: Vec3::ZERO, mass: node_mass, inv_mass: 1.0 / node_mass, fixed: false });
6596            }
6597        }
6598        let n = nodes.len();
6599        for i in 0..n {
6600            for j in i + 1..n {
6601                let dist = (nodes[i].position - nodes[j].position).length();
6602                if dist < radius * 0.8 {
6603                    springs.push(SoftBodySpring { node_a: i, node_b: j, rest_length: dist, stiffness: 1000.0, damping: 5.0 });
6604                }
6605            }
6606        }
6607        let total_mass = nodes.iter().map(|n| n.mass).sum();
6608        Self { nodes, springs, rest_volume: (4.0/3.0)*PI*radius*radius*radius, volume_stiffness: 500.0, pressure_stiffness: 100.0, density, total_mass }
6609    }
6610
6611    pub fn integrate(&mut self, dt: f32, gravity: Vec3) {
6612        for node in &mut self.nodes {
6613            if node.fixed { continue; }
6614            let acc = (node.force + gravity * node.mass) * node.inv_mass;
6615            node.velocity += acc * dt;
6616            node.velocity *= 0.99;
6617            node.position += node.velocity * dt;
6618            node.force = Vec3::ZERO;
6619        }
6620    }
6621
6622    pub fn solve_springs(&mut self) {
6623        for spring in &self.springs {
6624            let a = spring.node_a;
6625            let b = spring.node_b;
6626            let pa = self.nodes[a].position;
6627            let pb = self.nodes[b].position;
6628            let delta = pb - pa;
6629            let dist = delta.length();
6630            if dist < EPSILON { continue; }
6631            let stretch = dist - spring.rest_length;
6632            let n = delta / dist;
6633            let va = self.nodes[a].velocity;
6634            let vb = self.nodes[b].velocity;
6635            let damp = (vb - va).dot(n) * spring.damping;
6636            let force = n * (stretch * spring.stiffness + damp);
6637            let ia = self.nodes[a].inv_mass;
6638            let ib = self.nodes[b].inv_mass;
6639            let total = ia + ib;
6640            if total < EPSILON { continue; }
6641            self.nodes[a].force += force * ia / total;
6642            self.nodes[b].force -= force * ib / total;
6643        }
6644    }
6645
6646    pub fn compute_volume(&self) -> f32 {
6647        let center = self.nodes.iter().fold(Vec3::ZERO, |a, n| a + n.position) / self.nodes.len() as f32;
6648        let max_r = self.nodes.iter().map(|n| (n.position - center).length()).fold(0.0f32, f32::max);
6649        (4.0 / 3.0) * PI * max_r * max_r * max_r
6650    }
6651
6652    pub fn apply_pressure(&mut self) {
6653        let vol = self.compute_volume();
6654        let pressure = self.pressure_stiffness * (self.rest_volume - vol);
6655        let center = self.nodes.iter().fold(Vec3::ZERO, |a, n| a + n.position) / self.nodes.len() as f32;
6656        for node in &mut self.nodes {
6657            node.force += (node.position - center).normalize_or_zero() * pressure;
6658        }
6659    }
6660}
6661
6662// ============================================================
6663// MOTOR PID CONTROLLER
6664// ============================================================
6665
6666#[derive(Debug, Clone)]
6667pub struct MotorVelocityController {
6668    pub kp: f32, pub ki: f32, pub kd: f32,
6669    pub integral: f32,
6670    pub prev_error: f32,
6671    pub output_min: f32,
6672    pub output_max: f32,
6673    pub integral_limit: f32,
6674    pub target: f32,
6675}
6676impl MotorVelocityController {
6677    pub fn new(kp: f32, ki: f32, kd: f32) -> Self {
6678        Self { kp, ki, kd, integral: 0.0, prev_error: 0.0, output_min: -1000.0, output_max: 1000.0, integral_limit: 100.0, target: 0.0 }
6679    }
6680    pub fn update(&mut self, current: f32, dt: f32) -> f32 {
6681        let error = self.target - current;
6682        self.integral = (self.integral + error * dt).clamp(-self.integral_limit, self.integral_limit);
6683        let derivative = (error - self.prev_error) / dt.max(EPSILON);
6684        self.prev_error = error;
6685        (self.kp * error + self.ki * self.integral + self.kd * derivative).clamp(self.output_min, self.output_max)
6686    }
6687    pub fn reset(&mut self) { self.integral = 0.0; self.prev_error = 0.0; }
6688}
6689
6690// ============================================================
6691// INVERSE KINEMATICS
6692// ============================================================
6693
6694/// 2-bone analytical IK solve
6695pub fn two_bone_ik(root: Vec3, bone1_length: f32, bone2_length: f32, target: Vec3, hint: Vec3) -> (Vec3, Vec3) {
6696    let total_len = bone1_length + bone2_length;
6697    let rtt = target - root;
6698    let dist = rtt.length().min(total_len * 0.9999);
6699    let a = bone2_length; let b = bone1_length; let c = dist;
6700    let cos_b = ((b*b + c*c - a*a) / (2.0*b*c)).clamp(-1.0, 1.0);
6701    let angle_b = cos_b.acos();
6702    let forward = rtt.normalize_or_zero();
6703    let right = forward.cross(hint).normalize_or_zero();
6704    let up = right.cross(forward);
6705    let joint1 = root + forward * b * angle_b.cos() + up * b * angle_b.sin();
6706    (joint1, target)
6707}
6708
6709/// FABRIK N-bone chain solver
6710pub fn fabrik_solve(positions: &mut Vec<Vec3>, bone_lengths: &[f32], target: Vec3, iterations: u32, tolerance: f32) {
6711    if positions.len() < 2 || bone_lengths.len() != positions.len() - 1 { return; }
6712    let root = positions[0];
6713    let n = positions.len();
6714    let total_length: f32 = bone_lengths.iter().sum();
6715    if (target - root).length() >= total_length {
6716        let dir = (target - root).normalize_or_zero();
6717        for i in 1..n { positions[i] = positions[i-1] + dir * bone_lengths[i-1]; }
6718        return;
6719    }
6720    for _ in 0..iterations {
6721        *positions.last_mut().unwrap() = target;
6722        for i in (0..n-1).rev() {
6723            let dir = (positions[i] - positions[i+1]).normalize_or_zero();
6724            positions[i] = positions[i+1] + dir * bone_lengths[i];
6725        }
6726        positions[0] = root;
6727        for i in 0..n-1 {
6728            let dir = (positions[i+1] - positions[i]).normalize_or_zero();
6729            positions[i+1] = positions[i] + dir * bone_lengths[i];
6730        }
6731        if (*positions.last().unwrap() - target).length() < tolerance { break; }
6732    }
6733}
6734
6735// ============================================================
6736// FORCE PLATE SENSOR
6737// ============================================================
6738
6739#[derive(Debug, Clone)]
6740pub struct ForcePlate {
6741    pub id: u64,
6742    pub position: Vec3,
6743    pub size: Vec2,
6744    pub total_force: Vec3,
6745    pub total_torque: Vec3,
6746    pub cop: Vec3,
6747    pub active: bool,
6748    pub history: VecDeque<Vec3>,
6749    pub history_max: usize,
6750}
6751impl ForcePlate {
6752    pub fn new(id: u64, position: Vec3, size: Vec2) -> Self {
6753        Self { id, position, size, total_force: Vec3::ZERO, total_torque: Vec3::ZERO, cop: Vec3::ZERO, active: true, history: VecDeque::new(), history_max: 256 }
6754    }
6755    pub fn record_contact(&mut self, contact: &ContactPoint) {
6756        let local_pt = contact.world_position - self.position;
6757        let hx = self.size.x * 0.5; let hz = self.size.y * 0.5;
6758        if local_pt.x.abs() <= hx && local_pt.z.abs() <= hz {
6759            let f = contact.world_normal * contact.normal_impulse;
6760            self.total_force += f;
6761            self.total_torque += local_pt.cross(f);
6762        }
6763    }
6764    pub fn update(&mut self) {
6765        if self.history.len() >= self.history_max { self.history.pop_front(); }
6766        self.history.push_back(self.total_force);
6767        let fy = self.total_force.y.abs();
6768        if fy > EPSILON { self.cop = Vec3::new(-self.total_torque.z / fy, 0.0, self.total_torque.x / fy); }
6769        self.total_force = Vec3::ZERO;
6770        self.total_torque = Vec3::ZERO;
6771    }
6772}
6773
6774// ============================================================
6775// EXPLOSION WAVE
6776// ============================================================
6777
6778#[derive(Debug, Clone)]
6779pub struct ExplosionWave {
6780    pub center: Vec3,
6781    pub initial_radius: f32,
6782    pub current_radius: f32,
6783    pub max_radius: f32,
6784    pub propagation_speed: f32,
6785    pub peak_pressure: f32,
6786    pub decay_exponent: f32,
6787    pub active: bool,
6788    pub time: f32,
6789}
6790impl ExplosionWave {
6791    pub fn new(center: Vec3, peak_pressure: f32, max_radius: f32) -> Self {
6792        Self { center, initial_radius: 0.01, current_radius: 0.01, max_radius, propagation_speed: 340.0, peak_pressure, decay_exponent: 2.0, active: true, time: 0.0 }
6793    }
6794    pub fn update(&mut self, dt: f32) {
6795        self.time += dt;
6796        self.current_radius = self.initial_radius + self.propagation_speed * self.time;
6797        if self.current_radius > self.max_radius { self.active = false; }
6798    }
6799    pub fn pressure_at(&self, dist: f32) -> f32 {
6800        if !self.active || dist < EPSILON { return 0.0; }
6801        if (dist - self.current_radius).abs() > 0.5 { return 0.0; }
6802        self.peak_pressure / (dist / self.initial_radius).powf(self.decay_exponent)
6803    }
6804    pub fn force_on_body(&self, body_pos: Vec3, body_cross_section: f32) -> Vec3 {
6805        let diff = body_pos - self.center;
6806        let dist = diff.length();
6807        let p = self.pressure_at(dist);
6808        if p < EPSILON { return Vec3::ZERO; }
6809        diff.normalize_or_zero() * p * body_cross_section
6810    }
6811}
6812
6813// ============================================================
6814// PHYSICS UNIT CONVERSIONS
6815// ============================================================
6816
6817pub fn kg_to_pounds(kg: f32) -> f32 { kg * 2.20462 }
6818pub fn pounds_to_kg(lbs: f32) -> f32 { lbs / 2.20462 }
6819pub fn meters_to_feet(m: f32) -> f32 { m * 3.28084 }
6820pub fn feet_to_meters(ft: f32) -> f32 { ft / 3.28084 }
6821pub fn nm_to_ftlb(nm: f32) -> f32 { nm * 0.737562 }
6822pub fn ftlb_to_nm(ftlb: f32) -> f32 { ftlb / 0.737562 }
6823pub fn kph_to_ms(kph: f32) -> f32 { kph / 3.6 }
6824pub fn ms_to_kph(ms: f32) -> f32 { ms * 3.6 }
6825pub fn mph_to_ms(mph: f32) -> f32 { mph * 0.44704 }
6826pub fn ms_to_mph(ms: f32) -> f32 { ms / 0.44704 }
6827pub fn pa_to_psi(pa: f32) -> f32 { pa * 0.000145038 }
6828pub fn psi_to_pa(psi: f32) -> f32 { psi / 0.000145038 }
6829pub fn rpm_to_rads(rpm: f32) -> f32 { rpm * TWO_PI / 60.0 }
6830pub fn rads_to_rpm(rads: f32) -> f32 { rads * 60.0 / TWO_PI }
6831pub fn horsepower_to_watts(hp: f32) -> f32 { hp * 745.7 }
6832pub fn watts_to_horsepower(w: f32) -> f32 { w / 745.7 }
6833pub fn kwh_to_joules(kwh: f32) -> f32 { kwh * 3_600_000.0 }
6834pub fn joules_to_kwh(j: f32) -> f32 { j / 3_600_000.0 }
6835
6836// ============================================================
6837// EXTENDED FRICTION MODELS
6838// ============================================================
6839
6840pub fn anisotropic_friction(relative_velocity: Vec3, normal: Vec3, friction_x: f32, friction_z: f32, local_x: Vec3) -> Vec3 {
6841    let local_z = normal.cross(local_x).normalize_or_zero();
6842    let vx = relative_velocity.dot(local_x);
6843    let vz = relative_velocity.dot(local_z);
6844    -(local_x * vx * friction_x + local_z * vz * friction_z)
6845}
6846
6847pub fn rolling_friction_torque(normal_force: f32, roll_radius: f32, mu_r: f32) -> f32 {
6848    mu_r * normal_force * roll_radius
6849}
6850
6851pub fn stribeck_friction(relative_speed: f32, mu_static: f32, mu_kinetic: f32, stribeck_speed: f32) -> f32 {
6852    let delta = mu_static - mu_kinetic;
6853    mu_kinetic + delta * (-relative_speed / stribeck_speed.max(EPSILON)).exp()
6854}
6855
6856// ============================================================
6857// GJK SIMPLEX SOLVER
6858// ============================================================
6859
6860#[derive(Debug, Clone, Default)]
6861pub struct GjkSimplex {
6862    pub points: Vec<Vec3>,
6863}
6864
6865impl GjkSimplex {
6866    pub fn new() -> Self { Self { points: Vec::new() } }
6867    pub fn add(&mut self, p: Vec3) { self.points.push(p); }
6868    pub fn len(&self) -> usize { self.points.len() }
6869
6870    pub fn nearest_simplex(&mut self) -> Option<Vec3> {
6871        match self.len() {
6872            1 => Some(-self.points[0]),
6873            2 => self.line_case(),
6874            3 => self.triangle_case(),
6875            4 => self.tetrahedron_case(),
6876            _ => None,
6877        }
6878    }
6879
6880    fn line_case(&mut self) -> Option<Vec3> {
6881        let a = self.points[1]; let b = self.points[0];
6882        let ab = b - a; let ao = -a;
6883        if ab.dot(ao) > 0.0 {
6884            Some(ab.cross(ao).cross(ab))
6885        } else {
6886            self.points = vec![a];
6887            Some(ao)
6888        }
6889    }
6890
6891    fn triangle_case(&mut self) -> Option<Vec3> {
6892        let a = self.points[2]; let b = self.points[1]; let c = self.points[0];
6893        let ab = b - a; let ac = c - a; let ao = -a;
6894        let abc = ab.cross(ac);
6895        if abc.cross(ac).dot(ao) > 0.0 {
6896            if ac.dot(ao) > 0.0 {
6897                self.points = vec![c, a];
6898                return Some(ac.cross(ao).cross(ac));
6899            }
6900            self.points = vec![b, a];
6901            return self.line_case();
6902        }
6903        if ab.cross(abc).dot(ao) > 0.0 {
6904            self.points = vec![b, a];
6905            return self.line_case();
6906        }
6907        if abc.dot(ao) > 0.0 { Some(abc) } else { self.points = vec![b, c, a]; Some(-abc) }
6908    }
6909
6910    fn tetrahedron_case(&mut self) -> Option<Vec3> {
6911        let a = self.points[3]; let b = self.points[2]; let c = self.points[1]; let d = self.points[0];
6912        let ab = b - a; let ac = c - a; let ad = d - a; let ao = -a;
6913        if ab.cross(ac).dot(ao) > 0.0 {
6914            self.points = vec![c, b, a];
6915            return self.triangle_case();
6916        }
6917        if ac.cross(ad).dot(ao) > 0.0 {
6918            self.points = vec![d, c, a];
6919            return self.triangle_case();
6920        }
6921        if ad.cross(ab).dot(ao) > 0.0 {
6922            self.points = vec![b, d, a];
6923            return self.triangle_case();
6924        }
6925        None
6926    }
6927}
6928
6929pub fn gjk_intersect(
6930    support_a: impl Fn(Vec3) -> Vec3,
6931    support_b: impl Fn(Vec3) -> Vec3,
6932    initial_dir: Vec3, max_iter: u32,
6933) -> bool {
6934    let mut dir = if initial_dir.length_squared() > EPSILON { initial_dir.normalize() } else { Vec3::X };
6935    let mut simplex = GjkSimplex::new();
6936    let p0 = support_a(dir) - support_b(-dir);
6937    simplex.add(p0);
6938    let mut next_dir = -p0;
6939    for _ in 0..max_iter {
6940        if next_dir.length_squared() < EPSILON { return true; }
6941        let p = support_a(next_dir) - support_b(-next_dir);
6942        if p.dot(next_dir) < 0.0 { return false; }
6943        simplex.add(p);
6944        match simplex.nearest_simplex() {
6945            None => return true,
6946            Some(d) => next_dir = d,
6947        }
6948    }
6949    false
6950}
6951
6952// ============================================================
6953// CONVEX HULL 3D
6954// ============================================================
6955
6956#[derive(Debug, Clone)]
6957pub struct ConvexHullFace {
6958    pub vertices: [usize; 3],
6959    pub normal: Vec3,
6960}
6961
6962pub struct ConvexHull3D {
6963    pub vertices: Vec<Vec3>,
6964    pub faces: Vec<ConvexHullFace>,
6965}
6966
6967impl ConvexHull3D {
6968    pub fn from_points(points: &[Vec3]) -> Self {
6969        if points.len() < 4 { return Self { vertices: points.to_vec(), faces: Vec::new() }; }
6970        let mut hull = Self { vertices: points.to_vec(), faces: Vec::new() };
6971        let (i0, i1, i2, i3) = find_initial_tetrahedron(points);
6972        let p0 = points[i0]; let p1 = points[i1]; let p2 = points[i2]; let p3 = points[i3];
6973        let n = (p1 - p0).cross(p2 - p0);
6974        let triplets: [(usize,usize,usize); 4] = if n.dot(p3 - p0) < 0.0 {
6975            [(i0,i1,i2),(i0,i3,i1),(i0,i2,i3),(i1,i3,i2)]
6976        } else {
6977            [(i0,i2,i1),(i0,i1,i3),(i0,i3,i2),(i1,i2,i3)]
6978        };
6979        for (a,b,c) in &triplets {
6980            let normal = (points[*b]-points[*a]).cross(points[*c]-points[*a]).normalize_or_zero();
6981            hull.faces.push(ConvexHullFace { vertices: [*a,*b,*c], normal });
6982        }
6983        hull
6984    }
6985
6986    pub fn volume(&self) -> f32 {
6987        let mut vol = 0.0f32;
6988        for face in &self.faces {
6989            let a = self.vertices[face.vertices[0]];
6990            let b = self.vertices[face.vertices[1]];
6991            let c = self.vertices[face.vertices[2]];
6992            vol += a.dot(b.cross(c));
6993        }
6994        vol.abs() / 6.0
6995    }
6996
6997    pub fn support(&self, dir: Vec3) -> Vec3 {
6998        self.vertices.iter().copied()
6999            .max_by(|a, b| a.dot(dir).partial_cmp(&b.dot(dir)).unwrap_or(std::cmp::Ordering::Equal))
7000            .unwrap_or(Vec3::ZERO)
7001    }
7002
7003    pub fn aabb(&self) -> Aabb { Aabb::from_points(&self.vertices) }
7004}
7005
7006fn find_initial_tetrahedron(points: &[Vec3]) -> (usize, usize, usize, usize) {
7007    let n = points.len();
7008    if n < 4 { return (0, 1.min(n-1), 2.min(n-1), 3.min(n-1)); }
7009    let i0 = 0usize;
7010    let i1 = (0..n).max_by(|&a,&b| (points[a]-points[i0]).length_squared().partial_cmp(&(points[b]-points[i0]).length_squared()).unwrap_or(std::cmp::Ordering::Equal)).unwrap_or(1);
7011    let i2 = (0..n).max_by(|&a,&b| {
7012        let d = (points[i1]-points[i0]).normalize_or_zero();
7013        ((points[a]-points[i0]).cross(d).length_squared()).partial_cmp(&(points[b]-points[i0]).cross(d).length_squared()).unwrap_or(std::cmp::Ordering::Equal)
7014    }).unwrap_or(2);
7015    let n012 = (points[i1]-points[i0]).cross(points[i2]-points[i0]).normalize_or_zero();
7016    let i3 = (0..n).max_by(|&a,&b| (points[a]-points[i0]).dot(n012).abs().partial_cmp(&(points[b]-points[i0]).dot(n012).abs()).unwrap_or(std::cmp::Ordering::Equal)).unwrap_or(3);
7017    (i0, i1, i2, i3)
7018}
7019
7020// ============================================================
7021// PENDULUM CHAIN
7022// ============================================================
7023
7024#[derive(Debug, Clone)]
7025pub struct PendulumChain {
7026    pub nodes: Vec<Vec3>,
7027    pub velocities: Vec<Vec3>,
7028    pub masses: Vec<f32>,
7029    pub lengths: Vec<f32>,
7030    pub fixed: Vec<bool>,
7031    pub gravity: Vec3,
7032    pub iterations: u32,
7033    pub damping: f32,
7034}
7035impl PendulumChain {
7036    pub fn new_simple(num_links: usize, link_length: f32, mass_per_link: f32, pivot: Vec3) -> Self {
7037        let mut nodes = Vec::new();
7038        let mut velocities = Vec::new();
7039        let mut masses = Vec::new();
7040        let mut lengths = Vec::new();
7041        let mut fixed = Vec::new();
7042        for i in 0..=num_links {
7043            nodes.push(pivot + Vec3::new(0.0, -(i as f32 * link_length), 0.0));
7044            velocities.push(Vec3::ZERO);
7045            masses.push(if i == 0 { 0.0 } else { mass_per_link });
7046            fixed.push(i == 0);
7047            if i > 0 { lengths.push(link_length); }
7048        }
7049        Self { nodes, velocities, masses, lengths, fixed, gravity: Vec3::new(0.0, -GRAVITY, 0.0), iterations: 20, damping: 0.999 }
7050    }
7051
7052    pub fn integrate(&mut self, dt: f32) {
7053        let dt2 = dt * dt;
7054        for i in 0..self.nodes.len() {
7055            if self.fixed[i] { continue; }
7056            let prev = self.nodes[i] - self.velocities[i] * dt;
7057            let new_pos = self.nodes[i] * 2.0 - prev + self.gravity * dt2;
7058            self.velocities[i] = (new_pos - self.nodes[i]) / dt;
7059            self.velocities[i] *= self.damping;
7060            self.nodes[i] = new_pos;
7061        }
7062        for _ in 0..self.iterations {
7063            for j in 0..self.lengths.len() {
7064                let a = j; let b = j + 1;
7065                let delta = self.nodes[b] - self.nodes[a];
7066                let dist = delta.length();
7067                if dist < EPSILON { continue; }
7068                let diff = (dist - self.lengths[j]) / dist;
7069                let ma = if self.fixed[a] { f32::INFINITY } else { self.masses[a] };
7070                let mb = if self.fixed[b] { f32::INFINITY } else { self.masses[b] };
7071                let total = 1.0/ma + 1.0/mb;
7072                if total < EPSILON { continue; }
7073                let correction = delta * diff;
7074                if !self.fixed[a] { self.nodes[a] += correction * (1.0/ma) / total; }
7075                if !self.fixed[b] { self.nodes[b] -= correction * (1.0/mb) / total; }
7076            }
7077        }
7078    }
7079
7080    pub fn total_energy(&self) -> f32 {
7081        let mut e = 0.0f32;
7082        for i in 0..self.nodes.len() {
7083            if self.fixed[i] { continue; }
7084            e += 0.5 * self.masses[i] * self.velocities[i].length_squared();
7085            e += self.masses[i] * (-self.gravity.y) * self.nodes[i].y;
7086        }
7087        e
7088    }
7089}
7090
7091// ============================================================
7092// SHALLOW WATER EQUATIONS
7093// ============================================================
7094
7095#[derive(Debug, Clone)]
7096pub struct ShallowWaterSim {
7097    pub width: usize,
7098    pub height: usize,
7099    pub cell_size: f32,
7100    pub depth: Vec<f32>,
7101    pub velocity_x: Vec<f32>,
7102    pub velocity_z: Vec<f32>,
7103    pub base_height: f32,
7104    pub gravity: f32,
7105    pub damping: f32,
7106}
7107impl ShallowWaterSim {
7108    pub fn new(width: usize, height: usize, cell_size: f32, base_h: f32) -> Self {
7109        let n = width * height;
7110        Self { width, height, cell_size, depth: vec![base_h; n], velocity_x: vec![0.0; (width+1)*height], velocity_z: vec![0.0; width*(height+1)], base_height: base_h, gravity: GRAVITY, damping: 0.999 }
7111    }
7112    pub fn perturb(&mut self, cx: usize, cz: usize, amplitude: f32) {
7113        for dz in -2i32..=2 {
7114            for dx in -2i32..=2 {
7115                let x = cx as i32 + dx; let z = cz as i32 + dz;
7116                if x >= 0 && x < self.width as i32 && z >= 0 && z < self.height as i32 {
7117                    let r = ((dx*dx+dz*dz) as f32).sqrt();
7118                    let w = (1.0 - r/3.0).max(0.0);
7119                    self.depth[z as usize * self.width + x as usize] += amplitude * w;
7120                }
7121            }
7122        }
7123    }
7124    pub fn step(&mut self, dt: f32) {
7125        let w = self.width; let h = self.height;
7126        let dx = self.cell_size; let g = self.gravity; let damp = self.damping;
7127        for z in 0..h {
7128            for x in 1..w {
7129                let grad = (self.depth[z*w+x] - self.depth[z*w+(x-1)]) / dx;
7130                let vi = z*(w+1)+x;
7131                self.velocity_x[vi] -= g * grad * dt;
7132                self.velocity_x[vi] *= damp;
7133            }
7134        }
7135        for z in 1..h {
7136            for x in 0..w {
7137                let grad = (self.depth[z*w+x] - self.depth[(z-1)*w+x]) / dx;
7138                let vi = z*w+x;
7139                self.velocity_z[vi] -= g * grad * dt;
7140                self.velocity_z[vi] *= damp;
7141            }
7142        }
7143        let mut new_depth = self.depth.clone();
7144        for z in 0..h {
7145            for x in 0..w {
7146                let vx_r = self.velocity_x[z*(w+1)+x+1];
7147                let vx_l = self.velocity_x[z*(w+1)+x];
7148                let vz_t = if z+1 < h { self.velocity_z[(z+1)*w+x] } else { 0.0 };
7149                let vz_b = self.velocity_z[z*w+x];
7150                new_depth[z*w+x] -= self.depth[z*w+x] * (vx_r - vx_l + vz_t - vz_b) / dx * dt;
7151            }
7152        }
7153        self.depth = new_depth;
7154    }
7155    pub fn height_at(&self, x: usize, z: usize) -> f32 {
7156        if x < self.width && z < self.height { self.depth[z*self.width+x] } else { self.base_height }
7157    }
7158}
7159
7160// ============================================================
7161// PHYSICS ANIMATION CURVES
7162// ============================================================
7163
7164#[derive(Debug, Clone, PartialEq)]
7165pub enum CurveLoopMode { Once, Loop, PingPong }
7166
7167#[derive(Debug, Clone)]
7168pub struct PhysicsPropertyCurve {
7169    pub name: String,
7170    pub keys: Vec<(f32, f32)>,
7171    pub loop_mode: CurveLoopMode,
7172}
7173impl PhysicsPropertyCurve {
7174    pub fn new(name: &str) -> Self { Self { name: name.to_string(), keys: Vec::new(), loop_mode: CurveLoopMode::Once } }
7175    pub fn add_key(&mut self, t: f32, v: f32) {
7176        self.keys.push((t, v));
7177        self.keys.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
7178    }
7179    pub fn evaluate(&self, t: f32) -> f32 {
7180        if self.keys.is_empty() { return 0.0; }
7181        let dur = self.keys.last().map(|k| k.0).unwrap_or(0.0);
7182        let tw = match self.loop_mode {
7183            CurveLoopMode::Once => t.clamp(0.0, dur),
7184            CurveLoopMode::Loop => if dur > EPSILON { t % dur } else { 0.0 },
7185            CurveLoopMode::PingPong => {
7186                if dur > EPSILON { let c = t % (2.0*dur); if c <= dur { c } else { 2.0*dur-c } } else { 0.0 }
7187            }
7188        };
7189        for i in 0..self.keys.len()-1 {
7190            if tw >= self.keys[i].0 && tw <= self.keys[i+1].0 {
7191                let dt = self.keys[i+1].0 - self.keys[i].0;
7192                let frac = if dt > EPSILON { (tw - self.keys[i].0) / dt } else { 0.0 };
7193                return self.keys[i].1 + (self.keys[i+1].1 - self.keys[i].1) * frac;
7194            }
7195        }
7196        self.keys.last().map(|k| k.1).unwrap_or(0.0)
7197    }
7198    pub fn duration(&self) -> f32 { self.keys.last().map(|k| k.0).unwrap_or(0.0) }
7199}
7200
7201// ============================================================
7202// PHYSICS EDITOR COMPLETE
7203// ============================================================