Editor/AutoRig/RigPreviewWidget.cs

Editor widget that previews a rigged mesh. It draws a capped wireframe and a stick-skeleton, runs a per-joint "wiggle" deformation test, allows dragging joints to translate subtrees, and CPU-skins a cached subset of vertices for preview solid rendering.

NetworkingFile Access
using System;
using System.Collections.Generic;
using System.Linq;
using AutoRig.Mesh;
using AutoRig.Rig;
using Editor;
using Sandbox;
using VecN = System.Numerics.Vector3;
using QuatN = System.Numerics.Quaternion;

namespace AutoRig.Editor;

/// <summary>
/// Pre-compile rig preview: the source mesh as a capped wireframe and the generated
/// skeleton as stick bones with joint markers, in an orbitable editor scene (same
/// idiom as humanoid-retargeter's PreviewWidget). The wiggle test rotates each joint
/// ±15° in sequence and CPU-skins the wireframe so a novice can SEE whether the rig
/// deforms sensibly before exporting.
/// </summary>
public sealed class RigPreviewWidget : SceneRenderingWidget
{
    const int MaxWireSegments = 9000;
    const int MaxSkinnedVertices = 150_000;
    const float WiggleDegrees = 15f;
    const float SecondsPerJoint = 0.9f;

    SceneLineObject _wire;
    SceneLineObject _bones;

    RigMesh _mesh;
    RigResult _rig;

    // Precomputed FK/skin state.
    VecN[] _bindWorld;          // joint bind positions
    VecN[] _posedWorld;         // joint posed positions (wiggle)
    QuatN[] _poseRotation;      // per-joint world rotation during wiggle
    int[] _wireVertexIndices;   // mesh vertex ids used by the capped wireframe, unique
    Vector3[] _wirePositions;   // posed positions for those vertices
    Dictionary<int, int> _wireSlotOf;
    (int A, int B)[] _wireSegments;

    float _yaw = 35f;
    float _zoom = 1f;
    int _wiggleFrame;
    Vector2 _lastMouse;
    float _wiggleTime = -1f;    // < 0 = not wiggling
    bool _skinnedWiggle;

    // ComputeBounds is O(all vertices); it was being called every frame from
    // both UpdateCamera and RedrawWire, which pegged a core with a big mesh in
    // the preview. Cache the extent + center once per SetRig instead.
    float _boundsLength = 1f;
    Vector3 _boundsCenter;      // scene-space
    int _dragJoint = -1;        // joint being dragged (move/deform preview), or -1
    bool _dragMoved;            // this gesture actually moved the joint
    bool _posedActive;          // the live pose diverges from the bind pose

    /// <summary>Fired once, at the first movement of a joint-drag, BEFORE the
    /// pose changes - so the dialog can snapshot for undo.</summary>
    public Action BeforeJointMove { get; set; }

    /// <summary>Ctrl+Z inside the viewport (the widget usually holds focus after
    /// a click/drag) - the dialog runs its undo.</summary>
    public Action UndoRequested { get; set; }

    protected override void OnKeyPress( KeyEvent e )
    {
        if ( e.Key == KeyCode.Z && e.HasCtrl )
        {
            UndoRequested?.Invoke();
            return;
        }
        base.OnKeyPress( e );
    }

    /// <summary>The currently highlighted joint name ("" when none).</summary>
    public string SelectedJoint => _highlight;

    /// <summary>Current live joint positions (mesh space), for undo snapshots.</summary>
    public VecN[] GetPose() => _posedWorld is null ? null : (VecN[])_posedWorld.Clone();

    /// <summary>Restores a joint pose captured by <see cref="GetPose"/>.</summary>
    public void SetPose( VecN[] pose )
    {
        if ( pose is null || _posedWorld is null || pose.Length != _posedWorld.Length )
            return;
        Array.Copy( pose, _posedWorld, pose.Length );
        _posedActive = false;
        for ( var i = 0; i < _posedWorld.Length && !_posedActive; i++ )
            if ( _bindWorld is not null && _posedWorld[i] != _bindWorld[i] )
                _posedActive = true;
        if ( _skinnedWiggle )
            RebuildSolid( posed: _posedActive );
        RebuildSkeletonGeometry();
    }

    /// <summary>True while the wiggle test runs.</summary>
    public bool Wiggling => _wiggleTime >= 0f;

    /// <summary>Name of the joint currently wiggling ("" when idle).</summary>
    public string WigglingJoint { get; private set; } = "";

    SceneObject _solid;

