Skip to main content

proof_engine/editor/
bone_rigger.rs

1//! Bone Rigger — click-to-place bone anchors on the SDF surface and define
2//! joint hierarchies for skeletal animation.
3//!
4//! # Overview
5//!
6//! The rigger owns a `BoneSkeleton` — a flat list of `BoneDesc` values wired
7//! into a tree through parent–child relationships.  Each bone has:
8//!   - a `head` and `tail` in object space (placed by clicking on the SDF
9//!     surface)
10//!   - a roll angle around the bone's Y axis
11//!   - optional weight envelope (capsule) parameters that control how much
12//!     each surface particle is influenced by this bone
13//!   - FK and IK flags
14//!
15//! # Interaction flow
16//!
17//! 1. User enters `PlacingHead` mode; clicks on the SDF surface → sets `head`.
18//! 2. User clicks again → sets `tail`, finalises bone and enters `Idle`.
19//! 3. With a bone selected, user can parent it to another bone.
20//! 4. `BoneSkeleton::bind_weights` computes per-particle skinning weights from
21//!    the envelope capsules using a dual-quaternion weighting scheme.
22//!
23//! # IK chains
24//!
25//! `IkChain` represents a chain of bones from a root to an effector tip.
26//! `IkChain::solve_ccd` runs the Cyclic Coordinate Descent algorithm for n
27//! iterations, pulling the tip toward the target.
28
29use glam::{Vec2, Vec3, Vec4, Quat, Mat4};
30use std::collections::HashMap;
31
32// ─────────────────────────────────────────────────────────────────────────────
33// BoneId
34// ─────────────────────────────────────────────────────────────────────────────
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
37pub struct BoneId(pub u32);
38
39impl BoneId {
40    pub const ROOT: BoneId = BoneId(0);
41    pub const NONE: BoneId = BoneId(u32::MAX);
42}
43
44impl Default for BoneId {
45    fn default() -> Self { BoneId::NONE }
46}
47
48impl std::fmt::Display for BoneId {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        if *self == BoneId::NONE { write!(f, "NONE") }
51        else { write!(f, "B{}", self.0) }
52    }
53}
54
55// ─────────────────────────────────────────────────────────────────────────────
56// BoneRole
57// ─────────────────────────────────────────────────────────────────────────────
58
59/// Semantic role of a bone, used by the auto-rigger.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
61pub enum BoneRole {
62    Root,
63    Spine, Chest, Neck, Head,
64    ShoulderL, ShoulderR,
65    UpperArmL, UpperArmR,
66    ForearmL, ForearmR,
67    HandL, HandR,
68    FingerL(u8), FingerR(u8),
69    HipL, HipR,
70    ThighL, ThighR,
71    ShinL, ShinR,
72    FootL, FootR,
73    ToeL, ToeR,
74    Tail(u8),
75    Custom,
76}
77
78impl BoneRole {
79    pub fn label(self) -> String {
80        match self {
81            BoneRole::Root        => "Root".into(),
82            BoneRole::Spine       => "Spine".into(),
83            BoneRole::Chest       => "Chest".into(),
84            BoneRole::Neck        => "Neck".into(),
85            BoneRole::Head        => "Head".into(),
86            BoneRole::ShoulderL   => "Shoulder.L".into(),
87            BoneRole::ShoulderR   => "Shoulder.R".into(),
88            BoneRole::UpperArmL   => "UpperArm.L".into(),
89            BoneRole::UpperArmR   => "UpperArm.R".into(),
90            BoneRole::ForearmL    => "Forearm.L".into(),
91            BoneRole::ForearmR    => "Forearm.R".into(),
92            BoneRole::HandL       => "Hand.L".into(),
93            BoneRole::HandR       => "Hand.R".into(),
94            BoneRole::FingerL(n)  => format!("Finger.L.{n}"),
95            BoneRole::FingerR(n)  => format!("Finger.R.{n}"),
96            BoneRole::HipL        => "Hip.L".into(),
97            BoneRole::HipR        => "Hip.R".into(),
98            BoneRole::ThighL      => "Thigh.L".into(),
99            BoneRole::ThighR      => "Thigh.R".into(),
100            BoneRole::ShinL       => "Shin.L".into(),
101            BoneRole::ShinR       => "Shin.R".into(),
102            BoneRole::FootL       => "Foot.L".into(),
103            BoneRole::FootR       => "Foot.R".into(),
104            BoneRole::ToeL        => "Toe.L".into(),
105            BoneRole::ToeR        => "Toe.R".into(),
106            BoneRole::Tail(n)     => format!("Tail.{n}"),
107            BoneRole::Custom      => "Custom".into(),
108        }
109    }
110
111    pub fn is_left(self) -> bool {
112        matches!(self,
113            BoneRole::ShoulderL | BoneRole::UpperArmL | BoneRole::ForearmL |
114            BoneRole::HandL | BoneRole::FingerL(_) | BoneRole::HipL |
115            BoneRole::ThighL | BoneRole::ShinL | BoneRole::FootL | BoneRole::ToeL
116        )
117    }
118
119    pub fn mirror(self) -> BoneRole {
120        match self {
121            BoneRole::ShoulderL   => BoneRole::ShoulderR,
122            BoneRole::ShoulderR   => BoneRole::ShoulderL,
123            BoneRole::UpperArmL   => BoneRole::UpperArmR,
124            BoneRole::UpperArmR   => BoneRole::UpperArmL,
125            BoneRole::ForearmL    => BoneRole::ForearmR,
126            BoneRole::ForearmR    => BoneRole::ForearmL,
127            BoneRole::HandL       => BoneRole::HandR,
128            BoneRole::HandR       => BoneRole::HandL,
129            BoneRole::FingerL(n)  => BoneRole::FingerR(n),
130            BoneRole::FingerR(n)  => BoneRole::FingerL(n),
131            BoneRole::HipL        => BoneRole::HipR,
132            BoneRole::HipR        => BoneRole::HipL,
133            BoneRole::ThighL      => BoneRole::ThighR,
134            BoneRole::ThighR      => BoneRole::ThighL,
135            BoneRole::ShinL       => BoneRole::ShinR,
136            BoneRole::ShinR       => BoneRole::ShinL,
137            BoneRole::FootL       => BoneRole::FootR,
138            BoneRole::FootR       => BoneRole::FootL,
139            BoneRole::ToeL        => BoneRole::ToeR,
140            BoneRole::ToeR        => BoneRole::ToeL,
141            other                 => other,
142        }
143    }
144}
145
146impl Default for BoneRole { fn default() -> Self { BoneRole::Custom } }
147
148// ─────────────────────────────────────────────────────────────────────────────
149// BoneConstraint
150// ─────────────────────────────────────────────────────────────────────────────
151
152/// Rotation limits for a joint.
153#[derive(Debug, Clone, PartialEq)]
154pub struct BoneConstraint {
155    /// Minimum rotation around local X, Y, Z (radians).
156    pub min_angles: Vec3,
157    /// Maximum rotation around local X, Y, Z (radians).
158    pub max_angles: Vec3,
159    /// Whether to lock each axis.
160    pub locked:     [bool; 3],
161}
162
163impl Default for BoneConstraint {
164    fn default() -> Self {
165        let pi = std::f32::consts::PI;
166        Self {
167            min_angles: Vec3::new(-pi, -pi, -pi),
168            max_angles: Vec3::new( pi,  pi,  pi),
169            locked:     [false; 3],
170        }
171    }
172}
173
174impl BoneConstraint {
175    pub fn hinge(axis: usize, min_deg: f32, max_deg: f32) -> Self {
176        let pi = std::f32::consts::PI;
177        let mut c = Self {
178            min_angles: Vec3::new(-pi, -pi, -pi),
179            max_angles: Vec3::new( pi,  pi,  pi),
180            locked: [true; 3],
181        };
182        c.locked[axis] = false;
183        let to_rad = |d: f32| d * std::f32::consts::PI / 180.0;
184        match axis {
185            0 => { c.min_angles.x = to_rad(min_deg); c.max_angles.x = to_rad(max_deg); }
186            1 => { c.min_angles.y = to_rad(min_deg); c.max_angles.y = to_rad(max_deg); }
187            2 => { c.min_angles.z = to_rad(min_deg); c.max_angles.z = to_rad(max_deg); }
188            _ => {}
189        }
190        c
191    }
192
193    pub fn ball_socket(half_angle_deg: f32) -> Self {
194        let r = half_angle_deg * std::f32::consts::PI / 180.0;
195        Self {
196            min_angles: Vec3::splat(-r),
197            max_angles: Vec3::splat( r),
198            locked: [false; 3],
199        }
200    }
201
202    pub fn clamp_rotation(&self, euler: Vec3) -> Vec3 {
203        let mut out = euler;
204        let mins = [self.min_angles.x, self.min_angles.y, self.min_angles.z];
205        let maxs = [self.max_angles.x, self.max_angles.y, self.max_angles.z];
206        let components = [&mut out.x, &mut out.y, &mut out.z];
207        for (i, v) in components.into_iter().enumerate() {
208            if self.locked[i] {
209                *v = 0.0;
210            } else {
211                *v = v.clamp(mins[i], maxs[i]);
212            }
213        }
214        out
215    }
216}
217
218// ─────────────────────────────────────────────────────────────────────────────
219// BoneDesc
220// ─────────────────────────────────────────────────────────────────────────────
221
222/// Descriptor for a single bone in the skeleton.
223#[derive(Debug, Clone)]
224pub struct BoneDesc {
225    pub id:            BoneId,
226    pub name:          String,
227    pub role:          BoneRole,
228    pub parent:        BoneId,
229    pub children:      Vec<BoneId>,
230    /// Head position in object (rest) space.
231    pub head:          Vec3,
232    /// Tail position in object (rest) space.
233    pub tail:          Vec3,
234    /// Roll angle around the bone's Y axis (radians).
235    pub roll:          f32,
236    /// Envelope capsule radius (for weight painting).
237    pub envelope_radius: f32,
238    /// Envelope head/tail blending distances.
239    pub envelope_head_dist: f32,
240    pub envelope_tail_dist: f32,
241    /// Rest rotation (local space).
242    pub rest_rotation: Quat,
243    /// Current pose rotation (local space, FK only).
244    pub pose_rotation: Quat,
245    /// Current pose translation offset from rest.
246    pub pose_translate: Vec3,
247    /// Constraint on rotation angles.
248    pub constraint:    BoneConstraint,
249    /// Whether this bone participates in IK.
250    pub ik_enabled:    bool,
251    /// IK stretch factor [0, 1].
252    pub ik_stretch:    f32,
253    pub visible:       bool,
254    pub locked:        bool,
255}
256
257impl BoneDesc {
258    pub fn new(id: BoneId, name: impl Into<String>, head: Vec3, tail: Vec3) -> Self {
259        Self {
260            id,
261            name: name.into(),
262            role: BoneRole::Custom,
263            parent: BoneId::NONE,
264            children: Vec::new(),
265            head, tail,
266            roll: 0.0,
267            envelope_radius: 0.05,
268            envelope_head_dist: 0.0,
269            envelope_tail_dist: 0.0,
270            rest_rotation: Quat::IDENTITY,
271            pose_rotation: Quat::IDENTITY,
272            pose_translate: Vec3::ZERO,
273            constraint: BoneConstraint::default(),
274            ik_enabled: false,
275            ik_stretch: 0.0,
276            visible: true,
277            locked: false,
278        }
279    }
280
281    /// Bone vector (head → tail).
282    pub fn vector(&self) -> Vec3 { self.tail - self.head }
283
284    /// Bone length.
285    pub fn length(&self) -> f32 { self.vector().length() }
286
287    /// Bone direction (normalised).
288    pub fn direction(&self) -> Vec3 { self.vector().normalize_or_zero() }
289
290    /// Mid-point of the bone.
291    pub fn midpoint(&self) -> Vec3 { (self.head + self.tail) * 0.5 }
292
293    /// Local bone-space matrix (bone Y = bone direction, X = perp).
294    pub fn bone_matrix(&self) -> Mat4 {
295        let y = self.direction();
296        let ref_up = if y.dot(Vec3::Z).abs() < 0.99 { Vec3::Z } else { Vec3::Y };
297        let x = y.cross(ref_up).normalize_or_zero();
298        let z = x.cross(y);
299        Mat4::from_cols(
300            x.extend(0.0),
301            y.extend(0.0),
302            z.extend(0.0),
303            self.head.extend(1.0),
304        )
305    }
306
307    /// Compute skinning weight for a surface point `p` using capsule envelope.
308    pub fn envelope_weight(&self, p: Vec3) -> f32 {
309        let len = self.length().max(1e-6);
310        let t = (p - self.head).dot(self.direction()) / len;
311        let t_clamped = t.clamp(0.0, 1.0);
312        let closest = self.head + self.direction() * (t_clamped * len);
313        let d = (p - closest).length();
314        let r = self.envelope_radius;
315        if d >= r { return 0.0; }
316        let q = d / r;
317        let w = 1.0 - q * q * (3.0 - 2.0 * q); // smoothstep
318        w.max(0.0)
319    }
320
321    /// FK: compute world-space head position given parent's world matrix.
322    pub fn world_head(&self, parent_world: Mat4) -> Vec3 {
323        parent_world.transform_point3(self.head)
324    }
325
326    /// FK: compute world-space tail position.
327    pub fn world_tail(&self, parent_world: Mat4) -> Vec3 {
328        parent_world.transform_point3(self.tail)
329    }
330}
331
332// ─────────────────────────────────────────────────────────────────────────────
333// IkChain
334// ─────────────────────────────────────────────────────────────────────────────
335
336/// A chain of bone IDs from root to tip used for IK solving.
337#[derive(Debug, Clone)]
338pub struct IkChain {
339    pub name:   String,
340    /// Bones from root (index 0) to effector (last).
341    pub bones:  Vec<BoneId>,
342    pub target: Vec3,
343    pub pole:   Option<Vec3>,
344    pub iterations: u32,
345    pub tolerance:  f32,
346}
347
348impl IkChain {
349    pub fn new(name: impl Into<String>, bones: Vec<BoneId>, target: Vec3) -> Self {
350        Self {
351            name: name.into(),
352            bones,
353            target,
354            pole: None,
355            iterations: 10,
356            tolerance: 0.001,
357        }
358    }
359
360    /// CCD IK solver — modifies bone rotations to reach `target`.
361    /// Returns the distance from tip to target after solving.
362    pub fn solve_ccd(&self, skeleton: &mut BoneSkeleton) -> f32 {
363        for _iter in 0..self.iterations {
364            let tip_pos = self.tip_position(skeleton);
365            let dist = (tip_pos - self.target).length();
366            if dist < self.tolerance { return dist; }
367
368            // Iterate from tip toward root
369            for i in (0..self.bones.len().saturating_sub(1)).rev() {
370                let bone_id = self.bones[i];
371                if let Some(bone) = skeleton.get_mut(bone_id) {
372                    if bone.locked { continue; }
373                }
374                let tip_pos   = self.tip_position(skeleton);
375                let bone_head = skeleton.world_head(bone_id);
376                let to_tip    = (tip_pos    - bone_head).normalize_or_zero();
377                let to_target = (self.target - bone_head).normalize_or_zero();
378                let axis  = to_tip.cross(to_target).normalize_or_zero();
379                let angle = to_tip.dot(to_target).clamp(-1.0, 1.0).acos();
380                if angle.abs() < 1e-5 { continue; }
381                let delta = Quat::from_axis_angle(axis, angle * 0.5);
382                if let Some(bone) = skeleton.get_mut(bone_id) {
383                    let new_rot = delta * bone.pose_rotation;
384                    let euler = new_rot.to_euler(glam::EulerRot::XYZ);
385                    let euler = Vec3::new(euler.0, euler.1, euler.2);
386                    let clamped = bone.constraint.clamp_rotation(euler);
387                    bone.pose_rotation = Quat::from_euler(
388                        glam::EulerRot::XYZ, clamped.x, clamped.y, clamped.z
389                    );
390                }
391            }
392        }
393        let tip_pos = self.tip_position(skeleton);
394        (tip_pos - self.target).length()
395    }
396
397    fn tip_position(&self, skeleton: &BoneSkeleton) -> Vec3 {
398        if let Some(&tip_id) = self.bones.last() {
399            if let Some(bone) = skeleton.get(tip_id) {
400                return bone.tail;
401            }
402        }
403        Vec3::ZERO
404    }
405}
406
407// ─────────────────────────────────────────────────────────────────────────────
408// SkinWeight
409// ─────────────────────────────────────────────────────────────────────────────
410
411/// Up to 4 bone influences per surface point.
412#[derive(Debug, Clone, Default)]
413pub struct SkinWeight {
414    pub bones:   [BoneId; 4],
415    pub weights: [f32; 4],
416}
417
418impl SkinWeight {
419    pub fn new_single(bone: BoneId, weight: f32) -> Self {
420        Self {
421            bones: [bone, BoneId::NONE, BoneId::NONE, BoneId::NONE],
422            weights: [weight, 0.0, 0.0, 0.0],
423        }
424    }
425
426    /// Normalise weights so they sum to 1.
427    pub fn normalise(&mut self) {
428        let sum: f32 = self.weights.iter().sum();
429        if sum > 1e-6 {
430            for w in &mut self.weights { *w /= sum; }
431        }
432    }
433
434    /// Sort influences by weight descending, keeping only top 4.
435    pub fn from_unsorted(mut pairs: Vec<(BoneId, f32)>) -> Self {
436        pairs.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
437        pairs.truncate(4);
438        let mut sw = SkinWeight::default();
439        for (i, (bone, w)) in pairs.into_iter().enumerate() {
440            sw.bones[i] = bone;
441            sw.weights[i] = w;
442        }
443        sw.normalise();
444        sw
445    }
446}
447
448// ─────────────────────────────────────────────────────────────────────────────
449// BoneSkeleton
450// ─────────────────────────────────────────────────────────────────────────────
451
452/// The full skeleton — flat map of bones plus hierarchy helpers.
453#[derive(Debug)]
454pub struct BoneSkeleton {
455    bones:      HashMap<BoneId, BoneDesc>,
456    next_id:    u32,
457    pub root:   BoneId,
458    pub ik_chains: Vec<IkChain>,
459    /// Cached skin weights for the last set of surface points.
460    pub skin_weights: Vec<SkinWeight>,
461}
462
463impl BoneSkeleton {
464    pub fn new() -> Self {
465        Self {
466            bones: HashMap::new(),
467            next_id: 1,
468            root: BoneId::NONE,
469            ik_chains: Vec::new(),
470            skin_weights: Vec::new(),
471        }
472    }
473
474    // ── Bone management ───────────────────────────────────────────────────
475
476    fn alloc_id(&mut self) -> BoneId {
477        let id = BoneId(self.next_id);
478        self.next_id += 1;
479        id
480    }
481
482    pub fn add_bone(&mut self, name: impl Into<String>, head: Vec3, tail: Vec3) -> BoneId {
483        let id = self.alloc_id();
484        let bone = BoneDesc::new(id, name, head, tail);
485        if self.root == BoneId::NONE { self.root = id; }
486        self.bones.insert(id, bone);
487        id
488    }
489
490    pub fn remove_bone(&mut self, id: BoneId) -> Option<BoneDesc> {
491        let bone = self.bones.remove(&id)?;
492        // Unlink from parent
493        if let Some(parent) = self.get_mut(bone.parent) {
494            parent.children.retain(|&c| c != id);
495        }
496        // Orphan children
497        let children: Vec<_> = bone.children.clone();
498        for child_id in children {
499            if let Some(child) = self.get_mut(child_id) {
500                child.parent = BoneId::NONE;
501            }
502        }
503        Some(bone)
504    }
505
506    pub fn get(&self, id: BoneId) -> Option<&BoneDesc> { self.bones.get(&id) }
507    pub fn get_mut(&mut self, id: BoneId) -> Option<&mut BoneDesc> { self.bones.get_mut(&id) }
508
509    pub fn bones(&self) -> impl Iterator<Item = &BoneDesc> { self.bones.values() }
510    pub fn bone_count(&self) -> usize { self.bones.len() }
511
512    // ── Hierarchy ─────────────────────────────────────────────────────────
513
514    pub fn set_parent(&mut self, child: BoneId, parent: BoneId) {
515        // Remove from old parent
516        if let Some(old_parent_id) = self.bones.get(&child).map(|b| b.parent) {
517            if let Some(old_parent) = self.bones.get_mut(&old_parent_id) {
518                old_parent.children.retain(|&c| c != child);
519            }
520        }
521        // Set new parent
522        if let Some(bone) = self.bones.get_mut(&child) {
523            bone.parent = parent;
524        }
525        if let Some(parent_bone) = self.bones.get_mut(&parent) {
526            if !parent_bone.children.contains(&child) {
527                parent_bone.children.push(child);
528            }
529        }
530    }
531
532    pub fn children_of(&self, id: BoneId) -> &[BoneId] {
533        self.bones.get(&id).map(|b| b.children.as_slice()).unwrap_or(&[])
534    }
535
536    pub fn ancestors_of(&self, id: BoneId) -> Vec<BoneId> {
537        let mut result = Vec::new();
538        let mut current = id;
539        while let Some(bone) = self.bones.get(&current) {
540            if bone.parent == BoneId::NONE { break; }
541            result.push(bone.parent);
542            current = bone.parent;
543        }
544        result
545    }
546
547    pub fn depth_of(&self, id: BoneId) -> usize {
548        self.ancestors_of(id).len()
549    }
550
551    // ── Transforms ───────────────────────────────────────────────────────
552
553    /// World-space head of a bone (accumulates parent chain).
554    pub fn world_head(&self, id: BoneId) -> Vec3 {
555        self.world_matrix(id).transform_point3(Vec3::ZERO)
556    }
557
558    /// Accumulated world matrix for a bone's head.
559    pub fn world_matrix(&self, id: BoneId) -> Mat4 {
560        let mut mats = Vec::new();
561        let mut current = id;
562        loop {
563            let Some(bone) = self.bones.get(&current) else { break; };
564            let local = Mat4::from_rotation_translation(bone.pose_rotation, bone.pose_translate);
565            mats.push(local);
566            if bone.parent == BoneId::NONE { break; }
567            current = bone.parent;
568        }
569        let mut result = Mat4::IDENTITY;
570        for m in mats.iter().rev() { result = result * *m; }
571        result
572    }
573
574    // ── Skinning weights ──────────────────────────────────────────────────
575
576    /// Compute smooth bind weights for a list of surface points.
577    pub fn bind_weights(&mut self, surface_points: &[Vec3]) {
578        self.skin_weights.clear();
579        for &p in surface_points {
580            let mut pairs: Vec<(BoneId, f32)> = self.bones.values()
581                .map(|bone| (bone.id, bone.envelope_weight(p)))
582                .filter(|&(_, w)| w > 1e-4)
583                .collect();
584            if pairs.is_empty() {
585                // Assign to nearest bone
586                if let Some(nearest) = self.bones.values()
587                    .min_by(|a, b| {
588                        let da = (a.midpoint() - p).length();
589                        let db = (b.midpoint() - p).length();
590                        da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
591                    })
592                {
593                    pairs.push((nearest.id, 1.0));
594                }
595            }
596            self.skin_weights.push(SkinWeight::from_unsorted(pairs));
597        }
598    }
599
600    // ── Auto-rig ──────────────────────────────────────────────────────────
601
602    /// Build a standard humanoid skeleton from SDF body proportions.
603    pub fn build_humanoid(height: f32) -> Self {
604        let mut sk = BoneSkeleton::new();
605
606        // Scale everything to `height`
607        let h = height;
608        let root_id = sk.add_bone("Root",        Vec3::new(0.0, h * 0.52, 0.0), Vec3::new(0.0, h * 0.55, 0.0));
609        sk.bones.get_mut(&root_id).unwrap().role = BoneRole::Root;
610
611        let spine_id = sk.add_bone("Spine",       Vec3::new(0.0, h * 0.55, 0.0), Vec3::new(0.0, h * 0.68, 0.0));
612        sk.bones.get_mut(&spine_id).unwrap().role = BoneRole::Spine;
613        sk.set_parent(spine_id, root_id);
614
615        let chest_id = sk.add_bone("Chest",       Vec3::new(0.0, h * 0.68, 0.0), Vec3::new(0.0, h * 0.78, 0.0));
616        sk.bones.get_mut(&chest_id).unwrap().role = BoneRole::Chest;
617        sk.set_parent(chest_id, spine_id);
618
619        let neck_id  = sk.add_bone("Neck",        Vec3::new(0.0, h * 0.83, 0.0), Vec3::new(0.0, h * 0.88, 0.0));
620        sk.bones.get_mut(&neck_id).unwrap().role = BoneRole::Neck;
621        sk.set_parent(neck_id, chest_id);
622
623        let head_id  = sk.add_bone("Head",        Vec3::new(0.0, h * 0.88, 0.0), Vec3::new(0.0, h * 1.00, 0.0));
624        sk.bones.get_mut(&head_id).unwrap().role = BoneRole::Head;
625        sk.set_parent(head_id, neck_id);
626
627        // Arms
628        let sh_l = sk.add_bone("Shoulder.L",  Vec3::new(-h*0.14, h*0.78, 0.0), Vec3::new(-h*0.20, h*0.78, 0.0));
629        sk.bones.get_mut(&sh_l).unwrap().role = BoneRole::ShoulderL;
630        sk.set_parent(sh_l, chest_id);
631
632        let ua_l = sk.add_bone("UpperArm.L", Vec3::new(-h*0.20, h*0.78, 0.0), Vec3::new(-h*0.38, h*0.68, 0.0));
633        sk.bones.get_mut(&ua_l).unwrap().role = BoneRole::UpperArmL;
634        sk.set_parent(ua_l, sh_l);
635        let mut c = BoneConstraint::ball_socket(90.0); c.locked[2] = false;
636        sk.bones.get_mut(&ua_l).unwrap().constraint = c;
637
638        let fa_l = sk.add_bone("Forearm.L",  Vec3::new(-h*0.38, h*0.68, 0.0), Vec3::new(-h*0.48, h*0.52, 0.0));
639        sk.bones.get_mut(&fa_l).unwrap().role = BoneRole::ForearmL;
640        sk.set_parent(fa_l, ua_l);
641        sk.bones.get_mut(&fa_l).unwrap().constraint = BoneConstraint::hinge(0, 0.0, 145.0);
642
643        let hand_l = sk.add_bone("Hand.L",    Vec3::new(-h*0.48, h*0.52, 0.0), Vec3::new(-h*0.52, h*0.45, 0.0));
644        sk.bones.get_mut(&hand_l).unwrap().role = BoneRole::HandL;
645        sk.set_parent(hand_l, fa_l);
646
647        let sh_r = sk.add_bone("Shoulder.R",  Vec3::new( h*0.14, h*0.78, 0.0), Vec3::new( h*0.20, h*0.78, 0.0));
648        sk.bones.get_mut(&sh_r).unwrap().role = BoneRole::ShoulderR;
649        sk.set_parent(sh_r, chest_id);
650
651        let ua_r = sk.add_bone("UpperArm.R", Vec3::new( h*0.20, h*0.78, 0.0), Vec3::new( h*0.38, h*0.68, 0.0));
652        sk.bones.get_mut(&ua_r).unwrap().role = BoneRole::UpperArmR;
653        sk.set_parent(ua_r, sh_r);
654
655        let fa_r = sk.add_bone("Forearm.R",  Vec3::new( h*0.38, h*0.68, 0.0), Vec3::new( h*0.48, h*0.52, 0.0));
656        sk.bones.get_mut(&fa_r).unwrap().role = BoneRole::ForearmR;
657        sk.set_parent(fa_r, ua_r);
658        sk.bones.get_mut(&fa_r).unwrap().constraint = BoneConstraint::hinge(0, 0.0, 145.0);
659
660        let hand_r = sk.add_bone("Hand.R",    Vec3::new( h*0.48, h*0.52, 0.0), Vec3::new( h*0.52, h*0.45, 0.0));
661        sk.bones.get_mut(&hand_r).unwrap().role = BoneRole::HandR;
662        sk.set_parent(hand_r, fa_r);
663
664        // Legs
665        let hip_l = sk.add_bone("Hip.L",      Vec3::new(-h*0.10, h*0.52, 0.0), Vec3::new(-h*0.12, h*0.50, 0.0));
666        sk.bones.get_mut(&hip_l).unwrap().role = BoneRole::HipL;
667        sk.set_parent(hip_l, root_id);
668
669        let thigh_l = sk.add_bone("Thigh.L",  Vec3::new(-h*0.12, h*0.50, 0.0), Vec3::new(-h*0.14, h*0.28, 0.0));
670        sk.bones.get_mut(&thigh_l).unwrap().role = BoneRole::ThighL;
671        sk.set_parent(thigh_l, hip_l);
672
673        let shin_l = sk.add_bone("Shin.L",    Vec3::new(-h*0.14, h*0.28, 0.0), Vec3::new(-h*0.14, h*0.06, 0.0));
674        sk.bones.get_mut(&shin_l).unwrap().role = BoneRole::ShinL;
675        sk.set_parent(shin_l, thigh_l);
676        sk.bones.get_mut(&shin_l).unwrap().constraint = BoneConstraint::hinge(0, -145.0, 0.0);
677
678        let foot_l = sk.add_bone("Foot.L",    Vec3::new(-h*0.14, h*0.06, 0.0), Vec3::new(-h*0.14,-h*0.01, h*0.08));
679        sk.bones.get_mut(&foot_l).unwrap().role = BoneRole::FootL;
680        sk.set_parent(foot_l, shin_l);
681
682        let hip_r = sk.add_bone("Hip.R",      Vec3::new( h*0.10, h*0.52, 0.0), Vec3::new( h*0.12, h*0.50, 0.0));
683        sk.bones.get_mut(&hip_r).unwrap().role = BoneRole::HipR;
684        sk.set_parent(hip_r, root_id);
685
686        let thigh_r = sk.add_bone("Thigh.R",  Vec3::new( h*0.12, h*0.50, 0.0), Vec3::new( h*0.14, h*0.28, 0.0));
687        sk.bones.get_mut(&thigh_r).unwrap().role = BoneRole::ThighR;
688        sk.set_parent(thigh_r, hip_r);
689
690        let shin_r = sk.add_bone("Shin.R",    Vec3::new( h*0.14, h*0.28, 0.0), Vec3::new( h*0.14, h*0.06, 0.0));
691        sk.bones.get_mut(&shin_r).unwrap().role = BoneRole::ShinR;
692        sk.set_parent(shin_r, thigh_r);
693        sk.bones.get_mut(&shin_r).unwrap().constraint = BoneConstraint::hinge(0, -145.0, 0.0);
694
695        let foot_r = sk.add_bone("Foot.R",    Vec3::new( h*0.14, h*0.06, 0.0), Vec3::new( h*0.14,-h*0.01, h*0.08));
696        sk.bones.get_mut(&foot_r).unwrap().role = BoneRole::FootR;
697        sk.set_parent(foot_r, shin_r);
698
699        // IK chains for hands and feet
700        sk.ik_chains.push(IkChain::new("IK.Arm.L",
701            vec![ua_l, fa_l, hand_l],
702            sk.bones[&hand_l].tail,
703        ));
704        sk.ik_chains.push(IkChain::new("IK.Arm.R",
705            vec![ua_r, fa_r, hand_r],
706            sk.bones[&hand_r].tail,
707        ));
708        sk.ik_chains.push(IkChain::new("IK.Leg.L",
709            vec![thigh_l, shin_l, foot_l],
710            sk.bones[&foot_l].tail,
711        ));
712        sk.ik_chains.push(IkChain::new("IK.Leg.R",
713            vec![thigh_r, shin_r, foot_r],
714            sk.bones[&foot_r].tail,
715        ));
716
717        sk.root = root_id;
718        sk
719    }
720}
721
722impl Default for BoneSkeleton { fn default() -> Self { Self::new() } }
723
724// ─────────────────────────────────────────────────────────────────────────────
725// RiggerMode
726// ─────────────────────────────────────────────────────────────────────────────
727
728#[derive(Debug, Clone, Copy, PartialEq)]
729pub enum RiggerMode {
730    Idle,
731    PlacingHead,
732    PlacingTail { head: [f32; 3] },
733    WeightPainting { bone: BoneId },
734    EditingConstraint { bone: BoneId },
735    IkSetup,
736}
737
738impl Default for RiggerMode { fn default() -> Self { RiggerMode::Idle } }
739
740// ─────────────────────────────────────────────────────────────────────────────
741// BoneRigger
742// ─────────────────────────────────────────────────────────────────────────────
743
744/// Full bone rigging tool state.
745#[derive(Debug)]
746pub struct BoneRigger {
747    pub skeleton:     BoneSkeleton,
748    pub mode:         RiggerMode,
749    pub selected:     Option<BoneId>,
750    pub hovered:      Option<BoneId>,
751    undo_stack:       Vec<RiggerUndoEntry>,
752    redo_stack:       Vec<RiggerUndoEntry>,
753    /// Whether to show bone axes gizmos.
754    pub show_axes:    bool,
755    /// Whether to show envelope capsules.
756    pub show_envelopes: bool,
757    /// Whether to show IK target spheres.
758    pub show_ik:      bool,
759    /// Whether to show weight colours on surface.
760    pub show_weights: bool,
761    /// Whether X-axis mirror mode is on (mirrors bone placement).
762    pub mirror_x:     bool,
763    /// Display roll angle in degrees vs radians.
764    pub degrees_mode: bool,
765    /// Current pending bone name for placement.
766    pub pending_name: String,
767    /// Current pending bone role.
768    pub pending_role: BoneRole,
769}
770
771#[derive(Debug, Clone)]
772enum RiggerUndoEntry {
773    AddBone(BoneId),
774    RemoveBone(BoneDesc),
775    SetParent { child: BoneId, old_parent: BoneId, new_parent: BoneId },
776    MoveBone  { id: BoneId, old_head: Vec3, old_tail: Vec3, new_head: Vec3, new_tail: Vec3 },
777}
778
779impl BoneRigger {
780    pub fn new() -> Self {
781        Self {
782            skeleton: BoneSkeleton::new(),
783            mode: RiggerMode::Idle,
784            selected: None,
785            hovered: None,
786            undo_stack: Vec::new(),
787            redo_stack: Vec::new(),
788            show_axes: true,
789            show_envelopes: true,
790            show_ik: true,
791            show_weights: false,
792            mirror_x: false,
793            degrees_mode: true,
794            pending_name: "Bone".into(),
795            pending_role: BoneRole::Custom,
796        }
797    }
798
799    // ── Click-to-place ────────────────────────────────────────────────────
800
801    /// User clicks on the SDF surface — advance the placement state machine.
802    pub fn surface_click(&mut self, surface_p: Vec3) {
803        match self.mode {
804            RiggerMode::Idle => {
805                self.mode = RiggerMode::PlacingHead;
806            }
807            RiggerMode::PlacingHead => {
808                self.mode = RiggerMode::PlacingTail { head: surface_p.to_array() };
809            }
810            RiggerMode::PlacingTail { head } => {
811                let head_p = Vec3::from(head);
812                let id = self.skeleton.add_bone(
813                    self.pending_name.clone(), head_p, surface_p,
814                );
815                if let Some(bone) = self.skeleton.get_mut(id) {
816                    bone.role = self.pending_role;
817                }
818                if let Some(sel) = self.selected {
819                    self.skeleton.set_parent(id, sel);
820                }
821                // Mirror if enabled
822                if self.mirror_x {
823                    let mirrored_head = Vec3::new(-head_p.x, head_p.y, head_p.z);
824                    let mirrored_tail = Vec3::new(-surface_p.x, surface_p.y, surface_p.z);
825                    let mid = self.skeleton.add_bone(
826                        format!("{}.mirror", self.pending_name),
827                        mirrored_head, mirrored_tail,
828                    );
829                    if let Some(bone) = self.skeleton.get_mut(mid) {
830                        bone.role = self.pending_role.mirror();
831                    }
832                    if let Some(sel) = self.selected {
833                        self.skeleton.set_parent(mid, sel);
834                    }
835                }
836                self.undo_stack.push(RiggerUndoEntry::AddBone(id));
837                self.redo_stack.clear();
838                self.selected = Some(id);
839                self.mode = RiggerMode::Idle;
840                self.pending_name = format!("Bone.{}", self.skeleton.bone_count());
841            }
842            _ => {}
843        }
844    }
845
846    // ── Selection ─────────────────────────────────────────────────────────
847
848    pub fn select_nearest(&mut self, ray_origin: Vec3, ray_dir: Vec3) -> Option<BoneId> {
849        let mut best_id   = None;
850        let mut best_dist = f32::MAX;
851        for bone in self.skeleton.bones() {
852            // Distance from ray to line segment (head..tail)
853            let ab = bone.tail - bone.head;
854            let ao = ray_origin - bone.head;
855            let ab_len = ab.length().max(1e-6);
856            let t = ao.dot(ab) / (ab_len * ab_len);
857            let closest = bone.head + ab * t.clamp(0.0, 1.0);
858            let d = (closest - ray_origin).length();
859            if d < best_dist {
860                best_dist = d;
861                best_id = Some(bone.id);
862            }
863        }
864        self.selected = best_id;
865        best_id
866    }
867
868    // ── Undo ──────────────────────────────────────────────────────────────
869
870    pub fn undo(&mut self) {
871        if let Some(entry) = self.undo_stack.pop() {
872            match entry {
873                RiggerUndoEntry::AddBone(id) => { self.skeleton.remove_bone(id); }
874                RiggerUndoEntry::RemoveBone(bone) => {
875                    self.skeleton.bones.insert(bone.id, bone);
876                }
877                RiggerUndoEntry::MoveBone { id, old_head, old_tail, .. } => {
878                    if let Some(bone) = self.skeleton.get_mut(id) {
879                        bone.head = old_head;
880                        bone.tail = old_tail;
881                    }
882                }
883                RiggerUndoEntry::SetParent { child, old_parent, .. } => {
884                    self.skeleton.set_parent(child, old_parent);
885                }
886            }
887        }
888    }
889
890    // ── Display ───────────────────────────────────────────────────────────
891
892    pub fn status_line(&self) -> String {
893        let mode = match self.mode {
894            RiggerMode::Idle         => "Idle".to_string(),
895            RiggerMode::PlacingHead  => "Click SDF surface for bone HEAD".to_string(),
896            RiggerMode::PlacingTail{..} => "Click SDF surface for bone TAIL".to_string(),
897            RiggerMode::WeightPainting{bone} => format!("Weight-painting bone {}", bone),
898            RiggerMode::EditingConstraint{bone} => format!("Editing constraint on {}", bone),
899            RiggerMode::IkSetup      => "IK Setup".to_string(),
900        };
901        format!(
902            "Bone Rigger — {} | {} bones | sel {:?} | {}",
903            mode, self.skeleton.bone_count(), self.selected,
904            if self.mirror_x { "MIRROR" } else { "" }
905        )
906    }
907}
908
909impl Default for BoneRigger { fn default() -> Self { Self::new() } }
910
911// ─────────────────────────────────────────────────────────────────────────────
912// Tests
913// ─────────────────────────────────────────────────────────────────────────────
914
915#[cfg(test)]
916mod tests {
917    use super::*;
918
919    #[test]
920    fn humanoid_skeleton_builds() {
921        let sk = BoneSkeleton::build_humanoid(1.8);
922        assert!(sk.bone_count() > 15);
923        assert!(sk.root != BoneId::NONE);
924    }
925
926    #[test]
927    fn envelope_weight() {
928        let sk = BoneSkeleton::build_humanoid(1.8);
929        for bone in sk.bones() {
930            let mid = bone.midpoint();
931            let w = bone.envelope_weight(mid);
932            // midpoint should always have some weight for a default radius
933            // (only if radius > 0)
934            if bone.envelope_radius > 0.0 {
935                assert!(w >= 0.0);
936            }
937        }
938    }
939
940    #[test]
941    fn skin_weight_normalises() {
942        let mut sw = SkinWeight::from_unsorted(vec![
943            (BoneId(1), 2.0), (BoneId(2), 3.0), (BoneId(3), 1.0),
944        ]);
945        sw.normalise();
946        let sum: f32 = sw.weights.iter().sum();
947        assert!((sum - 1.0).abs() < 1e-5);
948    }
949
950    #[test]
951    fn constraint_hinge_clamps() {
952        let c = BoneConstraint::hinge(0, -90.0, 0.0);
953        let e = Vec3::new(2.0, 0.5, 0.1);
954        let clamped = c.clamp_rotation(e);
955        let max_x = 0.0f32;
956        assert!(clamped.x <= max_x + 1e-5);
957        assert!((clamped.y).abs() < 1e-5); // locked
958        assert!((clamped.z).abs() < 1e-5); // locked
959    }
960
961    #[test]
962    fn rigger_place_bone() {
963        let mut r = BoneRigger::new();
964        r.surface_click(Vec3::ZERO);    // enter PlacingHead
965        r.surface_click(Vec3::Y);       // set head
966        r.surface_click(Vec3::Y * 2.0); // set tail, finalise
967        assert_eq!(r.skeleton.bone_count(), 1);
968    }
969
970    #[test]
971    fn rigger_undo() {
972        let mut r = BoneRigger::new();
973        r.surface_click(Vec3::ZERO);
974        r.surface_click(Vec3::Y);
975        r.surface_click(Vec3::Y * 2.0);
976        assert_eq!(r.skeleton.bone_count(), 1);
977        r.undo();
978        assert_eq!(r.skeleton.bone_count(), 0);
979    }
980}