s&box Package Code Search

Search C# source code, UI razor templates, shaders, and configs across s&box packages.

Showing code results for query: * (23 total matches found)
bobicolab.bobicosoftbones / BoBiCo Soft-Physics/Components/BoBiCoSoftBoneChain.cs
Game library
//=========================================================================================================================
// 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 );
	}
}
bobicolab.bobicosoftbones / BoBiCo Soft-Physics/SoftBoneAssembly.cs
Game library
// I don't know, I kept these here to avoid random errors.

global using Sandbox;
global using System.Collections.Generic;
global using System.Linq;
bobicolab.bobicosoftbones / BoBiCo Soft-Physics/SoftBoneSolver.cs
Game library
using System;

/// <summary>
/// Integrates one SoftBone link in renderer-local space.
/// </summary>
public static class BoBiCoSoftBoneSolver
{
	private const float Epsilon = 0.000008f;

	/// <summary>
	/// One integration step.  All positions/vectors in renderer-local space.
	/// Returns the solved end position (renderer-local) and the new velocity.
	/// </summary>
	public static Vector3 IntegrateAdvanced(
		Vector3 begin,
		Vector3 endPoint,           // current physics end (may have had Immobile applied before call)
		Vector3 previousEndPoint,   // endPoint before Immobile, for velocity storage
		Vector3 poseVector,         // animated rest direction (renderer-local)
		Vector3 previousVector,     // last frame's (solvedEnd - begin)
		Vector3 previousVelocity,
		float minLength,
		float maxLength,
		float pull,
		float momentum,
		float stiffness,
		Vector3 gravityTargetEnd,   // begin + gravity-blended direction * restLength
		float frameRatio,           // Time.Delta * 60, used for frameLerp
		out Vector3 newVelocity )
	{
		var delta = previousVelocity * momentum;
		delta += (gravityTargetEnd - (endPoint + delta)) * pull;

		if ( stiffness > Epsilon )
		{
			var nextVector = endPoint + delta - begin;
			delta += (previousVector - nextVector) * stiffness;
		}

		endPoint += delta;
		var boundedVector = endPoint - begin;
		var boundedLength = Math.Clamp( boundedVector.Length, minLength, maxLength );
		endPoint = begin + SafeNormal( boundedVector, poseVector ) * boundedLength;

		// Limit per-step commitment so long frames do not inject a velocity spike.
		var frameLerp = Math.Clamp( (1f / Lerp( 1f, frameRatio, pull )) * frameRatio, 0f, 1f );
		endPoint = Lerp( previousEndPoint, endPoint, frameLerp );

		// velocity = where we actually moved to, not the raw delta
		newVelocity = endPoint - previousEndPoint;

		return endPoint;
	}

	/// <summary>
	/// Solve the head bone rotation to point toward the simulated tail.
	/// Works in renderer-local space.
	/// </summary>
	public static Rotation SolveHeadRotation(
		Vector3 poseVector,         // animated rest direction (renderer-local)
		Vector3 solvedVector,       // solvedEnd - begin (renderer-local)
		Rotation animatedRotation )
	{
		var restAxis = SafeNormal( poseVector, Vector3.Forward );
		var solvedAxis = SafeNormal( solvedVector, restAxis );
		return Rotation.FromToRotation( restAxis, solvedAxis ) * animatedRotation;
	}

	// ── Helpers (all renderer-local) ────────────────────────────────────────────

	public static Vector3 SafeNormal( Vector3 value, Vector3 fallback )
	{
		if ( value.Length <= Epsilon )
			return fallback.Length > Epsilon ? fallback.Normal : Vector3.Forward;

		return value.Normal;
	}

	private static Vector3 Lerp( Vector3 a, Vector3 b, float t ) => a + (b - a) * t;
	private static float Lerp( float a, float b, float t ) => a + (b - a) * t;
}
bobicolab.bobicosoftbones / Code/BoBiCo Soft-Physics/SoftBoneAngleLimit.cs
Game library
using System;

public enum SoftBoneLimitType 
{ 
	/// <summary>
	/// No Limits, The chain can move freely without angular limits.
	/// </summary>
	None, 

	/// <summary>
	/// Limits the movement within a cone-shaped angle from the rest direction.
	/// </summary>
	Cone, 

	/// <summary>
	/// Limits the movement to one side of a plane, preventing the chain from crossing it.
	/// </summary>
	Plane, 

	/// <summary>
	/// Limits the movement within an elliptical cone, allowing different horizontal and vertical angle limits.
	/// </summary>
	Ellipse 
}

public static class SoftBoneAngleLimit
{
	private const float Epsilon = 0.000008f;

	public static Vector3 Apply( Vector3 poseVector, Vector3 solvedDirection, SoftBoneLimitType type, float maxAngle, float maxYaw, float maxPitch, Angles limitRotation )
	{
		var direction = SafeNormal( solvedDirection, poseVector );
		if ( type == SoftBoneLimitType.None ) return direction;
		GetAxes( poseVector, limitRotation, out var axisX, out var axisY, out var axisZ );
		return type switch
		{
			SoftBoneLimitType.Cone => ClampCone( direction, axisX, axisY, maxAngle ),
			SoftBoneLimitType.Plane => ClampPlane( direction, axisX, axisY, axisZ, maxAngle ),
			SoftBoneLimitType.Ellipse => ClampEllipse( direction, axisX, axisY, axisZ, maxYaw, maxPitch ),
			_ => direction
		};
	}

	public static void GetAxes( Vector3 poseVector, Angles limitRotation, out Vector3 axisX, out Vector3 axisY, out Vector3 axisZ )
	{
		var frame = Rotation.FromToRotation( Vector3.Up, SafeNormal( poseVector, Vector3.Up ) ) * limitRotation.ToRotation();
		axisX = SafeNormal( frame * Vector3.Right, Vector3.Right);
		axisY = SafeNormal( frame * Vector3.Up, Vector3.Up );
		axisZ = SafeNormal( Vector3.Cross( axisX, axisY ), Vector3.Forward );
	}

	private static Vector3 ClampCone( Vector3 direction, Vector3 axisX, Vector3 axisY, float maxAngle )
	{
		if ( maxAngle >= 180f ) return direction;
		var angle = MathF.Acos( Math.Clamp( Vector3.Dot( axisY, direction ), -1f, 1f ) ) * 180f / MathF.PI;
		if ( angle <= maxAngle ) return direction;
		var rotationAxis = Vector3.Cross( axisY, direction );
		if ( rotationAxis.Length <= Epsilon ) rotationAxis = axisX;
		return Rotation.FromAxis( SafeNormal( rotationAxis, axisX ), maxAngle ) * axisY;
	}

	private static Vector3 ClampPlane( Vector3 direction, Vector3 axisX, Vector3 axisY, Vector3 axisZ, float maxAngle )
	{
		var planar = direction - axisX * Vector3.Dot( direction, axisX );
		planar = SafeNormal( planar, axisY );
		return ClampCone( planar, axisZ, axisY, maxAngle );
	}

	private static Vector3 ClampEllipse( Vector3 direction, Vector3 axisX, Vector3 axisY, Vector3 axisZ, float maxYaw, float maxPitch )
	{
		var x = Vector3.Dot( direction, axisZ );
		var y = Vector3.Dot( direction, axisX );
		var z = Vector3.Dot( direction, axisY );
		var yaw = Math.Clamp( MathF.Atan2( y, z ), -maxYaw * MathF.PI / 180f, maxYaw * MathF.PI / 180f );
		var pitch = Math.Clamp( MathF.Asin( Math.Clamp( x, -1f, 1f ) ), -maxPitch * MathF.PI / 180f, maxPitch * MathF.PI / 180f );
		var horizontal = MathF.Cos( pitch );
		return SafeNormal( axisZ * MathF.Sin( pitch ) + axisX * (MathF.Sin( yaw ) * horizontal) + axisY * (MathF.Cos( yaw ) * horizontal), axisY );
	}

	private static Vector3 SafeNormal( Vector3 value, Vector3 fallback ) => value.Length > Epsilon ? value.Normal : fallback.Length > Epsilon ? fallback.Normal : Vector3.Forward;
}
bobicolab.bobicosoftbones / Code/BoBiCo Soft-Physics/SoftBoneSkinnedModelUtility.cs
Game library
using System;

public static class SoftBoneSkinnedModelUtility
{
	private const float Epsilon = 0.000008f;

	public static bool TryGetBoneAnimationLocal( SkinnedModelRenderer renderer, BoneCollection.Bone bone, out Transform transform )
	{
		transform = default;

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

		if ( renderer.TryGetBoneTransformAnimation( bone, out var worldTransform ) )
		{
			transform = renderer.WorldTransform.ToLocal( worldTransform );
			return true;
		}

		if ( renderer.TryGetBoneTransform( bone, out worldTransform ) )
		{
			transform = renderer.WorldTransform.ToLocal( worldTransform );
			return true;
		}

		return false;
	}

	public static Vector3 ToWorld( SkinnedModelRenderer renderer, Vector3 localPosition )
	{
		if ( renderer is null || !renderer.IsValid() )
			return localPosition;

		return renderer.WorldTransform.PointToWorld( localPosition );
	}

	public static Vector3 ToLocal( SkinnedModelRenderer renderer, Vector3 worldPosition )
	{
		if ( renderer is null || !renderer.IsValid() )
			return worldPosition;

		return renderer.WorldTransform.PointToLocal( worldPosition );
	}

	public static Vector3 SafeNormal( Vector3 value, Vector3 fallback )
	{
		if ( value.Length <= Epsilon )
			return fallback.Length > Epsilon ? fallback.Normal : Vector3.Forward;

		return value.Normal;
	}
}
bobicolab.bobicosoftbones / BoBiCo Soft-Physics/SoftBoneAngleLimit.cs
Game library
using System;

public enum SoftBoneLimitType 
{ 
	/// <summary>
	/// No Limits, The chain can move freely without angular limits.
	/// </summary>
	None, 

	/// <summary>
	/// Limits the movement within a cone-shaped angle from the rest direction.
	/// </summary>
	Cone, 

	/// <summary>
	/// Limits the movement to one side of a plane, preventing the chain from crossing it.
	/// </summary>
	Plane, 

	/// <summary>
	/// Limits the movement within an elliptical cone, allowing different horizontal and vertical angle limits.
	/// </summary>
	Ellipse 
}

public static class SoftBoneAngleLimit
{
	private const float Epsilon = 0.000008f;

	public static Vector3 Apply( Vector3 poseVector, Vector3 solvedDirection, SoftBoneLimitType type, float maxAngle, float maxYaw, float maxPitch, Angles limitRotation )
	{
		var direction = SafeNormal( solvedDirection, poseVector );
		if ( type == SoftBoneLimitType.None ) return direction;
		GetAxes( poseVector, limitRotation, out var axisX, out var axisY, out var axisZ );
		return type switch
		{
			SoftBoneLimitType.Cone => ClampCone( direction, axisX, axisY, maxAngle ),
			SoftBoneLimitType.Plane => ClampPlane( direction, axisX, axisY, axisZ, maxAngle ),
			SoftBoneLimitType.Ellipse => ClampEllipse( direction, axisX, axisY, axisZ, maxYaw, maxPitch ),
			_ => direction
		};
	}

	public static void GetAxes( Vector3 poseVector, Angles limitRotation, out Vector3 axisX, out Vector3 axisY, out Vector3 axisZ )
	{
		var frame = Rotation.FromToRotation( Vector3.Up, SafeNormal( poseVector, Vector3.Up ) ) * limitRotation.ToRotation();
		axisX = SafeNormal( frame * Vector3.Right, Vector3.Right);
		axisY = SafeNormal( frame * Vector3.Up, Vector3.Up );
		axisZ = SafeNormal( Vector3.Cross( axisX, axisY ), Vector3.Forward );
	}

	private static Vector3 ClampCone( Vector3 direction, Vector3 axisX, Vector3 axisY, float maxAngle )
	{
		if ( maxAngle >= 180f ) return direction;
		var angle = MathF.Acos( Math.Clamp( Vector3.Dot( axisY, direction ), -1f, 1f ) ) * 180f / MathF.PI;
		if ( angle <= maxAngle ) return direction;
		var rotationAxis = Vector3.Cross( axisY, direction );
		if ( rotationAxis.Length <= Epsilon ) rotationAxis = axisX;
		return Rotation.FromAxis( SafeNormal( rotationAxis, axisX ), maxAngle ) * axisY;
	}

	private static Vector3 ClampPlane( Vector3 direction, Vector3 axisX, Vector3 axisY, Vector3 axisZ, float maxAngle )
	{
		var planar = direction - axisX * Vector3.Dot( direction, axisX );
		planar = SafeNormal( planar, axisY );
		return ClampCone( planar, axisZ, axisY, maxAngle );
	}

	private static Vector3 ClampEllipse( Vector3 direction, Vector3 axisX, Vector3 axisY, Vector3 axisZ, float maxYaw, float maxPitch )
	{
		var x = Vector3.Dot( direction, axisZ );
		var y = Vector3.Dot( direction, axisX );
		var z = Vector3.Dot( direction, axisY );
		var yaw = Math.Clamp( MathF.Atan2( y, z ), -maxYaw * MathF.PI / 180f, maxYaw * MathF.PI / 180f );
		var pitch = Math.Clamp( MathF.Asin( Math.Clamp( x, -1f, 1f ) ), -maxPitch * MathF.PI / 180f, maxPitch * MathF.PI / 180f );
		var horizontal = MathF.Cos( pitch );
		return SafeNormal( axisZ * MathF.Sin( pitch ) + axisX * (MathF.Sin( yaw ) * horizontal) + axisY * (MathF.Cos( yaw ) * horizontal), axisY );
	}

	private static Vector3 SafeNormal( Vector3 value, Vector3 fallback ) => value.Length > Epsilon ? value.Normal : fallback.Length > Epsilon ? fallback.Normal : Vector3.Forward;
}
bobicolab.bobicosoftbones / Code/BoBiCo Soft-Physics/SoftBoneJointState.cs
Game library
using System;

/// <summary>Runtime state for one simulated SoftBone joint.</summary>
public sealed class SoftBoneJointState
{
	public BoneCollection.Bone Bone;

	// Rest-pose data
	public float Length;
	public Vector3 LocalPoseVector;
	public Transform RestTransformLocal;
	public Vector3 OriginalLocalGravityNormal;

	// Per-frame state. Positions are renderer-local unless suffixed with World.
	public Vector3 PreviousBegin;
	public Vector3 PreviousEnd;
	public Vector3 PreviousAnimatedBegin;
	public Rotation PreviousAnimatedRotation;
	public Rotation PreviousSimulationRotation;
	public Vector3 ImmobileEnd;
	public Vector3 PreviousEndWorld;
	public Vector3 PreviousVector;
	public Vector3 PreviousVelocity;
	public bool HasHistory;
}
bobicolab.bobicosoftbones / BoBiCo Soft-Physics/Components/BoBiCoSoftBoneGroup.cs
Game library
//=========================================================================================================================
// BoBiCo Soft-Bone Group Component
//=========================================================================================================================

using System;
using System.Collections.Generic;

[Title( "SoftBone Group" )]
[Category( "BoBiCo" )]
[Icon( "account_tree" )]
[HelpUrl( "https://bobico-lab.vercel.app/softbone-docs/bobico-softbones/core-components/softbone-group" )]
public sealed class BoBiCoSoftBoneGroup : Component, Component.ExecuteInEditor
{
	private const int MaxFixedSteps = 6;

//=========================================================================================================================
// Group Setup
//=========================================================================================================================

	/// <summary>
	/// Assign one or more SoftBone Chains for this group to manage and simulate.
    /// Chains outside of the group will be ignored.
	/// </summary>
	[Property, Title( "SoftBone Chains" ), Group( "Chains" )]
	public List<GameObject> Chains { get; set; } = new();

	// ── Settings ───────────────────────────────────────────────────────────────

	/// <summary>
	/// Physics update rate in Hz. 
	/// Lower values are cheaper and laggy; higher values are smoother.
	/// </summary>
	[Property, Range( 15, 240 ), Title( "Update Rate (Hz)" ), Group( "Group Settings" )]
	public float UpdateRate { get; set; } = 120f;

	/// <summary>
	/// Skip simulation entirely when the main camera is further than this distance.
	/// Bones freeze in place until the camera comes back within range.
	/// </summary>
	[Property, Title( "Disable In Far Distance" ), Group( "Group Settings" )]
	public bool DisableFarDistance { get; set; } = false;

	/// <summary>
	/// The distance at which the simulation is disabled.
	/// </summary>
	[Property, Title( "Far Distance" ), Group( "Group Settings" ), ShowIf( nameof( DisableFarDistance ), true )]
	public float FarDistance { get; set; } = 1000f;

//=========================================================================================================================
// Private States & Functions
//=========================================================================================================================

	private SkinnedModelRenderer _resolvedRenderer;
	private IDisposable _updateBonesHook;
	private bool _dirty = true;
	private float _accumulatedTime;

	public void MarkDirty() => _dirty = true;

	protected override void OnEnabled()
	{
		_dirty = true;
		_accumulatedTime = 0f;
		RegisterUpdateBonesHook();
		EnsureSetup( true );
	}

	protected override void OnDisabled()
	{
		SetCollisionCulled( false );
		_updateBonesHook?.Dispose();
		_updateBonesHook = null;
		_resolvedRenderer = null;
		_accumulatedTime = 0f;
	}

	protected override void OnValidate()
	{
		Chains ??= new List<GameObject>();
		UpdateRate = Math.Clamp( UpdateRate, 15f, 240f );
		FarDistance = Math.Max( FarDistance, 0f );
		_dirty = true;
	}

	protected override void OnUpdate()
	{
		if ( _updateBonesHook is not null || !EnsureSetup() )
			return;

		RunWithAccumulator();
	}

	private void RegisterUpdateBonesHook()
	{
		_updateBonesHook?.Dispose();
		_updateBonesHook = Scene?.AddHook(
			GameObjectSystem.Stage.UpdateBones,
			1,
			() => UpdateBones(),
			nameof( BoBiCoSoftBoneGroup ),
			"Solve SoftBone chains after animation" );
	}

	private void UpdateBones()
	{
		if ( !Enabled || !EnsureSetup() )
			return;

		RunWithAccumulator();
	}

	
	private void SimulateAll( float frameRatio )
	{
		var renderer = _resolvedRenderer;
		if ( renderer is null || !renderer.IsValid() )
			return;

		foreach ( var chainObject in Chains )
		{
			if ( chainObject is null || !chainObject.IsValid() )
				continue;

			var chain = chainObject.GetComponent<BoBiCoSoftBoneChain>();
			if ( chain is null || !chain.Enabled )
				continue;

			if ( _dirty || chain.Joints.Count == 0 || chain.IsDirty )
				chain.Rebuild( renderer );

			chain.PublishCollisionShapes( renderer );
		}

		SoftBoneCollisionRegistry.BeginStep( Scene );

		foreach ( var chainObject in Chains )
		{
			if ( chainObject is null || !chainObject.IsValid() )
				continue;

			var chain = chainObject.GetComponent<BoBiCoSoftBoneChain>();
			if ( chain is null || !chain.Enabled )
				continue;

			if ( _dirty || chain.Joints.Count == 0 || chain.IsDirty )
				chain.Rebuild( renderer );

			chain.Simulate( renderer, frameRatio );
		}

		_dirty = false;
	}

	private bool EnsureSetup( bool force = false )
	{
		if ( !force && !_dirty && _resolvedRenderer is not null && _resolvedRenderer.IsValid() )
			return true;

		_resolvedRenderer = ResolveRenderer();

		if ( _resolvedRenderer is null || !_resolvedRenderer.IsValid() )
			return false;

		Chains ??= new List<GameObject>();

		foreach ( var chainObject in Chains )
		{
			if ( chainObject is null || !chainObject.IsValid() )
				continue;

			chainObject.GetComponent<BoBiCoSoftBoneChain>()?.Rebuild( _resolvedRenderer );
		}

		_dirty = false;
		return true;
	}

	private SkinnedModelRenderer ResolveRenderer()
	{
		Chains ??= new List<GameObject>();
		foreach ( var chainObject in Chains )
		{
			if ( chainObject is null || !chainObject.IsValid() )
				continue;

			var renderer = chainObject.GetComponent<BoBiCoSoftBoneChain>()?.GetTargetRenderer();
			if ( renderer is not null && renderer.IsValid() )
				return renderer;
		}

		return null;
	}


//=========================================================================================================================
// Bone & Collision Culling
//=========================================================================================================================

	private Vector3? GetMainCameraPosition()
	{
		foreach ( var cam in Scene.GetAllComponents<CameraComponent>() )
		{
			if ( cam.IsMainCamera )
				return cam.WorldPosition;
		}
		return null;
	}

	private void RunWithAccumulator()
	{
		var culled = false;
		if ( DisableFarDistance )
		{
			var camPos = GetMainCameraPosition();
			if ( camPos.HasValue )
			{
				var dist = Vector3.DistanceBetween( WorldPosition, camPos.Value );
				culled = dist > FarDistance;
			}
		}

		SetCollisionCulled( culled );
		if ( culled )
		{
			_accumulatedTime = 0f;
			return;
		}

		var rate = Math.Clamp( UpdateRate, 15f, 120f );
		_accumulatedTime += Time.Delta;

		var executions = Math.Min( _accumulatedTime * rate, MaxFixedSteps );
		var numSteps = (int)MathF.Ceiling( executions );

		if ( numSteps <= 1 )
		{
			SimulateAll( executions );
		}
		else
		{
			for ( var step = 0; step < numSteps; step++ )
				SimulateAll( Math.Min( 1f, executions - step ) );
		}

		if ( executions >= 1f )
			_accumulatedTime = (executions - MathF.Floor( executions )) / rate;
	}

	private void SetCollisionCulled( bool culled )
	{
		SoftBoneCollisionRegistry.SetRendererCulled( _resolvedRenderer, culled );
		if ( !culled ) return;

		foreach ( var chainObject in Chains )
		{
			var chain = chainObject?.GetComponent<BoBiCoSoftBoneChain>();
			if ( chain is not null ) SoftBoneCollisionRegistry.Remove( chain );
		}
	}
}
bobicolab.bobicosoftbones / BoBiCo Soft-Physics/SoftBoneCollisionRegistry.cs
Game library
using System.Collections.Generic;
using Sandbox;
using System.Linq;


public readonly struct SoftBoneChainColliderSnapshot
{
	public readonly SkinnedModelRenderer Renderer;
	public readonly SoftBoneColliderShape Shape;

	public SoftBoneChainColliderSnapshot( SkinnedModelRenderer renderer, SoftBoneColliderShape shape )
	{
		Renderer = renderer;
		Shape = shape;
	}
}

public readonly struct SoftBoneBodyColliderSnapshot
{
	public readonly SkinnedModelRenderer Renderer;
	public readonly SoftBoneColliderShape Shape;

	public SoftBoneBodyColliderSnapshot( SkinnedModelRenderer renderer, SoftBoneColliderShape shape )
	{
		Renderer = renderer;
		Shape = shape;
	}
}

/// <summary>Publishes the previous solved link shapes used by cross-chain contacts.</summary>
public static class SoftBoneCollisionRegistry
{
	private static readonly Dictionary<BoBiCoSoftBoneChain, List<SoftBoneChainColliderSnapshot>> PublishedShapes = new();
	private static readonly Dictionary<BoBiCoSoftBoneChain, List<SoftBoneChainColliderSnapshot>> QueryShapes = new();
	private static readonly List<SoftBoneBodyColliderSnapshot> QueryBodyShapes = new();
	private static readonly HashSet<SkinnedModelRenderer> CulledRenderers = new();

	public static void SetRendererCulled( SkinnedModelRenderer renderer, bool culled )
	{
		if ( renderer is null || !renderer.IsValid() ) return;
		if ( culled ) CulledRenderers.Add( renderer );
		else CulledRenderers.Remove( renderer );
	}

	public static void Publish( BoBiCoSoftBoneChain chain, SkinnedModelRenderer renderer, List<SoftBoneChainColliderSnapshot> shapes )
	{
		PublishedShapes[chain] = new List<SoftBoneChainColliderSnapshot>( shapes );
	}

	public static void BeginStep( Scene scene )
	{
		QueryShapes.Clear();
		foreach ( var entry in PublishedShapes )
			QueryShapes.Add( entry.Key, new List<SoftBoneChainColliderSnapshot>( entry.Value ) );

		QueryBodyShapes.Clear();
		if ( scene is null )
			return;

		foreach ( var collider in scene.GetAllComponents<BoBiCoSoftBoneCollider>() )
		{
			if ( collider is null || !collider.Enabled )
				continue;

			var renderer = collider.GetOwningRenderer();
			if ( renderer is not null && CulledRenderers.Contains( renderer ) )
				continue;
			if ( collider.TryGetWorldShape( renderer, out var shape ) )
				QueryBodyShapes.Add( new SoftBoneBodyColliderSnapshot( renderer, shape ) );
		}
	}

	public static void Remove( BoBiCoSoftBoneChain chain )
	{
		PublishedShapes.Remove( chain );
		QueryShapes.Remove( chain );
	}

	public static void CollectChainShapes( BoBiCoSoftBoneChain owner, SkinnedModelRenderer renderer, SoftBoneCollisionMode mode, List<SoftBoneColliderShape> output )
	{
		if ( mode == SoftBoneCollisionMode.BaseCollision )
			return;

		foreach ( var entry in QueryShapes )
		{
			if ( entry.Key == owner || !entry.Key.Enabled )
				continue;

			var sameRenderer = entry.Value.Count > 0 && entry.Value[0].Renderer == renderer;
			if ( !sameRenderer && mode != SoftBoneCollisionMode.FullCollision )
				continue;

			foreach ( var snapshot in entry.Value )
				output.Add( snapshot.Shape );
		}
	}

	public static void CollectExternalBodyShapes( SkinnedModelRenderer ownerRenderer, List<SoftBoneColliderShape> output )
	{
		foreach ( var snapshot in QueryBodyShapes )
			if ( snapshot.Renderer != ownerRenderer )
				output.Add( snapshot.Shape );
	}
}
bobicolab.bobicosoftbones / Code/BoBiCo Soft-Physics/SoftBoneCollisionRegistry.cs
Game library
using System.Collections.Generic;
using Sandbox;
using System.Linq;


public readonly struct SoftBoneChainColliderSnapshot
{
	public readonly SkinnedModelRenderer Renderer;
	public readonly SoftBoneColliderShape Shape;

	public SoftBoneChainColliderSnapshot( SkinnedModelRenderer renderer, SoftBoneColliderShape shape )
	{
		Renderer = renderer;
		Shape = shape;
	}
}

public readonly struct SoftBoneBodyColliderSnapshot
{
	public readonly SkinnedModelRenderer Renderer;
	public readonly SoftBoneColliderShape Shape;

	public SoftBoneBodyColliderSnapshot( SkinnedModelRenderer renderer, SoftBoneColliderShape shape )
	{
		Renderer = renderer;
		Shape = shape;
	}
}

/// <summary>Publishes the previous solved link shapes used by cross-chain contacts.</summary>
public static class SoftBoneCollisionRegistry
{
	private static readonly Dictionary<BoBiCoSoftBoneChain, List<SoftBoneChainColliderSnapshot>> PublishedShapes = new();
	private static readonly Dictionary<BoBiCoSoftBoneChain, List<SoftBoneChainColliderSnapshot>> QueryShapes = new();
	private static readonly List<SoftBoneBodyColliderSnapshot> QueryBodyShapes = new();
	private static readonly HashSet<SkinnedModelRenderer> CulledRenderers = new();

	public static void SetRendererCulled( SkinnedModelRenderer renderer, bool culled )
	{
		if ( renderer is null || !renderer.IsValid() ) return;
		if ( culled ) CulledRenderers.Add( renderer );
		else CulledRenderers.Remove( renderer );
	}

	public static void Publish( BoBiCoSoftBoneChain chain, SkinnedModelRenderer renderer, List<SoftBoneChainColliderSnapshot> shapes )
	{
		PublishedShapes[chain] = new List<SoftBoneChainColliderSnapshot>( shapes );
	}

	public static void BeginStep( Scene scene )
	{
		QueryShapes.Clear();
		foreach ( var entry in PublishedShapes )
			QueryShapes.Add( entry.Key, new List<SoftBoneChainColliderSnapshot>( entry.Value ) );

		QueryBodyShapes.Clear();
		if ( scene is null )
			return;

		foreach ( var collider in scene.GetAllComponents<BoBiCoSoftBoneCollider>() )
		{
			if ( collider is null || !collider.Enabled )
				continue;

			var renderer = collider.GetOwningRenderer();
			if ( renderer is not null && CulledRenderers.Contains( renderer ) )
				continue;
			if ( collider.TryGetWorldShape( renderer, out var shape ) )
				QueryBodyShapes.Add( new SoftBoneBodyColliderSnapshot( renderer, shape ) );
		}
	}

	public static void Remove( BoBiCoSoftBoneChain chain )
	{
		PublishedShapes.Remove( chain );
		QueryShapes.Remove( chain );
	}

	public static void CollectChainShapes( BoBiCoSoftBoneChain owner, SkinnedModelRenderer renderer, SoftBoneCollisionMode mode, List<SoftBoneColliderShape> output )
	{
		if ( mode == SoftBoneCollisionMode.BaseCollision )
			return;

		foreach ( var entry in QueryShapes )
		{
			if ( entry.Key == owner || !entry.Key.Enabled )
				continue;

			var sameRenderer = entry.Value.Count > 0 && entry.Value[0].Renderer == renderer;
			if ( !sameRenderer && mode != SoftBoneCollisionMode.FullCollision )
				continue;

			foreach ( var snapshot in entry.Value )
				output.Add( snapshot.Shape );
		}
	}

	public static void CollectExternalBodyShapes( SkinnedModelRenderer ownerRenderer, List<SoftBoneColliderShape> output )
	{
		foreach ( var snapshot in QueryBodyShapes )
			if ( snapshot.Renderer != ownerRenderer )
				output.Add( snapshot.Shape );
	}
}
bobicolab.bobicosoftbones / BoBiCo Soft-Physics/Components/BoBiCoSoftBoneCollider.cs
Game library
//=========================================================================================================================
// BoBiCo Soft-Bone Collider Component
//=========================================================================================================================

using System;

[Title( "SoftBone Collider" )]
[Category( "BoBiCo" )]
[Icon( "radio_button_unchecked" )]
[HelpUrl( "https://bobico-lab.vercel.app/softbone-docs/bobico-softbones/core-components/softbone-collider" )]
public sealed class BoBiCoSoftBoneCollider : Component, Component.ExecuteInEditor
{
public enum ColliderShape
{
	/// <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,
}

//=========================================================================================================================
// Collider Properties
//=========================================================================================================================

	/// <summary>
	/// Offsets the position of the collider.
	/// </summary>
	[Property, Order( 1 ), Title( "Position Offset" ), Group( "Collider Properties" )]
	public Vector3 PositionOffset { get; set; }

	/// <summary>
	/// Offsets the rotation of the collider.
	/// </summary>
	[Property, Order( 2 ), Title( "Rotation Offset" ), Group( "Collider Properties" )]
	public Angles RotationOffset { get; set; }

	/// <summary>
	/// Target bone to which the collider is attached. 
	/// </summary>
	[Property, Order( 3 ), Title( "Target Bone" ), Group( "Collider Properties" )]
	public GameObject TargetBone { get; set; }

//=========================================================================================================================
// Shape Settings
//=========================================================================================================================

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

    /// <summary>
    /// Radius of the chain's collision volume.
    /// Larger values create thicker collision boundaries.
    /// </summary>
	[Property, Order( 5 ), Title( "Collider Radius" ), Group( "Shape Settings" ), HideIf( nameof( Shape ), ColliderShape.Box )]
	public float ColliderRadius { get; set; } = 2f;

    /// <summary>
    /// Height of Capsule collider.
    /// </summary>
	[Property, Order( 6 ), Title( "Collider Height" ), Group( "Shape Settings" ), ShowIf( nameof( Shape ), ColliderShape.Capsule )]
	public float ColliderHeight { get; set; } = 10f;

    /// <summary>
    /// Size of Box colliders.
    /// Only applies when the collider shape is set to Box.
    /// </summary>
	[Property, Order( 7 ), Title( "Box Dimension" ), Group( "Shape Settings" ), ShowIf( nameof( Shape ), ColliderShape.Box )]
	public Vector3 BoxSize { get; set; } = new Vector3( 1.25f, 1.25f, 1.25f );

//=========================================================================================================================
// Collider Gizmo Settings
//=========================================================================================================================

    /// <summary>
    /// Displays the chain's collider in the Scene view.
    /// Useful for visualizing collision shapes and sizes.
    /// </summary>
	[Property, Order( 8 ), 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( 9 ), 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( 10 ), Title( "Collider Gizmo Color" ), Group( "Gizmo Settings" ), ShowIf( nameof( ShowColliderGizmo ), true )]
	public Color ColliderGizmoColor { get; set; } = Gizmo.Colors.Green;

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

//=========================================================================================================================
// Collider Shape Calculation
//=========================================================================================================================
	public bool TryGetWorldShape( SkinnedModelRenderer renderer, out SoftBoneColliderShape shape )
	{
		shape = default;
		var boneName = TargetBone is not null && TargetBone.IsValid() ? TargetBone.Name?.Trim() : "";
		if ( string.IsNullOrWhiteSpace( boneName ) )
			return false;

		var hasBoneTransform = renderer is not null && renderer.IsValid() && renderer.Model?.Bones is not null && renderer.Model.Bones.HasBone( boneName );
		if ( hasBoneTransform )
		{
			var bone = renderer.Model.Bones.GetBone( boneName );
			if ( renderer.TryGetBoneTransformAnimation( bone, out var animatedTransform ) || renderer.TryGetBoneTransform( bone, out animatedTransform ) )
				return BuildWorldShape( animatedTransform, out shape );
		}

		return BuildWorldShape( TargetBone.WorldTransform, out shape );
	}

	private bool BuildWorldShape( Transform boneTransform, out SoftBoneColliderShape shape )
	{
		shape = default;

		// Swap pitch/yaw to match S&box bone forward = X axis
		var fixedRotOffset = new Angles( RotationOffset.roll, RotationOffset.pitch, RotationOffset.yaw );
		var rotation = boneTransform.Rotation * fixedRotOffset.ToRotation();
		var radius = Math.Max( ColliderRadius, 0.001f );
		var height = Math.Max( ColliderHeight, radius * 2f );
		// Collider offsets use the existing VRC-style axis order: X=up, Y=forward, Z=right.
		var remappedOffset = new Vector3( PositionOffset.y, PositionOffset.z, PositionOffset.x );
		var center = boneTransform.PointToWorld( remappedOffset );

		if ( Shape == ColliderShape.Sphere || (Shape == ColliderShape.Capsule && height <= radius * 2f) )
		{
			shape = SoftBoneColliderShape.Sphere( center, radius );
			return true;
		}

		if ( Shape == ColliderShape.Capsule )
		{
			var halfLine = Math.Max( 0f, height * 0.5f - radius );
			// VRC capsules are authored along their local Up axis. Using Forward here
			// made S&box Roll rotate around the capsule's own length, which is invisible.
			var axis = rotation * Vector3.Forward;
			shape = SoftBoneColliderShape.Capsule( center - axis * halfLine, center + axis * halfLine, radius );
		}
		else if ( Shape == ColliderShape.Box )
			shape = SoftBoneColliderShape.Box( center, rotation, new Vector3( Math.Max( BoxSize.z, 0.001f ), Math.Max( BoxSize.x, 0.001f ), Math.Max( BoxSize.y, 0.001f ) ) );
		else
			shape = SoftBoneColliderShape.Flat( center, rotation * Vector3.Up );
		return true;
	}

//=========================================================================================================================
// Collider Gizmo Drawing
//=========================================================================================================================

