BoBiCo Soft-Physics/Components/BoBiCoSoftBoneChain.cs

A component implementing a soft-bone chain for character rigs. It builds a list of joint states from a SkinnedModelRenderer, simulates soft physics per-bone (pull, spring, stiffness, gravity, stretch/squish), resolves collisions with configured colliders and publishes gizmos for editor visualization.

File Access
//=========================================================================================================================
// BoBiCo Soft-bone Chain Component
//=========================================================================================================================

using System;
using System.Collections.Generic;

public enum SoftBoneLinkColliderShape
{
	/// <summary>
	/// A simple spherical collider.
	/// Best for rounded body parts and lightweight collision detection.
	/// </summary>
	Sphere,

	/// <summary>
	/// A capsule-shaped collider.
	/// Recommended for most bones and elongated body parts.
	/// </summary>
	Capsule,

	/// <summary>
	/// A box-shaped collider.
	/// Useful for broad collision areas and block-like shapes.
	/// </summary>
	Box,

	/// <summary>
	/// A one-sided flat collider.
	/// Useful for flat surface areas.
	/// </summary>
	Flat,
}

public enum SoftBoneCollisionMode
{
	/// <summary>
	/// Disables all collision detection for this chain.
	/// </summary>
	None,

	/// <summary>
	/// Collides only with body colliders.
	/// This is the recommended mode for most setups.
	/// </summary>
	BaseCollision,

	/// <summary>
	/// Collides with body colliders and collider chains from the same model.
	/// </summary>
	SelfCollision,

	/// <summary>
	/// Collides with body colliders and collider chains from all SoftBone-enabled models.
	/// </summary>
	FullCollision
}

[Title( "SoftBone Chain" )]
[Category( "BoBiCo" )]
[Icon( "linear_scale" )]
[HelpUrl( "https://bobico-lab.vercel.app/softbone-docs/bobico-softbones/core-components/softbone-chain" )]
public sealed class BoBiCoSoftBoneChain : Component, Component.ExecuteInEditor
{
	// prevents unstable math when values are extremely close to zero.
	private const float Epsilon = 0.000008f;

	private GameObject _targetBone;
	private SoftBoneControlMode _controlMode;
	private bool _curveDefaultsInitialized;

//=========================================================================================================================
// Chain Properties Section
//=========================================================================================================================

	/// <summary>
	/// Defines the root bone of the chain that'll be simulated.
	/// </summary>
	[Property, Order( 1 ), Title( "Target Bone" ), Group( "Chain Properties" )]
	public GameObject TargetBone
	{
		get => _targetBone;
		set
		{
			if ( _targetBone == value )
				return;

			_targetBone = value;
			UpdateOwnerName();
			MarkDirty();
		}
	}

	/// <summary>
	/// Rotates the chain in local space.
	/// </summary>
	[Property, Order( 2 ), Title( "Rotation Offset" ), Group( "Chain Properties" )]
	public Angles RotationOffset { get; set; }

	/// <summary>
	/// Control mode for the SoftBone physics parameters. 
	/// Slider mode uses a single value for each parameter, 
	/// Range mode uses a min/max range for each parameter, 
	/// and Curve mode uses a curve to define the parameter values along the chain.
	/// </summary>
	[Property, Order( 3 ), Title( "Control Mode" ), Group( "Chain Properties" )]
	public SoftBoneControlMode ControlMode
	{
		get => _controlMode;
		set
		{
			if ( _controlMode == value ) return;
			_controlMode = value;
			if ( value == SoftBoneControlMode.Curve && !_curveDefaultsInitialized ) InitializeCurveDefaults();
		}
	}

	public enum SoftBoneControlMode
    {
        Slider,
        Range,
        Curve
    }

	/// <summary>
	/// When enabled, the root bone itself is also physically simulated
	/// (wobbles independently). When disabled, only child bones simulate
	/// and the root follows animation exactly.
	/// </summary>
	[Property, Order( 4 ), Title( "Simulate Root" ), Group( "Chain Properties" )]
	public bool SimulateRoot { get; set; } = false;

	/// <summary>
	/// When enabled, only the first X chain bones are simulated.
	/// </summary>
	[Property, Order( 5 ), Title( "Limit Chain Length" ), Group( "Chain Properties" )]
	public bool LimitChainLength { get; set; } = false;

	/// <summary>
	/// Limit the amount of chain bones from tip to root that are simulated when Limit Children is enabled.
	/// </summary>
	[Property, Order( 6 ), Title( "Chain Length Limit" ), Group( "Chain Properties" ), ShowIf( nameof( LimitChainLength ), true )]
	public int ChainLimit { get; set; } = 0;

//=========================================================================================================================
// Physics Settings
//=========================================================================================================================

	/// <summary>	
	/// Controls how strongly the chain returns toward its animated rest pose.
	/// Higher values reduce swaying and make the chain settle more quickly.
	/// </summary>
	[Property, Order( 8 ), Range( 0, 1 ), Title( "Drag" ), Group( "Physics Settings" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Slider )]
	public float Pull { get; set; } = 0.20f;

	/// <summary>	
	/// Controls how strongly the chain returns toward its animated rest pose.
	/// Higher values reduce swaying and make the chain settle more quickly.
	/// </summary>
	[Property, Order( 8 ), Range( 0, 1 ), Title( "Drag" ), Group( "Physics Settings" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Range )]
	public Vector2 PullRange { get; set; } = new( 0.20f, 0.20f );

	/// <summary>	
	/// Controls how strongly the chain returns toward its animated rest pose.
	/// Higher values reduce swaying and make the chain settle more quickly.
	/// </summary>
	[Property, Order( 8 ), Title( "Drag" ), Group( "Physics Settings" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Curve )]
	public Curve PullCurve { get; set; } = ConstantCurve( 0.20f );

	/// <summary>
	/// Controls how much spring force is applied to the chain.
	/// Higher values create softer, bouncier motion, while lower values produce more stable movement.
	/// </summary>
	[Property, Order( 9 ), Range( 0, 1 ), Title( "Spring" ), Group( "Physics Settings" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Slider )]
	public float Spring { get; set; } = 0.50f;

	/// <summary>
	/// Controls how much spring force is applied to the chain.
	/// Higher values create softer, bouncier motion, while lower values produce more stable movement.
	/// </summary>
	[Property, Order( 9 ), Range( 0, 1 ), Title( "Spring" ), Group( "Physics Settings" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Range )]
	public Vector2 SpringRange { get; set; } = new( 0.50f, 0.50f );

	/// <summary>
	/// Controls how much spring force is applied to the chain.
	/// Higher values create softer, bouncier motion, while lower values produce more stable movement.
	/// </summary>
	[Property, Order( 9 ), Title( "Spring" ), Group( "Physics Settings" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Curve )]
	public Curve SpringCurve { get; set; } = ConstantCurve( 0.50f );


	/// <summary>
	/// Controls how resistant the chain is to bending.
	/// Higher values make the chain feel firmer, while lower values allow more flexibility.
	/// </summary>
	[Property, Order( 10 ), Range( 0, 1 ), Title( "Stiffness" ), Group( "Physics Settings" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Slider )]
	public float Stiffness { get; set; } = 0.30f;

	/// <summary>
	/// Controls how resistant the chain is to bending.
	/// Higher values make the chain feel firmer, while lower values allow more flexibility.
	/// </summary>	
	[Property, Order( 10 ), Range( 0, 1 ), Title( "Stiffness" ), Group( "Physics Settings" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Range )]
	public Vector2 StiffnessRange { get; set; } = new( 0.30f, 0.30f );

	/// <summary>
	/// Controls how resistant the chain is to bending.
	/// Higher values make the chain feel firmer, while lower values allow more flexibility.
	/// </summary>
	[Property, Order( 10 ), Title( "Stiffness" ), Group( "Physics Settings" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Curve )]
	public Curve StiffnessCurve { get; set; } = ConstantCurve( 0.30f );

	/// <summary>
	/// Applies a constant directional force to the chain.
	/// Positive values pull downward, while negative values pull upward.
	/// </summary>
	[Property, Order( 11 ), Range( -1, 1 ), Title( "Gravity" ), Group( "Physics Settings" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Slider )]
	public float Gravity { get; set; }