    public RigPreviewWidget( Widget parent ) : base( parent )
    {
        MinimumSize = new Vector2( 320, 320 );
        MouseTracking = true;

        Scene = Scene.CreateEditorScene();
        using ( Scene.Push() )
        {
            Camera = new GameObject( true, "camera" ).GetOrAddComponent<CameraComponent>( false );
            Camera.BackgroundColor = Theme.ControlBackground;
            Camera.ZNear = 0.01f;
            Camera.ZFar = 8192f;
            Camera.FieldOfView = 45f;
            Camera.Enabled = true;
        }

        // Same lighting rig as humanoid-retargeter's preview: a warm key and a
        // cooler fill, no shadows (lines don't cast; the solid mesh reads cleanly).
        var world = Scene.SceneWorld;
        new ScenePointLight( world, new Vector3( 120, 100, 120 ), 600, Color.White * 3.5f ).ShadowsEnabled = false;
        new ScenePointLight( world, new Vector3( -120, -100, 90 ), 600, Color.White * 2.0f ).ShadowsEnabled = false;

        _wire = new SceneLineObject( world ) { Opaque = false, Lighting = false };
        // No overlay-layer tricks: that pass is not rendered by this widget's
        // camera (bones vanished entirely). Rigged view hides the solid instead,
        // so the skeleton inside the translucent wireframe is always visible.
        _bones = new SceneLineObject( world ) { Opaque = false, Lighting = false };
    }

    /// <summary>Builds a lit solid model from the raw mesh (the wireframe alone was
    /// nearly invisible: fixed-width translucent lines, unlit scene).</summary>
    /// <param name="posed">CPU-skin against the current wiggle pose (the solid IS
    /// the model the user watches - the line wireframe never renders here).</param>
    void RebuildSolid( bool posed = false )
    {
        _solid?.Delete();
        _solid = null;
        if ( _mesh is null || _mesh.TriangleCount == 0 )
            return;

        try
        {
            var material = TexturedMaterial()
                ?? Material.Load( "materials/dev/reflectivity_30.vmat" )
                ?? Material.Load( "materials/default/white.vmat" );
            var sceneMesh = new Sandbox.Mesh( material );
            var vertices = new List<SimpleVertex>( _mesh.Triangles.Length );
            for ( var t = 0; t < _mesh.TriangleCount; t++ )
            {
                for ( var k = 0; k < 3; k++ )
                {
                    var v = _mesh.Triangles[t * 3 + k];
                    var p = posed && _rig is not null ? SkinVertex( v ) : _mesh.Positions[v];
                    var n = v < _mesh.Normals.Length ? _mesh.Normals[v] : System.Numerics.Vector3.UnitZ;
                    var uv = v < _mesh.Uvs.Length ? _mesh.Uvs[v] : default;
                    vertices.Add( new SimpleVertex(
                        new Vector3( p.X, p.Y, p.Z ),
                        new Vector3( n.X, n.Y, n.Z ),
                        Vector3.Zero,
                        new Vector2( uv.X, uv.Y ) ) );
                }
            }
            sceneMesh.CreateVertexBuffer( vertices.Count, SimpleVertex.Layout, vertices );
            var model = Model.Builder.AddMesh( sceneMesh ).Create();
            _solid = new SceneObject( Scene.SceneWorld, model,
                new Transform( Vector3.Zero, Rotation.FromAxis( new Vector3( 1, 0, 0 ), 90f ) ) )
            {
                ColorTint = new Color( 0.62f, 0.66f, 0.72f ),
            };
            if ( _rig is not null )
            {
                // Ghost immediately (per-frame rebuilds must not flicker opaque).
                _solid.ColorTint = new Color( 0.62f, 0.66f, 0.72f, 0.3f );
                _solid.Flags.IsTranslucent = true;
                _solid.Flags.IsOpaque = false;
            }
        }
        catch ( Exception )
        {
            _solid?.Delete();
            _solid = null;   // wireframe fallback still draws below
        }
    }

    Material _texturedMaterial;
    bool _texturedTried;

    /// <summary>The mesh's base-color image (glTF/FBX embedded texture) as a
    /// preview material, decoded once. Null when absent/undecodable - the flat
    /// dev material stands in.</summary>
    Material TexturedMaterial()
    {
        if ( _texturedTried )
            return _texturedMaterial;
        _texturedTried = true;
        var image = _mesh?.Materials?.FirstOrDefault( m => m.BaseColorImage is not null )
            ?.BaseColorImage;
        if ( image is null )
            return null;
        try
        {
            var bitmap = Bitmap.CreateFromBytes( image );
            if ( bitmap is null )
                return null;
            var texture = bitmap.ToTexture();
            var material = Material.Create( $"autorig_preview_{GetHashCode()}", "simple" );
            material.Set( "Color", texture );
            _texturedMaterial = material;
            return material;
        }
        catch ( Exception )
        {
            return null;
        }
    }