	protected override void DrawGizmos()
	{
		base.DrawGizmos();
		DrawColliderGizmo( AlwaysShowGizmo || Gizmo.IsSelected );
	}

	internal void DrawColliderGizmo( bool visible, bool force = false )
	{
		if ( !visible || ( !force && !ShowColliderGizmo ) || !TryGetWorldShape( FindRenderer(), out var shape ) )
			return;

		// Draw directly in world space so the sphere radius is never distorted by
		// the component's scale (which caused the squished ellipsoid).
		using ( Gizmo.Scope( "SoftBoneCollider" ) )
		{
			Gizmo.Transform = new Transform( Vector3.Zero, Rotation.Identity, 1f );
			Gizmo.Draw.IgnoreDepth = true;
			Gizmo.Draw.LineThickness = 1;
			Gizmo.Draw.Color = ColliderGizmoColor.WithAlpha( ColliderGizmoOpacity );

			if ( shape.IsSphere )
				Gizmo.Draw.LineSphere( new Sphere( shape.Start, shape.Radius ) );
			else if ( shape.Type == SoftBoneColliderShapeType.Capsule )
				Gizmo.Draw.LineCapsule( new Capsule( shape.Start, shape.End, shape.Radius ) );
			else if ( shape.Type == SoftBoneColliderShapeType.Box )
			{
				Gizmo.Transform = new Transform( shape.Center, shape.Rotation, 1f );
				Gizmo.Draw.LineBBox( BBox.FromPositionAndSize( Vector3.Zero, shape.Size ) );
			}
			else
			{
				var tangent = Vector3.Cross( shape.Normal, Math.Abs( Vector3.Dot( shape.Normal, Vector3.Up ) ) < 0.99f ? Vector3.Up : Vector3.Right ).Normal;
				var bitangent = Vector3.Cross( shape.Normal, tangent ).Normal;
				var size = ColliderRadius;
				var a = shape.Center + (tangent + bitangent) * size;
				var b = shape.Center + (tangent - bitangent) * size;
				var c = shape.Center + (-tangent - bitangent) * size;
				var d = shape.Center + (-tangent + bitangent) * size;
				Gizmo.Draw.Line( a, b ); Gizmo.Draw.Line( b, c ); Gizmo.Draw.Line( c, d ); Gizmo.Draw.Line( d, a );
				Gizmo.Draw.Line( shape.Center, shape.Center + shape.Normal * 8f );
			}
		}
	}

//=========================================================================================================================
// Collider Bone Lookup
//=========================================================================================================================

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

	internal SkinnedModelRenderer GetOwningRenderer() => FindRenderer();

	protected override void OnValidate()
	{
		ColliderRadius = Math.Max( ColliderRadius, 0.001f );
		ColliderHeight = Math.Max( ColliderHeight, ColliderRadius * 2f );
		BoxSize = new Vector3( Math.Max( BoxSize.x, 0.001f ), Math.Max( BoxSize.y, 0.001f ), Math.Max( BoxSize.z, 0.001f ) );
		ColliderGizmoOpacity = Math.Clamp( ColliderGizmoOpacity, 0f, 1f );
	}
}
bobicolab.bobicosoftbones / BoBiCo Soft-Physics/Components/BoBiCoSoftBoneColliderGroup.cs
Game library
//=========================================================================================================================
// BoBiCo Soft-Bone Collider Group Component
//=========================================================================================================================

using System.Collections.Generic;

[Title( "SoftBone Collider Group" )]
[Category( "BoBiCo" )]
[Icon( "category" )]
[HelpUrl( "https://bobico-lab.vercel.app/softbone-docs/bobico-softbones/core-components/softbone-collider-group" )]
public sealed class BoBiCoSoftBoneColliderGroup : Component, Component.ExecuteInEditor
{

//=========================================================================================================================
// Group Setup
//=========================================================================================================================

	/// <summary>
	/// List of colliders that are part of this group. These colliders will be used for collision detection with soft-bone chains.
	/// </summary>
	[Property, Title( "Colliders" ), Group( "Colliders" )]
	public List<GameObject> Colliders { get; set; } = new();

    // Collect all valid colliders from the group and add them to the output list, avoiding duplicates using the seen set.
	public void CollectColliders( List<BoBiCoSoftBoneCollider> output, HashSet<BoBiCoSoftBoneCollider> seen )
	{
		if ( !Enabled || Colliders is null )
			return;

		foreach ( var colliderObject in Colliders )
		{
			if ( colliderObject is null || !colliderObject.IsValid() )
				continue;

			var collider = colliderObject.GetComponent<BoBiCoSoftBoneCollider>();
			if ( collider is not null && collider.Enabled && seen.Add( collider ) )
				output.Add( collider );
		}
	}
}
bobicolab.bobicosoftbones / BoBiCo Soft-Physics/SoftBoneJointState.cs
Game library
using System;

/// <summary>Runtime state for one simulated SoftBone joint.</summary>
public sealed class SoftBoneJointState
{
	public BoneCollection.Bone Bone;

	// Rest-pose data
	public float Length;
	public Vector3 LocalPoseVector;
	public Transform RestTransformLocal;
	public Vector3 OriginalLocalGravityNormal;

	// Per-frame state. Positions are renderer-local unless suffixed with World.
	public Vector3 PreviousBegin;
	public Vector3 PreviousEnd;
	public Vector3 PreviousAnimatedBegin;
	public Rotation PreviousAnimatedRotation;
	public Rotation PreviousSimulationRotation;
	public Vector3 ImmobileEnd;
	public Vector3 PreviousEndWorld;
	public Vector3 PreviousVector;
	public Vector3 PreviousVelocity;
	public bool HasHistory;
}
bobicolab.bobicosoftbones / Code/BoBiCo Soft-Physics/SoftBoneAssembly.cs
Game library
// I don't know, I kept these here to avoid random errors.

global using Sandbox;
global using System.Collections.Generic;
global using System.Linq;
bobicolab.bobicosoftbones / BoBiCo Soft-Physics/SoftBoneSkinnedModelUtility.cs
Game library
using System;

public static class SoftBoneSkinnedModelUtility
{
	private const float Epsilon = 0.000008f;

	public static bool TryGetBoneAnimationLocal( SkinnedModelRenderer renderer, BoneCollection.Bone bone, out Transform transform )
	{
		transform = default;

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

		if ( renderer.TryGetBoneTransformAnimation( bone, out var worldTransform ) )
		{
			transform = renderer.WorldTransform.ToLocal( worldTransform );
			return true;
		}

		if ( renderer.TryGetBoneTransform( bone, out worldTransform ) )
		{
			transform = renderer.WorldTransform.ToLocal( worldTransform );
			return true;
		}

		return false;
	}

	public static Vector3 ToWorld( SkinnedModelRenderer renderer, Vector3 localPosition )
	{
		if ( renderer is null || !renderer.IsValid() )
			return localPosition;

		return renderer.WorldTransform.PointToWorld( localPosition );
	}

	public static Vector3 ToLocal( SkinnedModelRenderer renderer, Vector3 worldPosition )
	{
		if ( renderer is null || !renderer.IsValid() )
			return worldPosition;

		return renderer.WorldTransform.PointToLocal( worldPosition );
	}

	public static Vector3 SafeNormal( Vector3 value, Vector3 fallback )
	{
		if ( value.Length <= Epsilon )
			return fallback.Length > Epsilon ? fallback.Normal : Vector3.Forward;

		return value.Normal;
	}
}
bobicolab.bobicosoftbones / Code/BoBiCo Soft-Physics/Components/BoBiCoSoftBoneChain.cs
Game library
//=========================================================================================================================
// 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 );
	}
}
bobicolab.bobicosoftbones / Code/BoBiCo Soft-Physics/SoftBoneSolver.cs
Game library
using System;

/// <summary>
/// Integrates one SoftBone link in renderer-local space.
/// </summary>
public static class BoBiCoSoftBoneSolver
{
	private const float Epsilon = 0.000008f;

	/// <summary>
	/// One integration step.  All positions/vectors in renderer-local space.
	/// Returns the solved end position (renderer-local) and the new velocity.
	/// </summary>
	public static Vector3 IntegrateAdvanced(
		Vector3 begin,
		Vector3 endPoint,           // current physics end (may have had Immobile applied before call)
		Vector3 previousEndPoint,   // endPoint before Immobile, for velocity storage
		Vector3 poseVector,         // animated rest direction (renderer-local)
		Vector3 previousVector,     // last frame's (solvedEnd - begin)
		Vector3 previousVelocity,
		float minLength,
		float maxLength,
		float pull,
		float momentum,
		float stiffness,
		Vector3 gravityTargetEnd,   // begin + gravity-blended direction * restLength
		float frameRatio,           // Time.Delta * 60, used for frameLerp
		out Vector3 newVelocity )
	{
		var delta = previousVelocity * momentum;
		delta += (gravityTargetEnd - (endPoint + delta)) * pull;

		if ( stiffness > Epsilon )
		{
			var nextVector = endPoint + delta - begin;
			delta += (previousVector - nextVector) * stiffness;
		}

		endPoint += delta;
		var boundedVector = endPoint - begin;
		var boundedLength = Math.Clamp( boundedVector.Length, minLength, maxLength );
		endPoint = begin + SafeNormal( boundedVector, poseVector ) * boundedLength;

		// Limit per-step commitment so long frames do not inject a velocity spike.
		var frameLerp = Math.Clamp( (1f / Lerp( 1f, frameRatio, pull )) * frameRatio, 0f, 1f );
		endPoint = Lerp( previousEndPoint, endPoint, frameLerp );

		// velocity = where we actually moved to, not the raw delta
		newVelocity = endPoint - previousEndPoint;

		return endPoint;
	}

	/// <summary>
	/// Solve the head bone rotation to point toward the simulated tail.
	/// Works in renderer-local space.
	/// </summary>
	public static Rotation SolveHeadRotation(
		Vector3 poseVector,         // animated rest direction (renderer-local)
		Vector3 solvedVector,       // solvedEnd - begin (renderer-local)
		Rotation animatedRotation )
	{
		var restAxis = SafeNormal( poseVector, Vector3.Forward );
		var solvedAxis = SafeNormal( solvedVector, restAxis );
		return Rotation.FromToRotation( restAxis, solvedAxis ) * animatedRotation;
	}

	// ── Helpers (all renderer-local) ────────────────────────────────────────────

	public static Vector3 SafeNormal( Vector3 value, Vector3 fallback )
	{
		if ( value.Length <= Epsilon )
			return fallback.Length > Epsilon ? fallback.Normal : Vector3.Forward;

		return value.Normal;
	}

	private static Vector3 Lerp( Vector3 a, Vector3 b, float t ) => a + (b - a) * t;
	private static float Lerp( float a, float b, float t ) => a + (b - a) * t;
}
bobicolab.bobicosoftbones / .obj/__compiler_extra.cs
Game library
global using static Sandbox.Internal.GlobalGameNamespace;
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Rendering;
[assembly: global::System.Reflection.AssemblyMetadata( "AddonTitle", "BoBiCo SoftBones" )]
[assembly: global::System.Reflection.AssemblyMetadata( "AddonIdent", "bobicosoftbones" )]
[assembly: global::System.Reflection.AssemblyMetadata( "OrgIdent", "bobicolab" )]
[assembly: global::System.Reflection.AssemblyMetadata( "Ident", "bobicolab.bobicosoftbones" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineVersion", "28" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineMinorVersion", "1" )]

[assembly: System.Runtime.Versioning.TargetFramework( ".NETCoreApp,Version=v9.0", FrameworkDisplayName = ".NET 9.0" )]
[assembly: global::System.Reflection.AssemblyMetadata( "CompileTime", "2026-07-29T06:05:00.6415890Z" )]
[assembly: global::System.Reflection.AssemblyVersion("0.0.118.0")]
[assembly: global::System.Reflection.AssemblyFileVersion("0.0.118.0")]
bobicolab.bobicosoftbones / Code/BoBiCo Soft-Physics/Components/BoBiCoSoftBoneCollider.cs
Game library
//=========================================================================================================================
// BoBiCo Soft-Bone Collider Component
//=========================================================================================================================

using System;

[Title( "SoftBone Collider" )]
[Category( "BoBiCo" )]
[Icon( "radio_button_unchecked" )]
[HelpUrl( "https://bobico-lab.vercel.app/softbone-docs/bobico-softbones/core-components/softbone-collider" )]
public sealed class BoBiCoSoftBoneCollider : Component, Component.ExecuteInEditor
{
public enum ColliderShape
{
	/// <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,
}

//=========================================================================================================================
// Collider Properties
//=========================================================================================================================

	/// <summary>
	/// Offsets the position of the collider.
	/// </summary>
	[Property, Order( 1 ), Title( "Position Offset" ), Group( "Collider Properties" )]
	public Vector3 PositionOffset { get; set; }

	/// <summary>
	/// Offsets the rotation of the collider.
	/// </summary>
	[Property, Order( 2 ), Title( "Rotation Offset" ), Group( "Collider Properties" )]
	public Angles RotationOffset { get; set; }

	/// <summary>
	/// Target bone to which the collider is attached. 
	/// </summary>
	[Property, Order( 3 ), Title( "Target Bone" ), Group( "Collider Properties" )]
	public GameObject TargetBone { get; set; }

//=========================================================================================================================
// Shape Settings
//=========================================================================================================================

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

    /// <summary>
    /// Radius of the chain's collision volume.
    /// Larger values create thicker collision boundaries.
    /// </summary>
	[Property, Order( 5 ), Title( "Collider Radius" ), Group( "Shape Settings" ), HideIf( nameof( Shape ), ColliderShape.Box )]
	public float ColliderRadius { get; set; } = 2f;

    /// <summary>
    /// Height of Capsule collider.
    /// </summary>
	[Property, Order( 6 ), Title( "Collider Height" ), Group( "Shape Settings" ), ShowIf( nameof( Shape ), ColliderShape.Capsule )]
	public float ColliderHeight { get; set; } = 10f;

    /// <summary>
    /// Size of Box colliders.
    /// Only applies when the collider shape is set to Box.
    /// </summary>
	[Property, Order( 7 ), Title( "Box Dimension" ), Group( "Shape Settings" ), ShowIf( nameof( Shape ), ColliderShape.Box )]
	public Vector3 BoxSize { get; set; } = new Vector3( 1.25f, 1.25f, 1.25f );

//=========================================================================================================================
// Collider Gizmo Settings
//=========================================================================================================================

    /// <summary>
    /// Displays the chain's collider in the Scene view.
    /// Useful for visualizing collision shapes and sizes.
    /// </summary>
	[Property, Order( 8 ), 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( 9 ), 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( 10 ), Title( "Collider Gizmo Color" ), Group( "Gizmo Settings" ), ShowIf( nameof( ShowColliderGizmo ), true )]
	public Color ColliderGizmoColor { get; set; } = Gizmo.Colors.Green;

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

//=========================================================================================================================
// Collider Shape Calculation
//=========================================================================================================================
	public bool TryGetWorldShape( SkinnedModelRenderer renderer, out SoftBoneColliderShape shape )
	{
		shape = default;
		var boneName = TargetBone is not null && TargetBone.IsValid() ? TargetBone.Name?.Trim() : "";
		if ( string.IsNullOrWhiteSpace( boneName ) )
			return false;

		var hasBoneTransform = renderer is not null && renderer.IsValid() && renderer.Model?.Bones is not null && renderer.Model.Bones.HasBone( boneName );
		if ( hasBoneTransform )
		{
			var bone = renderer.Model.Bones.GetBone( boneName );
			if ( renderer.TryGetBoneTransformAnimation( bone, out var animatedTransform ) || renderer.TryGetBoneTransform( bone, out animatedTransform ) )
				return BuildWorldShape( animatedTransform, out shape );
		}

		return BuildWorldShape( TargetBone.WorldTransform, out shape );
	}

	private bool BuildWorldShape( Transform boneTransform, out SoftBoneColliderShape shape )
	{
		shape = default;

		// Swap pitch/yaw to match S&box bone forward = X axis
		var fixedRotOffset = new Angles( RotationOffset.roll, RotationOffset.pitch, RotationOffset.yaw );
		var rotation = boneTransform.Rotation * fixedRotOffset.ToRotation();
		var radius = Math.Max( ColliderRadius, 0.001f );
		var height = Math.Max( ColliderHeight, radius * 2f );
		// Collider offsets use the existing VRC-style axis order: X=up, Y=forward, Z=right.
		var remappedOffset = new Vector3( PositionOffset.y, PositionOffset.z, PositionOffset.x );
		var center = boneTransform.PointToWorld( remappedOffset );

		if ( Shape == ColliderShape.Sphere || (Shape == ColliderShape.Capsule && height <= radius * 2f) )
		{
			shape = SoftBoneColliderShape.Sphere( center, radius );
			return true;
		}

		if ( Shape == ColliderShape.Capsule )
		{
			var halfLine = Math.Max( 0f, height * 0.5f - radius );
			// VRC capsules are authored along their local Up axis. Using Forward here
			// made S&box Roll rotate around the capsule's own length, which is invisible.
			var axis = rotation * Vector3.Forward;
			shape = SoftBoneColliderShape.Capsule( center - axis * halfLine, center + axis * halfLine, radius );
		}
		else if ( Shape == ColliderShape.Box )
			shape = SoftBoneColliderShape.Box( center, rotation, new Vector3( Math.Max( BoxSize.z, 0.001f ), Math.Max( BoxSize.x, 0.001f ), Math.Max( BoxSize.y, 0.001f ) ) );
		else
			shape = SoftBoneColliderShape.Flat( center, rotation * Vector3.Up );
		return true;
	}

//=========================================================================================================================
// Collider Gizmo Drawing
//=========================================================================================================================

	protected override void DrawGizmos()
	{
		base.DrawGizmos();
		DrawColliderGizmo( AlwaysShowGizmo || Gizmo.IsSelected );
	}

	internal void DrawColliderGizmo( bool visible, bool force = false )
	{
		if ( !visible || ( !force && !ShowColliderGizmo ) || !TryGetWorldShape( FindRenderer(), out var shape ) )
			return;

		// Draw directly in world space so the sphere radius is never distorted by
		// the component's scale (which caused the squished ellipsoid).
		using ( Gizmo.Scope( "SoftBoneCollider" ) )
		{
			Gizmo.Transform = new Transform( Vector3.Zero, Rotation.Identity, 1f );
			Gizmo.Draw.IgnoreDepth = true;
			Gizmo.Draw.LineThickness = 1;
			Gizmo.Draw.Color = ColliderGizmoColor.WithAlpha( ColliderGizmoOpacity );

			if ( shape.IsSphere )
				Gizmo.Draw.LineSphere( new Sphere( shape.Start, shape.Radius ) );
			else if ( shape.Type == SoftBoneColliderShapeType.Capsule )
				Gizmo.Draw.LineCapsule( new Capsule( shape.Start, shape.End, shape.Radius ) );
			else if ( shape.Type == SoftBoneColliderShapeType.Box )
			{
				Gizmo.Transform = new Transform( shape.Center, shape.Rotation, 1f );
				Gizmo.Draw.LineBBox( BBox.FromPositionAndSize( Vector3.Zero, shape.Size ) );
			}
			else
			{
				var tangent = Vector3.Cross( shape.Normal, Math.Abs( Vector3.Dot( shape.Normal, Vector3.Up ) ) < 0.99f ? Vector3.Up : Vector3.Right ).Normal;
				var bitangent = Vector3.Cross( shape.Normal, tangent ).Normal;
				var size = ColliderRadius;
				var a = shape.Center + (tangent + bitangent) * size;
				var b = shape.Center + (tangent - bitangent) * size;
				var c = shape.Center + (-tangent - bitangent) * size;
				var d = shape.Center + (-tangent + bitangent) * size;
				Gizmo.Draw.Line( a, b ); Gizmo.Draw.Line( b, c ); Gizmo.Draw.Line( c, d ); Gizmo.Draw.Line( d, a );
				Gizmo.Draw.Line( shape.Center, shape.Center + shape.Normal * 8f );
			}
		}
	}

//=========================================================================================================================
// Collider Bone Lookup
//=========================================================================================================================

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

	internal SkinnedModelRenderer GetOwningRenderer() => FindRenderer();

	protected override void OnValidate()
	{
		ColliderRadius = Math.Max( ColliderRadius, 0.001f );
		ColliderHeight = Math.Max( ColliderHeight, ColliderRadius * 2f );
		BoxSize = new Vector3( Math.Max( BoxSize.x, 0.001f ), Math.Max( BoxSize.y, 0.001f ), Math.Max( BoxSize.z, 0.001f ) );
		ColliderGizmoOpacity = Math.Clamp( ColliderGizmoOpacity, 0f, 1f );
	}
}
bobicolab.bobicosoftbones / Code/BoBiCo Soft-Physics/Components/BoBiCoSoftBoneColliderGroup.cs
Game library
//=========================================================================================================================
// BoBiCo Soft-Bone Collider Group Component
//=========================================================================================================================

using System.Collections.Generic;

[Title( "SoftBone Collider Group" )]
[Category( "BoBiCo" )]
[Icon( "category" )]
[HelpUrl( "https://bobico-lab.vercel.app/softbone-docs/bobico-softbones/core-components/softbone-collider-group" )]
public sealed class BoBiCoSoftBoneColliderGroup : Component, Component.ExecuteInEditor
{

//=========================================================================================================================
// Group Setup
//=========================================================================================================================

	/// <summary>
	/// List of colliders that are part of this group. These colliders will be used for collision detection with soft-bone chains.
	/// </summary>
	[Property, Title( "Colliders" ), Group( "Colliders" )]
	public List<GameObject> Colliders { get; set; } = new();