	/// <summary>
	/// Applies a constant directional force to the chain.
	/// Positive values pull downward, while negative values pull upward.Positive values pull the chain down, negative values pull the chain up.
	/// </summary>
	[Property, Order( 11 ), Range( 0, 1 ), Title( "Gravity" ), Group( "Physics Settings" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Range )]
	public Vector2 GravityRange { get; set; }

	/// <summary>
	/// Applies a constant directional force to the chain.
	/// Positive values pull downward, while negative values pull upward.Positive values pull the chain down, negative values pull the chain up.
	/// </summary>
	[Property, Order( 11 ), Title( "Gravity" ), Group( "Physics Settings" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Curve )]
	public Curve GravityCurve { get; set; } = ConstantCurve( 0f );

	/// <summary>
	/// Reduces gravity while the chain is near its animated rest pose.
	/// Higher values keep idle chains more stable while preserving gravity during motion.
	/// </summary>
	[Property, Order( 12 ), Range( 0, 1 ), Title( "Gravity Falloff" ), Group( "Physics Settings" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Slider )]
	public float GravityFalloff { get; set; }

	/// <summary>
	/// Reduces gravity while the chain is near its animated rest pose.
	/// Higher values keep idle chains more stable while preserving gravity during motion.
	/// </summary>
	[Property, Order( 12 ), Range( 0, 1 ), Title( "Gravity Falloff" ), Group( "Physics Settings" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Range )]
	public Vector2 GravityFalloffRange { get; set; }

	/// <summary>
	/// Reduces gravity while the chain is near its animated rest pose.
	/// Higher values keep idle chains more stable while preserving gravity during motion.
	/// </summary>
	[Property, Order( 12 ), Title( "Gravity Falloff" ), Group( "Physics Settings" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Curve )]
	public Curve GravityFalloffCurve { get; set; } = ConstantCurve( 0f );


	/// <summary>
	/// Controls how much the chain can extend beyond its rest length.
	/// Higher values allow longer, softer stretching under force.
	/// </summary>
	[Property, Order( 13 ), Range( 0, 2 ), Title( "Stretch" ), Group( "Physics Settings" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Slider )]
	public float Stretch { get; set; }

	/// <summary>
	/// Controls how much the chain can extend beyond its rest length.
	/// Higher values allow longer, softer stretching under force.
	/// </summary>
	[Property, Order( 13 ), Range( 0, 2 ), Title( "Stretch" ), Group( "Physics Settings" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Range )]
	public Vector2 StretchRange { get; set; }

	/// <summary>
	/// Controls how much the chain can extend beyond its rest length.
	/// Higher values allow longer, softer stretching under force.
	/// </summary>
	[Property, Order( 13 ), Title( "Stretch" ), Group( "Physics Settings" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Curve )]
	public Curve StretchCurve { get; set; } = ConstantCurve( 0f );


	/// <summary>
	/// Controls how much the chain can compress below its rest length.
	/// Higher values allow greater compression.
	/// </summary>
	[Property, Order( 14 ), Range( 0, 1 ), Title( "Squish" ), Group( "Physics Settings" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Slider )]
	public float Squish { get; set; }

	/// <summary>
	/// Controls how much the chain can compress below its rest length.
	/// Higher values allow greater compression.
	/// </summary>
	[Property, Order( 14 ), Range( 0, 1 ), Title( "Squish" ), Group( "Physics Settings" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Range )]
	public Vector2 SquishRange { get; set; }

	/// <summary>
	/// Controls how much the chain can compress below its rest length.
	/// Higher values allow greater compression.
	/// </summary>
	[Property, Order( 14 ), Title( "Squish" ), Group( "Physics Settings" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Curve )]
	public Curve SquishCurve { get; set; } = ConstantCurve( 0f );

	/// <summary>
	/// Reduces movement caused by the character's own motion.
	/// Higher values make the chain feel heavier and less reactive while the character moves.
	/// </summary>
	[Property, Order( 15 ), Range( 0, 1 ), Title( "Movement Resistance" ), Group( "Physics Settings" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Slider )]
	public float Immobile { get; set; } = 1.00f;

	/// <summary>
	/// Reduces movement caused by the character's own motion.
	/// Higher values make the chain feel heavier and less reactive while the character moves.
	/// </summary>
	[Property, Order( 15 ), Range( 0, 1 ), Title( "Movement Resistance" ), Group( "Physics Settings" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Range )]
	public Vector2 ImmobileRange { get; set; } = new( 1.00f, 1.00f );

	/// <summary>
	/// Reduces movement caused by the character's own motion.
	/// Higher values make the chain feel heavier and less reactive while the character moves.
	/// </summary>
	[Property, Order( 15 ), Title( "Movement Resistance" ), Group( "Physics Settings" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Curve )]
	public Curve ImmobileCurve { get; set; } = ConstantCurve( 1.00f );

//=========================================================================================================================
// Limit Settings Section
//=========================================================================================================================

    /// <summary>
    /// Defines how the chain's movement is constrained.
    /// Different limit types provide different ways of restricting bone rotation.
    /// </summary>
	[Property, Order( 16 ), Title( "Limit Type" ), Group( "Limit Settings" )]
	public SoftBoneLimitType LimitType { get; set; } = SoftBoneLimitType.Cone;

    /// <summary>
    /// Maximum angle the chain can rotate away from its rest direction.
    /// Higher values allow greater freedom of movement.
    /// </summary>
	[Property, Order( 17 ), Range( 0, 180 ), Title( "Max Angle" ), Group( "Limit Settings" ), ShowIf( nameof( ShowMaxAngleSlider ), true )]
	public float MaxAngle { get; set; } = 95f;

	/// <summary>
    /// Maximum angle the chain can rotate away from its rest direction.
    /// Higher values allow greater freedom of movement.
    /// </summary>
	[Property, Order( 17 ), Range( 0, 1 ), Title( "Max Angle" ), Group( "Limit Settings" ), ShowIf( nameof( ShowMaxAngleRange ), true )]
	public Vector2 MaxAngleRange { get; set; } = new( 95f, 95f );

	/// <summary>
    /// Maximum angle the chain can rotate away from its rest direction.
    /// Higher values allow greater freedom of movement.
    /// </summary>
	[Property, Order( 17 ), Title( "Max Angle" ), Group( "Limit Settings" ), ShowIf( nameof( ShowMaxAngleCurve ), true )]
	public Curve MaxAngleCurve { get; set; } = ConstantCurve( 95f / 180f );
	public bool UsesMaxAngle => LimitType == SoftBoneLimitType.Cone || LimitType == SoftBoneLimitType.Plane;
	public bool ShowMaxAngleSlider => UsesMaxAngle && ControlMode == SoftBoneControlMode.Slider;
	public bool ShowMaxAngleRange => UsesMaxAngle && ControlMode == SoftBoneControlMode.Range;
	public bool ShowMaxAngleCurve => UsesMaxAngle && ControlMode == SoftBoneControlMode.Curve;

    /// <summary>
    /// Maximum horizontal rotation allowed for Ellipse limits.
    /// Increase to allow more side-to-side movement.
    /// </summary>
	[Property, Order( 18 ), Range( 0, 180 ), Title( "Max Yaw" ), Group( "Limit Settings" ), ShowIf( nameof( ShowMaxYawSlider ), true )]
	public float MaxYaw { get; set; } = 95f;

	/// <summary>
    /// Maximum horizontal rotation allowed for Ellipse limits.
    /// Increase to allow more side-to-side movement.
    /// </summary>
	[Property, Order( 18 ), Range( 0, 180 ), Title( "Max Yaw" ), Group( "Limit Settings" ), ShowIf( nameof( ShowMaxYawRange ), true )]
	public Vector2 MaxYawRange { get; set; } = new( 95f, 95f );