    /// <summary>Installs the mesh + rig to display (null rig = mesh only).</summary>
    public void SetRig( RigMesh mesh, RigResult rig )
    {
        _mesh = mesh;
        _rig = rig;
        _wiggleTime = -1f;
        WigglingJoint = "";

        if ( mesh is null )
        {
            _wire.Clear();
            _bones.Clear();
            _solid?.Delete();
            _solid = null;
            _skeletonObject?.Delete();
            _skeletonObject = null;
            _boneObject?.Delete();
            _boneObject = null;
            _highlightObject?.Delete();
            _highlightObject = null;
            return;
        }

        var meshBounds = mesh.ComputeBounds();
        _boundsLength = MathF.Max( meshBounds.Size.Length(), 1e-3f );
        _boundsCenter = ToVector3( meshBounds.Center );
        RebuildSolid();
        BuildWireTopology();
        if ( rig is not null )
        {
            _bindWorld = new VecN[rig.Skeleton.Joints.Count];
            _posedWorld = new VecN[rig.Skeleton.Joints.Count];
            _poseRotation = new QuatN[rig.Skeleton.Joints.Count];
            for ( var i = 0; i < rig.Skeleton.Joints.Count; i++ )
            {
                _bindWorld[i] = rig.Skeleton.Joints[i].Position;
                _posedWorld[i] = _bindWorld[i];
                _poseRotation[i] = QuatN.Identity;
            }
            // No vertex cap: a static model during the wiggle test defeats the test.
            // Heavy meshes throttle the rebuild rate instead (see PreFrame).
            _skinnedWiggle = rig.Weights is not null;
        }

        RedrawWire( bindPose: true );
        RedrawBones();
        RebuildSkeletonGeometry();
        UpdateLineBounds();
    }

    /// <summary>SceneLineObjects keep tiny default bounds and get frustum-culled -
    /// the reason neither the wireframe nor the skeleton ever showed. Give both the
    /// mesh's (scene-space) bounds, padded.</summary>
    void UpdateLineBounds()
    {
        if ( _mesh is null )
            return;
        var bounds = _mesh.ComputeBounds();
        var a = ToVector3( bounds.Min );
        var b = ToVector3( bounds.Max );
        var box = new BBox( Vector3.Min( a, b ), Vector3.Max( a, b ) );
        box = box.Grow( bounds.Size.Length() * 0.5f + 1f );
        _wire.Bounds = box;
        _bones.Bounds = box;
    }

    /// <summary>Starts the wiggle test (each joint ±15° in sequence; root skipped).</summary>
    public void StartWiggle()
    {
        if ( _rig is null )
            return;
        _wiggleTime = 0f;
        _wiggleFrame = 0;
        Log.Info( $"[auto-rig] wiggle: {_mesh?.Positions.Length ?? 0} verts, "
            + $"skinned deform = {_skinnedWiggle}" );
    }

    /// <summary>Stops the wiggle test and returns to the bind pose.</summary>
    public void StopWiggle()
    {
        _wiggleTime = -1f;
        WigglingJoint = "";
        if ( _rig is not null )
        {
            for ( var i = 0; i < _poseRotation.Length; i++ )
            {
                _poseRotation[i] = QuatN.Identity;
                _posedWorld[i] = _bindWorld[i];
            }
        }
        RedrawWire( bindPose: true );
        RedrawBones();
        RebuildSkeletonGeometry();
        RebuildSolid();   // back to the bind pose
    }