    // Collect all valid colliders from the group and add them to the output list, avoiding duplicates using the seen set.
	public void CollectColliders( List<BoBiCoSoftBoneCollider> output, HashSet<BoBiCoSoftBoneCollider> seen )
	{
		if ( !Enabled || Colliders is null )
			return;

		foreach ( var colliderObject in Colliders )
		{
			if ( colliderObject is null || !colliderObject.IsValid() )
				continue;

			var collider = colliderObject.GetComponent<BoBiCoSoftBoneCollider>();
			if ( collider is not null && collider.Enabled && seen.Add( collider ) )
				output.Add( collider );
		}
	}
}
Debug: View Raw JSON Response
{
    "TotalCount": 23,
    "Files": [
        {
            "Ident": "bobicolab.bobicosoftbones",
            "Path": "BoBiCo Soft-Physics/Components/BoBiCoSoftBoneChain.cs",
            "FileName": "BoBiCoSoftBoneChain.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 336846,
            "IsPrivate": false,
            "Code": "//=========================================================================================================================\n// BoBiCo Soft-bone Chain Component\n//=========================================================================================================================\n\nusing System;\nusing System.Collections.Generic;\n\npublic enum SoftBoneLinkColliderShape\n{\n\t/// <summary>\n\t/// A simple spherical collider.\n\t/// Best for rounded body parts and lightweight collision detection.\n\t/// </summary>\n\tSphere,\n\n\t/// <summary>\n\t/// A capsule-shaped collider.\n\t/// Recommended for most bones and elongated body parts.\n\t/// </summary>\n\tCapsule,\n\n\t/// <summary>\n\t/// A box-shaped collider.\n\t/// Useful for broad collision areas and block-like shapes.\n\t/// </summary>\n\tBox,\n\n\t/// <summary>\n\t/// A one-sided flat collider.\n\t/// Useful for flat surface areas.\n\t/// </summary>\n\tFlat,\n}\n\npublic enum SoftBoneCollisionMode\n{\n\t/// <summary>\n\t/// Disables all collision detection for this chain.\n\t/// </summary>\n\tNone,\n\n\t/// <summary>\n\t/// Collides only with body colliders.\n\t/// This is the recommended mode for most setups.\n\t/// </summary>\n\tBaseCollision,\n\n\t/// <summary>\n\t/// Collides with body colliders and collider chains from the same model.\n\t/// </summary>\n\tSelfCollision,\n\n\t/// <summary>\n\t/// Collides with body colliders and collider chains from all SoftBone-enabled models.\n\t/// </summary>\n\tFullCollision\n}\n\n[Title( \"SoftBone Chain\" )]\n[Category( \"BoBiCo\" )]\n[Icon( \"linear_scale\" )]\n[HelpUrl( \"https://bobico-lab.vercel.app/softbone-docs/bobico-softbones/core-components/softbone-chain\" )]\npublic sealed class BoBiCoSoftBoneChain : Component, Component.ExecuteInEditor\n{\n\t// prevents unstable math when values are extremely close to zero.\n\tprivate const float Epsilon = 0.000008f;\n\n\tprivate GameObject _targetBone;\n\tprivate SoftBoneControlMode _controlMode;\n\tprivate bool _curveDefaultsInitialized;\n\n//=========================================================================================================================\n// Chain Properties Section\n//=========================================================================================================================\n\n\t/// <summary>\n\t/// Defines the root bone of the chain that'll be simulated.\n\t/// </summary>\n\t[Property, Order( 1 ), Title( \"Target Bone\" ), Group( \"Chain Properties\" )]\n\tpublic GameObject TargetBone\n\t{\n\t\tget => _targetBone;\n\t\tset\n\t\t{\n\t\t\tif ( _targetBone == value )\n\t\t\t\treturn;\n\n\t\t\t_targetBone = value;\n\t\t\tUpdateOwnerName();\n\t\t\tMarkDirty();\n\t\t}\n\t}\n\n\t/// <summary>\n\t/// Rotates the chain in local space.\n\t/// </summary>\n\t[Property, Order( 2 ), Title( \"Rotation Offset\" ), Group( \"Chain Properties\" )]\n\tpublic Angles RotationOffset { get; set; }\n\n\t/// <summary>\n\t/// Control mode for the SoftBone physics parameters. \n\t/// Slider mode uses a single value for each parameter, \n\t/// Range mode uses a min/max range for each parameter, \n\t/// and Curve mode uses a curve to define the parameter values along the chain.\n\t/// </summary>\n\t[Property, Order( 3 ), Title( \"Control Mode\" ), Group( \"Chain Properties\" )]\n\tpublic SoftBoneControlMode ControlMode\n\t{\n\t\tget => _controlMode;\n\t\tset\n\t\t{\n\t\t\tif ( _controlMode == value ) return;\n\t\t\t_controlMode = value;\n\t\t\tif ( value == SoftBoneControlMode.Curve && !_curveDefaultsInitialized ) InitializeCurveDefaults();\n\t\t}\n\t}\n\n\tpublic enum SoftBoneControlMode\n    {\n        Slider,\n        Range,\n        Curve\n    }\n\n\t/// <summary>\n\t/// When enabled, the root bone itself is also physically simulated\n\t/// (wobbles independently). When disabled, only child bones simulate\n\t/// and the root follows animation exactly.\n\t/// </summary>\n\t[Property, Order( 4 ), Title( \"Simulate Root\" ), Group( \"Chain Properties\" )]\n\tpublic bool SimulateRoot { get; set; } = false;\n\n\t/// <summary>\n\t/// When enabled, only the first X chain bones are simulated.\n\t/// </summary>\n\t[Property, Order( 5 ), Title( \"Limit Chain Length\" ), Group( \"Chain Properties\" )]\n\tpublic bool LimitChainLength { get; set; } = false;\n\n\t/// <summary>\n\t/// Limit the amount of chain bones from tip to root that are simulated when Limit Children is enabled.\n\t/// </summary>\n\t[Property, Order( 6 ), Title( \"Chain Length Limit\" ), Group( \"Chain Properties\" ), ShowIf( nameof( LimitChainLength ), true )]\n\tpublic int ChainLimit { get; set; } = 0;\n\n//=========================================================================================================================\n// Physics Settings\n//=========================================================================================================================\n\n\t/// <summary>\t\n\t/// Controls how strongly the chain returns toward its animated rest pose.\n\t/// Higher values reduce swaying and make the chain settle more quickly.\n\t/// </summary>\n\t[Property, Order( 8 ), Range( 0, 1 ), Title( \"Drag\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Slider )]\n\tpublic float Pull { get; set; } = 0.20f;\n\n\t/// <summary>\t\n\t/// Controls how strongly the chain returns toward its animated rest pose.\n\t/// Higher values reduce swaying and make the chain settle more quickly.\n\t/// </summary>\n\t[Property, Order( 8 ), Range( 0, 1 ), Title( \"Drag\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Range )]\n\tpublic Vector2 PullRange { get; set; } = new( 0.20f, 0.20f );\n\n\t/// <summary>\t\n\t/// Controls how strongly the chain returns toward its animated rest pose.\n\t/// Higher values reduce swaying and make the chain settle more quickly.\n\t/// </summary>\n\t[Property, Order( 8 ), Title( \"Drag\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Curve )]\n\tpublic Curve PullCurve { get; set; } = ConstantCurve( 0.20f );\n\n\t/// <summary>\n\t/// Controls how much spring force is applied to the chain.\n\t/// Higher values create softer, bouncier motion, while lower values produce more stable movement.\n\t/// </summary>\n\t[Property, Order( 9 ), Range( 0, 1 ), Title( \"Spring\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Slider )]\n\tpublic float Spring { get; set; } = 0.50f;\n\n\t/// <summary>\n\t/// Controls how much spring force is applied to the chain.\n\t/// Higher values create softer, bouncier motion, while lower values produce more stable movement.\n\t/// </summary>\n\t[Property, Order( 9 ), Range( 0, 1 ), Title( \"Spring\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Range )]\n\tpublic Vector2 SpringRange { get; set; } = new( 0.50f, 0.50f );\n\n\t/// <summary>\n\t/// Controls how much spring force is applied to the chain.\n\t/// Higher values create softer, bouncier motion, while lower values produce more stable movement.\n\t/// </summary>\n\t[Property, Order( 9 ), Title( \"Spring\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Curve )]\n\tpublic Curve SpringCurve { get; set; } = ConstantCurve( 0.50f );\n\n\n\t/// <summary>\n\t/// Controls how resistant the chain is to bending.\n\t/// Higher values make the chain feel firmer, while lower values allow more flexibility.\n\t/// </summary>\n\t[Property, Order( 10 ), Range( 0, 1 ), Title( \"Stiffness\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Slider )]\n\tpublic float Stiffness { get; set; } = 0.30f;\n\n\t/// <summary>\n\t/// Controls how resistant the chain is to bending.\n\t/// Higher values make the chain feel firmer, while lower values allow more flexibility.\n\t/// </summary>\t\n\t[Property, Order( 10 ), Range( 0, 1 ), Title( \"Stiffness\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Range )]\n\tpublic Vector2 StiffnessRange { get; set; } = new( 0.30f, 0.30f );\n\n\t/// <summary>\n\t/// Controls how resistant the chain is to bending.\n\t/// Higher values make the chain feel firmer, while lower values allow more flexibility.\n\t/// </summary>\n\t[Property, Order( 10 ), Title( \"Stiffness\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Curve )]\n\tpublic Curve StiffnessCurve { get; set; } = ConstantCurve( 0.30f );\n\n\t/// <summary>\n\t/// Applies a constant directional force to the chain.\n\t/// Positive values pull downward, while negative values pull upward.\n\t/// </summary>\n\t[Property, Order( 11 ), Range( -1, 1 ), Title( \"Gravity\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Slider )]\n\tpublic float Gravity { get; set; }\n\n\t/// <summary>\n\t/// Applies a constant directional force to the chain.\n\t/// Positive values pull downward, while negative values pull upward.Positive values pull the chain down, negative values pull the chain up.\n\t/// </summary>\n\t[Property, Order( 11 ), Range( 0, 1 ), Title( \"Gravity\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Range )]\n\tpublic Vector2 GravityRange { get; set; }\n\n\t/// <summary>\n\t/// Applies a constant directional force to the chain.\n\t/// Positive values pull downward, while negative values pull upward.Positive values pull the chain down, negative values pull the chain up.\n\t/// </summary>\n\t[Property, Order( 11 ), Title( \"Gravity\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Curve )]\n\tpublic Curve GravityCurve { get; set; } = ConstantCurve( 0f );\n\n\t/// <summary>\n\t/// Reduces gravity while the chain is near its animated rest pose.\n\t/// Higher values keep idle chains more stable while preserving gravity during motion.\n\t/// </summary>\n\t[Property, Order( 12 ), Range( 0, 1 ), Title( \"Gravity Falloff\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Slider )]\n\tpublic float GravityFalloff { get; set; }\n\n\t/// <summary>\n\t/// Reduces gravity while the chain is near its animated rest pose.\n\t/// Higher values keep idle chains more stable while preserving gravity during motion.\n\t/// </summary>\n\t[Property, Order( 12 ), Range( 0, 1 ), Title( \"Gravity Falloff\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Range )]\n\tpublic Vector2 GravityFalloffRange { get; set; }\n\n\t/// <summary>\n\t/// Reduces gravity while the chain is near its animated rest pose.\n\t/// Higher values keep idle chains more stable while preserving gravity during motion.\n\t/// </summary>\n\t[Property, Order( 12 ), Title( \"Gravity Falloff\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Curve )]\n\tpublic Curve GravityFalloffCurve { get; set; } = ConstantCurve( 0f );\n\n\n\t/// <summary>\n\t/// Controls how much the chain can extend beyond its rest length.\n\t/// Higher values allow longer, softer stretching under force.\n\t/// </summary>\n\t[Property, Order( 13 ), Range( 0, 2 ), Title( \"Stretch\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Slider )]\n\tpublic float Stretch { get; set; }\n\n\t/// <summary>\n\t/// Controls how much the chain can extend beyond its rest length.\n\t/// Higher values allow longer, softer stretching under force.\n\t/// </summary>\n\t[Property, Order( 13 ), Range( 0, 2 ), Title( \"Stretch\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Range )]\n\tpublic Vector2 StretchRange { get; set; }\n\n\t/// <summary>\n\t/// Controls how much the chain can extend beyond its rest length.\n\t/// Higher values allow longer, softer stretching under force.\n\t/// </summary>\n\t[Property, Order( 13 ), Title( \"Stretch\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Curve )]\n\tpublic Curve StretchCurve { get; set; } = ConstantCurve( 0f );\n\n\n\t/// <summary>\n\t/// Controls how much the chain can compress below its rest length.\n\t/// Higher values allow greater compression.\n\t/// </summary>\n\t[Property, Order( 14 ), Range( 0, 1 ), Title( \"Squish\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Slider )]\n\tpublic float Squish { get; set; }\n\n\t/// <summary>\n\t/// Controls how much the chain can compress below its rest length.\n\t/// Higher values allow greater compression.\n\t/// </summary>\n\t[Property, Order( 14 ), Range( 0, 1 ), Title( \"Squish\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Range )]\n\tpublic Vector2 SquishRange { get; set; }\n\n\t/// <summary>\n\t/// Controls how much the chain can compress below its rest length.\n\t/// Higher values allow greater compression.\n\t/// </summary>\n\t[Property, Order( 14 ), Title( \"Squish\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Curve )]\n\tpublic Curve SquishCurve { get; set; } = ConstantCurve( 0f );\n\n\t/// <summary>\n\t/// Reduces movement caused by the character's own motion.\n\t/// Higher values make the chain feel heavier and less reactive while the character moves.\n\t/// </summary>\n\t[Property, Order( 15 ), Range( 0, 1 ), Title( \"Movement Resistance\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Slider )]\n\tpublic float Immobile { get; set; } = 1.00f;\n\n\t/// <summary>\n\t/// Reduces movement caused by the character's own motion.\n\t/// Higher values make the chain feel heavier and less reactive while the character moves.\n\t/// </summary>\n\t[Property, Order( 15 ), Range( 0, 1 ), Title( \"Movement Resistance\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Range )]\n\tpublic Vector2 ImmobileRange { get; set; } = new( 1.00f, 1.00f );\n\n\t/// <summary>\n\t/// Reduces movement caused by the character's own motion.\n\t/// Higher values make the chain feel heavier and less reactive while the character moves.\n\t/// </summary>\n\t[Property, Order( 15 ), Title( \"Movement Resistance\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Curve )]\n\tpublic Curve ImmobileCurve { get; set; } = ConstantCurve( 1.00f );\n\n//=========================================================================================================================\n// Limit Settings Section\n//=========================================================================================================================\n\n    /// <summary>\n    /// Defines how the chain's movement is constrained.\n    /// Different limit types provide different ways of restricting bone rotation.\n    /// </summary>\n\t[Property, Order( 16 ), Title( \"Limit Type\" ), Group( \"Limit Settings\" )]\n\tpublic SoftBoneLimitType LimitType { get; set; } = SoftBoneLimitType.Cone;\n\n    /// <summary>\n    /// Maximum angle the chain can rotate away from its rest direction.\n    /// Higher values allow greater freedom of movement.\n    /// </summary>\n\t[Property, Order( 17 ), Range( 0, 180 ), Title( \"Max Angle\" ), Group( \"Limit Settings\" ), ShowIf( nameof( ShowMaxAngleSlider ), true )]\n\tpublic float MaxAngle { get; set; } = 95f;\n\n\t/// <summary>\n    /// Maximum angle the chain can rotate away from its rest direction.\n    /// Higher values allow greater freedom of movement.\n    /// </summary>\n\t[Property, Order( 17 ), Range( 0, 1 ), Title( \"Max Angle\" ), Group( \"Limit Settings\" ), ShowIf( nameof( ShowMaxAngleRange ), true )]\n\tpublic Vector2 MaxAngleRange { get; set; } = new( 95f, 95f );\n\n\t/// <summary>\n    /// Maximum angle the chain can rotate away from its rest direction.\n    /// Higher values allow greater freedom of movement.\n    /// </summary>\n\t[Property, Order( 17 ), Title( \"Max Angle\" ), Group( \"Limit Settings\" ), ShowIf( nameof( ShowMaxAngleCurve ), true )]\n\tpublic Curve MaxAngleCurve { get; set; } = ConstantCurve( 95f / 180f );\n\tpublic bool UsesMaxAngle => LimitType == SoftBoneLimitType.Cone || LimitType == SoftBoneLimitType.Plane;\n\tpublic bool ShowMaxAngleSlider => UsesMaxAngle && ControlMode == SoftBoneControlMode.Slider;\n\tpublic bool ShowMaxAngleRange => UsesMaxAngle && ControlMode == SoftBoneControlMode.Range;\n\tpublic bool ShowMaxAngleCurve => UsesMaxAngle && ControlMode == SoftBoneControlMode.Curve;\n\n    /// <summary>\n    /// Maximum horizontal rotation allowed for Ellipse limits.\n    /// Increase to allow more side-to-side movement.\n    /// </summary>\n\t[Property, Order( 18 ), Range( 0, 180 ), Title( \"Max Yaw\" ), Group( \"Limit Settings\" ), ShowIf( nameof( ShowMaxYawSlider ), true )]\n\tpublic float MaxYaw { get; set; } = 95f;\n\n\t/// <summary>\n    /// Maximum horizontal rotation allowed for Ellipse limits.\n    /// Increase to allow more side-to-side movement.\n    /// </summary>\n\t[Property, Order( 18 ), Range( 0, 180 ), Title( \"Max Yaw\" ), Group( \"Limit Settings\" ), ShowIf( nameof( ShowMaxYawRange ), true )]\n\tpublic Vector2 MaxYawRange { get; set; } = new( 95f, 95f );\n\n    /// <summary>\n    /// Maximum horizontal rotation allowed for Ellipse limits.\n    /// Increase to allow more side-to-side movement.\n    /// </summary>\n\t[Property, Order( 18 ), Title( \"Max Yaw\" ), Group( \"Limit Settings\" ), ShowIf( nameof( ShowMaxYawCurve ), true )]\n\tpublic Curve MaxYawCurve { get; set; } = ConstantCurve( 95f / 180f );\n\tpublic bool ShowMaxYawSlider => LimitType == SoftBoneLimitType.Ellipse && ControlMode == SoftBoneControlMode.Slider;\n\tpublic bool ShowMaxYawRange => LimitType == SoftBoneLimitType.Ellipse && ControlMode == SoftBoneControlMode.Range;\n\tpublic bool ShowMaxYawCurve => LimitType == SoftBoneLimitType.Ellipse && ControlMode == SoftBoneControlMode.Curve;\n\n\n    /// <summary>\n    /// Maximum vertical rotation allowed for Ellipse limits.\n    /// Increase to allow more up-and-down movement.\n    /// </summary>\n\t[Property, Order( 19 ), Range( 0, 180 ), Title( \"Max Pitch\" ), Group( \"Limit Settings\" ), ShowIf( nameof( ShowMaxPitchSlider ), true )]\n\tpublic float MaxPitch { get; set; } = 95f;\n\n    /// <summary>\n    /// Maximum vertical rotation allowed for Ellipse limits.\n    /// Increase to allow more up-and-down movement.\n    /// </summary>\n\t[Property, Order( 19 ), Range( 0, 180 ), Title( \"Max Pitch\" ), Group( \"Limit Settings\" ), ShowIf( nameof( ShowMaxPitchRange ), true )]\n\tpublic Vector2 MaxPitchRange { get; set; } = new( 95f, 95f );\n\n    /// <summary>\n    /// Maximum vertical rotation allowed for Ellipse limits.\n    /// Increase to allow more up-and-down movement.\n    /// </summary>\n\t[Property, Order( 19 ), Title( \"Max Pitch\" ), Group( \"Limit Settings\" ), ShowIf( nameof( ShowMaxPitchCurve ), true )]\n\tpublic Curve MaxPitchCurve { get; set; } = ConstantCurve( 95f / 180f );\n\tpublic bool ShowMaxPitchSlider => LimitType == SoftBoneLimitType.Ellipse && ControlMode == SoftBoneControlMode.Slider;\n\tpublic bool ShowMaxPitchRange => LimitType == SoftBoneLimitType.Ellipse && ControlMode == SoftBoneControlMode.Range;\n\tpublic bool ShowMaxPitchCurve => ControlMode == SoftBoneControlMode.Curve && LimitType == SoftBoneLimitType.Ellipse;\n\n\n    /// <summary>\n    /// Rotates the limit shape relative to the chain.\n    /// </summary>\n\t[Property, Order( 20 ), Title( \"Limit Rotation\" ), Group( \"Limit Settings\" ), HideIf( nameof( LimitType ), SoftBoneLimitType.None )]\n\tpublic Angles LimitRotation { get; set; }\n\n    /// <summary>\n    /// Shape used for this chain's collision volume.\n    /// Different shapes provide different collision behavior and coverage.\n    /// </summary>\n\t[Property, Order( 21 ), Title( \"Collider Shape\" ), Group( \"Collision Settings\" )]\n\tpublic SoftBoneLinkColliderShape ColliderShape { get; set; } = SoftBoneLinkColliderShape.Capsule;\n\n    /// <summary>\n    /// Controls which colliders this chain can interact with.\n    /// Higher modes enable more realistic but more expensive collision behavior.\n    /// </summary>\n\t[Property, Order( 22 ), Title( \"Collision Mode\" ), Group( \"Collision Settings\" )]\n\tpublic SoftBoneCollisionMode CollisionMode { get; set; } = SoftBoneCollisionMode.BaseCollision;\n\n\t/// <summary>\n\t/// Assign one or more SoftBone Collider Groups for this chain to interact with.\n\t/// Colliders outside of these groups will be ignored.\n\t/// </summary>\n\t[Property, Order( 23 ), Title( \"Collider Groups\" ), Group( \"Collision Settings\" )]\n\tpublic List<GameObject> ColliderGroups { get; set; } = new();\n\n    /// <summary>\n    /// Allows the colliders to collide with the root collider.\n    /// Disable if the root should remain free.\n    /// </summary>\n\t[Property, Order( 24 ), Title( \"Collide With Root\" ), Group( \"Collision Settings\" )]\n\tpublic bool CollideWithRoot { get; set; }\n\n\t// Helpers for conditional property visibility\n\tprivate bool ShowRadiusSlider => ControlMode == SoftBoneControlMode.Slider && ColliderShape != SoftBoneLinkColliderShape.Box;\n\tprivate bool ShowRadiusRange  => ControlMode == SoftBoneControlMode.Range  && ColliderShape != SoftBoneLinkColliderShape.Box;\n\tprivate bool ShowRadiusCurve  => ControlMode == SoftBoneControlMode.Curve  && ColliderShape != SoftBoneLinkColliderShape.Box;\n\n    /// <summary>\n    /// Radius of the chain's collision volume.\n    /// Larger values create thicker collision boundaries.\n    /// </summary>\n\t[Property, Order( 25 ), Title( \"Collider Radius\" ), Group( \"Collision Settings\" ), ShowIf( nameof( ShowRadiusSlider ), true )]\n\tpublic float ColliderRadius { get; set; } = 0.5f;\n\n    /// <summary>\n    /// Radius of the chain's collision volume.\n    /// Larger values create thicker collision boundaries.\n    /// </summary>\n\t[Property, Order( 25 ), Title( \"Collider Radius\" ), Group( \"Collision Settings\" ), ShowIf( nameof( ShowRadiusRange ), true )]\n\tpublic Vector2 ColliderRadiusRange { get; set; } = new( 0.5f, 0.5f );\n\n    /// <summary>\n    /// Radius of the chain's collision volume.\n    /// Larger values create thicker collision boundaries.\n    /// </summary>\n\t[Property, Order( 25 ), Title( \"Collider Radius\" ), Group( \"Collision Settings\" ), ShowIf( nameof( ShowRadiusCurve ), true )]\n\tpublic Curve ColliderRadiusCurve { get; set; } = ConstantCurve( 0.5f );\n\n    /// <summary>\n    /// Height of Capsule collider.\n\t/// Available only when the collider shape is set to Capsule.\n    /// </summary>\n\t[Property, Order( 26 ), Title( \"Collider Height\" ), Group( \"Collision Settings\" ), ShowIf( nameof( ColliderShape ), SoftBoneLinkColliderShape.Capsule )]\n\tpublic float ColliderHeight { get; set; } = 1.25f;\n\n    /// <summary>\n    /// Defines the dimensions of the box collider.\n    /// Available only when the collider shape is set to Box.\n    /// </summary>\n\t[Property, Order( 27 ), Title( \"Box Dimensions\" ), Group( \"Collision Settings\" ), ShowIf( nameof( ColliderShape ), SoftBoneLinkColliderShape.Box )]\n\tpublic Vector3 BoxDimension { get; set; } = new Vector3( 1.25f, 1.25f, 1.25f );\n\n//=========================================================================================================================\n// Gizmo Setting Section\n//=========================================================================================================================\n\n    /// <summary>\n    /// Displays the chain's angle limit in the Scene view.\n    /// Useful for visualizing and adjusting movement constraints.\n    /// </summary>\n\t[Property, Order( 28 ), Header( \"Limit Gizmo\" ), Title( \"Limit Gizmo\" ), Group( \"Gizmo Settings\" )]\n\tpublic bool ShowLimitGizmo { get; set; } = true;\n\n    /// <summary>\n    /// Keeps the limit gizmo visible even when the chain is not selected.\n    /// Useful for debugging multiple chains at once.\n    /// </summary>\n\t[Property, Order( 29 ), Title( \"Always Show Gizmo\" ), Group( \"Gizmo Settings\" ), ShowIf( nameof( ShowLimitGizmo ), true )]\n\tpublic bool AlwaysShowLimitGizmo { get; set; }\n\n    /// <summary>\n    /// Color used for rendering the limit gizmo.\n    /// </summary>\n\t[Property, Order( 30 ), Title( \"Limit Gizmo Color\" ), Group( \"Gizmo Settings\" ), ShowIf( nameof( ShowLimitGizmo ), true )]\n\tpublic Color LimitGizmoColor { get; set; } = Color.Orange;\n\n    /// <summary>\n    /// Controls the transparency of the limit gizmo.\n    /// </summary>\n\t[Property, Order( 31 ), Range( 0, 1 ), Title( \"Gizmo Opacity\" ), Group( \"Gizmo Settings\" ), ShowIf( nameof( ShowLimitGizmo ), true )]\n\tpublic float LimitGizmoOpacity { get; set; } = 1f;\n\n    /// <summary>\n    /// Controls the size of the limit gizmo in the Scene view.\n    /// Adjust if the visualization appears too large or too small.\n    /// </summary>\n\t[Property, Order( 32 ), Range( 0.1f, 1f ), Title( \"Limit Gizmo Size\" ), Group( \"Gizmo Settings\" ), ShowIf( nameof( ShowLimitGizmo ), true )]\n\tpublic float LimitGizmoSize { get; set; } = 0.5f;\n\n    /// <summary>\n    /// Displays the chain's collider in the Scene view.\n    /// Useful for visualizing collision shapes and sizes.\n    /// </summary>\n\t[Property, Order( 33 ), Header( \"Collider Gizmo\" ), Title( \"Collider Gizmo\" ), Group( \"Gizmo Settings\" )]\n\tpublic bool ShowColliderGizmo { get; set; } = true;\n\n    /// <summary>\n    /// Keeps the collider gizmo visible even when the chain is not selected.\n    /// Useful for debugging collision setups.\n    /// </summary>\n\t[Property, Order( 34 ), Title( \"Always Show Gizmo\" ), Group( \"Gizmo Settings\" ), ShowIf( nameof( ShowColliderGizmo ), true )]\n\tpublic bool AlwaysShowGizmo { get; set; }\n\n    /// <summary>\n    /// Color used for rendering the collider gizmo.\n    /// </summary>\n\t[Property, Order( 35 ), Title( \"Collider Gizmo Color\" ), Group( \"Gizmo Settings\" ), ShowIf( nameof( ShowColliderGizmo ), true )]\n\tpublic Color ColliderGizmoColor { get; set; } = Gizmo.Colors.Blue;\n\n    /// <summary>\n    /// Controls the transparency of the collider gizmo.\n    /// </summary>\n\t[Property, Order( 36 ), Range( 0, 1 ), Title( \"Gizmo Opacity\" ), Group( \"Gizmo Settings\" ), ShowIf( nameof( ShowColliderGizmo ), true )]\n\tpublic float ColliderGizmoOpacity { get; set; } = 1f;\n\n//=========================================================================================================================\n// Private States\n//=========================================================================================================================\n\n\tprivate readonly List<SoftBoneJointState> _joints = new();\n\tprivate readonly List<BoBiCoSoftBoneCollider> _activeColliders = new();\n\tprivate readonly List<SoftBoneColliderShape> _externalBodyShapes = new();\n\tprivate readonly List<SoftBoneColliderShape> _externalChainShapes = new();\n\tprivate readonly List<SoftBoneChainColliderSnapshot> _publishedChainShapes = new();\n\tprivate readonly HashSet<BoBiCoSoftBoneCollider> _colliderSet = new();\n\tprivate readonly List<Transform> _framePose = new();\n\tprivate readonly List<Transform> _ragdollAnimationPose = new();\n\tprivate bool _dirty = true;\n\tprivate bool _usingPhysicsPose;\n\n\tpublic IReadOnlyList<SoftBoneJointState> Joints => _joints;\n\n\tpublic bool IsDirty => _dirty;\n\n\tpublic void MarkDirty() => _dirty = true;\n\n//=========================================================================================================================\n// Rebuild Section\n//=========================================================================================================================\n\n\tpublic bool Rebuild( SkinnedModelRenderer rendererOverride = null )\n\t{\n\t\tvar renderer = rendererOverride;\n\t\tif ( renderer is null )\n\t\t\treturn false;\n\n\t\tif ( !renderer.IsValid() || renderer.Model?.Bones is null )\n\t\t\treturn false;\n\n\t\tReleaseCurrentChain( renderer );\n\n\t\tvar boneName = \"\";\n\t\tif ( TargetBone is not null && TargetBone.IsValid() )\n\t\t\tboneName = TargetBone.Name;\n\t\tboneName = boneName?.Trim() ?? \"\";\n\n\t\tif ( string.IsNullOrWhiteSpace( boneName ) || !renderer.Model.Bones.HasBone( boneName ) )\n\t\t\treturn false;\n\n\t\tvar current = renderer.Model.Bones.GetBone( boneName );\n\n\t\twhile ( current is not null )\n\t\t{\n\t\t\tif ( !SoftBoneSkinnedModelUtility.TryGetBoneAnimationLocal( renderer, current, out var animated ) )\n\t\t\t\tbreak;\n\n\t\t\tvar poseVector = GetAnimatedBoneVectorLocal( renderer, current, animated );\n\t\t\tvar length = Math.Max( poseVector.Length, Epsilon );\n\t\t\tvar localPoseVector = animated.Rotation.Inverse * poseVector;\n\n\t\t\tvar rendererDownLocal = RendererDownLocal( renderer );\n\t\t\tvar gravityNormalLocal = animated.Rotation.Inverse * rendererDownLocal;\n\n\t\t\tvar joint = new SoftBoneJointState\n\t\t\t{\n\t\t\t\tBone = current,\n\t\t\t\tOriginalLocalGravityNormal = gravityNormalLocal,\n\t\t\t\tLocalPoseVector = localPoseVector,\n\t\t\t\tRestTransformLocal = animated,\n\t\t\t\tLength = length,\n\t\t\t\tPreviousBegin = animated.Position,\n\t\t\t\tPreviousEnd = animated.Position + poseVector,\n\t\t\t\tPreviousAnimatedBegin = animated.Position,\n\t\t\t\tPreviousAnimatedRotation = animated.Rotation,\n\t\t\t\tPreviousSimulationRotation = animated.Rotation,\n\t\t\t\tImmobileEnd = SoftBoneSkinnedModelUtility.ToWorld( renderer, animated.Position + poseVector ),\n\t\t\t\tPreviousEndWorld = SoftBoneSkinnedModelUtility.ToWorld( renderer, animated.Position + poseVector ),\n\t\t\t\tPreviousVector = poseVector,\n\t\t\t\tPreviousVelocity = Vector3.Zero,\n\t\t\t\tHasHistory = false\n\t\t\t};\n\n\t\t\t_joints.Add( joint );\n\t\t\tif ( current.Children.Count == 0 )\n\t\t\t\tbreak;\n\n\t\t\tcurrent = current.Children[0];\n\t\t}\n\n\t\t_dirty = false;\n\t\treturn _joints.Count > 0;\n\t}\n\n\tprivate void ReleaseCurrentChain( SkinnedModelRenderer renderer )\n\t{\n\t\tif ( renderer is not null && renderer.IsValid() && renderer.SceneModel.IsValid() )\n\t\t{\n\t\t\tforeach ( var joint in _joints )\n\t\t\t{\n\t\t\t\tif ( joint.Bone is not null && renderer.TryGetBoneTransformAnimation( joint.Bone, out var animatedWorld ) )\n\t\t\t\t{\n\t\t\t\t\tvar animatedLocal = renderer.WorldTransform.ToLocal( animatedWorld );\n\t\t\t\t\trenderer.SceneModel.SetBoneOverride( joint.Bone.Index, in animatedLocal );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t_joints.Clear();\n\t\t_framePose.Clear();\n\t\t_ragdollAnimationPose.Clear();\n\t\t_publishedChainShapes.Clear();\n\t\tSoftBoneCollisionRegistry.Remove( this );\n\t}\n\n\tprivate void UpdateOwnerName()\n\t{\n\t\tif ( GameObject is null || !GameObject.IsValid() )\n\t\t\treturn;\n\n\t\tvar desiredName = TargetBone is not null && TargetBone.IsValid()\n\t\t\t? TargetBone.Name?.Trim()\n\t\t\t: \"Empty Chain\";\n\t\tif ( !string.IsNullOrWhiteSpace( desiredName ) && GameObject.Name != desiredName )\n\t\t\tGameObject.Name = desiredName;\n\t}\n\n//=========================================================================================================================\n// Simulation Section\n//=========================================================================================================================\n\n\tpublic void Simulate( SkinnedModelRenderer renderer, float frameRatio )\n\t{\n\t\t// Auto-rebuild if dirty or empty \u2014 chain is self-healing so the Group\n\t\t// doesn't need to orchestrate the Rebuild/Simulate order externally.\n\t\tif ( _dirty || _joints.Count == 0 )\n\t\t{\n\t\t\tif ( !Rebuild( renderer ) )\n\t\t\t\treturn;\n\t\t}\n\n\t\tif ( renderer is null || !renderer.IsValid() || !renderer.SceneModel.IsValid() )\n\t\t\treturn;\n\n\t\tvar flag = frameRatio >= 1f;\n\t\tvar rendererDownLocal = RendererDownLocal( renderer );\n\t\tCollectActiveColliders();\n\t\tvar usePhysicsPose = IsPhysicsDrivingRenderer( renderer );\n\t\tif ( usePhysicsPose != _usingPhysicsPose )\n\t\t{\n\t\t\t_usingPhysicsPose = usePhysicsPose;\n\t\t\tforeach ( var joint in _joints ) joint.HasHistory = false;\n\t\t\tif ( usePhysicsPose && !CaptureRagdollAnimationPose( renderer ) )\n\t\t\t\treturn;\n\t\t\tif ( !usePhysicsPose ) _ragdollAnimationPose.Clear();\n\t\t}\n\n\t\tif ( !CaptureFramePose( renderer, usePhysicsPose ) )\n\t\t\treturn;\n\n\t\tvar hasParentEnd = false;\n\t\tvar parentEnd = Vector3.Zero;\n\t\tvar parentAnimatedRotation = Rotation.Identity;\n\t\tvar parentSimulationRotation = Rotation.Identity;\n\n\t\t// LimitChainLength: simulate only the last ChainLimit bones (tip-to-root).\n\t\t// Handled here instead of Rebuild so SimulateRoot logic stays tied to the\n\t\t// ACTUAL chain root (index 0), not whichever bone happens to be first after slicing.\n\t\tvar startIndex = GetSimulationStartIndex();\n\t\tResetExcludedJointsToPose( renderer, startIndex );\n\n\t\tfor ( var i = startIndex; i < _joints.Count; i++ )\n\t\t{\n\t\t\tvar joint = _joints[i];\n\t\t\tvar chainPosition = GetChainPosition( i );\n\t\t\tvar pull = EvaluateUnit( Pull, ControlMode, PullRange, PullCurve, chainPosition );\n\t\t\tvar spring = EvaluateUnit( Spring, ControlMode, SpringRange, SpringCurve, chainPosition );\n\t\t\tvar stiffness = EvaluateUnit( Stiffness, ControlMode, StiffnessRange, StiffnessCurve, chainPosition );\n\t\t\tvar immobile = EvaluateUnit( Immobile, ControlMode, ImmobileRange, ImmobileCurve, chainPosition );\n\t\t\tvar stretch = EvaluateNonNegative( Stretch, ControlMode, StretchRange, StretchCurve, chainPosition );\n\t\t\tvar squish = EvaluateNonNegative( Squish, ControlMode, SquishRange, SquishCurve, chainPosition );\n\t\t\tvar gravity = EvaluateSignedUnit( Gravity, ControlMode, GravityRange, GravityCurve, chainPosition );\n\t\t\tvar gravityFalloff = EvaluateUnit( GravityFalloff, ControlMode, GravityFalloffRange, GravityFalloffCurve, chainPosition );\n\t\t\tvar colliderRadius = EvaluateColliderRadius( chainPosition );\n\t\t\tvar maxAngle = EvaluateAngle( MaxAngle, ControlMode, MaxAngleRange, MaxAngleCurve, chainPosition );\n\t\t\tvar maxYaw = EvaluateAngle( MaxYaw, ControlMode, MaxYawRange, MaxYawCurve, chainPosition );\n\t\t\tvar maxPitch = EvaluateAngle( MaxPitch, ControlMode, MaxPitchRange, MaxPitchCurve, chainPosition );\n\t\t\tvar isActualRoot = i == 0;\n\t\t\tvar isSimulationRoot = i == startIndex;\n\t\t\tvar isRootSimulated = isActualRoot && SimulateRoot;\n\t\t\tvar animated = _framePose[i];\n\n\t\t\tvar begin = hasParentEnd ? parentEnd : animated.Position;\n\n\t\t\tvar animatedPoseVector = GetFramePoseVector( i, joint, animated );\n\t\t\tvar parentDeflection = isSimulationRoot ? Rotation.Identity : GetParentDeflection( i, parentAnimatedRotation, parentSimulationRotation );\n\t\t\tvar simulationRotation = parentDeflection * animated.Rotation;\n\t\t\tvar poseVector = parentDeflection * animatedPoseVector;\n\t\t\tif ( isSimulationRoot && RotationOffset != default )\n\t\t\t{\n\t\t\t\tvar offsetRotation = new Angles( RotationOffset.yaw, RotationOffset.pitch, RotationOffset.roll ).ToRotation();\n\t\t\t\tsimulationRotation = animated.Rotation * offsetRotation;\n\t\t\t\tposeVector = simulationRotation * (animated.Rotation.Inverse * animatedPoseVector);\n\t\t\t}\n\n\t\t\tvar restLength = Math.Max( poseVector.Length, Epsilon );\n\t\t\tvar minLength = Math.Max( 0f, restLength * (1f - squish) );\n\t\t\tvar maxLength = Math.Max( minLength, restLength * (1f + stretch) );\n\n\t\t\tvar endPoint = joint.HasHistory\n\t\t\t\t? renderer.WorldTransform.PointToLocal( joint.PreviousEndWorld )\n\t\t\t\t: begin + poseVector;\n\n\t\t\tif ( joint.HasHistory )\n\t\t\t{\n\t\t\t\tvar rootDelta = begin - joint.PreviousAnimatedBegin;\n\t\t\t\tendPoint += rootDelta;\n\n\t\t\t\tvar rotationDelta = animated.Rotation * joint.PreviousAnimatedRotation.Inverse;\n\t\t\t\tendPoint = begin + rotationDelta * (endPoint - begin);\n\t\t\t}\n\n\t\t\t// SimulateRoot=false: only skip physics on the TRUE chain root, not on bones that\n\t\t\t// happen to be first because LimitChainLength trimmed the start of the iteration.\n\t\t\tif ( isActualRoot && !isRootSimulated && !CollideWithRoot )\n\t\t\t{\n\t\t\t\tResetToRestPose( renderer, joint, animated, begin, poseVector, simulationRotation );\n\t\t\t\tparentEnd = joint.PreviousEnd;\n\t\t\t\tparentAnimatedRotation = animated.Rotation;\n\t\t\t\tparentSimulationRotation = joint.PreviousSimulationRotation;\n\t\t\t\thasParentEnd = true;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif ( pull <= Epsilon )\n\t\t\t{\n\t\t\t\tResetToRestPose( renderer, joint, animated, begin, poseVector, simulationRotation );\n\t\t\t\tif ( !isActualRoot )\n\t\t\t\t\tApplySolvedTransform( renderer, joint, animated, simulationRotation );\n\t\t\t\tparentEnd = joint.PreviousEnd;\n\t\t\t\tparentAnimatedRotation = animated.Rotation;\n\t\t\t\tparentSimulationRotation = joint.PreviousSimulationRotation;\n\t\t\t\thasParentEnd = true;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tvar previousEndPoint = endPoint;\n\n\t\t\tif ( joint.HasHistory && immobile > Epsilon )\n\t\t\t{\n\t\t\t\tvar immobileTarget = renderer.WorldTransform.PointToLocal( joint.ImmobileEnd );\n\t\t\t\tendPoint += (immobileTarget - endPoint) * immobile;\n\t\t\t}\n\n\t\t\tvar gravityAmount = GetGravityAmount( joint, animated, rendererDownLocal, gravity, gravityFalloff );\n\t\t\tvar gravityTargetEnd = ApplyGravityTarget( begin, poseVector, restLength, gravityAmount, gravity, rendererDownLocal );\n\n\t\t\tvar solvedEndPoint = BoBiCoSoftBoneSolver.IntegrateAdvanced(\n\t\t\t\tbegin, endPoint, previousEndPoint,\n\t\t\t\tposeVector, joint.PreviousVector, joint.PreviousVelocity,\n\t\t\t\tminLength, maxLength, pull, spring, stiffness, gravityTargetEnd, frameRatio,\n\t\t\t\tout var newVelocity );\n\n\t\t\tvar solvedDirection = BoBiCoSoftBoneSolver.SafeNormal( solvedEndPoint - begin, poseVector );\n\t\t\tsolvedDirection = SoftBoneAngleLimit.Apply( poseVector, solvedDirection, LimitType, maxAngle, maxYaw, maxPitch, LimitRotation );\n\t\t\tvar solvedLength = Math.Clamp( (solvedEndPoint - begin).Length, minLength, maxLength );\n\t\t\tvar solvedEnd = begin + solvedDirection * solvedLength;\n\t\t\tsolvedEnd = ResolveCollisions( renderer, begin, solvedEnd, poseVector, minLength, maxLength, colliderRadius, maxAngle, maxYaw, maxPitch, isActualRoot && CollideWithRoot, out var wasColliding );\n\t\t\tsolvedDirection = BoBiCoSoftBoneSolver.SafeNormal( solvedEnd - begin, poseVector );\n\t\t\tnewVelocity = solvedEnd - previousEndPoint;\n\t\t\tif ( wasColliding ) newVelocity *= 0.75f;\n\n\t\t\tif ( flag )\n\t\t\t{\n\t\t\t\tjoint.PreviousEndWorld = SoftBoneSkinnedModelUtility.ToWorld( renderer, solvedEnd );\n\t\t\t\tjoint.ImmobileEnd = joint.PreviousEndWorld;\n\t\t\t\tjoint.PreviousVector = solvedEnd - begin;\n\t\t\t\tjoint.PreviousVelocity = newVelocity;\n\t\t\t\tjoint.PreviousAnimatedBegin = begin;\n\t\t\t\tjoint.PreviousAnimatedRotation = animated.Rotation;\n\t\t\t}\n\n\t\t\tjoint.PreviousBegin = begin;\n\t\t\tjoint.PreviousEnd = solvedEnd;\n\t\t\tjoint.PreviousSimulationRotation = BoBiCoSoftBoneSolver.SolveHeadRotation( poseVector, solvedEnd - begin, simulationRotation );\n\t\t\tjoint.HasHistory = true;\n\n\t\t\tif ( !isActualRoot )\n\t\t\t\tApplySolvedTransform( renderer, joint, animated, joint.PreviousSimulationRotation );\n\n\t\t\tparentEnd = solvedEnd;\n\t\t\tparentAnimatedRotation = animated.Rotation;\n\t\t\tparentSimulationRotation = joint.PreviousSimulationRotation;\n\t\t\thasParentEnd = true;\n\t\t}\n\n\t\tPublishCollisionShapes( renderer );\n\t}\n\n\tprivate void CollectActiveColliders()\n\t{\n\t\t_activeColliders.Clear();\n\t\t_colliderSet.Clear();\n\n\t\tif ( ColliderGroups is not null )\n\t\t{\n\t\t\tforeach ( var sourceObject in ColliderGroups )\n\t\t\t{\n\t\t\t\tif ( sourceObject is null || !sourceObject.IsValid() )\n\t\t\t\t\tcontinue;\n\n\t\t\t\tsourceObject.GetComponent<BoBiCoSoftBoneColliderGroup>()?.CollectColliders( _activeColliders, _colliderSet );\n\t\t\t}\n\t\t}\n\n\t}\n\n\tprivate void ResetExcludedJointsToPose( SkinnedModelRenderer renderer, int startIndex )\n\t{\n\t\tfor ( var i = 0; i < startIndex; i++ )\n\t\t{\n\t\t\tvar joint = _joints[i];\n\t\t\tvar animated = _framePose[i];\n\t\t\tvar poseVector = GetFramePoseVector( i, joint, animated );\n\t\t\tResetToRestPose( renderer, joint, animated, animated.Position, poseVector, animated.Rotation );\n\t\t\tif ( i > 0 )\n\t\t\t\tApplyAnimationTransform( renderer, joint, animated );\n\t\t}\n\t}\n\n\tprivate 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 )\n\t{\n\t\tcollided = false;\n\t\tif ( CollisionMode == SoftBoneCollisionMode.None || colliderRadius <= Epsilon )\n\t\t\treturn solvedEnd;\n\n\t\tvar worldBegin = SoftBoneSkinnedModelUtility.ToWorld( renderer, begin );\n\t\tvar worldEnd = SoftBoneSkinnedModelUtility.ToWorld( renderer, solvedEnd );\n\t\tforeach ( var collider in _activeColliders )\n\t\t{\n\t\t\tif ( collider.TryGetWorldShape( collider.GetOwningRenderer(), out var shape ) )\n\t\t\t\tcollided |= SoftBoneCollisionSolver.Resolve( ref worldEnd, worldBegin, includeRootHead ? worldBegin : GetCollisionStart( worldBegin, worldEnd, colliderRadius ), colliderRadius, minLength, maxLength, shape );\n\t\t}\n\n\t\tif ( CollisionMode == SoftBoneCollisionMode.FullCollision )\n\t\t{\n\t\t\t_externalBodyShapes.Clear();\n\t\t\tSoftBoneCollisionRegistry.CollectExternalBodyShapes( renderer, _externalBodyShapes );\n\t\t\tforeach ( var shape in _externalBodyShapes )\n\t\t\t\tcollided |= SoftBoneCollisionSolver.Resolve( ref worldEnd, worldBegin, includeRootHead ? worldBegin : GetCollisionStart( worldBegin, worldEnd, colliderRadius ), colliderRadius, minLength, maxLength, shape );\n\t\t}\n\n\t\t_externalChainShapes.Clear();\n\t\tSoftBoneCollisionRegistry.CollectChainShapes( this, renderer, CollisionMode, _externalChainShapes );\n\t\tforeach ( var shape in _externalChainShapes )\n\t\t\tcollided |= SoftBoneCollisionSolver.Resolve( ref worldEnd, worldBegin, includeRootHead ? worldBegin : GetCollisionStart( worldBegin, worldEnd, colliderRadius ), colliderRadius, minLength, maxLength, shape );\n\n\t\tif ( !collided )\n\t\t\treturn solvedEnd;\n\n\t\tvar localDelta = SoftBoneSkinnedModelUtility.ToLocal( renderer, worldEnd ) - begin;\n\t\tvar localDirection = SoftBoneAngleLimit.Apply( poseVector, BoBiCoSoftBoneSolver.SafeNormal( localDelta, poseVector ), LimitType, maxAngle, maxYaw, maxPitch, LimitRotation );\n\t\treturn begin + localDirection * Math.Clamp( localDelta.Length, minLength, maxLength );\n\t}\n\n\tprivate Vector3 GetCollisionStart( Vector3 worldBegin, Vector3 worldEnd, float colliderRadius )\n\t{\n\t\tif ( ColliderShape != SoftBoneLinkColliderShape.Capsule )\n\t\t\treturn worldEnd;\n\n\t\tvar axis = worldEnd - worldBegin;\n\t\tvar segmentLength = Math.Max( 0f, ColliderHeight - colliderRadius * 2f );\n\t\tif ( axis.Length <= Epsilon || segmentLength <= Epsilon )\n\t\t\treturn worldEnd;\n\n\t\treturn worldEnd - axis.Normal * Math.Min( axis.Length, segmentLength );\n\t}\n\n\tprivate int GetSimulationStartIndex()\n\t{\n\t\treturn LimitChainLength && ChainLimit > 0 && ChainLimit < _joints.Count\n\t\t\t? _joints.Count - ChainLimit\n\t\t\t: 0;\n\t}\n\n\tprivate float GetChainPosition( int index ) => _joints.Count <= 1 ? 0f : index / (float)(_joints.Count - 1);\n\tprivate static Curve ConstantCurve( float value ) => new( new Curve.Frame( 0f, value ), new Curve.Frame( 1f, value ) );\n\n\tprivate void InitializeCurveDefaults()\n\t{\n\t\t_curveDefaultsInitialized = true;\n\t\tPullCurve = ConstantCurve( Pull );\n\t\tSpringCurve = ConstantCurve( Spring );\n\t\tStiffnessCurve = ConstantCurve( Stiffness );\n\t\tImmobileCurve = ConstantCurve( Immobile );\n\t\tStretchCurve = ConstantCurve( Stretch );\n\t\tSquishCurve = ConstantCurve( Squish );\n\t\tGravityCurve = ConstantCurve( Gravity );\n\t\tGravityFalloffCurve = ConstantCurve( GravityFalloff );\n\t\tMaxAngleCurve = ConstantCurve( MaxAngle / 180f );\n\t\tMaxYawCurve = ConstantCurve( MaxYaw / 180f );\n\t\tMaxPitchCurve = ConstantCurve( MaxPitch / 180f );\n\t\tColliderRadiusCurve = ConstantCurve( ColliderRadius );\n\t}\n\n\tprivate static float EvaluateUnit( float value, SoftBoneControlMode mode, Vector2 range, Curve curve, float position ) => Math.Clamp( EvaluateControl( value, mode, range, curve, position ), 0f, 1f );\n\tprivate static float EvaluateSignedUnit( float value, SoftBoneControlMode mode, Vector2 range, Curve curve, float position ) => Math.Clamp( EvaluateControl( value, mode, range, curve, position ), -1f, 1f );\n\tprivate static float EvaluateNonNegative( float value, SoftBoneControlMode mode, Vector2 range, Curve curve, float position ) => Math.Max( 0f, EvaluateControl( value, mode, range, curve, position ) );\n\tprivate static float EvaluateAngle( float value, SoftBoneControlMode mode, Vector2 range, Curve curve, float position )\n\t{\n\t\tif ( mode == SoftBoneControlMode.Curve )\n\t\t\treturn Math.Clamp( (curve.Length > 0 ? curve.Evaluate( position ) : 1f) * 180f, 0f, 180f );\n\n\t\treturn Math.Clamp( EvaluateControl( value, mode, range, curve, position ), 0f, 180f );\n\t}\n\tprivate float EvaluateColliderRadius( float position ) => Math.Max( 0.001f, EvaluateControl( ColliderRadius, ControlMode, ColliderRadiusRange, ColliderRadiusCurve, position ) );\n\tprivate static float EvaluateControl( float value, SoftBoneControlMode mode, Vector2 range, Curve curve, float position )\n\t{\n\t\treturn mode switch\n\t\t{\n\t\t\tSoftBoneControlMode.Range => range.x + (range.y - range.x) * position,\n\t\t\tSoftBoneControlMode.Curve => curve.Length > 0 ? curve.Evaluate( position ) : value,\n\t\t\t_ => value\n\t\t};\n\t}\n\n\tinternal void PublishCollisionShapes( SkinnedModelRenderer renderer )\n\t{\n\t\tif ( CollisionMode == SoftBoneCollisionMode.None )\n\t\t{\n\t\t\tSoftBoneCollisionRegistry.Remove( this );\n\t\t\treturn;\n\t\t}\n\n\t\t_publishedChainShapes.Clear();\n\t\tfor ( var i = GetSimulationStartIndex(); i < _joints.Count; i++ )\n\t\t{\n\t\t\tif ( i == 0 && !CollideWithRoot )\n\t\t\t\tcontinue;\n\n\t\t\tvar joint = _joints[i];\n\t\t\tif ( !joint.HasHistory )\n\t\t\t\tcontinue;\n\n\t\t\tvar begin = SoftBoneSkinnedModelUtility.ToWorld( renderer, joint.PreviousBegin );\n\t\t\tvar end = SoftBoneSkinnedModelUtility.ToWorld( renderer, joint.PreviousEnd );\n\t\t\tvar radius = EvaluateColliderRadius( GetChainPosition( i ) );\n\t\t\tvar direction = BoBiCoSoftBoneSolver.SafeNormal( end - begin, Vector3.Forward );\n\t\t\tSoftBoneColliderShape shape;\n\t\t\tswitch ( ColliderShape )\n\t\t\t{\n\t\t\t\tcase SoftBoneLinkColliderShape.Sphere:\n\t\t\t\t\tshape = SoftBoneColliderShape.Sphere( begin, radius );\n\t\t\t\t\tbreak;\n\t\t\t\tcase SoftBoneLinkColliderShape.Box:\n\t\t\t\t\tshape = SoftBoneColliderShape.Box( begin, Rotation.FromToRotation( Vector3.Forward, direction ), new Vector3( BoxDimension.z, BoxDimension.x, BoxDimension.y ) );\n\t\t\t\t\tbreak;\n\t\t\t\tcase SoftBoneLinkColliderShape.Flat:\n\t\t\t\t\tshape = SoftBoneColliderShape.Flat( begin, direction );\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tvar halfLine = Math.Max( 0f, ColliderHeight * 0.5f - radius );\n\t\t\t\t\tshape = SoftBoneColliderShape.Capsule( begin - direction * halfLine, begin + direction * halfLine, radius );\n\t\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t_publishedChainShapes.Add( new SoftBoneChainColliderSnapshot( renderer, shape ) );\n\t\t}\n\n\t\tSoftBoneCollisionRegistry.Publish( this, renderer, _publishedChainShapes );\n\t}\n\n\n//=========================================================================================================================\n// Helpers\n//=========================================================================================================================\n\n\tprivate bool CaptureFramePose( SkinnedModelRenderer renderer, bool usePhysicsPose )\n\t{\n\t\t_framePose.Clear();\n\n\t\tTransform rootWorld = default;\n\t\tif ( !renderer.TryGetBoneTransform( _joints[0].Bone, out rootWorld )\n\t\t\t&& !renderer.TryGetBoneTransformAnimation( _joints[0].Bone, out rootWorld ) )\n\t\t\treturn false;\n\t\tvar rootPose = renderer.WorldTransform.ToLocal( rootWorld );\n\t\t_framePose.Add( rootPose );\n\n\t\tfor ( var i = 1; i < _joints.Count; i++ )\n\t\t{\n\t\t\tvar joint = _joints[i];\n\t\t\tTransform animationPose;\n\t\t\tif ( usePhysicsPose )\n\t\t\t{\n\t\t\t\tif ( _ragdollAnimationPose.Count != _joints.Count ) return false;\n\t\t\t\tanimationPose = ApplyRootPoseDelta( _ragdollAnimationPose[i], _ragdollAnimationPose[0], rootPose );\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tif ( !renderer.TryGetBoneTransformAnimation( joint.Bone, out var worldTransform ) )\n\t\t\t\t\treturn false;\n\t\t\t\tanimationPose = renderer.WorldTransform.ToLocal( worldTransform );\n\t\t\t}\n\n\t\t\t_framePose.Add( animationPose );\n\t\t}\n\n\t\treturn true;\n\t}\n\n\tprivate bool CaptureRagdollAnimationPose( SkinnedModelRenderer renderer )\n\t{\n\t\t_ragdollAnimationPose.Clear();\n\t\tforeach ( var joint in _joints )\n\t\t{\n\t\t\tif ( !renderer.TryGetBoneTransformAnimation( joint.Bone, out var worldTransform ) )\n\t\t\t{\n\t\t\t\t_ragdollAnimationPose.Clear();\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\t_ragdollAnimationPose.Add( renderer.WorldTransform.ToLocal( worldTransform ) );\n\t\t}\n\n\t\treturn true;\n\t}\n\n\tprivate static Transform ApplyRootPoseDelta( Transform pose, Transform animationRoot, Transform finalRoot )\n\t{\n\t\tvar rotationDelta = finalRoot.Rotation * animationRoot.Rotation.Inverse;\n\t\tvar positionDelta = finalRoot.Position - rotationDelta * animationRoot.Position;\n\t\treturn new Transform( positionDelta + rotationDelta * pose.Position, rotationDelta * pose.Rotation, pose.Scale );\n\t}\n\n\tprivate bool IsPhysicsDrivingRenderer( SkinnedModelRenderer renderer )\n\t{\n\t\tif ( Scene is null ) return false;\n\t\tforeach ( var physics in Scene.GetAllComponents<ModelPhysics>() )\n\t\t\tif ( physics is not null && physics.Enabled && physics.MotionEnabled && physics.Renderer == renderer )\n\t\t\t\treturn true;\n\t\treturn false;\n\t}\n\n\tprivate bool TryGetSolveTransformLocal( SkinnedModelRenderer renderer, SoftBoneJointState joint, out Transform transform )\n\t{\n\t\tif ( renderer is not null && renderer.IsValid() && joint.Bone is not null\n\t\t\t&& renderer.TryGetBoneTransformAnimation( joint.Bone, out var worldTransform ) )\n\t\t{\n\t\t\ttransform = renderer.WorldTransform.ToLocal( worldTransform );\n\t\t\treturn true;\n\t\t}\n\n\t\ttransform = joint.RestTransformLocal;\n\t\treturn joint.RestTransformLocal.Scale != Vector3.Zero;\n\t}\n\n\tprivate static Vector3 GetAnimatedBoneVectorLocal( SkinnedModelRenderer renderer, BoneCollection.Bone bone, Transform animated )\n\t{\n\t\tif ( bone.Children.Count > 0 && SoftBoneSkinnedModelUtility.TryGetBoneAnimationLocal( renderer, bone.Children[0], out var child ) )\n\t\t{\n\t\t\tvar direction = child.Position - animated.Position;\n\t\t\tif ( direction.Length > Epsilon )\n\t\t\t\treturn direction;\n\t\t}\n\n\t\tvar fallback = animated.Rotation * Vector3.Forward;\n\t\treturn fallback.Length > Epsilon ? fallback : Vector3.Forward;\n\t}\n\n\tprivate Vector3 GetFramePoseVector( int index, SoftBoneJointState joint, Transform pose )\n\t{\n\t\tif ( index + 1 < _framePose.Count )\n\t\t{\n\t\t\tvar direction = _framePose[index + 1].Position - pose.Position;\n\t\t\tif ( direction.Length > Epsilon ) return direction;\n\t\t}\n\n\t\treturn GetPoseVectorLocal( joint, pose );\n\t}\n\n\tprivate static Vector3 GetPoseVectorLocal( SoftBoneJointState joint, Transform animated )\n\t{\n\t\tvar poseVector = animated.Rotation * joint.LocalPoseVector;\n\t\tif ( poseVector.Length > Epsilon )\n\t\t\treturn poseVector;\n\n\t\tvar fallback = animated.Rotation * Vector3.Forward;\n\t\treturn fallback.Length > Epsilon ? fallback : Vector3.Forward;\n\t}\n\n\tprivate static Rotation GetParentDeflection( int index, Rotation parentAnimatedRotation, Rotation parentSimulationRotation )\n\t{\n\t\tif ( index <= 0 )\n\t\t\treturn Rotation.Identity;\n\n\t\treturn parentSimulationRotation * parentAnimatedRotation.Inverse;\n\t}\n\n\tprivate float GetGravityAmount( SoftBoneJointState joint, Transform animated, Vector3 rendererDownLocal, float gravity, float gravityFalloff )\n\t{\n\t\tif ( Math.Abs( gravity ) <= Epsilon )\n\t\t\treturn 0f;\n\n\t\tvar gravityAmount = Math.Abs( gravity );\n\t\tif ( gravityFalloff > Epsilon )\n\t\t{\n\t\t\tvar gravityNormal = animated.Rotation * joint.OriginalLocalGravityNormal;\n\t\t\tvar gravityBias = Math.Min( 1f, 1f - Vector3.Dot(\n\t\t\t\tBoBiCoSoftBoneSolver.SafeNormal( gravityNormal, rendererDownLocal ),\n\t\t\t\trendererDownLocal ) );\n\t\t\tgravityAmount *= (1f - gravityFalloff) + gravityFalloff * gravityBias;\n\t\t}\n\n\t\treturn Math.Clamp( gravityAmount, 0f, 1f );\n\t}\n\n\tprivate static Vector3 ApplyGravityTarget( Vector3 begin, Vector3 poseVector, float restLength, float gravityAmount, float gravity, Vector3 rendererDownLocal )\n\t{\n\t\tif ( gravityAmount <= Epsilon )\n\t\t\treturn begin + poseVector;\n\n\t\tvar gravityVector = (gravity > 0f ? rendererDownLocal : -rendererDownLocal) * restLength;\n\t\tvar targetVector = poseVector + (gravityVector - poseVector) * gravityAmount;\n\t\treturn begin + BoBiCoSoftBoneSolver.SafeNormal( targetVector, poseVector ) * restLength;\n\t}\n\n\tprivate static void ResetToRestPose( SkinnedModelRenderer renderer, SoftBoneJointState joint, Transform animated, Vector3 begin, Vector3 poseVector, Rotation simulationRotation )\n\t{\n\t\tvar restEnd = begin + poseVector;\n\t\tjoint.PreviousBegin = begin;\n\t\tjoint.PreviousEnd = restEnd;\n\t\tjoint.PreviousAnimatedBegin = begin;\n\t\tjoint.PreviousAnimatedRotation = animated.Rotation;\n\t\tjoint.PreviousSimulationRotation = simulationRotation;\n\t\tjoint.ImmobileEnd = SoftBoneSkinnedModelUtility.ToWorld( renderer, restEnd );\n\t\tjoint.PreviousEndWorld = joint.ImmobileEnd;\n\t\tjoint.PreviousVector = poseVector;\n\t\tjoint.PreviousVelocity = Vector3.Zero;\n\t\tjoint.HasHistory = true;\n\t}\n\n\tprivate static void ApplySolvedTransform( SkinnedModelRenderer renderer, SoftBoneJointState joint, Transform animated, Rotation solvedRotation )\n\t{\n\t\tvar localTransform = new Transform( joint.PreviousBegin, solvedRotation, animated.Scale );\n\t\trenderer.SceneModel.SetBoneOverride( joint.Bone.Index, in localTransform );\n\t}\n\n\tprivate static void ApplyAnimationTransform( SkinnedModelRenderer renderer, SoftBoneJointState joint, Transform animated )\n\t{\n\t\trenderer.SceneModel.SetBoneOverride( joint.Bone.Index, in animated );\n\t}\n\n\tprivate static Vector3 RendererDownLocal( SkinnedModelRenderer renderer )\n\t{\n\t\tif ( renderer is null || !renderer.IsValid() )\n\t\t\treturn Vector3.Down;\n\n\t\treturn BoBiCoSoftBoneSolver.SafeNormal( renderer.WorldTransform.Rotation.Inverse * Vector3.Down, Vector3.Down );\n\t}\n\n//=========================================================================================================================\n// Building Gizmo \n//=========================================================================================================================\n\n\tprotected override void DrawGizmos()\n\t{\n\t\tbase.DrawGizmos();\n\t\tvar drawCollider = CollisionMode != SoftBoneCollisionMode.None && ShowColliderGizmo && (AlwaysShowGizmo || Gizmo.IsSelected);\n\t\tvar drawLimit = ShowLimitGizmo && (AlwaysShowLimitGizmo || Gizmo.IsSelected) && LimitType != SoftBoneLimitType.None;\n\t\tif ( !drawCollider && !drawLimit )\n\t\t\treturn;\n\n\t\tvar renderer = FindRendererForGizmo();\n\t\tif ( renderer is null || !renderer.IsValid() )\n\t\t\treturn;\n\n\t\tif ( _dirty || _joints.Count == 0 )\n\t\t\tRebuild( renderer );\n\n\t\tusing ( Transform.DisableProxy() )\n\t\t{\n\t\t\tGizmo.Transform = new Transform( Vector3.Zero, Rotation.Identity, 1f );\n\t\t\tGizmo.Draw.IgnoreDepth = true;\n\t\t\tGizmo.Draw.LineThickness = 1;\n\t\t\tfor ( var i = GetSimulationStartIndex(); i < _joints.Count; i++ )\n\t\t\t{\n\t\t\t\tvar joint = _joints[i];\n\t\t\t\tif ( !TryGetSolveTransformLocal( renderer, joint, out var animated ) )\n\t\t\t\t\tcontinue;\n\n\t\t\t\tvar solvedBegin = joint.HasHistory ? joint.PreviousBegin : animated.Position;\n\t\t\t\tvar solvedEnd = joint.HasHistory ? joint.PreviousEnd : animated.Position + GetPoseVectorLocal( joint, animated );\n\t\t\t\tvar physicsBegin = SoftBoneSkinnedModelUtility.ToWorld( renderer, solvedBegin );\n\t\t\t\tvar boneObject = renderer.GetBoneObject( joint.Bone );\n\t\t\t\tvar begin = boneObject is not null && boneObject.IsValid()\n\t\t\t\t\t? boneObject.WorldPosition\n\t\t\t\t\t: physicsBegin;\n\t\t\t\tvar end = SoftBoneSkinnedModelUtility.ToWorld( renderer, solvedEnd );\n\t\t\t\t// Root bone: only draw when SimulateRoot is enabled.\n\t\t\t\t// Non-root bones: skip leaf/endpoint bones (Children.Count == 0) since their\n\t\t\t\t// endpoint is a synthetic forward poseVector, not a real child position.\n\t\t\t\t// The Children.Count gate must NOT apply to the simulated root \u2014 it may be a\n\t\t\t\t// leaf or near-leaf bone and still needs collision when SimulateRoot is on.\n\t\t\t\tvar drawJointCollider = drawCollider\n\t\t\t\t\t&& (i != 0 || (SimulateRoot && CollideWithRoot));\n\t\t\t\tif ( drawJointCollider )\n\t\t\t\t{\n\t\t\t\t\tGizmo.Draw.Color = ColliderGizmoColor.WithAlpha( ColliderGizmoOpacity );\n\t\t\t\t\tDrawLinkColliderGizmo( begin, end, EvaluateColliderRadius( GetChainPosition( i ) ) );\n\t\t\t\t}\n\n\t\t\t\tvar drawJointLimit = drawLimit && (i != 0 || SimulateRoot);\n\t\t\t\tif ( drawJointLimit )\n\t\t\t\t{\n\t\t\t\t\tvar solvedVector = solvedEnd - solvedBegin;\n\t\t\t\t\tvar chainPosition = GetChainPosition( i );\n\t\t\t\t\tvar colliderRadius = EvaluateColliderRadius( chainPosition );\n\t\t\t\t\tDrawLimitGizmo( 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 ) );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate void DrawLinkColliderGizmo( Vector3 worldStart, Vector3 worldEnd, float colliderRadius )\n\t{\n\t\tif ( ColliderShape == SoftBoneLinkColliderShape.Sphere )\n\t\t{\n\t\t\tGizmo.Draw.LineSphere( new Sphere( worldStart, colliderRadius ) );\n\t\t\treturn;\n\t\t}\n\t\tif ( ColliderShape == SoftBoneLinkColliderShape.Capsule )\n\t\t{\n\t\t\tvar direction = BoBiCoSoftBoneSolver.SafeNormal( worldEnd - worldStart, Vector3.Forward );\n\t\t\tvar halfLine = Math.Max( 0f, ColliderHeight * 0.5f - colliderRadius );\n\t\t\tGizmo.Draw.LineCapsule( new Capsule( worldStart - direction * halfLine, worldStart + direction * halfLine, colliderRadius ) );\n\t\t\treturn;\n\t\t}\n\n\t\tvar forward = BoBiCoSoftBoneSolver.SafeNormal( worldEnd - worldStart, Vector3.Forward );\n\t\tvar right = BoBiCoSoftBoneSolver.SafeNormal( Vector3.Cross( forward, Vector3.Up ), Vector3.Right );\n\t\tvar up = BoBiCoSoftBoneSolver.SafeNormal( Vector3.Cross( right, forward ), Vector3.Up );\n\t\tif ( ColliderShape == SoftBoneLinkColliderShape.Flat )\n\t\t{\n\t\t\tvar half = colliderRadius;\n\t\t\tvar a = worldStart + (right + up) * half; var b = worldStart + (right - up) * half;\n\t\t\tvar c = worldStart + (-right - up) * half; var d = worldStart + (-right + up) * half;\n\t\t\tGizmo.Draw.Line( a, b ); Gizmo.Draw.Line( b, c ); Gizmo.Draw.Line( c, d ); Gizmo.Draw.Line( d, a );\n\t\t\tGizmo.Draw.Line( worldStart, worldStart + forward * Math.Max( colliderRadius, 0.2f ) );\n\t\t\treturn;\n\t\t}\n\n\t\tvar halfSize = BoxDimension * 0.5f;\n\t\tvar corners = new Vector3[8];\n\t\tfor ( var corner = 0; corner < 8; corner++ )\n\t\t\tcorners[corner] = worldStart\n\t\t\t\t+ forward * (((corner & 1) == 0 ? -1f : 1f) * halfSize.z)\n\t\t\t\t+ right   * (((corner & 2) == 0 ? -1f : 1f) * halfSize.x)\n\t\t\t\t+ up      * (((corner & 4) == 0 ? -1f : 1f) * halfSize.y);\n\t\tint[] 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 };\n\t\tfor ( var edge = 0; edge < edges.Length; edge += 2 ) Gizmo.Draw.Line( corners[edges[edge]], corners[edges[edge + 1]] );\n\t}\n\n\tprivate void DrawLimitGizmo( SkinnedModelRenderer renderer, Vector3 worldBegin, Vector3 solvedVector, float radius, float maxAngle, float maxYaw, float maxPitch )\n\t{\n\t\tSoftBoneAngleLimit.GetAxes( solvedVector, LimitRotation, out var axisX, out var axisY, out var axisZ );\n\t\taxisX = renderer.WorldTransform.Rotation * axisX;\n\t\taxisY = renderer.WorldTransform.Rotation * axisY;\n\t\taxisZ = renderer.WorldTransform.Rotation * axisZ;\n\t\tvar gizmoRotation = Rotation.FromAxis( axisY, 90f );\n\t\taxisX = gizmoRotation * axisX;\n\t\taxisZ = gizmoRotation * axisZ;\n\t\tGizmo.Draw.Color = LimitGizmoColor.WithAlpha( LimitGizmoOpacity );\n\t\tconst int steps = 24;\n\t\tif ( LimitType == SoftBoneLimitType.Cone )\n\t\t{\n\t\t\tDrawArc( worldBegin, axisY, axisX, maxAngle, radius, steps );\n\t\t\tDrawArc( worldBegin, axisY, axisZ, maxAngle, radius, steps );\n\t\t}\n\t\telse if ( LimitType == SoftBoneLimitType.Plane )\n\t\t{\n\t\t\tDrawArc( worldBegin, axisY, axisZ, maxAngle, radius, steps );\n\t\t\tGizmo.Draw.Line( worldBegin - axisX * radius * 0.45f, worldBegin + axisX * radius * 0.45f );\n\t\t}\n\t\telse\n\t\t{\n\t\t\tDrawArc( worldBegin, axisY, axisX, maxYaw, radius, steps );\n\t\t\tDrawArc( worldBegin, axisY, axisZ, maxPitch, radius, steps );\n\t\t}\n\t}\n\n\tprivate static void DrawArc( Vector3 origin, Vector3 forward, Vector3 side, float angle, float radius, int steps )\n\t{\n\t\tvar previous = origin + Rotation.FromAxis( side, -angle ) * forward * radius;\n\t\tfor ( var i = 1; i <= steps; i++ )\n\t\t{\n\t\t\tvar point = origin + Rotation.FromAxis( side, -angle + angle * 2f * i / steps ) * forward * radius;\n\t\t\tGizmo.Draw.Line( previous, point );\n\t\t\tprevious = point;\n\t\t}\n\t}\n\n\tprivate SkinnedModelRenderer FindRendererForGizmo()\n\t{\n\t\tvar targetRenderer = TargetBone?.GetComponentInParent<SkinnedModelRenderer>( true, true );\n\t\tif ( targetRenderer is not null && targetRenderer.IsValid() )\n\t\t\treturn targetRenderer;\n\t\treturn null;\n\t}\n\n\tinternal SkinnedModelRenderer GetTargetRenderer() => FindRendererForGizmo();\n\n\tprotected override void OnValidate()\n\t{\n\t\tUpdateOwnerName();\n\t\tColliderGizmoOpacity = Math.Clamp( ColliderGizmoOpacity, 0f, 1f );\n\t\tLimitGizmoOpacity = Math.Clamp( LimitGizmoOpacity, 0f, 1f );\n\t\tColliderHeight = Math.Max( ColliderHeight, 0.002f );\n\t\tBoxDimension = new Vector3( Math.Max( BoxDimension.x, 0.001f ), Math.Max( BoxDimension.y, 0.001f ), Math.Max( BoxDimension.z, 0.001f ) );\n\t\tLimitGizmoSize = Math.Max( LimitGizmoSize, 0.1f );\n\t\tMarkDirty();\n\t}\n\n\tprotected override void OnDisabled()\n\t{\n\t\tSoftBoneCollisionRegistry.Remove( this );\n\t}\n}"
        },
        {
            "Ident": "bobicolab.bobicosoftbones",
            "Path": "BoBiCo Soft-Physics/SoftBoneAssembly.cs",
            "FileName": "SoftBoneAssembly.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 336846,
            "IsPrivate": false,
            "Code": "// I don't know, I kept these here to avoid random errors.\r\n\r\nglobal using Sandbox;\r\nglobal using System.Collections.Generic;\r\nglobal using System.Linq;"
        },
        {
            "Ident": "bobicolab.bobicosoftbones",
            "Path": "BoBiCo Soft-Physics/SoftBoneSolver.cs",
            "FileName": "SoftBoneSolver.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 336846,
            "IsPrivate": false,
            "Code": "using System;\n\n/// <summary>\n/// Integrates one SoftBone link in renderer-local space.\n/// </summary>\npublic static class BoBiCoSoftBoneSolver\n{\n\tprivate const float Epsilon = 0.000008f;\n\n\t/// <summary>\n\t/// One integration step.  All positions/vectors in renderer-local space.\n\t/// Returns the solved end position (renderer-local) and the new velocity.\n\t/// </summary>\n\tpublic static Vector3 IntegrateAdvanced(\n\t\tVector3 begin,\n\t\tVector3 endPoint,           // current physics end (may have had Immobile applied before call)\n\t\tVector3 previousEndPoint,   // endPoint before Immobile, for velocity storage\n\t\tVector3 poseVector,         // animated rest direction (renderer-local)\n\t\tVector3 previousVector,     // last frame's (solvedEnd - begin)\n\t\tVector3 previousVelocity,\n\t\tfloat minLength,\n\t\tfloat maxLength,\n\t\tfloat pull,\n\t\tfloat momentum,\n\t\tfloat stiffness,\n\t\tVector3 gravityTargetEnd,   // begin + gravity-blended direction * restLength\n\t\tfloat frameRatio,           // Time.Delta * 60, used for frameLerp\n\t\tout Vector3 newVelocity )\n\t{\n\t\tvar delta = previousVelocity * momentum;\n\t\tdelta += (gravityTargetEnd - (endPoint + delta)) * pull;\n\n\t\tif ( stiffness > Epsilon )\n\t\t{\n\t\t\tvar nextVector = endPoint + delta - begin;\n\t\t\tdelta += (previousVector - nextVector) * stiffness;\n\t\t}\n\n\t\tendPoint += delta;\n\t\tvar boundedVector = endPoint - begin;\n\t\tvar boundedLength = Math.Clamp( boundedVector.Length, minLength, maxLength );\n\t\tendPoint = begin + SafeNormal( boundedVector, poseVector ) * boundedLength;\n\n\t\t// Limit per-step commitment so long frames do not inject a velocity spike.\n\t\tvar frameLerp = Math.Clamp( (1f / Lerp( 1f, frameRatio, pull )) * frameRatio, 0f, 1f );\n\t\tendPoint = Lerp( previousEndPoint, endPoint, frameLerp );\n\n\t\t// velocity = where we actually moved to, not the raw delta\n\t\tnewVelocity = endPoint - previousEndPoint;\n\n\t\treturn endPoint;\n\t}\n\n\t/// <summary>\n\t/// Solve the head bone rotation to point toward the simulated tail.\n\t/// Works in renderer-local space.\n\t/// </summary>\n\tpublic static Rotation SolveHeadRotation(\n\t\tVector3 poseVector,         // animated rest direction (renderer-local)\n\t\tVector3 solvedVector,       // solvedEnd - begin (renderer-local)\n\t\tRotation animatedRotation )\n\t{\n\t\tvar restAxis = SafeNormal( poseVector, Vector3.Forward );\n\t\tvar solvedAxis = SafeNormal( solvedVector, restAxis );\n\t\treturn Rotation.FromToRotation( restAxis, solvedAxis ) * animatedRotation;\n\t}\n\n\t// \u2500\u2500 Helpers (all renderer-local) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n\tpublic static Vector3 SafeNormal( Vector3 value, Vector3 fallback )\n\t{\n\t\tif ( value.Length <= Epsilon )\n\t\t\treturn fallback.Length > Epsilon ? fallback.Normal : Vector3.Forward;\n\n\t\treturn value.Normal;\n\t}\n\n\tprivate static Vector3 Lerp( Vector3 a, Vector3 b, float t ) => a + (b - a) * t;\n\tprivate static float Lerp( float a, float b, float t ) => a + (b - a) * t;\n}\n"
        },
        {
            "Ident": "bobicolab.bobicosoftbones",
            "Path": "Code/BoBiCo Soft-Physics/SoftBoneAngleLimit.cs",
            "FileName": "SoftBoneAngleLimit.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 336846,
            "IsPrivate": false,
            "Code": "using System;\n\npublic enum SoftBoneLimitType \n{ \n\t/// <summary>\n\t/// No Limits, The chain can move freely without angular limits.\n\t/// </summary>\n\tNone, \n\n\t/// <summary>\n\t/// Limits the movement within a cone-shaped angle from the rest direction.\n\t/// </summary>\n\tCone, \n\n\t/// <summary>\n\t/// Limits the movement to one side of a plane, preventing the chain from crossing it.\n\t/// </summary>\n\tPlane, \n\n\t/// <summary>\n\t/// Limits the movement within an elliptical cone, allowing different horizontal and vertical angle limits.\n\t/// </summary>\n\tEllipse \n}\n\npublic static class SoftBoneAngleLimit\n{\n\tprivate const float Epsilon = 0.000008f;\n\n\tpublic static Vector3 Apply( Vector3 poseVector, Vector3 solvedDirection, SoftBoneLimitType type, float maxAngle, float maxYaw, float maxPitch, Angles limitRotation )\n\t{\n\t\tvar direction = SafeNormal( solvedDirection, poseVector );\n\t\tif ( type == SoftBoneLimitType.None ) return direction;\n\t\tGetAxes( poseVector, limitRotation, out var axisX, out var axisY, out var axisZ );\n\t\treturn type switch\n\t\t{\n\t\t\tSoftBoneLimitType.Cone => ClampCone( direction, axisX, axisY, maxAngle ),\n\t\t\tSoftBoneLimitType.Plane => ClampPlane( direction, axisX, axisY, axisZ, maxAngle ),\n\t\t\tSoftBoneLimitType.Ellipse => ClampEllipse( direction, axisX, axisY, axisZ, maxYaw, maxPitch ),\n\t\t\t_ => direction\n\t\t};\n\t}\n\n\tpublic static void GetAxes( Vector3 poseVector, Angles limitRotation, out Vector3 axisX, out Vector3 axisY, out Vector3 axisZ )\n\t{\n\t\tvar frame = Rotation.FromToRotation( Vector3.Up, SafeNormal( poseVector, Vector3.Up ) ) * limitRotation.ToRotation();\n\t\taxisX = SafeNormal( frame * Vector3.Right, Vector3.Right);\n\t\taxisY = SafeNormal( frame * Vector3.Up, Vector3.Up );\n\t\taxisZ = SafeNormal( Vector3.Cross( axisX, axisY ), Vector3.Forward );\n\t}\n\n\tprivate static Vector3 ClampCone( Vector3 direction, Vector3 axisX, Vector3 axisY, float maxAngle )\n\t{\n\t\tif ( maxAngle >= 180f ) return direction;\n\t\tvar angle = MathF.Acos( Math.Clamp( Vector3.Dot( axisY, direction ), -1f, 1f ) ) * 180f / MathF.PI;\n\t\tif ( angle <= maxAngle ) return direction;\n\t\tvar rotationAxis = Vector3.Cross( axisY, direction );\n\t\tif ( rotationAxis.Length <= Epsilon ) rotationAxis = axisX;\n\t\treturn Rotation.FromAxis( SafeNormal( rotationAxis, axisX ), maxAngle ) * axisY;\n\t}\n\n\tprivate static Vector3 ClampPlane( Vector3 direction, Vector3 axisX, Vector3 axisY, Vector3 axisZ, float maxAngle )\n\t{\n\t\tvar planar = direction - axisX * Vector3.Dot( direction, axisX );\n\t\tplanar = SafeNormal( planar, axisY );\n\t\treturn ClampCone( planar, axisZ, axisY, maxAngle );\n\t}\n\n\tprivate static Vector3 ClampEllipse( Vector3 direction, Vector3 axisX, Vector3 axisY, Vector3 axisZ, float maxYaw, float maxPitch )\n\t{\n\t\tvar x = Vector3.Dot( direction, axisZ );\n\t\tvar y = Vector3.Dot( direction, axisX );\n\t\tvar z = Vector3.Dot( direction, axisY );\n\t\tvar yaw = Math.Clamp( MathF.Atan2( y, z ), -maxYaw * MathF.PI / 180f, maxYaw * MathF.PI / 180f );\n\t\tvar pitch = Math.Clamp( MathF.Asin( Math.Clamp( x, -1f, 1f ) ), -maxPitch * MathF.PI / 180f, maxPitch * MathF.PI / 180f );\n\t\tvar horizontal = MathF.Cos( pitch );\n\t\treturn SafeNormal( axisZ * MathF.Sin( pitch ) + axisX * (MathF.Sin( yaw ) * horizontal) + axisY * (MathF.Cos( yaw ) * horizontal), axisY );\n\t}\n\n\tprivate static Vector3 SafeNormal( Vector3 value, Vector3 fallback ) => value.Length > Epsilon ? value.Normal : fallback.Length > Epsilon ? fallback.Normal : Vector3.Forward;\n}\n"
        },
        {
            "Ident": "bobicolab.bobicosoftbones",
            "Path": "Code/BoBiCo Soft-Physics/SoftBoneSkinnedModelUtility.cs",
            "FileName": "SoftBoneSkinnedModelUtility.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 336846,
            "IsPrivate": false,
            "Code": "using System;\n\npublic static class SoftBoneSkinnedModelUtility\n{\n\tprivate const float Epsilon = 0.000008f;\n\n\tpublic static bool TryGetBoneAnimationLocal( SkinnedModelRenderer renderer, BoneCollection.Bone bone, out Transform transform )\n\t{\n\t\ttransform = default;\n\n\t\tif ( renderer is null || !renderer.IsValid() || bone is null )\n\t\t\treturn false;\n\n\t\tif ( renderer.TryGetBoneTransformAnimation( bone, out var worldTransform ) )\n\t\t{\n\t\t\ttransform = renderer.WorldTransform.ToLocal( worldTransform );\n\t\t\treturn true;\n\t\t}\n\n\t\tif ( renderer.TryGetBoneTransform( bone, out worldTransform ) )\n\t\t{\n\t\t\ttransform = renderer.WorldTransform.ToLocal( worldTransform );\n\t\t\treturn true;\n\t\t}\n\n\t\treturn false;\n\t}\n\n\tpublic static Vector3 ToWorld( SkinnedModelRenderer renderer, Vector3 localPosition )\n\t{\n\t\tif ( renderer is null || !renderer.IsValid() )\n\t\t\treturn localPosition;\n\n\t\treturn renderer.WorldTransform.PointToWorld( localPosition );\n\t}\n\n\tpublic static Vector3 ToLocal( SkinnedModelRenderer renderer, Vector3 worldPosition )\n\t{\n\t\tif ( renderer is null || !renderer.IsValid() )\n\t\t\treturn worldPosition;\n\n\t\treturn renderer.WorldTransform.PointToLocal( worldPosition );\n\t}\n\n\tpublic static Vector3 SafeNormal( Vector3 value, Vector3 fallback )\n\t{\n\t\tif ( value.Length <= Epsilon )\n\t\t\treturn fallback.Length > Epsilon ? fallback.Normal : Vector3.Forward;\n\n\t\treturn value.Normal;\n\t}\n}\n"
        },
        {
            "Ident": "bobicolab.bobicosoftbones",
            "Path": "BoBiCo Soft-Physics/SoftBoneAngleLimit.cs",
            "FileName": "SoftBoneAngleLimit.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 336846,
            "IsPrivate": false,
            "Code": "using System;\n\npublic enum SoftBoneLimitType \n{ \n\t/// <summary>\n\t/// No Limits, The chain can move freely without angular limits.\n\t/// </summary>\n\tNone, \n\n\t/// <summary>\n\t/// Limits the movement within a cone-shaped angle from the rest direction.\n\t/// </summary>\n\tCone, \n\n\t/// <summary>\n\t/// Limits the movement to one side of a plane, preventing the chain from crossing it.\n\t/// </summary>\n\tPlane, \n\n\t/// <summary>\n\t/// Limits the movement within an elliptical cone, allowing different horizontal and vertical angle limits.\n\t/// </summary>\n\tEllipse \n}\n\npublic static class SoftBoneAngleLimit\n{\n\tprivate const float Epsilon = 0.000008f;\n\n\tpublic static Vector3 Apply( Vector3 poseVector, Vector3 solvedDirection, SoftBoneLimitType type, float maxAngle, float maxYaw, float maxPitch, Angles limitRotation )\n\t{\n\t\tvar direction = SafeNormal( solvedDirection, poseVector );\n\t\tif ( type == SoftBoneLimitType.None ) return direction;\n\t\tGetAxes( poseVector, limitRotation, out var axisX, out var axisY, out var axisZ );\n\t\treturn type switch\n\t\t{\n\t\t\tSoftBoneLimitType.Cone => ClampCone( direction, axisX, axisY, maxAngle ),\n\t\t\tSoftBoneLimitType.Plane => ClampPlane( direction, axisX, axisY, axisZ, maxAngle ),\n\t\t\tSoftBoneLimitType.Ellipse => ClampEllipse( direction, axisX, axisY, axisZ, maxYaw, maxPitch ),\n\t\t\t_ => direction\n\t\t};\n\t}\n\n\tpublic static void GetAxes( Vector3 poseVector, Angles limitRotation, out Vector3 axisX, out Vector3 axisY, out Vector3 axisZ )\n\t{\n\t\tvar frame = Rotation.FromToRotation( Vector3.Up, SafeNormal( poseVector, Vector3.Up ) ) * limitRotation.ToRotation();\n\t\taxisX = SafeNormal( frame * Vector3.Right, Vector3.Right);\n\t\taxisY = SafeNormal( frame * Vector3.Up, Vector3.Up );\n\t\taxisZ = SafeNormal( Vector3.Cross( axisX, axisY ), Vector3.Forward );\n\t}\n\n\tprivate static Vector3 ClampCone( Vector3 direction, Vector3 axisX, Vector3 axisY, float maxAngle )\n\t{\n\t\tif ( maxAngle >= 180f ) return direction;\n\t\tvar angle = MathF.Acos( Math.Clamp( Vector3.Dot( axisY, direction ), -1f, 1f ) ) * 180f / MathF.PI;\n\t\tif ( angle <= maxAngle ) return direction;\n\t\tvar rotationAxis = Vector3.Cross( axisY, direction );\n\t\tif ( rotationAxis.Length <= Epsilon ) rotationAxis = axisX;\n\t\treturn Rotation.FromAxis( SafeNormal( rotationAxis, axisX ), maxAngle ) * axisY;\n\t}\n\n\tprivate static Vector3 ClampPlane( Vector3 direction, Vector3 axisX, Vector3 axisY, Vector3 axisZ, float maxAngle )\n\t{\n\t\tvar planar = direction - axisX * Vector3.Dot( direction, axisX );\n\t\tplanar = SafeNormal( planar, axisY );\n\t\treturn ClampCone( planar, axisZ, axisY, maxAngle );\n\t}\n\n\tprivate static Vector3 ClampEllipse( Vector3 direction, Vector3 axisX, Vector3 axisY, Vector3 axisZ, float maxYaw, float maxPitch )\n\t{\n\t\tvar x = Vector3.Dot( direction, axisZ );\n\t\tvar y = Vector3.Dot( direction, axisX );\n\t\tvar z = Vector3.Dot( direction, axisY );\n\t\tvar yaw = Math.Clamp( MathF.Atan2( y, z ), -maxYaw * MathF.PI / 180f, maxYaw * MathF.PI / 180f );\n\t\tvar pitch = Math.Clamp( MathF.Asin( Math.Clamp( x, -1f, 1f ) ), -maxPitch * MathF.PI / 180f, maxPitch * MathF.PI / 180f );\n\t\tvar horizontal = MathF.Cos( pitch );\n\t\treturn SafeNormal( axisZ * MathF.Sin( pitch ) + axisX * (MathF.Sin( yaw ) * horizontal) + axisY * (MathF.Cos( yaw ) * horizontal), axisY );\n\t}\n\n\tprivate static Vector3 SafeNormal( Vector3 value, Vector3 fallback ) => value.Length > Epsilon ? value.Normal : fallback.Length > Epsilon ? fallback.Normal : Vector3.Forward;\n}\n"
        },
        {
            "Ident": "bobicolab.bobicosoftbones",
            "Path": "Code/BoBiCo Soft-Physics/SoftBoneJointState.cs",
            "FileName": "SoftBoneJointState.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 336846,
            "IsPrivate": false,
            "Code": "using System;\n\n/// <summary>Runtime state for one simulated SoftBone joint.</summary>\npublic sealed class SoftBoneJointState\n{\n\tpublic BoneCollection.Bone Bone;\n\n\t// Rest-pose data\n\tpublic float Length;\n\tpublic Vector3 LocalPoseVector;\n\tpublic Transform RestTransformLocal;\n\tpublic Vector3 OriginalLocalGravityNormal;\n\n\t// Per-frame state. Positions are renderer-local unless suffixed with World.\n\tpublic Vector3 PreviousBegin;\n\tpublic Vector3 PreviousEnd;\n\tpublic Vector3 PreviousAnimatedBegin;\n\tpublic Rotation PreviousAnimatedRotation;\n\tpublic Rotation PreviousSimulationRotation;\n\tpublic Vector3 ImmobileEnd;\n\tpublic Vector3 PreviousEndWorld;\n\tpublic Vector3 PreviousVector;\n\tpublic Vector3 PreviousVelocity;\n\tpublic bool HasHistory;\n}\n"
        },
        {
            "Ident": "bobicolab.bobicosoftbones",
            "Path": "BoBiCo Soft-Physics/Components/BoBiCoSoftBoneGroup.cs",
            "FileName": "BoBiCoSoftBoneGroup.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 336846,
            "IsPrivate": false,
            "Code": "//=========================================================================================================================\n// BoBiCo Soft-Bone Group Component\n//=========================================================================================================================\n\nusing System;\nusing System.Collections.Generic;\n\n[Title( \"SoftBone Group\" )]\n[Category( \"BoBiCo\" )]\n[Icon( \"account_tree\" )]\n[HelpUrl( \"https://bobico-lab.vercel.app/softbone-docs/bobico-softbones/core-components/softbone-group\" )]\npublic sealed class BoBiCoSoftBoneGroup : Component, Component.ExecuteInEditor\n{\n\tprivate const int MaxFixedSteps = 6;\n\n//=========================================================================================================================\n// Group Setup\n//=========================================================================================================================\n\n\t/// <summary>\n\t/// Assign one or more SoftBone Chains for this group to manage and simulate.\n    /// Chains outside of the group will be ignored.\n\t/// </summary>\n\t[Property, Title( \"SoftBone Chains\" ), Group( \"Chains\" )]\n\tpublic List<GameObject> Chains { get; set; } = new();\n\n\t// \u2500\u2500 Settings \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n\t/// <summary>\n\t/// Physics update rate in Hz. \n\t/// Lower values are cheaper and laggy; higher values are smoother.\n\t/// </summary>\n\t[Property, Range( 15, 240 ), Title( \"Update Rate (Hz)\" ), Group( \"Group Settings\" )]\n\tpublic float UpdateRate { get; set; } = 120f;\n\n\t/// <summary>\n\t/// Skip simulation entirely when the main camera is further than this distance.\n\t/// Bones freeze in place until the camera comes back within range.\n\t/// </summary>\n\t[Property, Title( \"Disable In Far Distance\" ), Group( \"Group Settings\" )]\n\tpublic bool DisableFarDistance { get; set; } = false;\n\n\t/// <summary>\n\t/// The distance at which the simulation is disabled.\n\t/// </summary>\n\t[Property, Title( \"Far Distance\" ), Group( \"Group Settings\" ), ShowIf( nameof( DisableFarDistance ), true )]\n\tpublic float FarDistance { get; set; } = 1000f;\n\n//=========================================================================================================================\n// Private States & Functions\n//=========================================================================================================================\n\n\tprivate SkinnedModelRenderer _resolvedRenderer;\n\tprivate IDisposable _updateBonesHook;\n\tprivate bool _dirty = true;\n\tprivate float _accumulatedTime;\n\n\tpublic void MarkDirty() => _dirty = true;\n\n\tprotected override void OnEnabled()\n\t{\n\t\t_dirty = true;\n\t\t_accumulatedTime = 0f;\n\t\tRegisterUpdateBonesHook();\n\t\tEnsureSetup( true );\n\t}\n\n\tprotected override void OnDisabled()\n\t{\n\t\tSetCollisionCulled( false );\n\t\t_updateBonesHook?.Dispose();\n\t\t_updateBonesHook = null;\n\t\t_resolvedRenderer = null;\n\t\t_accumulatedTime = 0f;\n\t}\n\n\tprotected override void OnValidate()\n\t{\n\t\tChains ??= new List<GameObject>();\n\t\tUpdateRate = Math.Clamp( UpdateRate, 15f, 240f );\n\t\tFarDistance = Math.Max( FarDistance, 0f );\n\t\t_dirty = true;\n\t}\n\n\tprotected override void OnUpdate()\n\t{\n\t\tif ( _updateBonesHook is not null || !EnsureSetup() )\n\t\t\treturn;\n\n\t\tRunWithAccumulator();\n\t}\n\n\tprivate void RegisterUpdateBonesHook()\n\t{\n\t\t_updateBonesHook?.Dispose();\n\t\t_updateBonesHook = Scene?.AddHook(\n\t\t\tGameObjectSystem.Stage.UpdateBones,\n\t\t\t1,\n\t\t\t() => UpdateBones(),\n\t\t\tnameof( BoBiCoSoftBoneGroup ),\n\t\t\t\"Solve SoftBone chains after animation\" );\n\t}\n\n\tprivate void UpdateBones()\n\t{\n\t\tif ( !Enabled || !EnsureSetup() )\n\t\t\treturn;\n\n\t\tRunWithAccumulator();\n\t}\n\n\t\n\tprivate void SimulateAll( float frameRatio )\n\t{\n\t\tvar renderer = _resolvedRenderer;\n\t\tif ( renderer is null || !renderer.IsValid() )\n\t\t\treturn;\n\n\t\tforeach ( var chainObject in Chains )\n\t\t{\n\t\t\tif ( chainObject is null || !chainObject.IsValid() )\n\t\t\t\tcontinue;\n\n\t\t\tvar chain = chainObject.GetComponent<BoBiCoSoftBoneChain>();\n\t\t\tif ( chain is null || !chain.Enabled )\n\t\t\t\tcontinue;\n\n\t\t\tif ( _dirty || chain.Joints.Count == 0 || chain.IsDirty )\n\t\t\t\tchain.Rebuild( renderer );\n\n\t\t\tchain.PublishCollisionShapes( renderer );\n\t\t}\n\n\t\tSoftBoneCollisionRegistry.BeginStep( Scene );\n\n\t\tforeach ( var chainObject in Chains )\n\t\t{\n\t\t\tif ( chainObject is null || !chainObject.IsValid() )\n\t\t\t\tcontinue;\n\n\t\t\tvar chain = chainObject.GetComponent<BoBiCoSoftBoneChain>();\n\t\t\tif ( chain is null || !chain.Enabled )\n\t\t\t\tcontinue;\n\n\t\t\tif ( _dirty || chain.Joints.Count == 0 || chain.IsDirty )\n\t\t\t\tchain.Rebuild( renderer );\n\n\t\t\tchain.Simulate( renderer, frameRatio );\n\t\t}\n\n\t\t_dirty = false;\n\t}\n\n\tprivate bool EnsureSetup( bool force = false )\n\t{\n\t\tif ( !force && !_dirty && _resolvedRenderer is not null && _resolvedRenderer.IsValid() )\n\t\t\treturn true;\n\n\t\t_resolvedRenderer = ResolveRenderer();\n\n\t\tif ( _resolvedRenderer is null || !_resolvedRenderer.IsValid() )\n\t\t\treturn false;\n\n\t\tChains ??= new List<GameObject>();\n\n\t\tforeach ( var chainObject in Chains )\n\t\t{\n\t\t\tif ( chainObject is null || !chainObject.IsValid() )\n\t\t\t\tcontinue;\n\n\t\t\tchainObject.GetComponent<BoBiCoSoftBoneChain>()?.Rebuild( _resolvedRenderer );\n\t\t}\n\n\t\t_dirty = false;\n\t\treturn true;\n\t}\n\n\tprivate SkinnedModelRenderer ResolveRenderer()\n\t{\n\t\tChains ??= new List<GameObject>();\n\t\tforeach ( var chainObject in Chains )\n\t\t{\n\t\t\tif ( chainObject is null || !chainObject.IsValid() )\n\t\t\t\tcontinue;\n\n\t\t\tvar renderer = chainObject.GetComponent<BoBiCoSoftBoneChain>()?.GetTargetRenderer();\n\t\t\tif ( renderer is not null && renderer.IsValid() )\n\t\t\t\treturn renderer;\n\t\t}\n\n\t\treturn null;\n\t}\n\n\n//=========================================================================================================================\n// Bone & Collision Culling\n//=========================================================================================================================\n\n\tprivate Vector3? GetMainCameraPosition()\n\t{\n\t\tforeach ( var cam in Scene.GetAllComponents<CameraComponent>() )\n\t\t{\n\t\t\tif ( cam.IsMainCamera )\n\t\t\t\treturn cam.WorldPosition;\n\t\t}\n\t\treturn null;\n\t}\n\n\tprivate void RunWithAccumulator()\n\t{\n\t\tvar culled = false;\n\t\tif ( DisableFarDistance )\n\t\t{\n\t\t\tvar camPos = GetMainCameraPosition();\n\t\t\tif ( camPos.HasValue )\n\t\t\t{\n\t\t\t\tvar dist = Vector3.DistanceBetween( WorldPosition, camPos.Value );\n\t\t\t\tculled = dist > FarDistance;\n\t\t\t}\n\t\t}\n\n\t\tSetCollisionCulled( culled );\n\t\tif ( culled )\n\t\t{\n\t\t\t_accumulatedTime = 0f;\n\t\t\treturn;\n\t\t}\n\n\t\tvar rate = Math.Clamp( UpdateRate, 15f, 120f );\n\t\t_accumulatedTime += Time.Delta;\n\n\t\tvar executions = Math.Min( _accumulatedTime * rate, MaxFixedSteps );\n\t\tvar numSteps = (int)MathF.Ceiling( executions );\n\n\t\tif ( numSteps <= 1 )\n\t\t{\n\t\t\tSimulateAll( executions );\n\t\t}\n\t\telse\n\t\t{\n\t\t\tfor ( var step = 0; step < numSteps; step++ )\n\t\t\t\tSimulateAll( Math.Min( 1f, executions - step ) );\n\t\t}\n\n\t\tif ( executions >= 1f )\n\t\t\t_accumulatedTime = (executions - MathF.Floor( executions )) / rate;\n\t}\n\n\tprivate void SetCollisionCulled( bool culled )\n\t{\n\t\tSoftBoneCollisionRegistry.SetRendererCulled( _resolvedRenderer, culled );\n\t\tif ( !culled ) return;\n\n\t\tforeach ( var chainObject in Chains )\n\t\t{\n\t\t\tvar chain = chainObject?.GetComponent<BoBiCoSoftBoneChain>();\n\t\t\tif ( chain is not null ) SoftBoneCollisionRegistry.Remove( chain );\n\t\t}\n\t}\n}\n"
        },
        {
            "Ident": "bobicolab.bobicosoftbones",
            "Path": "BoBiCo Soft-Physics/SoftBoneCollisionRegistry.cs",
            "FileName": "SoftBoneCollisionRegistry.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 336846,
            "IsPrivate": false,
            "Code": "using System.Collections.Generic;\nusing Sandbox;\nusing System.Linq;\n\n\npublic readonly struct SoftBoneChainColliderSnapshot\n{\n\tpublic readonly SkinnedModelRenderer Renderer;\n\tpublic readonly SoftBoneColliderShape Shape;\n\n\tpublic SoftBoneChainColliderSnapshot( SkinnedModelRenderer renderer, SoftBoneColliderShape shape )\n\t{\n\t\tRenderer = renderer;\n\t\tShape = shape;\n\t}\n}\n\npublic readonly struct SoftBoneBodyColliderSnapshot\n{\n\tpublic readonly SkinnedModelRenderer Renderer;\n\tpublic readonly SoftBoneColliderShape Shape;\n\n\tpublic SoftBoneBodyColliderSnapshot( SkinnedModelRenderer renderer, SoftBoneColliderShape shape )\n\t{\n\t\tRenderer = renderer;\n\t\tShape = shape;\n\t}\n}\n\n/// <summary>Publishes the previous solved link shapes used by cross-chain contacts.</summary>\npublic static class SoftBoneCollisionRegistry\n{\n\tprivate static readonly Dictionary<BoBiCoSoftBoneChain, List<SoftBoneChainColliderSnapshot>> PublishedShapes = new();\n\tprivate static readonly Dictionary<BoBiCoSoftBoneChain, List<SoftBoneChainColliderSnapshot>> QueryShapes = new();\n\tprivate static readonly List<SoftBoneBodyColliderSnapshot> QueryBodyShapes = new();\n\tprivate static readonly HashSet<SkinnedModelRenderer> CulledRenderers = new();\n\n\tpublic static void SetRendererCulled( SkinnedModelRenderer renderer, bool culled )\n\t{\n\t\tif ( renderer is null || !renderer.IsValid() ) return;\n\t\tif ( culled ) CulledRenderers.Add( renderer );\n\t\telse CulledRenderers.Remove( renderer );\n\t}\n\n\tpublic static void Publish( BoBiCoSoftBoneChain chain, SkinnedModelRenderer renderer, List<SoftBoneChainColliderSnapshot> shapes )\n\t{\n\t\tPublishedShapes[chain] = new List<SoftBoneChainColliderSnapshot>( shapes );\n\t}\n\n\tpublic static void BeginStep( Scene scene )\n\t{\n\t\tQueryShapes.Clear();\n\t\tforeach ( var entry in PublishedShapes )\n\t\t\tQueryShapes.Add( entry.Key, new List<SoftBoneChainColliderSnapshot>( entry.Value ) );\n\n\t\tQueryBodyShapes.Clear();\n\t\tif ( scene is null )\n\t\t\treturn;\n\n\t\tforeach ( var collider in scene.GetAllComponents<BoBiCoSoftBoneCollider>() )\n\t\t{\n\t\t\tif ( collider is null || !collider.Enabled )\n\t\t\t\tcontinue;\n\n\t\t\tvar renderer = collider.GetOwningRenderer();\n\t\t\tif ( renderer is not null && CulledRenderers.Contains( renderer ) )\n\t\t\t\tcontinue;\n\t\t\tif ( collider.TryGetWorldShape( renderer, out var shape ) )\n\t\t\t\tQueryBodyShapes.Add( new SoftBoneBodyColliderSnapshot( renderer, shape ) );\n\t\t}\n\t}\n\n\tpublic static void Remove( BoBiCoSoftBoneChain chain )\n\t{\n\t\tPublishedShapes.Remove( chain );\n\t\tQueryShapes.Remove( chain );\n\t}\n\n\tpublic static void CollectChainShapes( BoBiCoSoftBoneChain owner, SkinnedModelRenderer renderer, SoftBoneCollisionMode mode, List<SoftBoneColliderShape> output )\n\t{\n\t\tif ( mode == SoftBoneCollisionMode.BaseCollision )\n\t\t\treturn;\n\n\t\tforeach ( var entry in QueryShapes )\n\t\t{\n\t\t\tif ( entry.Key == owner || !entry.Key.Enabled )\n\t\t\t\tcontinue;\n\n\t\t\tvar sameRenderer = entry.Value.Count > 0 && entry.Value[0].Renderer == renderer;\n\t\t\tif ( !sameRenderer && mode != SoftBoneCollisionMode.FullCollision )\n\t\t\t\tcontinue;\n\n\t\t\tforeach ( var snapshot in entry.Value )\n\t\t\t\toutput.Add( snapshot.Shape );\n\t\t}\n\t}\n\n\tpublic static void CollectExternalBodyShapes( SkinnedModelRenderer ownerRenderer, List<SoftBoneColliderShape> output )\n\t{\n\t\tforeach ( var snapshot in QueryBodyShapes )\n\t\t\tif ( snapshot.Renderer != ownerRenderer )\n\t\t\t\toutput.Add( snapshot.Shape );\n\t}\n}\n"
        },
        {
            "Ident": "bobicolab.bobicosoftbones",
            "Path": "Code/BoBiCo Soft-Physics/SoftBoneCollisionRegistry.cs",
            "FileName": "SoftBoneCollisionRegistry.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 336846,
            "IsPrivate": false,
            "Code": "using System.Collections.Generic;\nusing Sandbox;\nusing System.Linq;\n\n\npublic readonly struct SoftBoneChainColliderSnapshot\n{\n\tpublic readonly SkinnedModelRenderer Renderer;\n\tpublic readonly SoftBoneColliderShape Shape;\n\n\tpublic SoftBoneChainColliderSnapshot( SkinnedModelRenderer renderer, SoftBoneColliderShape shape )\n\t{\n\t\tRenderer = renderer;\n\t\tShape = shape;\n\t}\n}\n\npublic readonly struct SoftBoneBodyColliderSnapshot\n{\n\tpublic readonly SkinnedModelRenderer Renderer;\n\tpublic readonly SoftBoneColliderShape Shape;\n\n\tpublic SoftBoneBodyColliderSnapshot( SkinnedModelRenderer renderer, SoftBoneColliderShape shape )\n\t{\n\t\tRenderer = renderer;\n\t\tShape = shape;\n\t}\n}\n\n/// <summary>Publishes the previous solved link shapes used by cross-chain contacts.</summary>\npublic static class SoftBoneCollisionRegistry\n{\n\tprivate static readonly Dictionary<BoBiCoSoftBoneChain, List<SoftBoneChainColliderSnapshot>> PublishedShapes = new();\n\tprivate static readonly Dictionary<BoBiCoSoftBoneChain, List<SoftBoneChainColliderSnapshot>> QueryShapes = new();\n\tprivate static readonly List<SoftBoneBodyColliderSnapshot> QueryBodyShapes = new();\n\tprivate static readonly HashSet<SkinnedModelRenderer> CulledRenderers = new();\n\n\tpublic static void SetRendererCulled( SkinnedModelRenderer renderer, bool culled )\n\t{\n\t\tif ( renderer is null || !renderer.IsValid() ) return;\n\t\tif ( culled ) CulledRenderers.Add( renderer );\n\t\telse CulledRenderers.Remove( renderer );\n\t}\n\n\tpublic static void Publish( BoBiCoSoftBoneChain chain, SkinnedModelRenderer renderer, List<SoftBoneChainColliderSnapshot> shapes )\n\t{\n\t\tPublishedShapes[chain] = new List<SoftBoneChainColliderSnapshot>( shapes );\n\t}\n\n\tpublic static void BeginStep( Scene scene )\n\t{\n\t\tQueryShapes.Clear();\n\t\tforeach ( var entry in PublishedShapes )\n\t\t\tQueryShapes.Add( entry.Key, new List<SoftBoneChainColliderSnapshot>( entry.Value ) );\n\n\t\tQueryBodyShapes.Clear();\n\t\tif ( scene is null )\n\t\t\treturn;\n\n\t\tforeach ( var collider in scene.GetAllComponents<BoBiCoSoftBoneCollider>() )\n\t\t{\n\t\t\tif ( collider is null || !collider.Enabled )\n\t\t\t\tcontinue;\n\n\t\t\tvar renderer = collider.GetOwningRenderer();\n\t\t\tif ( renderer is not null && CulledRenderers.Contains( renderer ) )\n\t\t\t\tcontinue;\n\t\t\tif ( collider.TryGetWorldShape( renderer, out var shape ) )\n\t\t\t\tQueryBodyShapes.Add( new SoftBoneBodyColliderSnapshot( renderer, shape ) );\n\t\t}\n\t}\n\n\tpublic static void Remove( BoBiCoSoftBoneChain chain )\n\t{\n\t\tPublishedShapes.Remove( chain );\n\t\tQueryShapes.Remove( chain );\n\t}\n\n\tpublic static void CollectChainShapes( BoBiCoSoftBoneChain owner, SkinnedModelRenderer renderer, SoftBoneCollisionMode mode, List<SoftBoneColliderShape> output )\n\t{\n\t\tif ( mode == SoftBoneCollisionMode.BaseCollision )\n\t\t\treturn;\n\n\t\tforeach ( var entry in QueryShapes )\n\t\t{\n\t\t\tif ( entry.Key == owner || !entry.Key.Enabled )\n\t\t\t\tcontinue;\n\n\t\t\tvar sameRenderer = entry.Value.Count > 0 && entry.Value[0].Renderer == renderer;\n\t\t\tif ( !sameRenderer && mode != SoftBoneCollisionMode.FullCollision )\n\t\t\t\tcontinue;\n\n\t\t\tforeach ( var snapshot in entry.Value )\n\t\t\t\toutput.Add( snapshot.Shape );\n\t\t}\n\t}\n\n\tpublic static void CollectExternalBodyShapes( SkinnedModelRenderer ownerRenderer, List<SoftBoneColliderShape> output )\n\t{\n\t\tforeach ( var snapshot in QueryBodyShapes )\n\t\t\tif ( snapshot.Renderer != ownerRenderer )\n\t\t\t\toutput.Add( snapshot.Shape );\n\t}\n}\n"
        },
        {
            "Ident": "bobicolab.bobicosoftbones",
            "Path": "BoBiCo Soft-Physics/Components/BoBiCoSoftBoneCollider.cs",
            "FileName": "BoBiCoSoftBoneCollider.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 336846,
            "IsPrivate": false,
            "Code": "//=========================================================================================================================\n// BoBiCo Soft-Bone Collider Component\n//=========================================================================================================================\n\nusing System;\n\n[Title( \"SoftBone Collider\" )]\n[Category( \"BoBiCo\" )]\n[Icon( \"radio_button_unchecked\" )]\n[HelpUrl( \"https://bobico-lab.vercel.app/softbone-docs/bobico-softbones/core-components/softbone-collider\" )]\npublic sealed class BoBiCoSoftBoneCollider : Component, Component.ExecuteInEditor\n{\npublic enum ColliderShape\n{\n\t/// <summary>\n\t/// A simple spherical collider.\n\t/// Best for rounded body parts and lightweight collision detection.\n\t/// </summary>\n\tSphere,\n\n\t/// <summary>\n\t/// A capsule-shaped collider.\n\t/// Recommended for most bones and elongated body parts.\n\t/// </summary>\n\tCapsule,\n\n\t/// <summary>\n\t/// A box-shaped collider.\n\t/// Useful for broad collision areas and block-like shapes.\n\t/// </summary>\n\tBox,\n\n\t/// <summary>\n\t/// A one-sided flat collider.\n\t/// Useful for flat surface areas.\n\t/// </summary>\n\tFlat,\n}\n\n//=========================================================================================================================\n// Collider Properties\n//=========================================================================================================================\n\n\t/// <summary>\n\t/// Offsets the position of the collider.\n\t/// </summary>\n\t[Property, Order( 1 ), Title( \"Position Offset\" ), Group( \"Collider Properties\" )]\n\tpublic Vector3 PositionOffset { get; set; }\n\n\t/// <summary>\n\t/// Offsets the rotation of the collider.\n\t/// </summary>\n\t[Property, Order( 2 ), Title( \"Rotation Offset\" ), Group( \"Collider Properties\" )]\n\tpublic Angles RotationOffset { get; set; }\n\n\t/// <summary>\n\t/// Target bone to which the collider is attached. \n\t/// </summary>\n\t[Property, Order( 3 ), Title( \"Target Bone\" ), Group( \"Collider Properties\" )]\n\tpublic GameObject TargetBone { get; set; }\n\n//=========================================================================================================================\n// Shape Settings\n//=========================================================================================================================\n\n    /// <summary>\n    /// Shape used for this chain's collision volume.\n    /// Different shapes provide different collision behavior and coverage.\n    /// </summary>\n\t[Property, Order( 4 ), Title( \"Collider Shape\" ), Group( \"Shape Settings\" )]\n\tpublic ColliderShape Shape { get; set; } = ColliderShape.Sphere;\n\n    /// <summary>\n    /// Radius of the chain's collision volume.\n    /// Larger values create thicker collision boundaries.\n    /// </summary>\n\t[Property, Order( 5 ), Title( \"Collider Radius\" ), Group( \"Shape Settings\" ), HideIf( nameof( Shape ), ColliderShape.Box )]\n\tpublic float ColliderRadius { get; set; } = 2f;\n\n    /// <summary>\n    /// Height of Capsule collider.\n    /// </summary>\n\t[Property, Order( 6 ), Title( \"Collider Height\" ), Group( \"Shape Settings\" ), ShowIf( nameof( Shape ), ColliderShape.Capsule )]\n\tpublic float ColliderHeight { get; set; } = 10f;\n\n    /// <summary>\n    /// Size of Box colliders.\n    /// Only applies when the collider shape is set to Box.\n    /// </summary>\n\t[Property, Order( 7 ), Title( \"Box Dimension\" ), Group( \"Shape Settings\" ), ShowIf( nameof( Shape ), ColliderShape.Box )]\n\tpublic Vector3 BoxSize { get; set; } = new Vector3( 1.25f, 1.25f, 1.25f );\n\n//=========================================================================================================================\n// Collider Gizmo Settings\n//=========================================================================================================================\n\n    /// <summary>\n    /// Displays the chain's collider in the Scene view.\n    /// Useful for visualizing collision shapes and sizes.\n    /// </summary>\n\t[Property, Order( 8 ), Title( \"Collider Gizmo\" ), Group( \"Gizmo Settings\" )]\n\tpublic bool ShowColliderGizmo { get; set; } = true;\n\n    /// <summary>\n    /// Keeps the collider gizmo visible even when the chain is not selected.\n    /// Useful for debugging collision setups.\n    /// </summary>\n\t[Property, Order( 9 ), Title( \"Always Show Gizmo\" ), Group( \"Gizmo Settings\" ), ShowIf( nameof( ShowColliderGizmo ), true )]\n\tpublic bool AlwaysShowGizmo { get; set; }\n\n    /// <summary>\n    /// Color used for rendering the collider gizmo.\n    /// </summary>\n\t[Property, Order( 10 ), Title( \"Collider Gizmo Color\" ), Group( \"Gizmo Settings\" ), ShowIf( nameof( ShowColliderGizmo ), true )]\n\tpublic Color ColliderGizmoColor { get; set; } = Gizmo.Colors.Green;\n\n    /// <summary>\n    /// Controls the transparency of the collider gizmo.\n    /// Lower values make the visualization less intrusive.\n    /// </summary>\n\t[Property, Order( 11 ), Range( 0, 1 ), Title( \"Gizmo Opacity\" ), Group( \"Gizmo Settings\" ), ShowIf( nameof( ShowColliderGizmo ), true )]\n\tpublic float ColliderGizmoOpacity { get; set; } = 1f;\t\n\n//=========================================================================================================================\n// Collider Shape Calculation\n//=========================================================================================================================\n\tpublic bool TryGetWorldShape( SkinnedModelRenderer renderer, out SoftBoneColliderShape shape )\n\t{\n\t\tshape = default;\n\t\tvar boneName = TargetBone is not null && TargetBone.IsValid() ? TargetBone.Name?.Trim() : \"\";\n\t\tif ( string.IsNullOrWhiteSpace( boneName ) )\n\t\t\treturn false;\n\n\t\tvar hasBoneTransform = renderer is not null && renderer.IsValid() && renderer.Model?.Bones is not null && renderer.Model.Bones.HasBone( boneName );\n\t\tif ( hasBoneTransform )\n\t\t{\n\t\t\tvar bone = renderer.Model.Bones.GetBone( boneName );\n\t\t\tif ( renderer.TryGetBoneTransformAnimation( bone, out var animatedTransform ) || renderer.TryGetBoneTransform( bone, out animatedTransform ) )\n\t\t\t\treturn BuildWorldShape( animatedTransform, out shape );\n\t\t}\n\n\t\treturn BuildWorldShape( TargetBone.WorldTransform, out shape );\n\t}\n\n\tprivate bool BuildWorldShape( Transform boneTransform, out SoftBoneColliderShape shape )\n\t{\n\t\tshape = default;\n\n\t\t// Swap pitch/yaw to match S&box bone forward = X axis\n\t\tvar fixedRotOffset = new Angles( RotationOffset.roll, RotationOffset.pitch, RotationOffset.yaw );\n\t\tvar rotation = boneTransform.Rotation * fixedRotOffset.ToRotation();\n\t\tvar radius = Math.Max( ColliderRadius, 0.001f );\n\t\tvar height = Math.Max( ColliderHeight, radius * 2f );\n\t\t// Collider offsets use the existing VRC-style axis order: X=up, Y=forward, Z=right.\n\t\tvar remappedOffset = new Vector3( PositionOffset.y, PositionOffset.z, PositionOffset.x );\n\t\tvar center = boneTransform.PointToWorld( remappedOffset );\n\n\t\tif ( Shape == ColliderShape.Sphere || (Shape == ColliderShape.Capsule && height <= radius * 2f) )\n\t\t{\n\t\t\tshape = SoftBoneColliderShape.Sphere( center, radius );\n\t\t\treturn true;\n\t\t}\n\n\t\tif ( Shape == ColliderShape.Capsule )\n\t\t{\n\t\t\tvar halfLine = Math.Max( 0f, height * 0.5f - radius );\n\t\t\t// VRC capsules are authored along their local Up axis. Using Forward here\n\t\t\t// made S&box Roll rotate around the capsule's own length, which is invisible.\n\t\t\tvar axis = rotation * Vector3.Forward;\n\t\t\tshape = SoftBoneColliderShape.Capsule( center - axis * halfLine, center + axis * halfLine, radius );\n\t\t}\n\t\telse if ( Shape == ColliderShape.Box )\n\t\t\tshape = SoftBoneColliderShape.Box( center, rotation, new Vector3( Math.Max( BoxSize.z, 0.001f ), Math.Max( BoxSize.x, 0.001f ), Math.Max( BoxSize.y, 0.001f ) ) );\n\t\telse\n\t\t\tshape = SoftBoneColliderShape.Flat( center, rotation * Vector3.Up );\n\t\treturn true;\n\t}\n\n//=========================================================================================================================\n// Collider Gizmo Drawing\n//=========================================================================================================================\n\n\tprotected override void DrawGizmos()\n\t{\n\t\tbase.DrawGizmos();\n\t\tDrawColliderGizmo( AlwaysShowGizmo || Gizmo.IsSelected );\n\t}\n\n\tinternal void DrawColliderGizmo( bool visible, bool force = false )\n\t{\n\t\tif ( !visible || ( !force && !ShowColliderGizmo ) || !TryGetWorldShape( FindRenderer(), out var shape ) )\n\t\t\treturn;\n\n\t\t// Draw directly in world space so the sphere radius is never distorted by\n\t\t// the component's scale (which caused the squished ellipsoid).\n\t\tusing ( Gizmo.Scope( \"SoftBoneCollider\" ) )\n\t\t{\n\t\t\tGizmo.Transform = new Transform( Vector3.Zero, Rotation.Identity, 1f );\n\t\t\tGizmo.Draw.IgnoreDepth = true;\n\t\t\tGizmo.Draw.LineThickness = 1;\n\t\t\tGizmo.Draw.Color = ColliderGizmoColor.WithAlpha( ColliderGizmoOpacity );\n\n\t\t\tif ( shape.IsSphere )\n\t\t\t\tGizmo.Draw.LineSphere( new Sphere( shape.Start, shape.Radius ) );\n\t\t\telse if ( shape.Type == SoftBoneColliderShapeType.Capsule )\n\t\t\t\tGizmo.Draw.LineCapsule( new Capsule( shape.Start, shape.End, shape.Radius ) );\n\t\t\telse if ( shape.Type == SoftBoneColliderShapeType.Box )\n\t\t\t{\n\t\t\t\tGizmo.Transform = new Transform( shape.Center, shape.Rotation, 1f );\n\t\t\t\tGizmo.Draw.LineBBox( BBox.FromPositionAndSize( Vector3.Zero, shape.Size ) );\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tvar tangent = Vector3.Cross( shape.Normal, Math.Abs( Vector3.Dot( shape.Normal, Vector3.Up ) ) < 0.99f ? Vector3.Up : Vector3.Right ).Normal;\n\t\t\t\tvar bitangent = Vector3.Cross( shape.Normal, tangent ).Normal;\n\t\t\t\tvar size = ColliderRadius;\n\t\t\t\tvar a = shape.Center + (tangent + bitangent) * size;\n\t\t\t\tvar b = shape.Center + (tangent - bitangent) * size;\n\t\t\t\tvar c = shape.Center + (-tangent - bitangent) * size;\n\t\t\t\tvar d = shape.Center + (-tangent + bitangent) * size;\n\t\t\t\tGizmo.Draw.Line( a, b ); Gizmo.Draw.Line( b, c ); Gizmo.Draw.Line( c, d ); Gizmo.Draw.Line( d, a );\n\t\t\t\tGizmo.Draw.Line( shape.Center, shape.Center + shape.Normal * 8f );\n\t\t\t}\n\t\t}\n\t}\n\n//=========================================================================================================================\n// Collider Bone Lookup\n//=========================================================================================================================\n\n\tprivate SkinnedModelRenderer FindRenderer()\n\t{\n\t\tvar targetRenderer = TargetBone?.GetComponentInParent<SkinnedModelRenderer>( true, true );\n\t\tif ( targetRenderer is not null && targetRenderer.IsValid() )\n\t\t\treturn targetRenderer;\n\t\treturn null;\n\t}\n\n\tinternal SkinnedModelRenderer GetOwningRenderer() => FindRenderer();\n\n\tprotected override void OnValidate()\n\t{\n\t\tColliderRadius = Math.Max( ColliderRadius, 0.001f );\n\t\tColliderHeight = Math.Max( ColliderHeight, ColliderRadius * 2f );\n\t\tBoxSize = new Vector3( Math.Max( BoxSize.x, 0.001f ), Math.Max( BoxSize.y, 0.001f ), Math.Max( BoxSize.z, 0.001f ) );\n\t\tColliderGizmoOpacity = Math.Clamp( ColliderGizmoOpacity, 0f, 1f );\n\t}\n}"
        },
        {
            "Ident": "bobicolab.bobicosoftbones",
            "Path": "BoBiCo Soft-Physics/Components/BoBiCoSoftBoneColliderGroup.cs",
            "FileName": "BoBiCoSoftBoneColliderGroup.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 336846,
            "IsPrivate": false,
            "Code": "//=========================================================================================================================\n// BoBiCo Soft-Bone Collider Group Component\n//=========================================================================================================================\n\nusing System.Collections.Generic;\n\n[Title( \"SoftBone Collider Group\" )]\n[Category( \"BoBiCo\" )]\n[Icon( \"category\" )]\n[HelpUrl( \"https://bobico-lab.vercel.app/softbone-docs/bobico-softbones/core-components/softbone-collider-group\" )]\npublic sealed class BoBiCoSoftBoneColliderGroup : Component, Component.ExecuteInEditor\n{\n\n//=========================================================================================================================\n// Group Setup\n//=========================================================================================================================\n\n\t/// <summary>\n\t/// List of colliders that are part of this group. These colliders will be used for collision detection with soft-bone chains.\n\t/// </summary>\n\t[Property, Title( \"Colliders\" ), Group( \"Colliders\" )]\n\tpublic List<GameObject> Colliders { get; set; } = new();\n\n    // Collect all valid colliders from the group and add them to the output list, avoiding duplicates using the seen set.\n\tpublic void CollectColliders( List<BoBiCoSoftBoneCollider> output, HashSet<BoBiCoSoftBoneCollider> seen )\n\t{\n\t\tif ( !Enabled || Colliders is null )\n\t\t\treturn;\n\n\t\tforeach ( var colliderObject in Colliders )\n\t\t{\n\t\t\tif ( colliderObject is null || !colliderObject.IsValid() )\n\t\t\t\tcontinue;\n\n\t\t\tvar collider = colliderObject.GetComponent<BoBiCoSoftBoneCollider>();\n\t\t\tif ( collider is not null && collider.Enabled && seen.Add( collider ) )\n\t\t\t\toutput.Add( collider );\n\t\t}\n\t}\n}\n"
        },
        {
            "Ident": "bobicolab.bobicosoftbones",
            "Path": "BoBiCo Soft-Physics/SoftBoneJointState.cs",
            "FileName": "SoftBoneJointState.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 336846,
            "IsPrivate": false,
            "Code": "using System;\n\n/// <summary>Runtime state for one simulated SoftBone joint.</summary>\npublic sealed class SoftBoneJointState\n{\n\tpublic BoneCollection.Bone Bone;\n\n\t// Rest-pose data\n\tpublic float Length;\n\tpublic Vector3 LocalPoseVector;\n\tpublic Transform RestTransformLocal;\n\tpublic Vector3 OriginalLocalGravityNormal;\n\n\t// Per-frame state. Positions are renderer-local unless suffixed with World.\n\tpublic Vector3 PreviousBegin;\n\tpublic Vector3 PreviousEnd;\n\tpublic Vector3 PreviousAnimatedBegin;\n\tpublic Rotation PreviousAnimatedRotation;\n\tpublic Rotation PreviousSimulationRotation;\n\tpublic Vector3 ImmobileEnd;\n\tpublic Vector3 PreviousEndWorld;\n\tpublic Vector3 PreviousVector;\n\tpublic Vector3 PreviousVelocity;\n\tpublic bool HasHistory;\n}\n"
        },
        {
            "Ident": "bobicolab.bobicosoftbones",
            "Path": "Code/BoBiCo Soft-Physics/SoftBoneAssembly.cs",
            "FileName": "SoftBoneAssembly.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 336846,
            "IsPrivate": false,
            "Code": "// I don't know, I kept these here to avoid random errors.\r\n\r\nglobal using Sandbox;\r\nglobal using System.Collections.Generic;\r\nglobal using System.Linq;"
        },
        {
            "Ident": "bobicolab.bobicosoftbones",
            "Path": "BoBiCo Soft-Physics/SoftBoneSkinnedModelUtility.cs",
            "FileName": "SoftBoneSkinnedModelUtility.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 336846,
            "IsPrivate": false,
            "Code": "using System;\n\npublic static class SoftBoneSkinnedModelUtility\n{\n\tprivate const float Epsilon = 0.000008f;\n\n\tpublic static bool TryGetBoneAnimationLocal( SkinnedModelRenderer renderer, BoneCollection.Bone bone, out Transform transform )\n\t{\n\t\ttransform = default;\n\n\t\tif ( renderer is null || !renderer.IsValid() || bone is null )\n\t\t\treturn false;\n\n\t\tif ( renderer.TryGetBoneTransformAnimation( bone, out var worldTransform ) )\n\t\t{\n\t\t\ttransform = renderer.WorldTransform.ToLocal( worldTransform );\n\t\t\treturn true;\n\t\t}\n\n\t\tif ( renderer.TryGetBoneTransform( bone, out worldTransform ) )\n\t\t{\n\t\t\ttransform = renderer.WorldTransform.ToLocal( worldTransform );\n\t\t\treturn true;\n\t\t}\n\n\t\treturn false;\n\t}\n\n\tpublic static Vector3 ToWorld( SkinnedModelRenderer renderer, Vector3 localPosition )\n\t{\n\t\tif ( renderer is null || !renderer.IsValid() )\n\t\t\treturn localPosition;\n\n\t\treturn renderer.WorldTransform.PointToWorld( localPosition );\n\t}\n\n\tpublic static Vector3 ToLocal( SkinnedModelRenderer renderer, Vector3 worldPosition )\n\t{\n\t\tif ( renderer is null || !renderer.IsValid() )\n\t\t\treturn worldPosition;\n\n\t\treturn renderer.WorldTransform.PointToLocal( worldPosition );\n\t}\n\n\tpublic static Vector3 SafeNormal( Vector3 value, Vector3 fallback )\n\t{\n\t\tif ( value.Length <= Epsilon )\n\t\t\treturn fallback.Length > Epsilon ? fallback.Normal : Vector3.Forward;\n\n\t\treturn value.Normal;\n\t}\n}\n"
        },
        {
            "Ident": "bobicolab.bobicosoftbones",
            "Path": "Code/BoBiCo Soft-Physics/Components/BoBiCoSoftBoneChain.cs",
            "FileName": "BoBiCoSoftBoneChain.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 336846,
            "IsPrivate": false,
            "Code": "//=========================================================================================================================\n// BoBiCo Soft-bone Chain Component\n//=========================================================================================================================\n\nusing System;\nusing System.Collections.Generic;\n\npublic enum SoftBoneLinkColliderShape\n{\n\t/// <summary>\n\t/// A simple spherical collider.\n\t/// Best for rounded body parts and lightweight collision detection.\n\t/// </summary>\n\tSphere,\n\n\t/// <summary>\n\t/// A capsule-shaped collider.\n\t/// Recommended for most bones and elongated body parts.\n\t/// </summary>\n\tCapsule,\n\n\t/// <summary>\n\t/// A box-shaped collider.\n\t/// Useful for broad collision areas and block-like shapes.\n\t/// </summary>\n\tBox,\n\n\t/// <summary>\n\t/// A one-sided flat collider.\n\t/// Useful for flat surface areas.\n\t/// </summary>\n\tFlat,\n}\n\npublic enum SoftBoneCollisionMode\n{\n\t/// <summary>\n\t/// Disables all collision detection for this chain.\n\t/// </summary>\n\tNone,\n\n\t/// <summary>\n\t/// Collides only with body colliders.\n\t/// This is the recommended mode for most setups.\n\t/// </summary>\n\tBaseCollision,\n\n\t/// <summary>\n\t/// Collides with body colliders and collider chains from the same model.\n\t/// </summary>\n\tSelfCollision,\n\n\t/// <summary>\n\t/// Collides with body colliders and collider chains from all SoftBone-enabled models.\n\t/// </summary>\n\tFullCollision\n}\n\n[Title( \"SoftBone Chain\" )]\n[Category( \"BoBiCo\" )]\n[Icon( \"linear_scale\" )]\n[HelpUrl( \"https://bobico-lab.vercel.app/softbone-docs/bobico-softbones/core-components/softbone-chain\" )]\npublic sealed class BoBiCoSoftBoneChain : Component, Component.ExecuteInEditor\n{\n\t// prevents unstable math when values are extremely close to zero.\n\tprivate const float Epsilon = 0.000008f;\n\n\tprivate GameObject _targetBone;\n\tprivate SoftBoneControlMode _controlMode;\n\tprivate bool _curveDefaultsInitialized;\n\n//=========================================================================================================================\n// Chain Properties Section\n//=========================================================================================================================\n\n\t/// <summary>\n\t/// Defines the root bone of the chain that'll be simulated.\n\t/// </summary>\n\t[Property, Order( 1 ), Title( \"Target Bone\" ), Group( \"Chain Properties\" )]\n\tpublic GameObject TargetBone\n\t{\n\t\tget => _targetBone;\n\t\tset\n\t\t{\n\t\t\tif ( _targetBone == value )\n\t\t\t\treturn;\n\n\t\t\t_targetBone = value;\n\t\t\tUpdateOwnerName();\n\t\t\tMarkDirty();\n\t\t}\n\t}\n\n\t/// <summary>\n\t/// Rotates the chain in local space.\n\t/// </summary>\n\t[Property, Order( 2 ), Title( \"Rotation Offset\" ), Group( \"Chain Properties\" )]\n\tpublic Angles RotationOffset { get; set; }\n\n\t/// <summary>\n\t/// Control mode for the SoftBone physics parameters. \n\t/// Slider mode uses a single value for each parameter, \n\t/// Range mode uses a min/max range for each parameter, \n\t/// and Curve mode uses a curve to define the parameter values along the chain.\n\t/// </summary>\n\t[Property, Order( 3 ), Title( \"Control Mode\" ), Group( \"Chain Properties\" )]\n\tpublic SoftBoneControlMode ControlMode\n\t{\n\t\tget => _controlMode;\n\t\tset\n\t\t{\n\t\t\tif ( _controlMode == value ) return;\n\t\t\t_controlMode = value;\n\t\t\tif ( value == SoftBoneControlMode.Curve && !_curveDefaultsInitialized ) InitializeCurveDefaults();\n\t\t}\n\t}\n\n\tpublic enum SoftBoneControlMode\n    {\n        Slider,\n        Range,\n        Curve\n    }\n\n\t/// <summary>\n\t/// When enabled, the root bone itself is also physically simulated\n\t/// (wobbles independently). When disabled, only child bones simulate\n\t/// and the root follows animation exactly.\n\t/// </summary>\n\t[Property, Order( 4 ), Title( \"Simulate Root\" ), Group( \"Chain Properties\" )]\n\tpublic bool SimulateRoot { get; set; } = false;\n\n\t/// <summary>\n\t/// When enabled, only the first X chain bones are simulated.\n\t/// </summary>\n\t[Property, Order( 5 ), Title( \"Limit Chain Length\" ), Group( \"Chain Properties\" )]\n\tpublic bool LimitChainLength { get; set; } = false;\n\n\t/// <summary>\n\t/// Limit the amount of chain bones from tip to root that are simulated when Limit Children is enabled.\n\t/// </summary>\n\t[Property, Order( 6 ), Title( \"Chain Length Limit\" ), Group( \"Chain Properties\" ), ShowIf( nameof( LimitChainLength ), true )]\n\tpublic int ChainLimit { get; set; } = 0;\n\n//=========================================================================================================================\n// Physics Settings\n//=========================================================================================================================\n\n\t/// <summary>\t\n\t/// Controls how strongly the chain returns toward its animated rest pose.\n\t/// Higher values reduce swaying and make the chain settle more quickly.\n\t/// </summary>\n\t[Property, Order( 8 ), Range( 0, 1 ), Title( \"Drag\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Slider )]\n\tpublic float Pull { get; set; } = 0.20f;\n\n\t/// <summary>\t\n\t/// Controls how strongly the chain returns toward its animated rest pose.\n\t/// Higher values reduce swaying and make the chain settle more quickly.\n\t/// </summary>\n\t[Property, Order( 8 ), Range( 0, 1 ), Title( \"Drag\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Range )]\n\tpublic Vector2 PullRange { get; set; } = new( 0.20f, 0.20f );\n\n\t/// <summary>\t\n\t/// Controls how strongly the chain returns toward its animated rest pose.\n\t/// Higher values reduce swaying and make the chain settle more quickly.\n\t/// </summary>\n\t[Property, Order( 8 ), Title( \"Drag\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Curve )]\n\tpublic Curve PullCurve { get; set; } = ConstantCurve( 0.20f );\n\n\t/// <summary>\n\t/// Controls how much spring force is applied to the chain.\n\t/// Higher values create softer, bouncier motion, while lower values produce more stable movement.\n\t/// </summary>\n\t[Property, Order( 9 ), Range( 0, 1 ), Title( \"Spring\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Slider )]\n\tpublic float Spring { get; set; } = 0.50f;\n\n\t/// <summary>\n\t/// Controls how much spring force is applied to the chain.\n\t/// Higher values create softer, bouncier motion, while lower values produce more stable movement.\n\t/// </summary>\n\t[Property, Order( 9 ), Range( 0, 1 ), Title( \"Spring\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Range )]\n\tpublic Vector2 SpringRange { get; set; } = new( 0.50f, 0.50f );\n\n\t/// <summary>\n\t/// Controls how much spring force is applied to the chain.\n\t/// Higher values create softer, bouncier motion, while lower values produce more stable movement.\n\t/// </summary>\n\t[Property, Order( 9 ), Title( \"Spring\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Curve )]\n\tpublic Curve SpringCurve { get; set; } = ConstantCurve( 0.50f );\n\n\n\t/// <summary>\n\t/// Controls how resistant the chain is to bending.\n\t/// Higher values make the chain feel firmer, while lower values allow more flexibility.\n\t/// </summary>\n\t[Property, Order( 10 ), Range( 0, 1 ), Title( \"Stiffness\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Slider )]\n\tpublic float Stiffness { get; set; } = 0.30f;\n\n\t/// <summary>\n\t/// Controls how resistant the chain is to bending.\n\t/// Higher values make the chain feel firmer, while lower values allow more flexibility.\n\t/// </summary>\t\n\t[Property, Order( 10 ), Range( 0, 1 ), Title( \"Stiffness\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Range )]\n\tpublic Vector2 StiffnessRange { get; set; } = new( 0.30f, 0.30f );\n\n\t/// <summary>\n\t/// Controls how resistant the chain is to bending.\n\t/// Higher values make the chain feel firmer, while lower values allow more flexibility.\n\t/// </summary>\n\t[Property, Order( 10 ), Title( \"Stiffness\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Curve )]\n\tpublic Curve StiffnessCurve { get; set; } = ConstantCurve( 0.30f );\n\n\t/// <summary>\n\t/// Applies a constant directional force to the chain.\n\t/// Positive values pull downward, while negative values pull upward.\n\t/// </summary>\n\t[Property, Order( 11 ), Range( -1, 1 ), Title( \"Gravity\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Slider )]\n\tpublic float Gravity { get; set; }\n\n\t/// <summary>\n\t/// Applies a constant directional force to the chain.\n\t/// Positive values pull downward, while negative values pull upward.Positive values pull the chain down, negative values pull the chain up.\n\t/// </summary>\n\t[Property, Order( 11 ), Range( 0, 1 ), Title( \"Gravity\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Range )]\n\tpublic Vector2 GravityRange { get; set; }\n\n\t/// <summary>\n\t/// Applies a constant directional force to the chain.\n\t/// Positive values pull downward, while negative values pull upward.Positive values pull the chain down, negative values pull the chain up.\n\t/// </summary>\n\t[Property, Order( 11 ), Title( \"Gravity\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Curve )]\n\tpublic Curve GravityCurve { get; set; } = ConstantCurve( 0f );\n\n\t/// <summary>\n\t/// Reduces gravity while the chain is near its animated rest pose.\n\t/// Higher values keep idle chains more stable while preserving gravity during motion.\n\t/// </summary>\n\t[Property, Order( 12 ), Range( 0, 1 ), Title( \"Gravity Falloff\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Slider )]\n\tpublic float GravityFalloff { get; set; }\n\n\t/// <summary>\n\t/// Reduces gravity while the chain is near its animated rest pose.\n\t/// Higher values keep idle chains more stable while preserving gravity during motion.\n\t/// </summary>\n\t[Property, Order( 12 ), Range( 0, 1 ), Title( \"Gravity Falloff\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Range )]\n\tpublic Vector2 GravityFalloffRange { get; set; }\n\n\t/// <summary>\n\t/// Reduces gravity while the chain is near its animated rest pose.\n\t/// Higher values keep idle chains more stable while preserving gravity during motion.\n\t/// </summary>\n\t[Property, Order( 12 ), Title( \"Gravity Falloff\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Curve )]\n\tpublic Curve GravityFalloffCurve { get; set; } = ConstantCurve( 0f );\n\n\n\t/// <summary>\n\t/// Controls how much the chain can extend beyond its rest length.\n\t/// Higher values allow longer, softer stretching under force.\n\t/// </summary>\n\t[Property, Order( 13 ), Range( 0, 2 ), Title( \"Stretch\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Slider )]\n\tpublic float Stretch { get; set; }\n\n\t/// <summary>\n\t/// Controls how much the chain can extend beyond its rest length.\n\t/// Higher values allow longer, softer stretching under force.\n\t/// </summary>\n\t[Property, Order( 13 ), Range( 0, 2 ), Title( \"Stretch\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Range )]\n\tpublic Vector2 StretchRange { get; set; }\n\n\t/// <summary>\n\t/// Controls how much the chain can extend beyond its rest length.\n\t/// Higher values allow longer, softer stretching under force.\n\t/// </summary>\n\t[Property, Order( 13 ), Title( \"Stretch\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Curve )]\n\tpublic Curve StretchCurve { get; set; } = ConstantCurve( 0f );\n\n\n\t/// <summary>\n\t/// Controls how much the chain can compress below its rest length.\n\t/// Higher values allow greater compression.\n\t/// </summary>\n\t[Property, Order( 14 ), Range( 0, 1 ), Title( \"Squish\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Slider )]\n\tpublic float Squish { get; set; }\n\n\t/// <summary>\n\t/// Controls how much the chain can compress below its rest length.\n\t/// Higher values allow greater compression.\n\t/// </summary>\n\t[Property, Order( 14 ), Range( 0, 1 ), Title( \"Squish\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Range )]\n\tpublic Vector2 SquishRange { get; set; }\n\n\t/// <summary>\n\t/// Controls how much the chain can compress below its rest length.\n\t/// Higher values allow greater compression.\n\t/// </summary>\n\t[Property, Order( 14 ), Title( \"Squish\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Curve )]\n\tpublic Curve SquishCurve { get; set; } = ConstantCurve( 0f );\n\n\t/// <summary>\n\t/// Reduces movement caused by the character's own motion.\n\t/// Higher values make the chain feel heavier and less reactive while the character moves.\n\t/// </summary>\n\t[Property, Order( 15 ), Range( 0, 1 ), Title( \"Movement Resistance\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Slider )]\n\tpublic float Immobile { get; set; } = 1.00f;\n\n\t/// <summary>\n\t/// Reduces movement caused by the character's own motion.\n\t/// Higher values make the chain feel heavier and less reactive while the character moves.\n\t/// </summary>\n\t[Property, Order( 15 ), Range( 0, 1 ), Title( \"Movement Resistance\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Range )]\n\tpublic Vector2 ImmobileRange { get; set; } = new( 1.00f, 1.00f );\n\n\t/// <summary>\n\t/// Reduces movement caused by the character's own motion.\n\t/// Higher values make the chain feel heavier and less reactive while the character moves.\n\t/// </summary>\n\t[Property, Order( 15 ), Title( \"Movement Resistance\" ), Group( \"Physics Settings\" ), ShowIf( nameof( ControlMode ), SoftBoneControlMode.Curve )]\n\tpublic Curve ImmobileCurve { get; set; } = ConstantCurve( 1.00f );\n\n//=========================================================================================================================\n// Limit Settings Section\n//=========================================================================================================================\n\n    /// <summary>\n    /// Defines how the chain's movement is constrained.\n    /// Different limit types provide different ways of restricting bone rotation.\n    /// </summary>\n\t[Property, Order( 16 ), Title( \"Limit Type\" ), Group( \"Limit Settings\" )]\n\tpublic SoftBoneLimitType LimitType { get; set; } = SoftBoneLimitType.Cone;\n\n    /// <summary>\n    /// Maximum angle the chain can rotate away from its rest direction.\n    /// Higher values allow greater freedom of movement.\n    /// </summary>\n\t[Property, Order( 17 ), Range( 0, 180 ), Title( \"Max Angle\" ), Group( \"Limit Settings\" ), ShowIf( nameof( ShowMaxAngleSlider ), true )]\n\tpublic float MaxAngle { get; set; } = 95f;\n\n\t/// <summary>\n    /// Maximum angle the chain can rotate away from its rest direction.\n    /// Higher values allow greater freedom of movement.\n    /// </summary>\n\t[Property, Order( 17 ), Range( 0, 1 ), Title( \"Max Angle\" ), Group( \"Limit Settings\" ), ShowIf( nameof( ShowMaxAngleRange ), true )]\n\tpublic Vector2 MaxAngleRange { get; set; } = new( 95f, 95f );\n\n\t/// <summary>\n    /// Maximum angle the chain can rotate away from its rest direction.\n    /// Higher values allow greater freedom of movement.\n    /// </summary>\n\t[Property, Order( 17 ), Title( \"Max Angle\" ), Group( \"Limit Settings\" ), ShowIf( nameof( ShowMaxAngleCurve ), true )]\n\tpublic Curve MaxAngleCurve { get; set; } = ConstantCurve( 95f / 180f );\n\tpublic bool UsesMaxAngle => LimitType == SoftBoneLimitType.Cone || LimitType == SoftBoneLimitType.Plane;\n\tpublic bool ShowMaxAngleSlider => UsesMaxAngle && ControlMode == SoftBoneControlMode.Slider;\n\tpublic bool ShowMaxAngleRange => UsesMaxAngle && ControlMode == SoftBoneControlMode.Range;\n\tpublic bool ShowMaxAngleCurve => UsesMaxAngle && ControlMode == SoftBoneControlMode.Curve;\n\n    /// <summary>\n    /// Maximum horizontal rotation allowed for Ellipse limits.\n    /// Increase to allow more side-to-side movement.\n    /// </summary>\n\t[Property, Order( 18 ), Range( 0, 180 ), Title( \"Max Yaw\" ), Group( \"Limit Settings\" ), ShowIf( nameof( ShowMaxYawSlider ), true )]\n\tpublic float MaxYaw { get; set; } = 95f;\n\n\t/// <summary>\n    /// Maximum horizontal rotation allowed for Ellipse limits.\n    /// Increase to allow more side-to-side movement.\n    /// </summary>\n\t[Property, Order( 18 ), Range( 0, 180 ), Title( \"Max Yaw\" ), Group( \"Limit Settings\" ), ShowIf( nameof( ShowMaxYawRange ), true )]\n\tpublic Vector2 MaxYawRange { get; set; } = new( 95f, 95f );\n\n    /// <summary>\n    /// Maximum horizontal rotation allowed for Ellipse limits.\n    /// Increase to allow more side-to-side movement.\n    /// </summary>\n\t[Property, Order( 18 ), Title( \"Max Yaw\" ), Group( \"Limit Settings\" ), ShowIf( nameof( ShowMaxYawCurve ), true )]\n\tpublic Curve MaxYawCurve { get; set; } = ConstantCurve( 95f / 180f );\n\tpublic bool ShowMaxYawSlider => LimitType == SoftBoneLimitType.Ellipse && ControlMode == SoftBoneControlMode.Slider;\n\tpublic bool ShowMaxYawRange => LimitType == SoftBoneLimitType.Ellipse && ControlMode == SoftBoneControlMode.Range;\n\tpublic bool ShowMaxYawCurve => LimitType == SoftBoneLimitType.Ellipse && ControlMode == SoftBoneControlMode.Curve;\n\n\n    /// <summary>\n    /// Maximum vertical rotation allowed for Ellipse limits.\n    /// Increase to allow more up-and-down movement.\n    /// </summary>\n\t[Property, Order( 19 ), Range( 0, 180 ), Title( \"Max Pitch\" ), Group( \"Limit Settings\" ), ShowIf( nameof( ShowMaxPitchSlider ), true )]\n\tpublic float MaxPitch { get; set; } = 95f;\n\n    /// <summary>\n    /// Maximum vertical rotation allowed for Ellipse limits.\n    /// Increase to allow more up-and-down movement.\n    /// </summary>\n\t[Property, Order( 19 ), Range( 0, 180 ), Title( \"Max Pitch\" ), Group( \"Limit Settings\" ), ShowIf( nameof( ShowMaxPitchRange ), true )]\n\tpublic Vector2 MaxPitchRange { get; set; } = new( 95f, 95f );\n\n    /// <summary>\n    /// Maximum vertical rotation allowed for Ellipse limits.\n    /// Increase to allow more up-and-down movement.\n    /// </summary>\n\t[Property, Order( 19 ), Title( \"Max Pitch\" ), Group( \"Limit Settings\" ), ShowIf( nameof( ShowMaxPitchCurve ), true )]\n\tpublic Curve MaxPitchCurve { get; set; } = ConstantCurve( 95f / 180f );\n\tpublic bool ShowMaxPitchSlider => LimitType == SoftBoneLimitType.Ellipse && ControlMode == SoftBoneControlMode.Slider;\n\tpublic bool ShowMaxPitchRange => LimitType == SoftBoneLimitType.Ellipse && ControlMode == SoftBoneControlMode.Range;\n\tpublic bool ShowMaxPitchCurve => ControlMode == SoftBoneControlMode.Curve && LimitType == SoftBoneLimitType.Ellipse;\n\n\n    /// <summary>\n    /// Rotates the limit shape relative to the chain.\n    /// </summary>\n\t[Property, Order( 20 ), Title( \"Limit Rotation\" ), Group( \"Limit Settings\" ), HideIf( nameof( LimitType ), SoftBoneLimitType.None )]\n\tpublic Angles LimitRotation { get; set; }\n\n    /// <summary>\n    /// Shape used for this chain's collision volume.\n    /// Different shapes provide different collision behavior and coverage.\n    /// </summary>\n\t[Property, Order( 21 ), Title( \"Collider Shape\" ), Group( \"Collision Settings\" )]\n\tpublic SoftBoneLinkColliderShape ColliderShape { get; set; } = SoftBoneLinkColliderShape.Capsule;\n\n    /// <summary>\n    /// Controls which colliders this chain can interact with.\n    /// Higher modes enable more realistic but more expensive collision behavior.\n    /// </summary>\n\t[Property, Order( 22 ), Title( \"Collision Mode\" ), Group( \"Collision Settings\" )]\n\tpublic SoftBoneCollisionMode CollisionMode { get; set; } = SoftBoneCollisionMode.BaseCollision;\n\n\t/// <summary>\n\t/// Assign one or more SoftBone Collider Groups for this chain to interact with.\n\t/// Colliders outside of these groups will be ignored.\n\t/// </summary>\n\t[Property, Order( 23 ), Title( \"Collider Groups\" ), Group( \"Collision Settings\" )]\n\tpublic List<GameObject> ColliderGroups { get; set; } = new();\n\n    /// <summary>\n    /// Allows the colliders to collide with the root collider.\n    /// Disable if the root should remain free.\n    /// </summary>\n\t[Property, Order( 24 ), Title( \"Collide With Root\" ), Group( \"Collision Settings\" )]\n\tpublic bool CollideWithRoot { get; set; }\n\n\t// Helpers for conditional property visibility\n\tprivate bool ShowRadiusSlider => ControlMode == SoftBoneControlMode.Slider && ColliderShape != SoftBoneLinkColliderShape.Box;\n\tprivate bool ShowRadiusRange  => ControlMode == SoftBoneControlMode.Range  && ColliderShape != SoftBoneLinkColliderShape.Box;\n\tprivate bool ShowRadiusCurve  => ControlMode == SoftBoneControlMode.Curve  && ColliderShape != SoftBoneLinkColliderShape.Box;\n\n    /// <summary>\n    /// Radius of the chain's collision volume.\n    /// Larger values create thicker collision boundaries.\n    /// </summary>\n\t[Property, Order( 25 ), Title( \"Collider Radius\" ), Group( \"Collision Settings\" ), ShowIf( nameof( ShowRadiusSlider ), true )]\n\tpublic float ColliderRadius { get; set; } = 0.5f;\n\n    /// <summary>\n    /// Radius of the chain's collision volume.\n    /// Larger values create thicker collision boundaries.\n    /// </summary>\n\t[Property, Order( 25 ), Title( \"Collider Radius\" ), Group( \"Collision Settings\" ), ShowIf( nameof( ShowRadiusRange ), true )]\n\tpublic Vector2 ColliderRadiusRange { get; set; } = new( 0.5f, 0.5f );\n\n    /// <summary>\n    /// Radius of the chain's collision volume.\n    /// Larger values create thicker collision boundaries.\n    /// </summary>\n\t[Property, Order( 25 ), Title( \"Collider Radius\" ), Group( \"Collision Settings\" ), ShowIf( nameof( ShowRadiusCurve ), true )]\n\tpublic Curve ColliderRadiusCurve { get; set; } = ConstantCurve( 0.5f );\n\n    /// <summary>\n    /// Height of Capsule collider.\n\t/// Available only when the collider shape is set to Capsule.\n    /// </summary>\n\t[Property, Order( 26 ), Title( \"Collider Height\" ), Group( \"Collision Settings\" ), ShowIf( nameof( ColliderShape ), SoftBoneLinkColliderShape.Capsule )]\n\tpublic float ColliderHeight { get; set; } = 1.25f;\n\n    /// <summary>\n    /// Defines the dimensions of the box collider.\n    /// Available only when the collider shape is set to Box.\n    /// </summary>\n\t[Property, Order( 27 ), Title( \"Box Dimensions\" ), Group( \"Collision Settings\" ), ShowIf( nameof( ColliderShape ), SoftBoneLinkColliderShape.Box )]\n\tpublic Vector3 BoxDimension { get; set; } = new Vector3( 1.25f, 1.25f, 1.25f );\n\n//=========================================================================================================================\n// Gizmo Setting Section\n//=========================================================================================================================\n\n    /// <summary>\n    /// Displays the chain's angle limit in the Scene view.\n    /// Useful for visualizing and adjusting movement constraints.\n    /// </summary>\n\t[Property, Order( 28 ), Header( \"Limit Gizmo\" ), Title( \"Limit Gizmo\" ), Group( \"Gizmo Settings\" )]\n\tpublic bool ShowLimitGizmo { get; set; } = true;\n\n    /// <summary>\n    /// Keeps the limit gizmo visible even when the chain is not selected.\n    /// Useful for debugging multiple chains at once.\n    /// </summary>\n\t[Property, Order( 29 ), Title( \"Always Show Gizmo\" ), Group( \"Gizmo Settings\" ), ShowIf( nameof( ShowLimitGizmo ), true )]\n\tpublic bool AlwaysShowLimitGizmo { get; set; }\n\n    /// <summary>\n    /// Color used for rendering the limit gizmo.\n    /// </summary>\n\t[Property, Order( 30 ), Title( \"Limit Gizmo Color\" ), Group( \"Gizmo Settings\" ), ShowIf( nameof( ShowLimitGizmo ), true )]\n\tpublic Color LimitGizmoColor { get; set; } = Color.Orange;\n\n    /// <summary>\n    /// Controls the transparency of the limit gizmo.\n    /// </summary>\n\t[Property, Order( 31 ), Range( 0, 1 ), Title( \"Gizmo Opacity\" ), Group( \"Gizmo Settings\" ), ShowIf( nameof( ShowLimitGizmo ), true )]\n\tpublic float LimitGizmoOpacity { get; set; } = 1f;\n\n    /// <summary>\n    /// Controls the size of the limit gizmo in the Scene view.\n    /// Adjust if the visualization appears too large or too small.\n    /// </summary>\n\t[Property, Order( 32 ), Range( 0.1f, 1f ), Title( \"Limit Gizmo Size\" ), Group( \"Gizmo Settings\" ), ShowIf( nameof( ShowLimitGizmo ), true )]\n\tpublic float LimitGizmoSize { get; set; } = 0.5f;\n\n    /// <summary>\n    /// Displays the chain's collider in the Scene view.\n    /// Useful for visualizing collision shapes and sizes.\n    /// </summary>\n\t[Property, Order( 33 ), Header( \"Collider Gizmo\" ), Title( \"Collider Gizmo\" ), Group( \"Gizmo Settings\" )]\n\tpublic bool ShowColliderGizmo { get; set; } = true;\n\n    /// <summary>\n    /// Keeps the collider gizmo visible even when the chain is not selected.\n    /// Useful for debugging collision setups.\n    /// </summary>\n\t[Property, Order( 34 ), Title( \"Always Show Gizmo\" ), Group( \"Gizmo Settings\" ), ShowIf( nameof( ShowColliderGizmo ), true )]\n\tpublic bool AlwaysShowGizmo { get; set; }\n\n    /// <summary>\n    /// Color used for rendering the collider gizmo.\n    /// </summary>\n\t[Property, Order( 35 ), Title( \"Collider Gizmo Color\" ), Group( \"Gizmo Settings\" ), ShowIf( nameof( ShowColliderGizmo ), true )]\n\tpublic Color ColliderGizmoColor { get; set; } = Gizmo.Colors.Blue;\n\n    /// <summary>\n    /// Controls the transparency of the collider gizmo.\n    /// </summary>\n\t[Property, Order( 36 ), Range( 0, 1 ), Title( \"Gizmo Opacity\" ), Group( \"Gizmo Settings\" ), ShowIf( nameof( ShowColliderGizmo ), true )]\n\tpublic float ColliderGizmoOpacity { get; set; } = 1f;\n\n//=========================================================================================================================\n// Private States\n//=========================================================================================================================\n\n\tprivate readonly List<SoftBoneJointState> _joints = new();\n\tprivate readonly List<BoBiCoSoftBoneCollider> _activeColliders = new();\n\tprivate readonly List<SoftBoneColliderShape> _externalBodyShapes = new();\n\tprivate readonly List<SoftBoneColliderShape> _externalChainShapes = new();\n\tprivate readonly List<SoftBoneChainColliderSnapshot> _publishedChainShapes = new();\n\tprivate readonly HashSet<BoBiCoSoftBoneCollider> _colliderSet = new();\n\tprivate readonly List<Transform> _framePose = new();\n\tprivate readonly List<Transform> _ragdollAnimationPose = new();\n\tprivate bool _dirty = true;\n\tprivate bool _usingPhysicsPose;\n\n\tpublic IReadOnlyList<SoftBoneJointState> Joints => _joints;\n\n\tpublic bool IsDirty => _dirty;\n\n\tpublic void MarkDirty() => _dirty = true;\n\n//=========================================================================================================================\n// Rebuild Section\n//=========================================================================================================================\n\n\tpublic bool Rebuild( SkinnedModelRenderer rendererOverride = null )\n\t{\n\t\tvar renderer = rendererOverride;\n\t\tif ( renderer is null )\n\t\t\treturn false;\n\n\t\tif ( !renderer.IsValid() || renderer.Model?.Bones is null )\n\t\t\treturn false;\n\n\t\tReleaseCurrentChain( renderer );\n\n\t\tvar boneName = \"\";\n\t\tif ( TargetBone is not null && TargetBone.IsValid() )\n\t\t\tboneName = TargetBone.Name;\n\t\tboneName = boneName?.Trim() ?? \"\";\n\n\t\tif ( string.IsNullOrWhiteSpace( boneName ) || !renderer.Model.Bones.HasBone( boneName ) )\n\t\t\treturn false;\n\n\t\tvar current = renderer.Model.Bones.GetBone( boneName );\n\n\t\twhile ( current is not null )\n\t\t{\n\t\t\tif ( !SoftBoneSkinnedModelUtility.TryGetBoneAnimationLocal( renderer, current, out var animated ) )\n\t\t\t\tbreak;\n\n\t\t\tvar poseVector = GetAnimatedBoneVectorLocal( renderer, current, animated );\n\t\t\tvar length = Math.Max( poseVector.Length, Epsilon );\n\t\t\tvar localPoseVector = animated.Rotation.Inverse * poseVector;\n\n\t\t\tvar rendererDownLocal = RendererDownLocal( renderer );\n\t\t\tvar gravityNormalLocal = animated.Rotation.Inverse * rendererDownLocal;\n\n\t\t\tvar joint = new SoftBoneJointState\n\t\t\t{\n\t\t\t\tBone = current,\n\t\t\t\tOriginalLocalGravityNormal = gravityNormalLocal,\n\t\t\t\tLocalPoseVector = localPoseVector,\n\t\t\t\tRestTransformLocal = animated,\n\t\t\t\tLength = length,\n\t\t\t\tPreviousBegin = animated.Position,\n\t\t\t\tPreviousEnd = animated.Position + poseVector,\n\t\t\t\tPreviousAnimatedBegin = animated.Position,\n\t\t\t\tPreviousAnimatedRotation = animated.Rotation,\n\t\t\t\tPreviousSimulationRotation = animated.Rotation,\n\t\t\t\tImmobileEnd = SoftBoneSkinnedModelUtility.ToWorld( renderer, animated.Position + poseVector ),\n\t\t\t\tPreviousEndWorld = SoftBoneSkinnedModelUtility.ToWorld( renderer, animated.Position + poseVector ),\n\t\t\t\tPreviousVector = poseVector,\n\t\t\t\tPreviousVelocity = Vector3.Zero,\n\t\t\t\tHasHistory = false\n\t\t\t};\n\n\t\t\t_joints.Add( joint );\n\t\t\tif ( current.Children.Count == 0 )\n\t\t\t\tbreak;\n\n\t\t\tcurrent = current.Children[0];\n\t\t}\n\n\t\t_dirty = false;\n\t\treturn _joints.Count > 0;\n\t}\n\n\tprivate void ReleaseCurrentChain( SkinnedModelRenderer renderer )\n\t{\n\t\tif ( renderer is not null && renderer.IsValid() && renderer.SceneModel.IsValid() )\n\t\t{\n\t\t\tforeach ( var joint in _joints )\n\t\t\t{\n\t\t\t\tif ( joint.Bone is not null && renderer.TryGetBoneTransformAnimation( joint.Bone, out var animatedWorld ) )\n\t\t\t\t{\n\t\t\t\t\tvar animatedLocal = renderer.WorldTransform.ToLocal( animatedWorld );\n\t\t\t\t\trenderer.SceneModel.SetBoneOverride( joint.Bone.Index, in animatedLocal );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t_joints.Clear();\n\t\t_framePose.Clear();\n\t\t_ragdollAnimationPose.Clear();\n\t\t_publishedChainShapes.Clear();\n\t\tSoftBoneCollisionRegistry.Remove( this );\n\t}\n\n\tprivate void UpdateOwnerName()\n\t{\n\t\tif ( GameObject is null || !GameObject.IsValid() )\n\t\t\treturn;\n\n\t\tvar desiredName = TargetBone is not null && TargetBone.IsValid()\n\t\t\t? TargetBone.Name?.Trim()\n\t\t\t: \"Empty Chain\";\n\t\tif ( !string.IsNullOrWhiteSpace( desiredName ) && GameObject.Name != desiredName )\n\t\t\tGameObject.Name = desiredName;\n\t}\n\n//=========================================================================================================================\n// Simulation Section\n//=========================================================================================================================\n\n\tpublic void Simulate( SkinnedModelRenderer renderer, float frameRatio )\n\t{\n\t\t// Auto-rebuild if dirty or empty \u2014 chain is self-healing so the Group\n\t\t// doesn't need to orchestrate the Rebuild/Simulate order externally.\n\t\tif ( _dirty || _joints.Count == 0 )\n\t\t{\n\t\t\tif ( !Rebuild( renderer ) )\n\t\t\t\treturn;\n\t\t}\n\n\t\tif ( renderer is null || !renderer.IsValid() || !renderer.SceneModel.IsValid() )\n\t\t\treturn;\n\n\t\tvar flag = frameRatio >= 1f;\n\t\tvar rendererDownLocal = RendererDownLocal( renderer );\n\t\tCollectActiveColliders();\n\t\tvar usePhysicsPose = IsPhysicsDrivingRenderer( renderer );\n\t\tif ( usePhysicsPose != _usingPhysicsPose )\n\t\t{\n\t\t\t_usingPhysicsPose = usePhysicsPose;\n\t\t\tforeach ( var joint in _joints ) joint.HasHistory = false;\n\t\t\tif ( usePhysicsPose && !CaptureRagdollAnimationPose( renderer ) )\n\t\t\t\treturn;\n\t\t\tif ( !usePhysicsPose ) _ragdollAnimationPose.Clear();\n\t\t}\n\n\t\tif ( !CaptureFramePose( renderer, usePhysicsPose ) )\n\t\t\treturn;\n\n\t\tvar hasParentEnd = false;\n\t\tvar parentEnd = Vector3.Zero;\n\t\tvar parentAnimatedRotation = Rotation.Identity;\n\t\tvar parentSimulationRotation = Rotation.Identity;\n\n\t\t// LimitChainLength: simulate only the last ChainLimit bones (tip-to-root).\n\t\t// Handled here instead of Rebuild so SimulateRoot logic stays tied to the\n\t\t// ACTUAL chain root (index 0), not whichever bone happens to be first after slicing.\n\t\tvar startIndex = GetSimulationStartIndex();\n\t\tResetExcludedJointsToPose( renderer, startIndex );\n\n\t\tfor ( var i = startIndex; i < _joints.Count; i++ )\n\t\t{\n\t\t\tvar joint = _joints[i];\n\t\t\tvar chainPosition = GetChainPosition( i );\n\t\t\tvar pull = EvaluateUnit( Pull, ControlMode, PullRange, PullCurve, chainPosition );\n\t\t\tvar spring = EvaluateUnit( Spring, ControlMode, SpringRange, SpringCurve, chainPosition );\n\t\t\tvar stiffness = EvaluateUnit( Stiffness, ControlMode, StiffnessRange, StiffnessCurve, chainPosition );\n\t\t\tvar immobile = EvaluateUnit( Immobile, ControlMode, ImmobileRange, ImmobileCurve, chainPosition );\n\t\t\tvar stretch = EvaluateNonNegative( Stretch, ControlMode, StretchRange, StretchCurve, chainPosition );\n\t\t\tvar squish = EvaluateNonNegative( Squish, ControlMode, SquishRange, SquishCurve, chainPosition );\n\t\t\tvar gravity = EvaluateSignedUnit( Gravity, ControlMode, GravityRange, GravityCurve, chainPosition );\n\t\t\tvar gravityFalloff = EvaluateUnit( GravityFalloff, ControlMode, GravityFalloffRange, GravityFalloffCurve, chainPosition );\n\t\t\tvar colliderRadius = EvaluateColliderRadius( chainPosition );\n\t\t\tvar maxAngle = EvaluateAngle( MaxAngle, ControlMode, MaxAngleRange, MaxAngleCurve, chainPosition );\n\t\t\tvar maxYaw = EvaluateAngle( MaxYaw, ControlMode, MaxYawRange, MaxYawCurve, chainPosition );\n\t\t\tvar maxPitch = EvaluateAngle( MaxPitch, ControlMode, MaxPitchRange, MaxPitchCurve, chainPosition );\n\t\t\tvar isActualRoot = i == 0;\n\t\t\tvar isSimulationRoot = i == startIndex;\n\t\t\tvar isRootSimulated = isActualRoot && SimulateRoot;\n\t\t\tvar animated = _framePose[i];\n\n\t\t\tvar begin = hasParentEnd ? parentEnd : animated.Position;\n\n\t\t\tvar animatedPoseVector = GetFramePoseVector( i, joint, animated );\n\t\t\tvar parentDeflection = isSimulationRoot ? Rotation.Identity : GetParentDeflection( i, parentAnimatedRotation, parentSimulationRotation );\n\t\t\tvar simulationRotation = parentDeflection * animated.Rotation;\n\t\t\tvar poseVector = parentDeflection * animatedPoseVector;\n\t\t\tif ( isSimulationRoot && RotationOffset != default )\n\t\t\t{\n\t\t\t\tvar offsetRotation = new Angles( RotationOffset.yaw, RotationOffset.pitch, RotationOffset.roll ).ToRotation();\n\t\t\t\tsimulationRotation = animated.Rotation * offsetRotation;\n\t\t\t\tposeVector = simulationRotation * (animated.Rotation.Inverse * animatedPoseVector);\n\t\t\t}\n\n\t\t\tvar restLength = Math.Max( poseVector.Length, Epsilon );\n\t\t\tvar minLength = Math.Max( 0f, restLength * (1f - squish) );\n\t\t\tvar maxLength = Math.Max( minLength, restLength * (1f + stretch) );\n\n\t\t\tvar endPoint = joint.HasHistory\n\t\t\t\t? renderer.WorldTransform.PointToLocal( joint.PreviousEndWorld )\n\t\t\t\t: begin + poseVector;\n\n\t\t\tif ( joint.HasHistory )\n\t\t\t{\n\t\t\t\tvar rootDelta = begin - joint.PreviousAnimatedBegin;\n\t\t\t\tendPoint += rootDelta;\n\n\t\t\t\tvar rotationDelta = animated.Rotation * joint.PreviousAnimatedRotation.Inverse;\n\t\t\t\tendPoint = begin + rotationDelta * (endPoint - begin);\n\t\t\t}\n\n\t\t\t// SimulateRoot=false: only skip physics on the TRUE chain root, not on bones that\n\t\t\t// happen to be first because LimitChainLength trimmed the start of the iteration.\n\t\t\tif ( isActualRoot && !isRootSimulated && !CollideWithRoot )\n\t\t\t{\n\t\t\t\tResetToRestPose( renderer, joint, animated, begin, poseVector, simulationRotation );\n\t\t\t\tparentEnd = joint.PreviousEnd;\n\t\t\t\tparentAnimatedRotation = animated.Rotation;\n\t\t\t\tparentSimulationRotation = joint.PreviousSimulationRotation;\n\t\t\t\thasParentEnd = true;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif ( pull <= Epsilon )\n\t\t\t{\n\t\t\t\tResetToRestPose( renderer, joint, animated, begin, poseVector, simulationRotation );\n\t\t\t\tif ( !isActualRoot )\n\t\t\t\t\tApplySolvedTransform( renderer, joint, animated, simulationRotation );\n\t\t\t\tparentEnd = joint.PreviousEnd;\n\t\t\t\tparentAnimatedRotation = animated.Rotation;\n\t\t\t\tparentSimulationRotation = joint.PreviousSimulationRotation;\n\t\t\t\thasParentEnd = true;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tvar previousEndPoint = endPoint;\n\n\t\t\tif ( joint.HasHistory && immobile > Epsilon )\n\t\t\t{\n\t\t\t\tvar immobileTarget = renderer.WorldTransform.PointToLocal( joint.ImmobileEnd );\n\t\t\t\tendPoint += (immobileTarget - endPoint) * immobile;\n\t\t\t}\n\n\t\t\tvar gravityAmount = GetGravityAmount( joint, animated, rendererDownLocal, gravity, gravityFalloff );\n\t\t\tvar gravityTargetEnd = ApplyGravityTarget( begin, poseVector, restLength, gravityAmount, gravity, rendererDownLocal );\n\n\t\t\tvar solvedEndPoint = BoBiCoSoftBoneSolver.IntegrateAdvanced(\n\t\t\t\tbegin, endPoint, previousEndPoint,\n\t\t\t\tposeVector, joint.PreviousVector, joint.PreviousVelocity,\n\t\t\t\tminLength, maxLength, pull, spring, stiffness, gravityTargetEnd, frameRatio,\n\t\t\t\tout var newVelocity );\n\n\t\t\tvar solvedDirection = BoBiCoSoftBoneSolver.SafeNormal( solvedEndPoint - begin, poseVector );\n\t\t\tsolvedDirection = SoftBoneAngleLimit.Apply( poseVector, solvedDirection, LimitType, maxAngle, maxYaw, maxPitch, LimitRotation );\n\t\t\tvar solvedLength = Math.Clamp( (solvedEndPoint - begin).Length, minLength, maxLength );\n\t\t\tvar solvedEnd = begin + solvedDirection * solvedLength;\n\t\t\tsolvedEnd = ResolveCollisions( renderer, begin, solvedEnd, poseVector, minLength, maxLength, colliderRadius, maxAngle, maxYaw, maxPitch, isActualRoot && CollideWithRoot, out var wasColliding );\n\t\t\tsolvedDirection = BoBiCoSoftBoneSolver.SafeNormal( solvedEnd - begin, poseVector );\n\t\t\tnewVelocity = solvedEnd - previousEndPoint;\n\t\t\tif ( wasColliding ) newVelocity *= 0.75f;\n\n\t\t\tif ( flag )\n\t\t\t{\n\t\t\t\tjoint.PreviousEndWorld = SoftBoneSkinnedModelUtility.ToWorld( renderer, solvedEnd );\n\t\t\t\tjoint.ImmobileEnd = joint.PreviousEndWorld;\n\t\t\t\tjoint.PreviousVector = solvedEnd - begin;\n\t\t\t\tjoint.PreviousVelocity = newVelocity;\n\t\t\t\tjoint.PreviousAnimatedBegin = begin;\n\t\t\t\tjoint.PreviousAnimatedRotation = animated.Rotation;\n\t\t\t}\n\n\t\t\tjoint.PreviousBegin = begin;\n\t\t\tjoint.PreviousEnd = solvedEnd;\n\t\t\tjoint.PreviousSimulationRotation = BoBiCoSoftBoneSolver.SolveHeadRotation( poseVector, solvedEnd - begin, simulationRotation );\n\t\t\tjoint.HasHistory = true;\n\n\t\t\tif ( !isActualRoot )\n\t\t\t\tApplySolvedTransform( renderer, joint, animated, joint.PreviousSimulationRotation );\n\n\t\t\tparentEnd = solvedEnd;\n\t\t\tparentAnimatedRotation = animated.Rotation;\n\t\t\tparentSimulationRotation = joint.PreviousSimulationRotation;\n\t\t\thasParentEnd = true;\n\t\t}\n\n\t\tPublishCollisionShapes( renderer );\n\t}\n\n\tprivate void CollectActiveColliders()\n\t{\n\t\t_activeColliders.Clear();\n\t\t_colliderSet.Clear();\n\n\t\tif ( ColliderGroups is not null )\n\t\t{\n\t\t\tforeach ( var sourceObject in ColliderGroups )\n\t\t\t{\n\t\t\t\tif ( sourceObject is null || !sourceObject.IsValid() )\n\t\t\t\t\tcontinue;\n\n\t\t\t\tsourceObject.GetComponent<BoBiCoSoftBoneColliderGroup>()?.CollectColliders( _activeColliders, _colliderSet );\n\t\t\t}\n\t\t}\n\n\t}\n\n\tprivate void ResetExcludedJointsToPose( SkinnedModelRenderer renderer, int startIndex )\n\t{\n\t\tfor ( var i = 0; i < startIndex; i++ )\n\t\t{\n\t\t\tvar joint = _joints[i];\n\t\t\tvar animated = _framePose[i];\n\t\t\tvar poseVector = GetFramePoseVector( i, joint, animated );\n\t\t\tResetToRestPose( renderer, joint, animated, animated.Position, poseVector, animated.Rotation );\n\t\t\tif ( i > 0 )\n\t\t\t\tApplyAnimationTransform( renderer, joint, animated );\n\t\t}\n\t}\n\n\tprivate 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 )\n\t{\n\t\tcollided = false;\n\t\tif ( CollisionMode == SoftBoneCollisionMode.None || colliderRadius <= Epsilon )\n\t\t\treturn solvedEnd;\n\n\t\tvar worldBegin = SoftBoneSkinnedModelUtility.ToWorld( renderer, begin );\n\t\tvar worldEnd = SoftBoneSkinnedModelUtility.ToWorld( renderer, solvedEnd );\n\t\tforeach ( var collider in _activeColliders )\n\t\t{\n\t\t\tif ( collider.TryGetWorldShape( collider.GetOwningRenderer(), out var shape ) )\n\t\t\t\tcollided |= SoftBoneCollisionSolver.Resolve( ref worldEnd, worldBegin, includeRootHead ? worldBegin : GetCollisionStart( worldBegin, worldEnd, colliderRadius ), colliderRadius, minLength, maxLength, shape );\n\t\t}\n\n\t\tif ( CollisionMode == SoftBoneCollisionMode.FullCollision )\n\t\t{\n\t\t\t_externalBodyShapes.Clear();\n\t\t\tSoftBoneCollisionRegistry.CollectExternalBodyShapes( renderer, _externalBodyShapes );\n\t\t\tforeach ( var shape in _externalBodyShapes )\n\t\t\t\tcollided |= SoftBoneCollisionSolver.Resolve( ref worldEnd, worldBegin, includeRootHead ? worldBegin : GetCollisionStart( worldBegin, worldEnd, colliderRadius ), colliderRadius, minLength, maxLength, shape );\n\t\t}\n\n\t\t_externalChainShapes.Clear();\n\t\tSoftBoneCollisionRegistry.CollectChainShapes( this, renderer, CollisionMode, _externalChainShapes );\n\t\tforeach ( var shape in _externalChainShapes )\n\t\t\tcollided |= SoftBoneCollisionSolver.Resolve( ref worldEnd, worldBegin, includeRootHead ? worldBegin : GetCollisionStart( worldBegin, worldEnd, colliderRadius ), colliderRadius, minLength, maxLength, shape );\n\n\t\tif ( !collided )\n\t\t\treturn solvedEnd;\n\n\t\tvar localDelta = SoftBoneSkinnedModelUtility.ToLocal( renderer, worldEnd ) - begin;\n\t\tvar localDirection = SoftBoneAngleLimit.Apply( poseVector, BoBiCoSoftBoneSolver.SafeNormal( localDelta, poseVector ), LimitType, maxAngle, maxYaw, maxPitch, LimitRotation );\n\t\treturn begin + localDirection * Math.Clamp( localDelta.Length, minLength, maxLength );\n\t}\n\n\tprivate Vector3 GetCollisionStart( Vector3 worldBegin, Vector3 worldEnd, float colliderRadius )\n\t{\n\t\tif ( ColliderShape != SoftBoneLinkColliderShape.Capsule )\n\t\t\treturn worldEnd;\n\n\t\tvar axis = worldEnd - worldBegin;\n\t\tvar segmentLength = Math.Max( 0f, ColliderHeight - colliderRadius * 2f );\n\t\tif ( axis.Length <= Epsilon || segmentLength <= Epsilon )\n\t\t\treturn worldEnd;\n\n\t\treturn worldEnd - axis.Normal * Math.Min( axis.Length, segmentLength );\n\t}\n\n\tprivate int GetSimulationStartIndex()\n\t{\n\t\treturn LimitChainLength && ChainLimit > 0 && ChainLimit < _joints.Count\n\t\t\t? _joints.Count - ChainLimit\n\t\t\t: 0;\n\t}\n\n\tprivate float GetChainPosition( int index ) => _joints.Count <= 1 ? 0f : index / (float)(_joints.Count - 1);\n\tprivate static Curve ConstantCurve( float value ) => new( new Curve.Frame( 0f, value ), new Curve.Frame( 1f, value ) );\n\n\tprivate void InitializeCurveDefaults()\n\t{\n\t\t_curveDefaultsInitialized = true;\n\t\tPullCurve = ConstantCurve( Pull );\n\t\tSpringCurve = ConstantCurve( Spring );\n\t\tStiffnessCurve = ConstantCurve( Stiffness );\n\t\tImmobileCurve = ConstantCurve( Immobile );\n\t\tStretchCurve = ConstantCurve( Stretch );\n\t\tSquishCurve = ConstantCurve( Squish );\n\t\tGravityCurve = ConstantCurve( Gravity );\n\t\tGravityFalloffCurve = ConstantCurve( GravityFalloff );\n\t\tMaxAngleCurve = ConstantCurve( MaxAngle / 180f );\n\t\tMaxYawCurve = ConstantCurve( MaxYaw / 180f );\n\t\tMaxPitchCurve = ConstantCurve( MaxPitch / 180f );\n\t\tColliderRadiusCurve = ConstantCurve( ColliderRadius );\n\t}\n\n\tprivate static float EvaluateUnit( float value, SoftBoneControlMode mode, Vector2 range, Curve curve, float position ) => Math.Clamp( EvaluateControl( value, mode, range, curve, position ), 0f, 1f );\n\tprivate static float EvaluateSignedUnit( float value, SoftBoneControlMode mode, Vector2 range, Curve curve, float position ) => Math.Clamp( EvaluateControl( value, mode, range, curve, position ), -1f, 1f );\n\tprivate static float EvaluateNonNegative( float value, SoftBoneControlMode mode, Vector2 range, Curve curve, float position ) => Math.Max( 0f, EvaluateControl( value, mode, range, curve, position ) );\n\tprivate static float EvaluateAngle( float value, SoftBoneControlMode mode, Vector2 range, Curve curve, float position )\n\t{\n\t\tif ( mode == SoftBoneControlMode.Curve )\n\t\t\treturn Math.Clamp( (curve.Length > 0 ? curve.Evaluate( position ) : 1f) * 180f, 0f, 180f );\n\n\t\treturn Math.Clamp( EvaluateControl( value, mode, range, curve, position ), 0f, 180f );\n\t}\n\tprivate float EvaluateColliderRadius( float position ) => Math.Max( 0.001f, EvaluateControl( ColliderRadius, ControlMode, ColliderRadiusRange, ColliderRadiusCurve, position ) );\n\tprivate static float EvaluateControl( float value, SoftBoneControlMode mode, Vector2 range, Curve curve, float position )\n\t{\n\t\treturn mode switch\n\t\t{\n\t\t\tSoftBoneControlMode.Range => range.x + (range.y - range.x) * position,\n\t\t\tSoftBoneControlMode.Curve => curve.Length > 0 ? curve.Evaluate( position ) : value,\n\t\t\t_ => value\n\t\t};\n\t}\n\n\tinternal void PublishCollisionShapes( SkinnedModelRenderer renderer )\n\t{\n\t\tif ( CollisionMode == SoftBoneCollisionMode.None )\n\t\t{\n\t\t\tSoftBoneCollisionRegistry.Remove( this );\n\t\t\treturn;\n\t\t}\n\n\t\t_publishedChainShapes.Clear();\n\t\tfor ( var i = GetSimulationStartIndex(); i < _joints.Count; i++ )\n\t\t{\n\t\t\tif ( i == 0 && !CollideWithRoot )\n\t\t\t\tcontinue;\n\n\t\t\tvar joint = _joints[i];\n\t\t\tif ( !joint.HasHistory )\n\t\t\t\tcontinue;\n\n\t\t\tvar begin = SoftBoneSkinnedModelUtility.ToWorld( renderer, joint.PreviousBegin );\n\t\t\tvar end = SoftBoneSkinnedModelUtility.ToWorld( renderer, joint.PreviousEnd );\n\t\t\tvar radius = EvaluateColliderRadius( GetChainPosition( i ) );\n\t\t\tvar direction = BoBiCoSoftBoneSolver.SafeNormal( end - begin, Vector3.Forward );\n\t\t\tSoftBoneColliderShape shape;\n\t\t\tswitch ( ColliderShape )\n\t\t\t{\n\t\t\t\tcase SoftBoneLinkColliderShape.Sphere:\n\t\t\t\t\tshape = SoftBoneColliderShape.Sphere( begin, radius );\n\t\t\t\t\tbreak;\n\t\t\t\tcase SoftBoneLinkColliderShape.Box:\n\t\t\t\t\tshape = SoftBoneColliderShape.Box( begin, Rotation.FromToRotation( Vector3.Forward, direction ), new Vector3( BoxDimension.z, BoxDimension.x, BoxDimension.y ) );\n\t\t\t\t\tbreak;\n\t\t\t\tcase SoftBoneLinkColliderShape.Flat:\n\t\t\t\t\tshape = SoftBoneColliderShape.Flat( begin, direction );\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tvar halfLine = Math.Max( 0f, ColliderHeight * 0.5f - radius );\n\t\t\t\t\tshape = SoftBoneColliderShape.Capsule( begin - direction * halfLine, begin + direction * halfLine, radius );\n\t\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t_publishedChainShapes.Add( new SoftBoneChainColliderSnapshot( renderer, shape ) );\n\t\t}\n\n\t\tSoftBoneCollisionRegistry.Publish( this, renderer, _publishedChainShapes );\n\t}\n\n\n//=========================================================================================================================\n// Helpers\n//=========================================================================================================================\n\n\tprivate bool CaptureFramePose( SkinnedModelRenderer renderer, bool usePhysicsPose )\n\t{\n\t\t_framePose.Clear();\n\n\t\tTransform rootWorld = default;\n\t\tif ( !renderer.TryGetBoneTransform( _joints[0].Bone, out rootWorld )\n\t\t\t&& !renderer.TryGetBoneTransformAnimation( _joints[0].Bone, out rootWorld ) )\n\t\t\treturn false;\n\t\tvar rootPose = renderer.WorldTransform.ToLocal( rootWorld );\n\t\t_framePose.Add( rootPose );\n\n\t\tfor ( var i = 1; i < _joints.Count; i++ )\n\t\t{\n\t\t\tvar joint = _joints[i];\n\t\t\tTransform animationPose;\n\t\t\tif ( usePhysicsPose )\n\t\t\t{\n\t\t\t\tif ( _ragdollAnimationPose.Count != _joints.Count ) return false;\n\t\t\t\tanimationPose = ApplyRootPoseDelta( _ragdollAnimationPose[i], _ragdollAnimationPose[0], rootPose );\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tif ( !renderer.TryGetBoneTransformAnimation( joint.Bone, out var worldTransform ) )\n\t\t\t\t\treturn false;\n\t\t\t\tanimationPose = renderer.WorldTransform.ToLocal( worldTransform );\n\t\t\t}\n\n\t\t\t_framePose.Add( animationPose );\n\t\t}\n\n\t\treturn true;\n\t}\n\n\tprivate bool CaptureRagdollAnimationPose( SkinnedModelRenderer renderer )\n\t{\n\t\t_ragdollAnimationPose.Clear();\n\t\tforeach ( var joint in _joints )\n\t\t{\n\t\t\tif ( !renderer.TryGetBoneTransformAnimation( joint.Bone, out var worldTransform ) )\n\t\t\t{\n\t\t\t\t_ragdollAnimationPose.Clear();\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\t_ragdollAnimationPose.Add( renderer.WorldTransform.ToLocal( worldTransform ) );\n\t\t}\n\n\t\treturn true;\n\t}\n\n\tprivate static Transform ApplyRootPoseDelta( Transform pose, Transform animationRoot, Transform finalRoot )\n\t{\n\t\tvar rotationDelta = finalRoot.Rotation * animationRoot.Rotation.Inverse;\n\t\tvar positionDelta = finalRoot.Position - rotationDelta * animationRoot.Position;\n\t\treturn new Transform( positionDelta + rotationDelta * pose.Position, rotationDelta * pose.Rotation, pose.Scale );\n\t}\n\n\tprivate bool IsPhysicsDrivingRenderer( SkinnedModelRenderer renderer )\n\t{\n\t\tif ( Scene is null ) return false;\n\t\tforeach ( var physics in Scene.GetAllComponents<ModelPhysics>() )\n\t\t\tif ( physics is not null && physics.Enabled && physics.MotionEnabled && physics.Renderer == renderer )\n\t\t\t\treturn true;\n\t\treturn false;\n\t}\n\n\tprivate bool TryGetSolveTransformLocal( SkinnedModelRenderer renderer, SoftBoneJointState joint, out Transform transform )\n\t{\n\t\tif ( renderer is not null && renderer.IsValid() && joint.Bone is not null\n\t\t\t&& renderer.TryGetBoneTransformAnimation( joint.Bone, out var worldTransform ) )\n\t\t{\n\t\t\ttransform = renderer.WorldTransform.ToLocal( worldTransform );\n\t\t\treturn true;\n\t\t}\n\n\t\ttransform = joint.RestTransformLocal;\n\t\treturn joint.RestTransformLocal.Scale != Vector3.Zero;\n\t}\n\n\tprivate static Vector3 GetAnimatedBoneVectorLocal( SkinnedModelRenderer renderer, BoneCollection.Bone bone, Transform animated )\n\t{\n\t\tif ( bone.Children.Count > 0 && SoftBoneSkinnedModelUtility.TryGetBoneAnimationLocal( renderer, bone.Children[0], out var child ) )\n\t\t{\n\t\t\tvar direction = child.Position - animated.Position;\n\t\t\tif ( direction.Length > Epsilon )\n\t\t\t\treturn direction;\n\t\t}\n\n\t\tvar fallback = animated.Rotation * Vector3.Forward;\n\t\treturn fallback.Length > Epsilon ? fallback : Vector3.Forward;\n\t}\n\n\tprivate Vector3 GetFramePoseVector( int index, SoftBoneJointState joint, Transform pose )\n\t{\n\t\tif ( index + 1 < _framePose.Count )\n\t\t{\n\t\t\tvar direction = _framePose[index + 1].Position - pose.Position;\n\t\t\tif ( direction.Length > Epsilon ) return direction;\n\t\t}\n\n\t\treturn GetPoseVectorLocal( joint, pose );\n\t}\n\n\tprivate static Vector3 GetPoseVectorLocal( SoftBoneJointState joint, Transform animated )\n\t{\n\t\tvar poseVector = animated.Rotation * joint.LocalPoseVector;\n\t\tif ( poseVector.Length > Epsilon )\n\t\t\treturn poseVector;\n\n\t\tvar fallback = animated.Rotation * Vector3.Forward;\n\t\treturn fallback.Length > Epsilon ? fallback : Vector3.Forward;\n\t}\n\n\tprivate static Rotation GetParentDeflection( int index, Rotation parentAnimatedRotation, Rotation parentSimulationRotation )\n\t{\n\t\tif ( index <= 0 )\n\t\t\treturn Rotation.Identity;\n\n\t\treturn parentSimulationRotation * parentAnimatedRotation.Inverse;\n\t}\n\n\tprivate float GetGravityAmount( SoftBoneJointState joint, Transform animated, Vector3 rendererDownLocal, float gravity, float gravityFalloff )\n\t{\n\t\tif ( Math.Abs( gravity ) <= Epsilon )\n\t\t\treturn 0f;\n\n\t\tvar gravityAmount = Math.Abs( gravity );\n\t\tif ( gravityFalloff > Epsilon )\n\t\t{\n\t\t\tvar gravityNormal = animated.Rotation * joint.OriginalLocalGravityNormal;\n\t\t\tvar gravityBias = Math.Min( 1f, 1f - Vector3.Dot(\n\t\t\t\tBoBiCoSoftBoneSolver.SafeNormal( gravityNormal, rendererDownLocal ),\n\t\t\t\trendererDownLocal ) );\n\t\t\tgravityAmount *= (1f - gravityFalloff) + gravityFalloff * gravityBias;\n\t\t}\n\n\t\treturn Math.Clamp( gravityAmount, 0f, 1f );\n\t}\n\n\tprivate static Vector3 ApplyGravityTarget( Vector3 begin, Vector3 poseVector, float restLength, float gravityAmount, float gravity, Vector3 rendererDownLocal )\n\t{\n\t\tif ( gravityAmount <= Epsilon )\n\t\t\treturn begin + poseVector;\n\n\t\tvar gravityVector = (gravity > 0f ? rendererDownLocal : -rendererDownLocal) * restLength;\n\t\tvar targetVector = poseVector + (gravityVector - poseVector) * gravityAmount;\n\t\treturn begin + BoBiCoSoftBoneSolver.SafeNormal( targetVector, poseVector ) * restLength;\n\t}\n\n\tprivate static void ResetToRestPose( SkinnedModelRenderer renderer, SoftBoneJointState joint, Transform animated, Vector3 begin, Vector3 poseVector, Rotation simulationRotation )\n\t{\n\t\tvar restEnd = begin + poseVector;\n\t\tjoint.PreviousBegin = begin;\n\t\tjoint.PreviousEnd = restEnd;\n\t\tjoint.PreviousAnimatedBegin = begin;\n\t\tjoint.PreviousAnimatedRotation = animated.Rotation;\n\t\tjoint.PreviousSimulationRotation = simulationRotation;\n\t\tjoint.ImmobileEnd = SoftBoneSkinnedModelUtility.ToWorld( renderer, restEnd );\n\t\tjoint.PreviousEndWorld = joint.ImmobileEnd;\n\t\tjoint.PreviousVector = poseVector;\n\t\tjoint.PreviousVelocity = Vector3.Zero;\n\t\tjoint.HasHistory = true;\n\t}\n\n\tprivate static void ApplySolvedTransform( SkinnedModelRenderer renderer, SoftBoneJointState joint, Transform animated, Rotation solvedRotation )\n\t{\n\t\tvar localTransform = new Transform( joint.PreviousBegin, solvedRotation, animated.Scale );\n\t\trenderer.SceneModel.SetBoneOverride( joint.Bone.Index, in localTransform );\n\t}\n\n\tprivate static void ApplyAnimationTransform( SkinnedModelRenderer renderer, SoftBoneJointState joint, Transform animated )\n\t{\n\t\trenderer.SceneModel.SetBoneOverride( joint.Bone.Index, in animated );\n\t}\n\n\tprivate static Vector3 RendererDownLocal( SkinnedModelRenderer renderer )\n\t{\n\t\tif ( renderer is null || !renderer.IsValid() )\n\t\t\treturn Vector3.Down;\n\n\t\treturn BoBiCoSoftBoneSolver.SafeNormal( renderer.WorldTransform.Rotation.Inverse * Vector3.Down, Vector3.Down );\n\t}\n\n//=========================================================================================================================\n// Building Gizmo \n//=========================================================================================================================\n\n\tprotected override void DrawGizmos()\n\t{\n\t\tbase.DrawGizmos();\n\t\tvar drawCollider = CollisionMode != SoftBoneCollisionMode.None && ShowColliderGizmo && (AlwaysShowGizmo || Gizmo.IsSelected);\n\t\tvar drawLimit = ShowLimitGizmo && (AlwaysShowLimitGizmo || Gizmo.IsSelected) && LimitType != SoftBoneLimitType.None;\n\t\tif ( !drawCollider && !drawLimit )\n\t\t\treturn;\n\n\t\tvar renderer = FindRendererForGizmo();\n\t\tif ( renderer is null || !renderer.IsValid() )\n\t\t\treturn;\n\n\t\tif ( _dirty || _joints.Count == 0 )\n\t\t\tRebuild( renderer );\n\n\t\tusing ( Transform.DisableProxy() )\n\t\t{\n\t\t\tGizmo.Transform = new Transform( Vector3.Zero, Rotation.Identity, 1f );\n\t\t\tGizmo.Draw.IgnoreDepth = true;\n\t\t\tGizmo.Draw.LineThickness = 1;\n\t\t\tfor ( var i = GetSimulationStartIndex(); i < _joints.Count; i++ )\n\t\t\t{\n\t\t\t\tvar joint = _joints[i];\n\t\t\t\tif ( !TryGetSolveTransformLocal( renderer, joint, out var animated ) )\n\t\t\t\t\tcontinue;\n\n\t\t\t\tvar solvedBegin = joint.HasHistory ? joint.PreviousBegin : animated.Position;\n\t\t\t\tvar solvedEnd = joint.HasHistory ? joint.PreviousEnd : animated.Position + GetPoseVectorLocal( joint, animated );\n\t\t\t\tvar physicsBegin = SoftBoneSkinnedModelUtility.ToWorld( renderer, solvedBegin );\n\t\t\t\tvar boneObject = renderer.GetBoneObject( joint.Bone );\n\t\t\t\tvar begin = boneObject is not null && boneObject.IsValid()\n\t\t\t\t\t? boneObject.WorldPosition\n\t\t\t\t\t: physicsBegin;\n\t\t\t\tvar end = SoftBoneSkinnedModelUtility.ToWorld( renderer, solvedEnd );\n\t\t\t\t// Root bone: only draw when SimulateRoot is enabled.\n\t\t\t\t// Non-root bones: skip leaf/endpoint bones (Children.Count == 0) since their\n\t\t\t\t// endpoint is a synthetic forward poseVector, not a real child position.\n\t\t\t\t// The Children.Count gate must NOT apply to the simulated root \u2014 it may be a\n\t\t\t\t// leaf or near-leaf bone and still needs collision when SimulateRoot is on.\n\t\t\t\tvar drawJointCollider = drawCollider\n\t\t\t\t\t&& (i != 0 || (SimulateRoot && CollideWithRoot));\n\t\t\t\tif ( drawJointCollider )\n\t\t\t\t{\n\t\t\t\t\tGizmo.Draw.Color = ColliderGizmoColor.WithAlpha( ColliderGizmoOpacity );\n\t\t\t\t\tDrawLinkColliderGizmo( begin, end, EvaluateColliderRadius( GetChainPosition( i ) ) );\n\t\t\t\t}\n\n\t\t\t\tvar drawJointLimit = drawLimit && (i != 0 || SimulateRoot);\n\t\t\t\tif ( drawJointLimit )\n\t\t\t\t{\n\t\t\t\t\tvar solvedVector = solvedEnd - solvedBegin;\n\t\t\t\t\tvar chainPosition = GetChainPosition( i );\n\t\t\t\t\tvar colliderRadius = EvaluateColliderRadius( chainPosition );\n\t\t\t\t\tDrawLimitGizmo( 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 ) );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate void DrawLinkColliderGizmo( Vector3 worldStart, Vector3 worldEnd, float colliderRadius )\n\t{\n\t\tif ( ColliderShape == SoftBoneLinkColliderShape.Sphere )\n\t\t{\n\t\t\tGizmo.Draw.LineSphere( new Sphere( worldStart, colliderRadius ) );\n\t\t\treturn;\n\t\t}\n\t\tif ( ColliderShape == SoftBoneLinkColliderShape.Capsule )\n\t\t{\n\t\t\tvar direction = BoBiCoSoftBoneSolver.SafeNormal( worldEnd - worldStart, Vector3.Forward );\n\t\t\tvar halfLine = Math.Max( 0f, ColliderHeight * 0.5f - colliderRadius );\n\t\t\tGizmo.Draw.LineCapsule( new Capsule( worldStart - direction * halfLine, worldStart + direction * halfLine, colliderRadius ) );\n\t\t\treturn;\n\t\t}\n\n\t\tvar forward = BoBiCoSoftBoneSolver.SafeNormal( worldEnd - worldStart, Vector3.Forward );\n\t\tvar right = BoBiCoSoftBoneSolver.SafeNormal( Vector3.Cross( forward, Vector3.Up ), Vector3.Right );\n\t\tvar up = BoBiCoSoftBoneSolver.SafeNormal( Vector3.Cross( right, forward ), Vector3.Up );\n\t\tif ( ColliderShape == SoftBoneLinkColliderShape.Flat )\n\t\t{\n\t\t\tvar half = colliderRadius;\n\t\t\tvar a = worldStart + (right + up) * half; var b = worldStart + (right - up) * half;\n\t\t\tvar c = worldStart + (-right - up) * half; var d = worldStart + (-right + up) * half;\n\t\t\tGizmo.Draw.Line( a, b ); Gizmo.Draw.Line( b, c ); Gizmo.Draw.Line( c, d ); Gizmo.Draw.Line( d, a );\n\t\t\tGizmo.Draw.Line( worldStart, worldStart + forward * Math.Max( colliderRadius, 0.2f ) );\n\t\t\treturn;\n\t\t}\n\n\t\tvar halfSize = BoxDimension * 0.5f;\n\t\tvar corners = new Vector3[8];\n\t\tfor ( var corner = 0; corner < 8; corner++ )\n\t\t\tcorners[corner] = worldStart\n\t\t\t\t+ forward * (((corner & 1) == 0 ? -1f : 1f) * halfSize.z)\n\t\t\t\t+ right   * (((corner & 2) == 0 ? -1f : 1f) * halfSize.x)\n\t\t\t\t+ up      * (((corner & 4) == 0 ? -1f : 1f) * halfSize.y);\n\t\tint[] 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 };\n\t\tfor ( var edge = 0; edge < edges.Length; edge += 2 ) Gizmo.Draw.Line( corners[edges[edge]], corners[edges[edge + 1]] );\n\t}\n\n\tprivate void DrawLimitGizmo( SkinnedModelRenderer renderer, Vector3 worldBegin, Vector3 solvedVector, float radius, float maxAngle, float maxYaw, float maxPitch )\n\t{\n\t\tSoftBoneAngleLimit.GetAxes( solvedVector, LimitRotation, out var axisX, out var axisY, out var axisZ );\n\t\taxisX = renderer.WorldTransform.Rotation * axisX;\n\t\taxisY = renderer.WorldTransform.Rotation * axisY;\n\t\taxisZ = renderer.WorldTransform.Rotation * axisZ;\n\t\tvar gizmoRotation = Rotation.FromAxis( axisY, 90f );\n\t\taxisX = gizmoRotation * axisX;\n\t\taxisZ = gizmoRotation * axisZ;\n\t\tGizmo.Draw.Color = LimitGizmoColor.WithAlpha( LimitGizmoOpacity );\n\t\tconst int steps = 24;\n\t\tif ( LimitType == SoftBoneLimitType.Cone )\n\t\t{\n\t\t\tDrawArc( worldBegin, axisY, axisX, maxAngle, radius, steps );\n\t\t\tDrawArc( worldBegin, axisY, axisZ, maxAngle, radius, steps );\n\t\t}\n\t\telse if ( LimitType == SoftBoneLimitType.Plane )\n\t\t{\n\t\t\tDrawArc( worldBegin, axisY, axisZ, maxAngle, radius, steps );\n\t\t\tGizmo.Draw.Line( worldBegin - axisX * radius * 0.45f, worldBegin + axisX * radius * 0.45f );\n\t\t}\n\t\telse\n\t\t{\n\t\t\tDrawArc( worldBegin, axisY, axisX, maxYaw, radius, steps );\n\t\t\tDrawArc( worldBegin, axisY, axisZ, maxPitch, radius, steps );\n\t\t}\n\t}\n\n\tprivate static void DrawArc( Vector3 origin, Vector3 forward, Vector3 side, float angle, float radius, int steps )\n\t{\n\t\tvar previous = origin + Rotation.FromAxis( side, -angle ) * forward * radius;\n\t\tfor ( var i = 1; i <= steps; i++ )\n\t\t{\n\t\t\tvar point = origin + Rotation.FromAxis( side, -angle + angle * 2f * i / steps ) * forward * radius;\n\t\t\tGizmo.Draw.Line( previous, point );\n\t\t\tprevious = point;\n\t\t}\n\t}\n\n\tprivate SkinnedModelRenderer FindRendererForGizmo()\n\t{\n\t\tvar targetRenderer = TargetBone?.GetComponentInParent<SkinnedModelRenderer>( true, true );\n\t\tif ( targetRenderer is not null && targetRenderer.IsValid() )\n\t\t\treturn targetRenderer;\n\t\treturn null;\n\t}\n\n\tinternal SkinnedModelRenderer GetTargetRenderer() => FindRendererForGizmo();\n\n\tprotected override void OnValidate()\n\t{\n\t\tUpdateOwnerName();\n\t\tColliderGizmoOpacity = Math.Clamp( ColliderGizmoOpacity, 0f, 1f );\n\t\tLimitGizmoOpacity = Math.Clamp( LimitGizmoOpacity, 0f, 1f );\n\t\tColliderHeight = Math.Max( ColliderHeight, 0.002f );\n\t\tBoxDimension = new Vector3( Math.Max( BoxDimension.x, 0.001f ), Math.Max( BoxDimension.y, 0.001f ), Math.Max( BoxDimension.z, 0.001f ) );\n\t\tLimitGizmoSize = Math.Max( LimitGizmoSize, 0.1f );\n\t\tMarkDirty();\n\t}\n\n\tprotected override void OnDisabled()\n\t{\n\t\tSoftBoneCollisionRegistry.Remove( this );\n\t}\n}"
        },
        {
            "Ident": "bobicolab.bobicosoftbones",
            "Path": "Code/BoBiCo Soft-Physics/SoftBoneSolver.cs",
            "FileName": "SoftBoneSolver.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 336846,
            "IsPrivate": false,
            "Code": "using System;\n\n/// <summary>\n/// Integrates one SoftBone link in renderer-local space.\n/// </summary>\npublic static class BoBiCoSoftBoneSolver\n{\n\tprivate const float Epsilon = 0.000008f;\n\n\t/// <summary>\n\t/// One integration step.  All positions/vectors in renderer-local space.\n\t/// Returns the solved end position (renderer-local) and the new velocity.\n\t/// </summary>\n\tpublic static Vector3 IntegrateAdvanced(\n\t\tVector3 begin,\n\t\tVector3 endPoint,           // current physics end (may have had Immobile applied before call)\n\t\tVector3 previousEndPoint,   // endPoint before Immobile, for velocity storage\n\t\tVector3 poseVector,         // animated rest direction (renderer-local)\n\t\tVector3 previousVector,     // last frame's (solvedEnd - begin)\n\t\tVector3 previousVelocity,\n\t\tfloat minLength,\n\t\tfloat maxLength,\n\t\tfloat pull,\n\t\tfloat momentum,\n\t\tfloat stiffness,\n\t\tVector3 gravityTargetEnd,   // begin + gravity-blended direction * restLength\n\t\tfloat frameRatio,           // Time.Delta * 60, used for frameLerp\n\t\tout Vector3 newVelocity )\n\t{\n\t\tvar delta = previousVelocity * momentum;\n\t\tdelta += (gravityTargetEnd - (endPoint + delta)) * pull;\n\n\t\tif ( stiffness > Epsilon )\n\t\t{\n\t\t\tvar nextVector = endPoint + delta - begin;\n\t\t\tdelta += (previousVector - nextVector) * stiffness;\n\t\t}\n\n\t\tendPoint += delta;\n\t\tvar boundedVector = endPoint - begin;\n\t\tvar boundedLength = Math.Clamp( boundedVector.Length, minLength, maxLength );\n\t\tendPoint = begin + SafeNormal( boundedVector, poseVector ) * boundedLength;\n\n\t\t// Limit per-step commitment so long frames do not inject a velocity spike.\n\t\tvar frameLerp = Math.Clamp( (1f / Lerp( 1f, frameRatio, pull )) * frameRatio, 0f, 1f );\n\t\tendPoint = Lerp( previousEndPoint, endPoint, frameLerp );\n\n\t\t// velocity = where we actually moved to, not the raw delta\n\t\tnewVelocity = endPoint - previousEndPoint;\n\n\t\treturn endPoint;\n\t}\n\n\t/// <summary>\n\t/// Solve the head bone rotation to point toward the simulated tail.\n\t/// Works in renderer-local space.\n\t/// </summary>\n\tpublic static Rotation SolveHeadRotation(\n\t\tVector3 poseVector,         // animated rest direction (renderer-local)\n\t\tVector3 solvedVector,       // solvedEnd - begin (renderer-local)\n\t\tRotation animatedRotation )\n\t{\n\t\tvar restAxis = SafeNormal( poseVector, Vector3.Forward );\n\t\tvar solvedAxis = SafeNormal( solvedVector, restAxis );\n\t\treturn Rotation.FromToRotation( restAxis, solvedAxis ) * animatedRotation;\n\t}\n\n\t// \u2500\u2500 Helpers (all renderer-local) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n\tpublic static Vector3 SafeNormal( Vector3 value, Vector3 fallback )\n\t{\n\t\tif ( value.Length <= Epsilon )\n\t\t\treturn fallback.Length > Epsilon ? fallback.Normal : Vector3.Forward;\n\n\t\treturn value.Normal;\n\t}\n\n\tprivate static Vector3 Lerp( Vector3 a, Vector3 b, float t ) => a + (b - a) * t;\n\tprivate static float Lerp( float a, float b, float t ) => a + (b - a) * t;\n}\n"
        },
        {
            "Ident": "bobicolab.bobicosoftbones",
            "Path": ".obj/__compiler_extra.cs",
            "FileName": "__compiler_extra.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 336846,
            "IsPrivate": false,
            "Code": "global using static Sandbox.Internal.GlobalGameNamespace;\r\nglobal using Microsoft.AspNetCore.Components;\r\nglobal using Microsoft.AspNetCore.Components.Rendering;\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"AddonTitle\", \"BoBiCo SoftBones\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"AddonIdent\", \"bobicosoftbones\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"OrgIdent\", \"bobicolab\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"Ident\", \"bobicolab.bobicosoftbones\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"EngineVersion\", \"28\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"EngineMinorVersion\", \"1\" )]\r\n\r\n[assembly: System.Runtime.Versioning.TargetFramework( \".NETCoreApp,Version=v9.0\", FrameworkDisplayName = \".NET 9.0\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"CompileTime\", \"2026-07-29T06:05:00.6415890Z\" )]\r\n[assembly: global::System.Reflection.AssemblyVersion(\"0.0.118.0\")]\r\n[assembly: global::System.Reflection.AssemblyFileVersion(\"0.0.118.0\")]"
        },
        {
            "Ident": "bobicolab.bobicosoftbones",
            "Path": "Code/BoBiCo Soft-Physics/Components/BoBiCoSoftBoneCollider.cs",
            "FileName": "BoBiCoSoftBoneCollider.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 336846,
            "IsPrivate": false,
            "Code": "//=========================================================================================================================\n// BoBiCo Soft-Bone Collider Component\n//=========================================================================================================================\n\nusing System;\n\n[Title( \"SoftBone Collider\" )]\n[Category( \"BoBiCo\" )]\n[Icon( \"radio_button_unchecked\" )]\n[HelpUrl( \"https://bobico-lab.vercel.app/softbone-docs/bobico-softbones/core-components/softbone-collider\" )]\npublic sealed class BoBiCoSoftBoneCollider : Component, Component.ExecuteInEditor\n{\npublic enum ColliderShape\n{\n\t/// <summary>\n\t/// A simple spherical collider.\n\t/// Best for rounded body parts and lightweight collision detection.\n\t/// </summary>\n\tSphere,\n\n\t/// <summary>\n\t/// A capsule-shaped collider.\n\t/// Recommended for most bones and elongated body parts.\n\t/// </summary>\n\tCapsule,\n\n\t/// <summary>\n\t/// A box-shaped collider.\n\t/// Useful for broad collision areas and block-like shapes.\n\t/// </summary>\n\tBox,\n\n\t/// <summary>\n\t/// A one-sided flat collider.\n\t/// Useful for flat surface areas.\n\t/// </summary>\n\tFlat,\n}\n\n//=========================================================================================================================\n// Collider Properties\n//=========================================================================================================================\n\n\t/// <summary>\n\t/// Offsets the position of the collider.\n\t/// </summary>\n\t[Property, Order( 1 ), Title( \"Position Offset\" ), Group( \"Collider Properties\" )]\n\tpublic Vector3 PositionOffset { get; set; }\n\n\t/// <summary>\n\t/// Offsets the rotation of the collider.\n\t/// </summary>\n\t[Property, Order( 2 ), Title( \"Rotation Offset\" ), Group( \"Collider Properties\" )]\n\tpublic Angles RotationOffset { get; set; }\n\n\t/// <summary>\n\t/// Target bone to which the collider is attached. \n\t/// </summary>\n\t[Property, Order( 3 ), Title( \"Target Bone\" ), Group( \"Collider Properties\" )]\n\tpublic GameObject TargetBone { get; set; }\n\n//=========================================================================================================================\n// Shape Settings\n//=========================================================================================================================\n\n    /// <summary>\n    /// Shape used for this chain's collision volume.\n    /// Different shapes provide different collision behavior and coverage.\n    /// </summary>\n\t[Property, Order( 4 ), Title( \"Collider Shape\" ), Group( \"Shape Settings\" )]\n\tpublic ColliderShape Shape { get; set; } = ColliderShape.Sphere;\n\n    /// <summary>\n    /// Radius of the chain's collision volume.\n    /// Larger values create thicker collision boundaries.\n    /// </summary>\n\t[Property, Order( 5 ), Title( \"Collider Radius\" ), Group( \"Shape Settings\" ), HideIf( nameof( Shape ), ColliderShape.Box )]\n\tpublic float ColliderRadius { get; set; } = 2f;\n\n    /// <summary>\n    /// Height of Capsule collider.\n    /// </summary>\n\t[Property, Order( 6 ), Title( \"Collider Height\" ), Group( \"Shape Settings\" ), ShowIf( nameof( Shape ), ColliderShape.Capsule )]\n\tpublic float ColliderHeight { get; set; } = 10f;\n\n    /// <summary>\n    /// Size of Box colliders.\n    /// Only applies when the collider shape is set to Box.\n    /// </summary>\n\t[Property, Order( 7 ), Title( \"Box Dimension\" ), Group( \"Shape Settings\" ), ShowIf( nameof( Shape ), ColliderShape.Box )]\n\tpublic Vector3 BoxSize { get; set; } = new Vector3( 1.25f, 1.25f, 1.25f );\n\n//=========================================================================================================================\n// Collider Gizmo Settings\n//=========================================================================================================================\n\n    /// <summary>\n    /// Displays the chain's collider in the Scene view.\n    /// Useful for visualizing collision shapes and sizes.\n    /// </summary>\n\t[Property, Order( 8 ), Title( \"Collider Gizmo\" ), Group( \"Gizmo Settings\" )]\n\tpublic bool ShowColliderGizmo { get; set; } = true;\n\n    /// <summary>\n    /// Keeps the collider gizmo visible even when the chain is not selected.\n    /// Useful for debugging collision setups.\n    /// </summary>\n\t[Property, Order( 9 ), Title( \"Always Show Gizmo\" ), Group( \"Gizmo Settings\" ), ShowIf( nameof( ShowColliderGizmo ), true )]\n\tpublic bool AlwaysShowGizmo { get; set; }\n\n    /// <summary>\n    /// Color used for rendering the collider gizmo.\n    /// </summary>\n\t[Property, Order( 10 ), Title( \"Collider Gizmo Color\" ), Group( \"Gizmo Settings\" ), ShowIf( nameof( ShowColliderGizmo ), true )]\n\tpublic Color ColliderGizmoColor { get; set; } = Gizmo.Colors.Green;\n\n    /// <summary>\n    /// Controls the transparency of the collider gizmo.\n    /// Lower values make the visualization less intrusive.\n    /// </summary>\n\t[Property, Order( 11 ), Range( 0, 1 ), Title( \"Gizmo Opacity\" ), Group( \"Gizmo Settings\" ), ShowIf( nameof( ShowColliderGizmo ), true )]\n\tpublic float ColliderGizmoOpacity { get; set; } = 1f;\t\n\n//=========================================================================================================================\n// Collider Shape Calculation\n//=========================================================================================================================\n\tpublic bool TryGetWorldShape( SkinnedModelRenderer renderer, out SoftBoneColliderShape shape )\n\t{\n\t\tshape = default;\n\t\tvar boneName = TargetBone is not null && TargetBone.IsValid() ? TargetBone.Name?.Trim() : \"\";\n\t\tif ( string.IsNullOrWhiteSpace( boneName ) )\n\t\t\treturn false;\n\n\t\tvar hasBoneTransform = renderer is not null && renderer.IsValid() && renderer.Model?.Bones is not null && renderer.Model.Bones.HasBone( boneName );\n\t\tif ( hasBoneTransform )\n\t\t{\n\t\t\tvar bone = renderer.Model.Bones.GetBone( boneName );\n\t\t\tif ( renderer.TryGetBoneTransformAnimation( bone, out var animatedTransform ) || renderer.TryGetBoneTransform( bone, out animatedTransform ) )\n\t\t\t\treturn BuildWorldShape( animatedTransform, out shape );\n\t\t}\n\n\t\treturn BuildWorldShape( TargetBone.WorldTransform, out shape );\n\t}\n\n\tprivate bool BuildWorldShape( Transform boneTransform, out SoftBoneColliderShape shape )\n\t{\n\t\tshape = default;\n\n\t\t// Swap pitch/yaw to match S&box bone forward = X axis\n\t\tvar fixedRotOffset = new Angles( RotationOffset.roll, RotationOffset.pitch, RotationOffset.yaw );\n\t\tvar rotation = boneTransform.Rotation * fixedRotOffset.ToRotation();\n\t\tvar radius = Math.Max( ColliderRadius, 0.001f );\n\t\tvar height = Math.Max( ColliderHeight, radius * 2f );\n\t\t// Collider offsets use the existing VRC-style axis order: X=up, Y=forward, Z=right.\n\t\tvar remappedOffset = new Vector3( PositionOffset.y, PositionOffset.z, PositionOffset.x );\n\t\tvar center = boneTransform.PointToWorld( remappedOffset );\n\n\t\tif ( Shape == ColliderShape.Sphere || (Shape == ColliderShape.Capsule && height <= radius * 2f) )\n\t\t{\n\t\t\tshape = SoftBoneColliderShape.Sphere( center, radius );\n\t\t\treturn true;\n\t\t}\n\n\t\tif ( Shape == ColliderShape.Capsule )\n\t\t{\n\t\t\tvar halfLine = Math.Max( 0f, height * 0.5f - radius );\n\t\t\t// VRC capsules are authored along their local Up axis. Using Forward here\n\t\t\t// made S&box Roll rotate around the capsule's own length, which is invisible.\n\t\t\tvar axis = rotation * Vector3.Forward;\n\t\t\tshape = SoftBoneColliderShape.Capsule( center - axis * halfLine, center + axis * halfLine, radius );\n\t\t}\n\t\telse if ( Shape == ColliderShape.Box )\n\t\t\tshape = SoftBoneColliderShape.Box( center, rotation, new Vector3( Math.Max( BoxSize.z, 0.001f ), Math.Max( BoxSize.x, 0.001f ), Math.Max( BoxSize.y, 0.001f ) ) );\n\t\telse\n\t\t\tshape = SoftBoneColliderShape.Flat( center, rotation * Vector3.Up );\n\t\treturn true;\n\t}\n\n//=========================================================================================================================\n// Collider Gizmo Drawing\n//=========================================================================================================================\n\n\tprotected override void DrawGizmos()\n\t{\n\t\tbase.DrawGizmos();\n\t\tDrawColliderGizmo( AlwaysShowGizmo || Gizmo.IsSelected );\n\t}\n\n\tinternal void DrawColliderGizmo( bool visible, bool force = false )\n\t{\n\t\tif ( !visible || ( !force && !ShowColliderGizmo ) || !TryGetWorldShape( FindRenderer(), out var shape ) )\n\t\t\treturn;\n\n\t\t// Draw directly in world space so the sphere radius is never distorted by\n\t\t// the component's scale (which caused the squished ellipsoid).\n\t\tusing ( Gizmo.Scope( \"SoftBoneCollider\" ) )\n\t\t{\n\t\t\tGizmo.Transform = new Transform( Vector3.Zero, Rotation.Identity, 1f );\n\t\t\tGizmo.Draw.IgnoreDepth = true;\n\t\t\tGizmo.Draw.LineThickness = 1;\n\t\t\tGizmo.Draw.Color = ColliderGizmoColor.WithAlpha( ColliderGizmoOpacity );\n\n\t\t\tif ( shape.IsSphere )\n\t\t\t\tGizmo.Draw.LineSphere( new Sphere( shape.Start, shape.Radius ) );\n\t\t\telse if ( shape.Type == SoftBoneColliderShapeType.Capsule )\n\t\t\t\tGizmo.Draw.LineCapsule( new Capsule( shape.Start, shape.End, shape.Radius ) );\n\t\t\telse if ( shape.Type == SoftBoneColliderShapeType.Box )\n\t\t\t{\n\t\t\t\tGizmo.Transform = new Transform( shape.Center, shape.Rotation, 1f );\n\t\t\t\tGizmo.Draw.LineBBox( BBox.FromPositionAndSize( Vector3.Zero, shape.Size ) );\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tvar tangent = Vector3.Cross( shape.Normal, Math.Abs( Vector3.Dot( shape.Normal, Vector3.Up ) ) < 0.99f ? Vector3.Up : Vector3.Right ).Normal;\n\t\t\t\tvar bitangent = Vector3.Cross( shape.Normal, tangent ).Normal;\n\t\t\t\tvar size = ColliderRadius;\n\t\t\t\tvar a = shape.Center + (tangent + bitangent) * size;\n\t\t\t\tvar b = shape.Center + (tangent - bitangent) * size;\n\t\t\t\tvar c = shape.Center + (-tangent - bitangent) * size;\n\t\t\t\tvar d = shape.Center + (-tangent + bitangent) * size;\n\t\t\t\tGizmo.Draw.Line( a, b ); Gizmo.Draw.Line( b, c ); Gizmo.Draw.Line( c, d ); Gizmo.Draw.Line( d, a );\n\t\t\t\tGizmo.Draw.Line( shape.Center, shape.Center + shape.Normal * 8f );\n\t\t\t}\n\t\t}\n\t}\n\n//=========================================================================================================================\n// Collider Bone Lookup\n//=========================================================================================================================\n\n\tprivate SkinnedModelRenderer FindRenderer()\n\t{\n\t\tvar targetRenderer = TargetBone?.GetComponentInParent<SkinnedModelRenderer>( true, true );\n\t\tif ( targetRenderer is not null && targetRenderer.IsValid() )\n\t\t\treturn targetRenderer;\n\t\treturn null;\n\t}\n\n\tinternal SkinnedModelRenderer GetOwningRenderer() => FindRenderer();\n\n\tprotected override void OnValidate()\n\t{\n\t\tColliderRadius = Math.Max( ColliderRadius, 0.001f );\n\t\tColliderHeight = Math.Max( ColliderHeight, ColliderRadius * 2f );\n\t\tBoxSize = new Vector3( Math.Max( BoxSize.x, 0.001f ), Math.Max( BoxSize.y, 0.001f ), Math.Max( BoxSize.z, 0.001f ) );\n\t\tColliderGizmoOpacity = Math.Clamp( ColliderGizmoOpacity, 0f, 1f );\n\t}\n}"
        },
        {
            "Ident": "bobicolab.bobicosoftbones",
            "Path": "Code/BoBiCo Soft-Physics/Components/BoBiCoSoftBoneColliderGroup.cs",
            "FileName": "BoBiCoSoftBoneColliderGroup.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 336846,
            "IsPrivate": false,
            "Code": "//=========================================================================================================================\n// BoBiCo Soft-Bone Collider Group Component\n//=========================================================================================================================\n\nusing System.Collections.Generic;\n\n[Title( \"SoftBone Collider Group\" )]\n[Category( \"BoBiCo\" )]\n[Icon( \"category\" )]\n[HelpUrl( \"https://bobico-lab.vercel.app/softbone-docs/bobico-softbones/core-components/softbone-collider-group\" )]\npublic sealed class BoBiCoSoftBoneColliderGroup : Component, Component.ExecuteInEditor\n{\n\n//=========================================================================================================================\n// Group Setup\n//=========================================================================================================================\n\n\t/// <summary>\n\t/// List of colliders that are part of this group. These colliders will be used for collision detection with soft-bone chains.\n\t/// </summary>\n\t[Property, Title( \"Colliders\" ), Group( \"Colliders\" )]\n\tpublic List<GameObject> Colliders { get; set; } = new();\n\n    // Collect all valid colliders from the group and add them to the output list, avoiding duplicates using the seen set.\n\tpublic void CollectColliders( List<BoBiCoSoftBoneCollider> output, HashSet<BoBiCoSoftBoneCollider> seen )\n\t{\n\t\tif ( !Enabled || Colliders is null )\n\t\t\treturn;\n\n\t\tforeach ( var colliderObject in Colliders )\n\t\t{\n\t\t\tif ( colliderObject is null || !colliderObject.IsValid() )\n\t\t\t\tcontinue;\n\n\t\t\tvar collider = colliderObject.GetComponent<BoBiCoSoftBoneCollider>();\n\t\t\tif ( collider is not null && collider.Enabled && seen.Add( collider ) )\n\t\t\t\toutput.Add( collider );\n\t\t}\n\t}\n}\n"
        }
    ]
}