    /// <summary>
    /// Maximum horizontal rotation allowed for Ellipse limits.
    /// Increase to allow more side-to-side movement.
    /// </summary>
	[Property, Order( 18 ), Title( "Max Yaw" ), Group( "Limit Settings" ), ShowIf( nameof( ShowMaxYawCurve ), true )]
	public Curve MaxYawCurve { get; set; } = ConstantCurve( 95f / 180f );
	public bool ShowMaxYawSlider => LimitType == SoftBoneLimitType.Ellipse && ControlMode == SoftBoneControlMode.Slider;
	public bool ShowMaxYawRange => LimitType == SoftBoneLimitType.Ellipse && ControlMode == SoftBoneControlMode.Range;
	public bool ShowMaxYawCurve => LimitType == SoftBoneLimitType.Ellipse && ControlMode == SoftBoneControlMode.Curve;


    /// <summary>
    /// Maximum vertical rotation allowed for Ellipse limits.
    /// Increase to allow more up-and-down movement.
    /// </summary>
	[Property, Order( 19 ), Range( 0, 180 ), Title( "Max Pitch" ), Group( "Limit Settings" ), ShowIf( nameof( ShowMaxPitchSlider ), true )]
	public float MaxPitch { get; set; } = 95f;

    /// <summary>
    /// Maximum vertical rotation allowed for Ellipse limits.
    /// Increase to allow more up-and-down movement.
    /// </summary>
	[Property, Order( 19 ), Range( 0, 180 ), Title( "Max Pitch" ), Group( "Limit Settings" ), ShowIf( nameof( ShowMaxPitchRange ), true )]
	public Vector2 MaxPitchRange { get; set; } = new( 95f, 95f );

    /// <summary>
    /// Maximum vertical rotation allowed for Ellipse limits.
    /// Increase to allow more up-and-down movement.
    /// </summary>
	[Property, Order( 19 ), Title( "Max Pitch" ), Group( "Limit Settings" ), ShowIf( nameof( ShowMaxPitchCurve ), true )]
	public Curve MaxPitchCurve { get; set; } = ConstantCurve( 95f / 180f );
	public bool ShowMaxPitchSlider => LimitType == SoftBoneLimitType.Ellipse && ControlMode == SoftBoneControlMode.Slider;
	public bool ShowMaxPitchRange => LimitType == SoftBoneLimitType.Ellipse && ControlMode == SoftBoneControlMode.Range;
	public bool ShowMaxPitchCurve => ControlMode == SoftBoneControlMode.Curve && LimitType == SoftBoneLimitType.Ellipse;


    /// <summary>
    /// Rotates the limit shape relative to the chain.
    /// </summary>
	[Property, Order( 20 ), Title( "Limit Rotation" ), Group( "Limit Settings" ), HideIf( nameof( LimitType ), SoftBoneLimitType.None )]
	public Angles LimitRotation { get; set; }

    /// <summary>
    /// Shape used for this chain's collision volume.
    /// Different shapes provide different collision behavior and coverage.
    /// </summary>
	[Property, Order( 21 ), Title( "Collider Shape" ), Group( "Collision Settings" )]
	public SoftBoneLinkColliderShape ColliderShape { get; set; } = SoftBoneLinkColliderShape.Capsule;

    /// <summary>
    /// Controls which colliders this chain can interact with.
    /// Higher modes enable more realistic but more expensive collision behavior.
    /// </summary>
	[Property, Order( 22 ), Title( "Collision Mode" ), Group( "Collision Settings" )]
	public SoftBoneCollisionMode CollisionMode { get; set; } = SoftBoneCollisionMode.BaseCollision;

	/// <summary>
	/// Assign one or more SoftBone Collider Groups for this chain to interact with.
	/// Colliders outside of these groups will be ignored.
	/// </summary>
	[Property, Order( 23 ), Title( "Collider Groups" ), Group( "Collision Settings" )]
	public List<GameObject> ColliderGroups { get; set; } = new();

    /// <summary>
    /// Allows the colliders to collide with the root collider.
    /// Disable if the root should remain free.
    /// </summary>
	[Property, Order( 24 ), Title( "Collide With Root" ), Group( "Collision Settings" )]
	public bool CollideWithRoot { get; set; }

	// Helpers for conditional property visibility
	private bool ShowRadiusSlider => ControlMode == SoftBoneControlMode.Slider && ColliderShape != SoftBoneLinkColliderShape.Box;
	private bool ShowRadiusRange  => ControlMode == SoftBoneControlMode.Range  && ColliderShape != SoftBoneLinkColliderShape.Box;
	private bool ShowRadiusCurve  => ControlMode == SoftBoneControlMode.Curve  && ColliderShape != SoftBoneLinkColliderShape.Box;

    /// <summary>
    /// Radius of the chain's collision volume.
    /// Larger values create thicker collision boundaries.
    /// </summary>
	[Property, Order( 25 ), Title( "Collider Radius" ), Group( "Collision Settings" ), ShowIf( nameof( ShowRadiusSlider ), true )]
	public float ColliderRadius { get; set; } = 0.5f;

    /// <summary>
    /// Radius of the chain's collision volume.
    /// Larger values create thicker collision boundaries.
    /// </summary>
	[Property, Order( 25 ), Title( "Collider Radius" ), Group( "Collision Settings" ), ShowIf( nameof( ShowRadiusRange ), true )]
	public Vector2 ColliderRadiusRange { get; set; } = new( 0.5f, 0.5f );

    /// <summary>
    /// Radius of the chain's collision volume.
    /// Larger values create thicker collision boundaries.
    /// </summary>
	[Property, Order( 25 ), Title( "Collider Radius" ), Group( "Collision Settings" ), ShowIf( nameof( ShowRadiusCurve ), true )]
	public Curve ColliderRadiusCurve { get; set; } = ConstantCurve( 0.5f );

    /// <summary>
    /// Height of Capsule collider.
	/// Available only when the collider shape is set to Capsule.
    /// </summary>
	[Property, Order( 26 ), Title( "Collider Height" ), Group( "Collision Settings" ), ShowIf( nameof( ColliderShape ), SoftBoneLinkColliderShape.Capsule )]
	public float ColliderHeight { get; set; } = 1.25f;

    /// <summary>
    /// Defines the dimensions of the box collider.
    /// Available only when the collider shape is set to Box.
    /// </summary>
	[Property, Order( 27 ), Title( "Box Dimensions" ), Group( "Collision Settings" ), ShowIf( nameof( ColliderShape ), SoftBoneLinkColliderShape.Box )]
	public Vector3 BoxDimension { get; set; } = new Vector3( 1.25f, 1.25f, 1.25f );

//=========================================================================================================================
// Gizmo Setting Section
//=========================================================================================================================

    /// <summary>
    /// Displays the chain's angle limit in the Scene view.
    /// Useful for visualizing and adjusting movement constraints.
    /// </summary>
	[Property, Order( 28 ), Header( "Limit Gizmo" ), Title( "Limit Gizmo" ), Group( "Gizmo Settings" )]
	public bool ShowLimitGizmo { get; set; } = true;

    /// <summary>
    /// Keeps the limit gizmo visible even when the chain is not selected.
    /// Useful for debugging multiple chains at once.
    /// </summary>
	[Property, Order( 29 ), Title( "Always Show Gizmo" ), Group( "Gizmo Settings" ), ShowIf( nameof( ShowLimitGizmo ), true )]
	public bool AlwaysShowLimitGizmo { get; set; }

    /// <summary>
    /// Color used for rendering the limit gizmo.
    /// </summary>
	[Property, Order( 30 ), Title( "Limit Gizmo Color" ), Group( "Gizmo Settings" ), ShowIf( nameof( ShowLimitGizmo ), true )]
	public Color LimitGizmoColor { get; set; } = Color.Orange;

    /// <summary>
    /// Controls the transparency of the limit gizmo.
    /// </summary>
	[Property, Order( 31 ), Range( 0, 1 ), Title( "Gizmo Opacity" ), Group( "Gizmo Settings" ), ShowIf( nameof( ShowLimitGizmo ), true )]
	public float LimitGizmoOpacity { get; set; } = 1f;

    /// <summary>
    /// Controls the size of the limit gizmo in the Scene view.
    /// Adjust if the visualization appears too large or too small.
    /// </summary>
	[Property, Order( 32 ), Range( 0.1f, 1f ), Title( "Limit Gizmo Size" ), Group( "Gizmo Settings" ), ShowIf( nameof( ShowLimitGizmo ), true )]
	public float LimitGizmoSize { get; set; } = 0.5f;