    protected override void PreFrame()
    {
        Scene.EditorTick( RealTime.Now, RealTime.Delta );
        UpdateCamera();

        // The wire/skeleton are SceneLineObjects - they must be re-submitted each
        // frame or they vanish. Submission is cheap (bounded by the wire cap); it
        // was the two O(all-verts) ComputeBounds() calls per frame (here + in
        // UpdateCamera) that pegged a core with a preview open - both now cached.
        if ( _mesh is not null && _wiggleTime < 0f )
        {
            RedrawWire( bindPose: !_posedActive );
            RedrawBones();
        }

        if ( _wiggleTime >= 0f && _rig is not null )
        {
            _wiggleTime += RealTime.Delta;
            var jointCount = _rig.Skeleton.Joints.Count;
            var wigglable = Math.Max( 1, jointCount - 1 ); // skip the root
            var slot = (int)(_wiggleTime / SecondsPerJoint);
            if ( slot >= wigglable )
            {
                StopWiggle();
                return;
            }
            var joint = slot + 1; // joints are parent-before-child; 0 is root
            WigglingJoint = _rig.Skeleton.Joints[joint].Name;

            var phase = (_wiggleTime % SecondsPerJoint) / SecondsPerJoint; // 0..1
            var angle = MathF.Sin( phase * MathF.Tau ) * WiggleDegrees * (MathF.PI / 180f);
            ApplyWigglePose( joint, angle );
            RedrawWire( bindPose: false );
            RedrawBones();
            RebuildSkeletonGeometry();
            // The solid IS what the user watches - deform it with the pose. Heavy
            // meshes rebuild every 3rd frame (choppier but MOVING).
            _wiggleFrame++;
            if ( _skinnedWiggle
                && (_mesh.Positions.Length <= 80_000 || _wiggleFrame % 3 == 0) )
                RebuildSolid( posed: true );
        }
    }

    /// <summary>FK pose with a single joint rotated about its hinge axis (or X).</summary>
    void ApplyWigglePose( int wiggleJoint, float angle )
    {
        var joints = _rig.Skeleton.Joints;
        var axis = joints[wiggleJoint].HingeAxis;
        var axisN = axis == VecN.Zero ? VecN.UnitX : VecN.Normalize( axis );
        var spin = QuatN.CreateFromAxisAngle( axisN, angle );

        for ( var i = 0; i < joints.Count; i++ )
        {
            var parent = joints[i].Parent;
            if ( parent < 0 )
            {
                _poseRotation[i] = i == wiggleJoint ? spin : QuatN.Identity;
                _posedWorld[i] = _bindWorld[i];
                continue;
            }
            var localOffset = _bindWorld[i] - _bindWorld[parent];
            var parentRotation = _poseRotation[parent];
            _posedWorld[i] = _posedWorld[parent] + VecN.Transform( localOffset, parentRotation );
            _poseRotation[i] = i == wiggleJoint
                ? parentRotation * spin
                : parentRotation;
        }
    }

    /// <summary>Collects a capped set of triangle edges (stride over triangles) plus the
    /// unique vertex list they reference, so wiggle re-skins only what is drawn.</summary>
    void BuildWireTopology()
    {
        var segments = new List<(int A, int B)>();
        _wireSlotOf = new Dictionary<int, int>();
        var vertexIds = new List<int>();

        int SlotOf( int vertex )
        {
            if ( !_wireSlotOf.TryGetValue( vertex, out var slot ) )
            {
                slot = vertexIds.Count;
                vertexIds.Add( vertex );
                _wireSlotOf.Add( vertex, slot );
            }
            return slot;
        }

        var stride = Math.Max( 1, _mesh.TriangleCount * 3 / MaxWireSegments );
        for ( var t = 0; t < _mesh.TriangleCount; t += stride )
        {
            var a = _mesh.Triangles[t * 3];
            var b = _mesh.Triangles[t * 3 + 1];
            var c = _mesh.Triangles[t * 3 + 2];
            segments.Add( (SlotOf( a ), SlotOf( b )) );
            segments.Add( (SlotOf( b ), SlotOf( c )) );
            segments.Add( (SlotOf( c ), SlotOf( a )) );
        }

        _wireSegments = segments.ToArray();
        _wireVertexIndices = vertexIds.ToArray();
        _wirePositions = new Vector3[_wireVertexIndices.Length];
    }

