Editor/EffigyEditor/EffigyViewport.Rig.cs
using Editor;
using Effigy;
using Sandbox;
using System;

namespace Marionette.EditorTools;

/// <summary>
/// Placing bones by clicking the model — separate from EffigyViewport.Sketching.cs's picking
/// modes because this one writes new geometry into a skeleton rather than selecting existing
/// geometry, and separate from the pose gizmo in EffigyViewport.cs because that drags a bone
/// that already exists.
///
/// The Skeleton itself is owned by EffigyRigPanel, not the viewport — same division as
/// BodyPickMode, where the viewport only ever reports what was clicked and the owner decides
/// what that means. Chaining (each click extending the previous point into a new bone, parented
/// to it) is Blender's armature-extrude gesture, and the reason the panel — not the viewport —
/// tracks the pending head and parent: it is the one deciding when a chain resets.
/// </summary>
internal sealed partial class EffigyViewport
{
	/// <summary>While true, left-clicking the model reports the point via BonePointPicked instead
	/// of selecting or posing a bone.</summary>
	public bool BoneToolActive { get; set; }

	/// <summary>World point of the chain's last placed joint, for the preview line. Set by the
	/// panel — null means the next click starts a fresh chain rather than extending one.</summary>
	public Vec3? PendingBoneHead { get; set; }

	/// <summary>Fires with the world-space point clicked while the tool is active.</summary>
	public Action<Vec3> BonePointPicked { get; set; }

	/// <summary>Escape while the tool is active. The panel decides whether that closes the current
	/// chain (if one is open) or turns the tool off entirely (if not) — the viewport has no notion
	/// of which, since PendingBoneHead is the panel's state mirrored here for drawing only.</summary>
	public Action BoneToolEscape { get; set; }

	private static readonly Color BoneToolPreviewColor = new( 1f, 0.85f, 0.2f, 0.55f );

	/// <summary>Length of the ghost bone shown before the first click of a chain has a real tail to
	/// aim at — a guess, since nothing here knows the model's scale. Matches DogBone's own knob
	/// math (knobR = boneLen * 0.16) against BoneHandleRadius (0.8) closely enough that the ghost
	/// reads as the same size class as a committed bone rather than conspicuously different.</summary>
	private const float PendingBonePreviewLength = 5f;

	/// <summary>
	/// Highlight the point under the cursor with the actual dog-bone shape the click would commit
	/// — not a placeholder dot, the real DrawDogBone — so what you see before clicking is what you
	/// get after, for BOTH clicks of the gesture:
	///
	/// - Second click of a segment: head is the pending point from the first click, tail is the
	///   cursor. Exact preview — the real head, the real tail, the real orientation.
	/// - First click of a chain: there is no head yet, so the tail is a guess — a fixed length
	///   standing off the surface along its normal. Direction and exact length are not meaningful
	///   yet (only the second click fixes those); what this answers is "a bone will appear roughly
	///   here, roughly this size," which a bare dot didn't.
	///
	/// Drawn depth-ignoring like the committed skeleton (DrawRigSkeleton) so the preview reads
	/// through the mesh the same way placed bones already do, rather than disappearing the moment
	/// it points away from the camera.
	///
	/// Same raycast MeshRaycast/_displayBodies pairing as the face and body picks — bones are
	/// placed ON the model, not in empty space.
	/// </summary>
	private void BoneToolFrame()
	{
		if ( !BoneToolActive || !_canvasHasCursor )
			return;

		var ray = Gizmo.CurrentRay;
		var origin = new Vec3( ray.Position.x, ray.Position.y, ray.Position.z );
		var direction = new Vec3( ray.Forward.x, ray.Forward.y, ray.Forward.z );

		if ( MeshRaycast.Raycast( _displayBodies, origin, direction ) is not { } hit )
			return;

		var point = hit.Hit.Point;
		var world = new Vector3( point.x, point.y, point.z );

		Gizmo.Draw.Color = BoneToolPreviewColor;
		Gizmo.Draw.IgnoreDepth = true;

		if ( PendingBoneHead is { } head && (point - head).Length > 0.01f )
		{
			var headWorld = new Vector3( head.x, head.y, head.z );
			var (xAxis, zAxis) = PreviewBasis( head, point );
			DrawDogBone( headWorld, world, xAxis, zAxis );
		}
		else if ( PendingBoneHead is null )
		{
			var tailGuess = point + hit.Hit.Normal * PendingBonePreviewLength;

			if ( (tailGuess - point).Length > 0.01f )
			{
				var tailWorld = new Vector3( tailGuess.x, tailGuess.y, tailGuess.z );
				var (xAxis, zAxis) = PreviewBasis( point, tailGuess );
				DrawDogBone( world, tailWorld, xAxis, zAxis );
			}
		}
		else
		{
			Gizmo.Draw.SolidSphere( world, BoneHandleRadius * 0.4f, 8, 8 );
		}

		Gizmo.Draw.IgnoreDepth = false;

		if ( Gizmo.WasLeftMousePressed )
			BonePointPicked?.Invoke( point );
	}

	/// <summary>
	/// The same head→tail aim-and-perpendicular construction Skeleton.LocalFromWorldPoints uses,
	/// kept here only far enough to get two axes for the PREVIEW's cross-section — the kernel
	/// owns the real thing once the click commits. Not shared code because it can't be: the kernel
	/// has no notion of a Vector3/Gizmo and shouldn't grow one for a rendering concern. Copied
	/// exactly, including the same seed-axis threshold, so the preview's roll matches the bone
	/// AddBoneFromPoints actually creates rather than merely resembling it.
	/// </summary>
	private static (Vector3 xAxis, Vector3 zAxis) PreviewBasis( Vec3 head, Vec3 tail )
	{
		var along = tail - head;
		var y = along / along.Length;

		var seed = MathF.Abs( y.x ) < 0.9f ? new Vec3( 1, 0, 0 ) : new Vec3( 0, 0, 1 );
		var x = Vec3.Cross( seed, y ).Normal;
		var z = Vec3.Cross( x, y );

		return (new Vector3( x.x, x.y, x.z ), new Vector3( z.x, z.y, z.z ));
	}
}