    /// <summary>
    /// Displays the chain's collider in the Scene view.
    /// Useful for visualizing collision shapes and sizes.
    /// </summary>
	[Property, Order( 33 ), Header( "Collider Gizmo" ), Title( "Collider Gizmo" ), Group( "Gizmo Settings" )]
	public bool ShowColliderGizmo { get; set; } = true;

    /// <summary>
    /// Keeps the collider gizmo visible even when the chain is not selected.
    /// Useful for debugging collision setups.
    /// </summary>
	[Property, Order( 34 ), Title( "Always Show Gizmo" ), Group( "Gizmo Settings" ), ShowIf( nameof( ShowColliderGizmo ), true )]
	public bool AlwaysShowGizmo { get; set; }

    /// <summary>
    /// Color used for rendering the collider gizmo.
    /// </summary>
	[Property, Order( 35 ), Title( "Collider Gizmo Color" ), Group( "Gizmo Settings" ), ShowIf( nameof( ShowColliderGizmo ), true )]
	public Color ColliderGizmoColor { get; set; } = Gizmo.Colors.Blue;

    /// <summary>
    /// Controls the transparency of the collider gizmo.
    /// </summary>
	[Property, Order( 36 ), Range( 0, 1 ), Title( "Gizmo Opacity" ), Group( "Gizmo Settings" ), ShowIf( nameof( ShowColliderGizmo ), true )]
	public float ColliderGizmoOpacity { get; set; } = 1f;

//=========================================================================================================================
// Private States
//=========================================================================================================================

	private readonly List<SoftBoneJointState> _joints = new();
	private readonly List<BoBiCoSoftBoneCollider> _activeColliders = new();
	private readonly List<SoftBoneColliderShape> _externalBodyShapes = new();
	private readonly List<SoftBoneColliderShape> _externalChainShapes = new();
	private readonly List<SoftBoneChainColliderSnapshot> _publishedChainShapes = new();
	private readonly HashSet<BoBiCoSoftBoneCollider> _colliderSet = new();
	private readonly List<Transform> _framePose = new();
	private readonly List<Transform> _ragdollAnimationPose = new();
	private bool _dirty = true;
	private bool _usingPhysicsPose;

	public IReadOnlyList<SoftBoneJointState> Joints => _joints;

	public bool IsDirty => _dirty;

	public void MarkDirty() => _dirty = true;

//=========================================================================================================================
// Rebuild Section
//=========================================================================================================================

	public bool Rebuild( SkinnedModelRenderer rendererOverride = null )
	{
		var renderer = rendererOverride;
		if ( renderer is null )
			return false;

		if ( !renderer.IsValid() || renderer.Model?.Bones is null )
			return false;

		ReleaseCurrentChain( renderer );

		var boneName = "";
		if ( TargetBone is not null && TargetBone.IsValid() )
			boneName = TargetBone.Name;
		boneName = boneName?.Trim() ?? "";

		if ( string.IsNullOrWhiteSpace( boneName ) || !renderer.Model.Bones.HasBone( boneName ) )
			return false;

		var current = renderer.Model.Bones.GetBone( boneName );

		while ( current is not null )
		{
			if ( !SoftBoneSkinnedModelUtility.TryGetBoneAnimationLocal( renderer, current, out var animated ) )
				break;

			var poseVector = GetAnimatedBoneVectorLocal( renderer, current, animated );
			var length = Math.Max( poseVector.Length, Epsilon );
			var localPoseVector = animated.Rotation.Inverse * poseVector;

			var rendererDownLocal = RendererDownLocal( renderer );
			var gravityNormalLocal = animated.Rotation.Inverse * rendererDownLocal;

			var joint = new SoftBoneJointState
			{
				Bone = current,
				OriginalLocalGravityNormal = gravityNormalLocal,
				LocalPoseVector = localPoseVector,
				RestTransformLocal = animated,
				Length = length,
				PreviousBegin = animated.Position,
				PreviousEnd = animated.Position + poseVector,
				PreviousAnimatedBegin = animated.Position,
				PreviousAnimatedRotation = animated.Rotation,
				PreviousSimulationRotation = animated.Rotation,
				ImmobileEnd = SoftBoneSkinnedModelUtility.ToWorld( renderer, animated.Position + poseVector ),
				PreviousEndWorld = SoftBoneSkinnedModelUtility.ToWorld( renderer, animated.Position + poseVector ),
				PreviousVector = poseVector,
				PreviousVelocity = Vector3.Zero,
				HasHistory = false
			};

			_joints.Add( joint );
			if ( current.Children.Count == 0 )
				break;

			current = current.Children[0];
		}

		_dirty = false;
		return _joints.Count > 0;
	}

	private void ReleaseCurrentChain( SkinnedModelRenderer renderer )
	{
		if ( renderer is not null && renderer.IsValid() && renderer.SceneModel.IsValid() )
		{
			foreach ( var joint in _joints )
			{
				if ( joint.Bone is not null && renderer.TryGetBoneTransformAnimation( joint.Bone, out var animatedWorld ) )
				{
					var animatedLocal = renderer.WorldTransform.ToLocal( animatedWorld );
					renderer.SceneModel.SetBoneOverride( joint.Bone.Index, in animatedLocal );
				}
			}
		}

		_joints.Clear();
		_framePose.Clear();
		_ragdollAnimationPose.Clear();
		_publishedChainShapes.Clear();
		SoftBoneCollisionRegistry.Remove( this );
	}

	private void UpdateOwnerName()
	{
		if ( GameObject is null || !GameObject.IsValid() )
			return;

		var desiredName = TargetBone is not null && TargetBone.IsValid()
			? TargetBone.Name?.Trim()
			: "Empty Chain";
		if ( !string.IsNullOrWhiteSpace( desiredName ) && GameObject.Name != desiredName )
			GameObject.Name = desiredName;
	}

//=========================================================================================================================
// Simulation Section
//=========================================================================================================================