    void RedrawWire( bool bindPose )
    {
        if ( _mesh is null || _wireSegments is null )
            return;

        for ( var s = 0; s < _wireVertexIndices.Length; s++ )
        {
            var v = _wireVertexIndices[s];
            var p = (!bindPose && _skinnedWiggle && _rig is not null)
                ? SkinVertex( v )
                : _mesh.Positions[v];
            _wirePositions[s] = ToVector3( p );
        }

        // Width relative to the model (a fixed width is dust on big models, a blob
        // on small ones). During a skinned wiggle the wire is the star: brighter,
        // and the static solid hides so the deformation reads.
        var scale = _boundsLength;
        var width = scale * 0.0012f;
        // The solid model stays visible ALWAYS (an empty viewport is worse than an
        // occluded bone); wire is a subtle shell, brighter while wiggling.
        var color = bindPose
            ? new Color( 0.65f, 0.75f, 0.85f, 0.2f )
            : new Color( 0.55f, 0.85f, 1f, 0.85f );
        if ( _solid is not null )
        {
            _solid.RenderingEnabled = true;   // the solid is the model, always shown
            // With a rig: ghost the body so the skeleton reads through it.
            var ghosted = _rig is not null;
            _solid.ColorTint = ghosted
                ? new Color( 0.62f, 0.66f, 0.72f, 0.3f )
                : new Color( 0.62f, 0.66f, 0.72f, 1f );
            _solid.Flags.IsTranslucent = ghosted;
            _solid.Flags.IsOpaque = !ghosted;
        }

        _wire.Clear();
        foreach ( var (a, b) in _wireSegments )
        {
            _wire.StartLine();
            _wire.AddLinePoint( _wirePositions[a], color, width );
            _wire.AddLinePoint( _wirePositions[b], color, width );
            _wire.EndLine();
        }
    }

    /// <summary>Linear-blend skin of one vertex against the current wiggle pose.</summary>
    VecN SkinVertex( int v )
    {
        var bind = _mesh.Positions[v];
        var result = VecN.Zero;
        float total = 0;
        for ( var k = 0; k < 4; k++ )
        {
            var w = _rig.Weights.Weights[v * 4 + k];
            if ( w <= 0f )
                continue;
            var bone = _rig.Weights.BoneIndices[v * 4 + k];
            var local = bind - _bindWorld[bone];
            result += (_posedWorld[bone] + VecN.Transform( local, _poseRotation[bone] )) * w;
            total += w;
        }
        return total > 1e-4f ? result / total : bind;
    }

    SceneObject _skeletonObject;   // joint octahedra (red, the traditional idiom)
    SceneObject _boneObject;       // bone bipyramids (blue)
    SceneObject _highlightObject;
    string _highlight = "";

    /// <summary>Marks one joint (by name) with a bigger yellow marker - driven by
    /// the adjust panel's selection.</summary>
    public void SetHighlight( string jointName )
    {
        _highlight = jointName ?? "";
        RebuildSkeletonGeometry();
    }

    /// <summary>Raised when the user clicks a joint in the viewport (joint name),
    /// or clicks empty space (null - selection cleared).</summary>
    public Action<string> JointPicked { get; set; }

    /// <summary>The joint under the given widget-local position, or -1. Joints
    /// are projected through the same orbit camera the viewport renders with;
    /// the nearest projected joint within a forgiving radius wins.</summary>
    int PickJoint( Vector2 local )
    {
        if ( _rig is null || _posedWorld is null || !Camera.IsValid() )
            return -1;

        var forward = Camera.WorldRotation.Forward;
        var right = Camera.WorldRotation.Right;
        var up = Camera.WorldRotation.Up;
        var origin = Camera.WorldPosition;

        // Vertical-FOV pinhole projection. Even if the engine's FOV convention
        // differs slightly, nearest-projected-joint keeps picks on target.
        var tanHalf = MathF.Tan( MathX.DegreeToRadian( Camera.FieldOfView ) * 0.5f );
        var aspect = Width > 0 ? (float)Width / Math.Max( (float)Height, 1f ) : 1f;

        var best = -1;
        var bestDistance = float.MaxValue;
        for ( var i = 0; i < _posedWorld.Length; i++ )
        {
            var world = ToVector3( _posedWorld[i] );
            var toJoint = world - origin;
            var depth = Vector3.Dot( toJoint, forward );
            if ( depth <= 0.001f )
                continue;   // behind the camera
            var ndcX = Vector3.Dot( toJoint, right ) / (depth * tanHalf * aspect);
            var ndcY = Vector3.Dot( toJoint, up ) / (depth * tanHalf);
            var px = (ndcX * 0.5f + 0.5f) * Width;
            var py = (0.5f - ndcY * 0.5f) * Height;
            var d = new Vector2( px, py ).Distance( local );
            if ( d < bestDistance )
            {
                bestDistance = d;
                best = i;
            }
        }

        // Forgiving click target: ~4% of the viewport's smaller side, min 14px.
        var tolerance = MathF.Max( MathF.Min( (float)Width, (float)Height ) * 0.04f, 14f );
        return bestDistance <= tolerance ? best : -1;
    }

    /// <summary>Right-clicked a joint (name) - the dialog raises its context menu.</summary>
    public Action<string> JointContextRequested { get; set; }