	public void Simulate( SkinnedModelRenderer renderer, float frameRatio )
	{
		// Auto-rebuild if dirty or empty — chain is self-healing so the Group
		// doesn't need to orchestrate the Rebuild/Simulate order externally.
		if ( _dirty || _joints.Count == 0 )
		{
			if ( !Rebuild( renderer ) )
				return;
		}

		if ( renderer is null || !renderer.IsValid() || !renderer.SceneModel.IsValid() )
			return;

		var flag = frameRatio >= 1f;
		var rendererDownLocal = RendererDownLocal( renderer );
		CollectActiveColliders();
		var usePhysicsPose = IsPhysicsDrivingRenderer( renderer );
		if ( usePhysicsPose != _usingPhysicsPose )
		{
			_usingPhysicsPose = usePhysicsPose;
			foreach ( var joint in _joints ) joint.HasHistory = false;
			if ( usePhysicsPose && !CaptureRagdollAnimationPose( renderer ) )
				return;
			if ( !usePhysicsPose ) _ragdollAnimationPose.Clear();
		}

		if ( !CaptureFramePose( renderer, usePhysicsPose ) )
			return;

		var hasParentEnd = false;
		var parentEnd = Vector3.Zero;
		var parentAnimatedRotation = Rotation.Identity;
		var parentSimulationRotation = Rotation.Identity;

		// LimitChainLength: simulate only the last ChainLimit bones (tip-to-root).
		// Handled here instead of Rebuild so SimulateRoot logic stays tied to the
		// ACTUAL chain root (index 0), not whichever bone happens to be first after slicing.
		var startIndex = GetSimulationStartIndex();
		ResetExcludedJointsToPose( renderer, startIndex );

		for ( var i = startIndex; i < _joints.Count; i++ )
		{
			var joint = _joints[i];
			var chainPosition = GetChainPosition( i );
			var pull = EvaluateUnit( Pull, ControlMode, PullRange, PullCurve, chainPosition );
			var spring = EvaluateUnit( Spring, ControlMode, SpringRange, SpringCurve, chainPosition );
			var stiffness = EvaluateUnit( Stiffness, ControlMode, StiffnessRange, StiffnessCurve, chainPosition );
			var immobile = EvaluateUnit( Immobile, ControlMode, ImmobileRange, ImmobileCurve, chainPosition );
			var stretch = EvaluateNonNegative( Stretch, ControlMode, StretchRange, StretchCurve, chainPosition );
			var squish = EvaluateNonNegative( Squish, ControlMode, SquishRange, SquishCurve, chainPosition );
			var gravity = EvaluateSignedUnit( Gravity, ControlMode, GravityRange, GravityCurve, chainPosition );
			var gravityFalloff = EvaluateUnit( GravityFalloff, ControlMode, GravityFalloffRange, GravityFalloffCurve, chainPosition );
			var colliderRadius = EvaluateColliderRadius( chainPosition );
			var maxAngle = EvaluateAngle( MaxAngle, ControlMode, MaxAngleRange, MaxAngleCurve, chainPosition );
			var maxYaw = EvaluateAngle( MaxYaw, ControlMode, MaxYawRange, MaxYawCurve, chainPosition );
			var maxPitch = EvaluateAngle( MaxPitch, ControlMode, MaxPitchRange, MaxPitchCurve, chainPosition );
			var isActualRoot = i == 0;
			var isSimulationRoot = i == startIndex;
			var isRootSimulated = isActualRoot && SimulateRoot;
			var animated = _framePose[i];

			var begin = hasParentEnd ? parentEnd : animated.Position;

			var animatedPoseVector = GetFramePoseVector( i, joint, animated );
			var parentDeflection = isSimulationRoot ? Rotation.Identity : GetParentDeflection( i, parentAnimatedRotation, parentSimulationRotation );
			var simulationRotation = parentDeflection * animated.Rotation;
			var poseVector = parentDeflection * animatedPoseVector;
			if ( isSimulationRoot && RotationOffset != default )
			{
				var offsetRotation = new Angles( RotationOffset.yaw, RotationOffset.pitch, RotationOffset.roll ).ToRotation();
				simulationRotation = animated.Rotation * offsetRotation;
				poseVector = simulationRotation * (animated.Rotation.Inverse * animatedPoseVector);
			}

			var restLength = Math.Max( poseVector.Length, Epsilon );
			var minLength = Math.Max( 0f, restLength * (1f - squish) );
			var maxLength = Math.Max( minLength, restLength * (1f + stretch) );

			var endPoint = joint.HasHistory
				? renderer.WorldTransform.PointToLocal( joint.PreviousEndWorld )
				: begin + poseVector;

			if ( joint.HasHistory )
			{
				var rootDelta = begin - joint.PreviousAnimatedBegin;
				endPoint += rootDelta;

				var rotationDelta = animated.Rotation * joint.PreviousAnimatedRotation.Inverse;
				endPoint = begin + rotationDelta * (endPoint - begin);
			}

			// SimulateRoot=false: only skip physics on the TRUE chain root, not on bones that
			// happen to be first because LimitChainLength trimmed the start of the iteration.
			if ( isActualRoot && !isRootSimulated && !CollideWithRoot )
			{
				ResetToRestPose( renderer, joint, animated, begin, poseVector, simulationRotation );
				parentEnd = joint.PreviousEnd;
				parentAnimatedRotation = animated.Rotation;
				parentSimulationRotation = joint.PreviousSimulationRotation;
				hasParentEnd = true;
				continue;
			}

			if ( pull <= Epsilon )
			{
				ResetToRestPose( renderer, joint, animated, begin, poseVector, simulationRotation );
				if ( !isActualRoot )
					ApplySolvedTransform( renderer, joint, animated, simulationRotation );
				parentEnd = joint.PreviousEnd;
				parentAnimatedRotation = animated.Rotation;
				parentSimulationRotation = joint.PreviousSimulationRotation;
				hasParentEnd = true;
				continue;
			}

			var previousEndPoint = endPoint;

			if ( joint.HasHistory && immobile > Epsilon )
			{
				var immobileTarget = renderer.WorldTransform.PointToLocal( joint.ImmobileEnd );
				endPoint += (immobileTarget - endPoint) * immobile;
			}

			var gravityAmount = GetGravityAmount( joint, animated, rendererDownLocal, gravity, gravityFalloff );
			var gravityTargetEnd = ApplyGravityTarget( begin, poseVector, restLength, gravityAmount, gravity, rendererDownLocal );

			var solvedEndPoint = BoBiCoSoftBoneSolver.IntegrateAdvanced(
				begin, endPoint, previousEndPoint,
				poseVector, joint.PreviousVector, joint.PreviousVelocity,
				minLength, maxLength, pull, spring, stiffness, gravityTargetEnd, frameRatio,
				out var newVelocity );

			var solvedDirection = BoBiCoSoftBoneSolver.SafeNormal( solvedEndPoint - begin, poseVector );
			solvedDirection = SoftBoneAngleLimit.Apply( poseVector, solvedDirection, LimitType, maxAngle, maxYaw, maxPitch, LimitRotation );
			var solvedLength = Math.Clamp( (solvedEndPoint - begin).Length, minLength, maxLength );
			var solvedEnd = begin + solvedDirection * solvedLength;
			solvedEnd = ResolveCollisions( renderer, begin, solvedEnd, poseVector, minLength, maxLength, colliderRadius, maxAngle, maxYaw, maxPitch, isActualRoot && CollideWithRoot, out var wasColliding );
			solvedDirection = BoBiCoSoftBoneSolver.SafeNormal( solvedEnd - begin, poseVector );
			newVelocity = solvedEnd - previousEndPoint;
			if ( wasColliding ) newVelocity *= 0.75f;

			if ( flag )
			{
				joint.PreviousEndWorld = SoftBoneSkinnedModelUtility.ToWorld( renderer, solvedEnd );
				joint.ImmobileEnd = joint.PreviousEndWorld;
				joint.PreviousVector = solvedEnd - begin;
				joint.PreviousVelocity = newVelocity;
				joint.PreviousAnimatedBegin = begin;
				joint.PreviousAnimatedRotation = animated.Rotation;
			}

			joint.PreviousBegin = begin;
			joint.PreviousEnd = solvedEnd;
			joint.PreviousSimulationRotation = BoBiCoSoftBoneSolver.SolveHeadRotation( poseVector, solvedEnd - begin, simulationRotation );
			joint.HasHistory = true;

			if ( !isActualRoot )
				ApplySolvedTransform( renderer, joint, animated, joint.PreviousSimulationRotation );

			parentEnd = solvedEnd;
			parentAnimatedRotation = animated.Rotation;
			parentSimulationRotation = joint.PreviousSimulationRotation;
			hasParentEnd = true;
		}

		PublishCollisionShapes( renderer );
	}

	private void CollectActiveColliders()
	{
		_activeColliders.Clear();
		_colliderSet.Clear();

		if ( ColliderGroups is not null )
		{
			foreach ( var sourceObject in ColliderGroups )
			{
				if ( sourceObject is null || !sourceObject.IsValid() )
					continue;

				sourceObject.GetComponent<BoBiCoSoftBoneColliderGroup>()?.CollectColliders( _activeColliders, _colliderSet );
			}
		}

	}

	private void ResetExcludedJointsToPose( SkinnedModelRenderer renderer, int startIndex )
	{
		for ( var i = 0; i < startIndex; i++ )
		{
			var joint = _joints[i];
			var animated = _framePose[i];
			var poseVector = GetFramePoseVector( i, joint, animated );
			ResetToRestPose( renderer, joint, animated, animated.Position, poseVector, animated.Rotation );
			if ( i > 0 )
				ApplyAnimationTransform( renderer, joint, animated );
		}
	}

	private Vector3 ResolveCollisions( SkinnedModelRenderer renderer, Vector3 begin, Vector3 solvedEnd, Vector3 poseVector, float minLength, float maxLength, float colliderRadius, float maxAngle, float maxYaw, float maxPitch, bool includeRootHead, out bool collided )
	{
		collided = false;
		if ( CollisionMode == SoftBoneCollisionMode.None || colliderRadius <= Epsilon )
			return solvedEnd;

		var worldBegin = SoftBoneSkinnedModelUtility.ToWorld( renderer, begin );
		var worldEnd = SoftBoneSkinnedModelUtility.ToWorld( renderer, solvedEnd );
		foreach ( var collider in _activeColliders )
		{
			if ( collider.TryGetWorldShape( collider.GetOwningRenderer(), out var shape ) )
				collided |= SoftBoneCollisionSolver.Resolve( ref worldEnd, worldBegin, includeRootHead ? worldBegin : GetCollisionStart( worldBegin, worldEnd, colliderRadius ), colliderRadius, minLength, maxLength, shape );
		}

		if ( CollisionMode == SoftBoneCollisionMode.FullCollision )
		{
			_externalBodyShapes.Clear();
			SoftBoneCollisionRegistry.CollectExternalBodyShapes( renderer, _externalBodyShapes );
			foreach ( var shape in _externalBodyShapes )
				collided |= SoftBoneCollisionSolver.Resolve( ref worldEnd, worldBegin, includeRootHead ? worldBegin : GetCollisionStart( worldBegin, worldEnd, colliderRadius ), colliderRadius, minLength, maxLength, shape );
		}

		_externalChainShapes.Clear();
		SoftBoneCollisionRegistry.CollectChainShapes( this, renderer, CollisionMode, _externalChainShapes );
		foreach ( var shape in _externalChainShapes )
			collided |= SoftBoneCollisionSolver.Resolve( ref worldEnd, worldBegin, includeRootHead ? worldBegin : GetCollisionStart( worldBegin, worldEnd, colliderRadius ), colliderRadius, minLength, maxLength, shape );

		if ( !collided )
			return solvedEnd;

		var localDelta = SoftBoneSkinnedModelUtility.ToLocal( renderer, worldEnd ) - begin;
		var localDirection = SoftBoneAngleLimit.Apply( poseVector, BoBiCoSoftBoneSolver.SafeNormal( localDelta, poseVector ), LimitType, maxAngle, maxYaw, maxPitch, LimitRotation );
		return begin + localDirection * Math.Clamp( localDelta.Length, minLength, maxLength );
	}

	private Vector3 GetCollisionStart( Vector3 worldBegin, Vector3 worldEnd, float colliderRadius )
	{
		if ( ColliderShape != SoftBoneLinkColliderShape.Capsule )
			return worldEnd;

		var axis = worldEnd - worldBegin;
		var segmentLength = Math.Max( 0f, ColliderHeight - colliderRadius * 2f );
		if ( axis.Length <= Epsilon || segmentLength <= Epsilon )
			return worldEnd;

		return worldEnd - axis.Normal * Math.Min( axis.Length, segmentLength );
	}

	private int GetSimulationStartIndex()
	{
		return LimitChainLength && ChainLimit > 0 && ChainLimit < _joints.Count
			? _joints.Count - ChainLimit
			: 0;
	}

	private float GetChainPosition( int index ) => _joints.Count <= 1 ? 0f : index / (float)(_joints.Count - 1);
	private static Curve ConstantCurve( float value ) => new( new Curve.Frame( 0f, value ), new Curve.Frame( 1f, value ) );

	private void InitializeCurveDefaults()
	{
		_curveDefaultsInitialized = true;
		PullCurve = ConstantCurve( Pull );
		SpringCurve = ConstantCurve( Spring );
		StiffnessCurve = ConstantCurve( Stiffness );
		ImmobileCurve = ConstantCurve( Immobile );
		StretchCurve = ConstantCurve( Stretch );
		SquishCurve = ConstantCurve( Squish );
		GravityCurve = ConstantCurve( Gravity );
		GravityFalloffCurve = ConstantCurve( GravityFalloff );
		MaxAngleCurve = ConstantCurve( MaxAngle / 180f );
		MaxYawCurve = ConstantCurve( MaxYaw / 180f );
		MaxPitchCurve = ConstantCurve( MaxPitch / 180f );
		ColliderRadiusCurve = ConstantCurve( ColliderRadius );
	}

	private static float EvaluateUnit( float value, SoftBoneControlMode mode, Vector2 range, Curve curve, float position ) => Math.Clamp( EvaluateControl( value, mode, range, curve, position ), 0f, 1f );
	private static float EvaluateSignedUnit( float value, SoftBoneControlMode mode, Vector2 range, Curve curve, float position ) => Math.Clamp( EvaluateControl( value, mode, range, curve, position ), -1f, 1f );
	private static float EvaluateNonNegative( float value, SoftBoneControlMode mode, Vector2 range, Curve curve, float position ) => Math.Max( 0f, EvaluateControl( value, mode, range, curve, position ) );
	private static float EvaluateAngle( float value, SoftBoneControlMode mode, Vector2 range, Curve curve, float position )
	{
		if ( mode == SoftBoneControlMode.Curve )
			return Math.Clamp( (curve.Length > 0 ? curve.Evaluate( position ) : 1f) * 180f, 0f, 180f );

		return Math.Clamp( EvaluateControl( value, mode, range, curve, position ), 0f, 180f );
	}
	private float EvaluateColliderRadius( float position ) => Math.Max( 0.001f, EvaluateControl( ColliderRadius, ControlMode, ColliderRadiusRange, ColliderRadiusCurve, position ) );
	private static float EvaluateControl( float value, SoftBoneControlMode mode, Vector2 range, Curve curve, float position )
	{
		return mode switch
		{
			SoftBoneControlMode.Range => range.x + (range.y - range.x) * position,
			SoftBoneControlMode.Curve => curve.Length > 0 ? curve.Evaluate( position ) : value,
			_ => value
		};
	}

	internal void PublishCollisionShapes( SkinnedModelRenderer renderer )
	{
		if ( CollisionMode == SoftBoneCollisionMode.None )
		{
			SoftBoneCollisionRegistry.Remove( this );
			return;
		}

		_publishedChainShapes.Clear();
		for ( var i = GetSimulationStartIndex(); i < _joints.Count; i++ )
		{
			if ( i == 0 && !CollideWithRoot )
				continue;

			var joint = _joints[i];
			if ( !joint.HasHistory )
				continue;

			var begin = SoftBoneSkinnedModelUtility.ToWorld( renderer, joint.PreviousBegin );
			var end = SoftBoneSkinnedModelUtility.ToWorld( renderer, joint.PreviousEnd );
			var radius = EvaluateColliderRadius( GetChainPosition( i ) );
			var direction = BoBiCoSoftBoneSolver.SafeNormal( end - begin, Vector3.Forward );
			SoftBoneColliderShape shape;
			switch ( ColliderShape )
			{
				case SoftBoneLinkColliderShape.Sphere:
					shape = SoftBoneColliderShape.Sphere( begin, radius );
					break;
				case SoftBoneLinkColliderShape.Box:
					shape = SoftBoneColliderShape.Box( begin, Rotation.FromToRotation( Vector3.Forward, direction ), new Vector3( BoxDimension.z, BoxDimension.x, BoxDimension.y ) );
					break;
				case SoftBoneLinkColliderShape.Flat:
					shape = SoftBoneColliderShape.Flat( begin, direction );
					break;
				default:
					var halfLine = Math.Max( 0f, ColliderHeight * 0.5f - radius );
					shape = SoftBoneColliderShape.Capsule( begin - direction * halfLine, begin + direction * halfLine, radius );
					break;
			}

			_publishedChainShapes.Add( new SoftBoneChainColliderSnapshot( renderer, shape ) );
		}

		SoftBoneCollisionRegistry.Publish( this, renderer, _publishedChainShapes );
	}


//=========================================================================================================================
// Helpers
//=========================================================================================================================

	private bool CaptureFramePose( SkinnedModelRenderer renderer, bool usePhysicsPose )
	{
		_framePose.Clear();

		Transform rootWorld = default;
		if ( !renderer.TryGetBoneTransform( _joints[0].Bone, out rootWorld )
			&& !renderer.TryGetBoneTransformAnimation( _joints[0].Bone, out rootWorld ) )
			return false;
		var rootPose = renderer.WorldTransform.ToLocal( rootWorld );
		_framePose.Add( rootPose );

		for ( var i = 1; i < _joints.Count; i++ )
		{
			var joint = _joints[i];
			Transform animationPose;
			if ( usePhysicsPose )
			{
				if ( _ragdollAnimationPose.Count != _joints.Count ) return false;
				animationPose = ApplyRootPoseDelta( _ragdollAnimationPose[i], _ragdollAnimationPose[0], rootPose );
			}
			else
			{
				if ( !renderer.TryGetBoneTransformAnimation( joint.Bone, out var worldTransform ) )
					return false;
				animationPose = renderer.WorldTransform.ToLocal( worldTransform );
			}

			_framePose.Add( animationPose );
		}

		return true;
	}