    /// <summary>Re-applies the (edited) rig after a delete/rename, rebuilding all
    /// preview geometry and clearing any live move-pose.</summary>
    public void Reload( RigResult rig )
    {
        _posedActive = false;
        SetRig( _mesh, rig );
    }

    /// <summary>Snaps the live move-pose back to the rest skeleton.</summary>
    public void ResetPose()
    {
        if ( _rig is null || _bindWorld is null )
            return;
        for ( var i = 0; i < _posedWorld.Length; i++ )
        {
            _posedWorld[i] = _bindWorld[i];
            _poseRotation[i] = QuatN.Identity;
        }
        _posedActive = false;
        RebuildSolid();
        RebuildSkeletonGeometry();
    }

    protected override void OnMousePress( MouseEvent e )
    {
        base.OnMousePress( e );
        _lastMouse = e.LocalPosition;
        if ( _rig is null )
            return;

        var picked = PickJoint( e.LocalPosition );

        if ( e.RightMouseButton && picked >= 0 )
        {
            SetHighlight( _rig.Skeleton.Joints[picked].Name );
            JointPicked?.Invoke( _rig.Skeleton.Joints[picked].Name );
            JointContextRequested?.Invoke( _rig.Skeleton.Joints[picked].Name );
            return;
        }

        if ( e.LeftMouseButton )
        {
            if ( picked >= 0 )
            {
                SetHighlight( _rig.Skeleton.Joints[picked].Name );
                JointPicked?.Invoke( _rig.Skeleton.Joints[picked].Name );
                _dragJoint = picked;   // a left-drag from a joint MOVES it (deform preview)
                _dragMoved = false;
            }
            else
            {
                _dragJoint = -1;
                if ( _highlight.Length > 0 )
                {
                    SetHighlight( "" );
                    JointPicked?.Invoke( null );
                }
            }
        }
    }

    protected override void OnMouseReleased( MouseEvent e )
    {
        base.OnMouseReleased( e );
        _dragJoint = -1;
        _dragMoved = false;
    }

    /// <summary>Mesh-space delta from a scene-space delta (inverse of ToVector3).</summary>
    static VecN FromVector3( Vector3 v ) => new( v.x, v.z, -v.y );

    /// <summary>Translates a joint AND its whole subtree in the LIVE pose (not the
    /// rest skeleton), so the mesh deforms via the existing skin path - a
    /// translation analogue of the wiggle test. Not persisted: it is a
    /// "does this joint drive the right vertices?" check.</summary>
    void MoveJointSubtree( int root, VecN meshDelta )
    {
        if ( _rig is null || _posedWorld is null )
            return;
        if ( !_dragMoved )
        {
            BeforeJointMove?.Invoke();   // snapshot the pre-move pose for undo
            _dragMoved = true;
        }
        var joints = _rig.Skeleton.Joints;
        var stack = new Stack<int>();
        stack.Push( root );
        while ( stack.Count > 0 )
        {
            var j = stack.Pop();
            _posedWorld[j] += meshDelta;
            for ( var c = 0; c < joints.Count; c++ )
                if ( joints[c].Parent == j )
                    stack.Push( c );
        }
        _posedActive = true;
        if ( _skinnedWiggle )
            RebuildSolid( posed: true );
        RebuildSkeletonGeometry();
    }