	private bool CaptureRagdollAnimationPose( SkinnedModelRenderer renderer )
	{
		_ragdollAnimationPose.Clear();
		foreach ( var joint in _joints )
		{
			if ( !renderer.TryGetBoneTransformAnimation( joint.Bone, out var worldTransform ) )
			{
				_ragdollAnimationPose.Clear();
				return false;
			}

			_ragdollAnimationPose.Add( renderer.WorldTransform.ToLocal( worldTransform ) );
		}

		return true;
	}

	private static Transform ApplyRootPoseDelta( Transform pose, Transform animationRoot, Transform finalRoot )
	{
		var rotationDelta = finalRoot.Rotation * animationRoot.Rotation.Inverse;
		var positionDelta = finalRoot.Position - rotationDelta * animationRoot.Position;
		return new Transform( positionDelta + rotationDelta * pose.Position, rotationDelta * pose.Rotation, pose.Scale );
	}

	private bool IsPhysicsDrivingRenderer( SkinnedModelRenderer renderer )
	{
		if ( Scene is null ) return false;
		foreach ( var physics in Scene.GetAllComponents<ModelPhysics>() )
			if ( physics is not null && physics.Enabled && physics.MotionEnabled && physics.Renderer == renderer )
				return true;
		return false;
	}

	private bool TryGetSolveTransformLocal( SkinnedModelRenderer renderer, SoftBoneJointState joint, out Transform transform )
	{
		if ( renderer is not null && renderer.IsValid() && joint.Bone is not null
			&& renderer.TryGetBoneTransformAnimation( joint.Bone, out var worldTransform ) )
		{
			transform = renderer.WorldTransform.ToLocal( worldTransform );
			return true;
		}

		transform = joint.RestTransformLocal;
		return joint.RestTransformLocal.Scale != Vector3.Zero;
	}

	private static Vector3 GetAnimatedBoneVectorLocal( SkinnedModelRenderer renderer, BoneCollection.Bone bone, Transform animated )
	{
		if ( bone.Children.Count > 0 && SoftBoneSkinnedModelUtility.TryGetBoneAnimationLocal( renderer, bone.Children[0], out var child ) )
		{
			var direction = child.Position - animated.Position;
			if ( direction.Length > Epsilon )
				return direction;
		}

		var fallback = animated.Rotation * Vector3.Forward;
		return fallback.Length > Epsilon ? fallback : Vector3.Forward;
	}

	private Vector3 GetFramePoseVector( int index, SoftBoneJointState joint, Transform pose )
	{
		if ( index + 1 < _framePose.Count )
		{
			var direction = _framePose[index + 1].Position - pose.Position;
			if ( direction.Length > Epsilon ) return direction;
		}

		return GetPoseVectorLocal( joint, pose );
	}

	private static Vector3 GetPoseVectorLocal( SoftBoneJointState joint, Transform animated )
	{
		var poseVector = animated.Rotation * joint.LocalPoseVector;
		if ( poseVector.Length > Epsilon )
			return poseVector;

		var fallback = animated.Rotation * Vector3.Forward;
		return fallback.Length > Epsilon ? fallback : Vector3.Forward;
	}

	private static Rotation GetParentDeflection( int index, Rotation parentAnimatedRotation, Rotation parentSimulationRotation )
	{
		if ( index <= 0 )
			return Rotation.Identity;

		return parentSimulationRotation * parentAnimatedRotation.Inverse;
	}

	private float GetGravityAmount( SoftBoneJointState joint, Transform animated, Vector3 rendererDownLocal, float gravity, float gravityFalloff )
	{
		if ( Math.Abs( gravity ) <= Epsilon )
			return 0f;

		var gravityAmount = Math.Abs( gravity );
		if ( gravityFalloff > Epsilon )
		{
			var gravityNormal = animated.Rotation * joint.OriginalLocalGravityNormal;
			var gravityBias = Math.Min( 1f, 1f - Vector3.Dot(
				BoBiCoSoftBoneSolver.SafeNormal( gravityNormal, rendererDownLocal ),
				rendererDownLocal ) );
			gravityAmount *= (1f - gravityFalloff) + gravityFalloff * gravityBias;
		}

		return Math.Clamp( gravityAmount, 0f, 1f );
	}

	private static Vector3 ApplyGravityTarget( Vector3 begin, Vector3 poseVector, float restLength, float gravityAmount, float gravity, Vector3 rendererDownLocal )
	{
		if ( gravityAmount <= Epsilon )
			return begin + poseVector;

		var gravityVector = (gravity > 0f ? rendererDownLocal : -rendererDownLocal) * restLength;
		var targetVector = poseVector + (gravityVector - poseVector) * gravityAmount;
		return begin + BoBiCoSoftBoneSolver.SafeNormal( targetVector, poseVector ) * restLength;
	}

	private static void ResetToRestPose( SkinnedModelRenderer renderer, SoftBoneJointState joint, Transform animated, Vector3 begin, Vector3 poseVector, Rotation simulationRotation )
	{
		var restEnd = begin + poseVector;
		joint.PreviousBegin = begin;
		joint.PreviousEnd = restEnd;
		joint.PreviousAnimatedBegin = begin;
		joint.PreviousAnimatedRotation = animated.Rotation;
		joint.PreviousSimulationRotation = simulationRotation;
		joint.ImmobileEnd = SoftBoneSkinnedModelUtility.ToWorld( renderer, restEnd );
		joint.PreviousEndWorld = joint.ImmobileEnd;
		joint.PreviousVector = poseVector;
		joint.PreviousVelocity = Vector3.Zero;
		joint.HasHistory = true;
	}

	private static void ApplySolvedTransform( SkinnedModelRenderer renderer, SoftBoneJointState joint, Transform animated, Rotation solvedRotation )
	{
		var localTransform = new Transform( joint.PreviousBegin, solvedRotation, animated.Scale );
		renderer.SceneModel.SetBoneOverride( joint.Bone.Index, in localTransform );
	}

	private static void ApplyAnimationTransform( SkinnedModelRenderer renderer, SoftBoneJointState joint, Transform animated )
	{
		renderer.SceneModel.SetBoneOverride( joint.Bone.Index, in animated );
	}

	private static Vector3 RendererDownLocal( SkinnedModelRenderer renderer )
	{
		if ( renderer is null || !renderer.IsValid() )
			return Vector3.Down;

		return BoBiCoSoftBoneSolver.SafeNormal( renderer.WorldTransform.Rotation.Inverse * Vector3.Down, Vector3.Down );
	}

//=========================================================================================================================
// Building Gizmo 
//=========================================================================================================================