    /// <summary>
    /// The skeleton as REAL geometry (SceneLineObject never renders in this widget;
    /// SceneObject+Model provably does): each bone an elongated bipyramid, each
    /// joint an octahedron, ivory-tinted, rebuilt from the current pose.
    /// </summary>
    void RebuildSkeletonGeometry()
    {
        _skeletonObject?.Delete();
        _skeletonObject = null;
        _boneObject?.Delete();
        _boneObject = null;
        _highlightObject?.Delete();
        _highlightObject = null;
        if ( _rig is null || _mesh is null )
            return;

        try
        {
            var joints = _rig.Skeleton.Joints;
            var scale = _boundsLength;
            var jointRadius = scale * 0.008f;
            var boneRadius = scale * 0.004f;

            var jointVertices = new List<SimpleVertex>();
            var boneVertices = new List<SimpleVertex>();
            var vertices = jointVertices;   // Triangle() writes into this target
            void Triangle( Vector3 a, Vector3 b, Vector3 c )
            {
                var normal = Vector3.Cross( b - a, c - a ).Normal;
                vertices.Add( new SimpleVertex( a, normal, Vector3.Zero, Vector2.Zero ) );
                vertices.Add( new SimpleVertex( b, normal, Vector3.Zero, Vector2.Zero ) );
                vertices.Add( new SimpleVertex( c, normal, Vector3.Zero, Vector2.Zero ) );
                // Backface too - bones must read from every side.
                vertices.Add( new SimpleVertex( a, -normal, Vector3.Zero, Vector2.Zero ) );
                vertices.Add( new SimpleVertex( c, -normal, Vector3.Zero, Vector2.Zero ) );
                vertices.Add( new SimpleVertex( b, -normal, Vector3.Zero, Vector2.Zero ) );
            }
            void Octahedron( Vector3 center, float radius )
            {
                foreach ( var sx in new[] { -1f, 1f } )
                    foreach ( var sy in new[] { -1f, 1f } )
                        foreach ( var sz in new[] { -1f, 1f } )
                            Triangle(
                                center + Vector3.Forward * radius * sx,
                                center + Vector3.Left * radius * sy,
                                center + Vector3.Up * radius * sz );
            }
            void Bone( Vector3 from, Vector3 to, float radius )
            {
                var direction = to - from;
                if ( direction.Length < 1e-6f )
                    return;
                var side = Vector3.Cross( direction.Normal, Vector3.Up );
                if ( side.Length < 1e-4f )
                    side = Vector3.Cross( direction.Normal, Vector3.Forward );
                var s1 = side.Normal * radius;
                var s2 = Vector3.Cross( direction.Normal, side.Normal ).Normal * radius;
                var hub = from + direction * 0.15f;
                foreach ( var (a, b) in new[] { (s1, s2), (s2, -s1), (-s1, -s2), (-s2, s1) } )
                {
                    Triangle( from, hub + a, hub + b );
                    Triangle( hub + a, to, hub + b );
                }
            }

            var highlighted = _highlight.Length > 0
                ? joints.FindIndex( j => j.Name == _highlight ) : -1;

            for ( var i = 0; i < joints.Count; i++ )
            {
                var pos = ToVector3( _posedWorld[i] );
                if ( i != highlighted )   // the selected joint is drawn GREEN below
                {
                    vertices = jointVertices;
                    Octahedron( pos, jointRadius );
                }
                if ( joints[i].Parent >= 0 )
                {
                    vertices = boneVertices;
                    Bone( ToVector3( _posedWorld[joints[i].Parent] ), pos, boneRadius );
                }
            }

            var material = Material.Load( "materials/dev/reflectivity_30.vmat" )
                ?? Material.Load( "materials/default/white.vmat" );

            // Traditional rig colors: RED joints, BLUE bones (user request).
            if ( jointVertices.Count > 0 )
            {
                var jointMesh = new Sandbox.Mesh( material );
                jointMesh.CreateVertexBuffer( jointVertices.Count, SimpleVertex.Layout, jointVertices );
                _skeletonObject = new SceneObject(
                    Scene.SceneWorld, Model.Builder.AddMesh( jointMesh ).Create(), Transform.Zero )
                {
                    ColorTint = new Color( 0.92f, 0.18f, 0.15f ),
                };
            }
            if ( boneVertices.Count > 0 )
            {
                var boneMesh = new Sandbox.Mesh( material );
                boneMesh.CreateVertexBuffer( boneVertices.Count, SimpleVertex.Layout, boneVertices );
                _boneObject = new SceneObject(
                    Scene.SceneWorld, Model.Builder.AddMesh( boneMesh ).Create(), Transform.Zero )
                {
                    ColorTint = new Color( 0.20f, 0.45f, 0.95f ),
                };
            }

            // The selected joint IS the marker: its own octahedron drawn in
            // fluorescent green (slightly enlarged so it pops) instead of the red
            // one - a highlight of the joint, not a second shape covering it.
            if ( highlighted >= 0 )
            {
                vertices = new List<SimpleVertex>();
                Octahedron( ToVector3( _posedWorld[highlighted] ), jointRadius * 1.35f );
                var highlightMesh = new Sandbox.Mesh( material );
                highlightMesh.CreateVertexBuffer( vertices.Count, SimpleVertex.Layout, vertices );
                _highlightObject = new SceneObject( Scene.SceneWorld,
                    Model.Builder.AddMesh( highlightMesh ).Create(), Transform.Zero )
                {
                    ColorTint = new Color( 0.30f, 1f, 0.15f ),   // fluorescent green
                };
            }
        }
        catch ( Exception )
        {
            _skeletonObject?.Delete();
            _skeletonObject = null;
            _boneObject?.Delete();
            _boneObject = null;
        }
    }