	protected override void DrawGizmos()
	{
		base.DrawGizmos();
		var drawCollider = CollisionMode != SoftBoneCollisionMode.None && ShowColliderGizmo && (AlwaysShowGizmo || Gizmo.IsSelected);
		var drawLimit = ShowLimitGizmo && (AlwaysShowLimitGizmo || Gizmo.IsSelected) && LimitType != SoftBoneLimitType.None;
		if ( !drawCollider && !drawLimit )
			return;

		var renderer = FindRendererForGizmo();
		if ( renderer is null || !renderer.IsValid() )
			return;

		if ( _dirty || _joints.Count == 0 )
			Rebuild( renderer );

		using ( Transform.DisableProxy() )
		{
			Gizmo.Transform = new Transform( Vector3.Zero, Rotation.Identity, 1f );
			Gizmo.Draw.IgnoreDepth = true;
			Gizmo.Draw.LineThickness = 1;
			for ( var i = GetSimulationStartIndex(); i < _joints.Count; i++ )
			{
				var joint = _joints[i];
				if ( !TryGetSolveTransformLocal( renderer, joint, out var animated ) )
					continue;

				var solvedBegin = joint.HasHistory ? joint.PreviousBegin : animated.Position;
				var solvedEnd = joint.HasHistory ? joint.PreviousEnd : animated.Position + GetPoseVectorLocal( joint, animated );
				var physicsBegin = SoftBoneSkinnedModelUtility.ToWorld( renderer, solvedBegin );
				var boneObject = renderer.GetBoneObject( joint.Bone );
				var begin = boneObject is not null && boneObject.IsValid()
					? boneObject.WorldPosition
					: physicsBegin;
				var end = SoftBoneSkinnedModelUtility.ToWorld( renderer, solvedEnd );
				// Root bone: only draw when SimulateRoot is enabled.
				// Non-root bones: skip leaf/endpoint bones (Children.Count == 0) since their
				// endpoint is a synthetic forward poseVector, not a real child position.
				// The Children.Count gate must NOT apply to the simulated root — it may be a
				// leaf or near-leaf bone and still needs collision when SimulateRoot is on.
				var drawJointCollider = drawCollider
					&& (i != 0 || (SimulateRoot && CollideWithRoot));
				if ( drawJointCollider )
				{
					Gizmo.Draw.Color = ColliderGizmoColor.WithAlpha( ColliderGizmoOpacity );
					DrawLinkColliderGizmo( begin, end, EvaluateColliderRadius( GetChainPosition( i ) ) );
				}

				var drawJointLimit = drawLimit && (i != 0 || SimulateRoot);
				if ( drawJointLimit )
				{
					var solvedVector = solvedEnd - solvedBegin;
					var chainPosition = GetChainPosition( i );
					var colliderRadius = EvaluateColliderRadius( chainPosition );
					DrawLimitGizmo( renderer, physicsBegin, solvedVector, Math.Max( joint.Length, colliderRadius * 2f ) * LimitGizmoSize, EvaluateAngle( MaxAngle, ControlMode, MaxAngleRange, MaxAngleCurve, chainPosition ), EvaluateAngle( MaxYaw, ControlMode, MaxYawRange, MaxYawCurve, chainPosition ), EvaluateAngle( MaxPitch, ControlMode, MaxPitchRange, MaxPitchCurve, chainPosition ) );
				}
			}
		}
	}

	private void DrawLinkColliderGizmo( Vector3 worldStart, Vector3 worldEnd, float colliderRadius )
	{
		if ( ColliderShape == SoftBoneLinkColliderShape.Sphere )
		{
			Gizmo.Draw.LineSphere( new Sphere( worldStart, colliderRadius ) );
			return;
		}
		if ( ColliderShape == SoftBoneLinkColliderShape.Capsule )
		{
			var direction = BoBiCoSoftBoneSolver.SafeNormal( worldEnd - worldStart, Vector3.Forward );
			var halfLine = Math.Max( 0f, ColliderHeight * 0.5f - colliderRadius );
			Gizmo.Draw.LineCapsule( new Capsule( worldStart - direction * halfLine, worldStart + direction * halfLine, colliderRadius ) );
			return;
		}

		var forward = BoBiCoSoftBoneSolver.SafeNormal( worldEnd - worldStart, Vector3.Forward );
		var right = BoBiCoSoftBoneSolver.SafeNormal( Vector3.Cross( forward, Vector3.Up ), Vector3.Right );
		var up = BoBiCoSoftBoneSolver.SafeNormal( Vector3.Cross( right, forward ), Vector3.Up );
		if ( ColliderShape == SoftBoneLinkColliderShape.Flat )
		{
			var half = colliderRadius;
			var a = worldStart + (right + up) * half; var b = worldStart + (right - up) * half;
			var c = worldStart + (-right - up) * half; var d = worldStart + (-right + up) * half;
			Gizmo.Draw.Line( a, b ); Gizmo.Draw.Line( b, c ); Gizmo.Draw.Line( c, d ); Gizmo.Draw.Line( d, a );
			Gizmo.Draw.Line( worldStart, worldStart + forward * Math.Max( colliderRadius, 0.2f ) );
			return;
		}

		var halfSize = BoxDimension * 0.5f;
		var corners = new Vector3[8];
		for ( var corner = 0; corner < 8; corner++ )
			corners[corner] = worldStart
				+ forward * (((corner & 1) == 0 ? -1f : 1f) * halfSize.z)
				+ right   * (((corner & 2) == 0 ? -1f : 1f) * halfSize.x)
				+ up      * (((corner & 4) == 0 ? -1f : 1f) * halfSize.y);
		int[] edges = { 0, 1, 0, 2, 0, 4, 1, 3, 1, 5, 2, 3, 2, 6, 3, 7, 4, 5, 4, 6, 5, 7, 6, 7 };
		for ( var edge = 0; edge < edges.Length; edge += 2 ) Gizmo.Draw.Line( corners[edges[edge]], corners[edges[edge + 1]] );
	}

	private void DrawLimitGizmo( SkinnedModelRenderer renderer, Vector3 worldBegin, Vector3 solvedVector, float radius, float maxAngle, float maxYaw, float maxPitch )
	{
		SoftBoneAngleLimit.GetAxes( solvedVector, LimitRotation, out var axisX, out var axisY, out var axisZ );
		axisX = renderer.WorldTransform.Rotation * axisX;
		axisY = renderer.WorldTransform.Rotation * axisY;
		axisZ = renderer.WorldTransform.Rotation * axisZ;
		var gizmoRotation = Rotation.FromAxis( axisY, 90f );
		axisX = gizmoRotation * axisX;
		axisZ = gizmoRotation * axisZ;
		Gizmo.Draw.Color = LimitGizmoColor.WithAlpha( LimitGizmoOpacity );
		const int steps = 24;
		if ( LimitType == SoftBoneLimitType.Cone )
		{
			DrawArc( worldBegin, axisY, axisX, maxAngle, radius, steps );
			DrawArc( worldBegin, axisY, axisZ, maxAngle, radius, steps );
		}
		else if ( LimitType == SoftBoneLimitType.Plane )
		{
			DrawArc( worldBegin, axisY, axisZ, maxAngle, radius, steps );
			Gizmo.Draw.Line( worldBegin - axisX * radius * 0.45f, worldBegin + axisX * radius * 0.45f );
		}
		else
		{
			DrawArc( worldBegin, axisY, axisX, maxYaw, radius, steps );
			DrawArc( worldBegin, axisY, axisZ, maxPitch, radius, steps );
		}
	}

	private static void DrawArc( Vector3 origin, Vector3 forward, Vector3 side, float angle, float radius, int steps )
	{
		var previous = origin + Rotation.FromAxis( side, -angle ) * forward * radius;
		for ( var i = 1; i <= steps; i++ )
		{
			var point = origin + Rotation.FromAxis( side, -angle + angle * 2f * i / steps ) * forward * radius;
			Gizmo.Draw.Line( previous, point );
			previous = point;
		}
	}

	private SkinnedModelRenderer FindRendererForGizmo()
	{
		var targetRenderer = TargetBone?.GetComponentInParent<SkinnedModelRenderer>( true, true );
		if ( targetRenderer is not null && targetRenderer.IsValid() )
			return targetRenderer;
		return null;
	}

	internal SkinnedModelRenderer GetTargetRenderer() => FindRendererForGizmo();

	protected override void OnValidate()
	{
		UpdateOwnerName();
		ColliderGizmoOpacity = Math.Clamp( ColliderGizmoOpacity, 0f, 1f );
		LimitGizmoOpacity = Math.Clamp( LimitGizmoOpacity, 0f, 1f );
		ColliderHeight = Math.Max( ColliderHeight, 0.002f );
		BoxDimension = new Vector3( Math.Max( BoxDimension.x, 0.001f ), Math.Max( BoxDimension.y, 0.001f ), Math.Max( BoxDimension.z, 0.001f ) );
		LimitGizmoSize = Math.Max( LimitGizmoSize, 0.1f );
		MarkDirty();
	}

	protected override void OnDisabled()
	{
		SoftBoneCollisionRegistry.Remove( this );
	}
}