    void RedrawBones()
    {
        _bones.Clear();
        if ( _rig is null )
            return;

        var joints = _rig.Skeleton.Joints;
        // Soft bone tones (no fluorescent green): warm ivory bones, blue joints.
        var boneColor = new Color( 0.93f, 0.82f, 0.6f, 0.95f );
        var jointColor = Theme.Blue.WithAlpha( 0.95f );
        var activeColor = Theme.Yellow;
        var scale = MathF.Max( _boundsLength, 1f );
        var tick = scale * 0.006f;

        for ( var i = 0; i < joints.Count; i++ )
        {
            var pos = ToVector3( _posedWorld[i] );
            var parent = joints[i].Parent;
            if ( parent >= 0 )
            {
                _bones.StartLine();
                _bones.AddLinePoint( ToVector3( _posedWorld[parent] ), boneColor, tick * 0.9f );
                _bones.AddLinePoint( pos, boneColor, tick * 0.9f );
                _bones.EndLine();
            }

            // Joint = a 3-axis star so it reads as a node from any angle.
            var isActive = joints[i].Name == WigglingJoint;
            var markerColor = isActive ? activeColor : jointColor;
            foreach ( var axis in new[] { Vector3.Up, Vector3.Left, Vector3.Forward } )
            {
                _bones.StartLine();
                _bones.AddLinePoint( pos - axis * tick * 1.5f, markerColor, tick * 1.6f );
                _bones.AddLinePoint( pos + axis * tick * 1.5f, markerColor, tick * 1.6f );
                _bones.EndLine();
            }
        }
    }

    /// <summary>Mesh space (Y-up, as our whole pipeline assumes) → s&box scene
    /// space (Z-up): rotate +90° about X. Without this everything lies sideways.</summary>
    static Vector3 ToVector3( VecN v ) => new( v.X, -v.Z, v.Y );

    void UpdateCamera()
    {
        if ( !Camera.IsValid() || _mesh is null )
            return;

        // Cached (ComputeBounds is O(verts) and this runs every frame).
        var center = _boundsCenter;
        var radius = MathF.Max( _boundsLength * 0.5f, 1f );
        var distance = MathX.SphereCameraDistance( radius, Camera.FieldOfView ) * 1.1f * _zoom;

        var yawRad = MathX.DegreeToRadian( _yaw );
        var dir = new Vector3( MathF.Cos( yawRad ), MathF.Sin( yawRad ), 0.35f ).Normal;
        Camera.WorldPosition = center + dir * distance;
        Camera.WorldRotation = Rotation.LookAt( -dir, Vector3.Up );
    }

    protected override void OnWheel( WheelEvent e )
    {
        base.OnWheel( e );
        _zoom = Math.Clamp( _zoom * (e.Delta > 0 ? 0.88f : 1.14f), 0.15f, 6f );
        e.Accept();
    }

    protected override void OnMouseMove( MouseEvent e )
    {
        base.OnMouseMove( e );
        var delta = e.LocalPosition - _lastMouse;
        _lastMouse = e.LocalPosition;
        if ( (e.ButtonState & MouseButtons.Left) == 0 )
            return;

        // Dragging a selected joint moves it in the camera-facing plane and the
        // mesh follows; dragging empty space orbits.
        if ( _dragJoint >= 0 && _posedWorld is not null && Camera.IsValid() )
        {
            var jointScene = ToVector3( _posedWorld[_dragJoint] );
            var depth = Vector3.Dot( jointScene - Camera.WorldPosition, Camera.WorldRotation.Forward );
            var tanHalf = MathF.Tan( MathX.DegreeToRadian( Camera.FieldOfView ) * 0.5f );
            var worldPerPixel = 2f * tanHalf * MathF.Max( depth, 0.01f ) / MathF.Max( (float)Height, 1f );
            var sceneDelta = Camera.WorldRotation.Right * (delta.x * worldPerPixel)
                + Camera.WorldRotation.Up * (-delta.y * worldPerPixel);
            MoveJointSubtree( _dragJoint, FromVector3( sceneDelta ) );
            return;
        }

        _yaw -= delta.x * 0.4f;
    }

    public override void OnDestroyed()
    {
        base.OnDestroyed();
        _solid?.Delete();
        _solid = null;
        _skeletonObject?.Delete();
        _skeletonObject = null;
        _boneObject?.Delete();
        _boneObject = null;
        _highlightObject?.Delete();
        _highlightObject = null;
        Scene?.Destroy();
        Scene = null;
    }
}