Editor/EffigyEditor/EffigyViewport.FaceDrag.cs
using Editor;
using Effigy;
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Marionette.EditorTools;

/// <summary>
/// Dragging a selected face in the viewport — push and pull with the mouse.
///
/// WHAT MAKES IT FEEL LIKE DIRECT EDITING rather than a dialog with a face in it. Move Face and the
/// face extrude both do the geometry already; what they ask for is a number typed into a panel, and
/// typing 0.4 to find out whether 0.4 was right is not how anybody shapes a part. Select a face,
/// drag it, watch the solid grow.
///
/// ONE ARROW, ALONG THE FACE'S OWN NORMAL. Not three. Gizmo.Control.Arrow takes the axis directly
/// and hands back a scalar distance along it, which is exactly the shape of a push-pull: one degree
/// of freedom, and the handle says so.
///
/// THIS REPLACED A THREE-AXIS Gizmo.Control.Position, AND THE ARGUMENT FOR THAT IS WORTH KEEPING
/// because it was a good argument and it was still wrong. All three arrows WERE honest: the drag
/// goes to FaceMove in TRANSLATE mode, where each face travels by dot(normal, displacement), so
/// dragging one planar face sideways correctly moved nothing — sliding a plane within itself does
/// not change the solid — and on the facing pair of a wall those same two arrows slid the wall.
/// Nothing was special-cased and both cases were right.
///
/// It was still the wrong interaction, for the reason honesty does not settle: on the single-face
/// pick, which is overwhelmingly the common one, two of the three arrows did nothing when dragged.
/// An inert arrow is not read as "the geometry says this is a no-op". It is read as broken, and it
/// invites the drag that teaches you it was pointless. po, 4 September 2026, having used it:
/// "i only want the gizmo to be able to pull in the direction the face is actually facing, not up
/// and down or left and right."
///
/// SLIDING A WALL IS NOW MOVE FACE'S TRANSLATE MODE, typed, rather than a handle that appears only
/// for a selection shape the user cannot see they have made. That is the trade, and it is the right
/// way round: the common case gets an honest handle and the rare one keeps a dialog it already had.
///
/// THE 10-DEGREE CULL IS THE ENGINE'S AND IT IS PROTECTIVE. Arrow hides itself when its axis comes
/// within cullAngle of the view direction, because screen movement stops mapping to axis movement
/// there. With three arrows something else was always grabbable; with one, looking straight down a
/// face's normal leaves no handle. That is correct — the drag would be meaningless at that angle —
/// but it means the handle can vanish, so orbit a few degrees rather than assuming it broke.
///
/// ONLY WHILE A TOOL THAT CONSUMES A FACE IS OPEN, and this is the part that keeps it parametric.
/// An earlier cut of this put the arrows on any face you clicked, so dragging one appended a Move
/// Face to the tree behind your back — which is a mesh editor wearing a history's clothes. In a
/// parametric modeller a face does not move because you shoved it; it moves because a FEATURE says
/// it does. So the handle is a way of answering the open feature's distance with the mouse instead
/// of the keyboard, and it exists exactly as long as that feature's dialog does.
///
/// THE VIEWPORT RESOLVES THE DRAG AND STOPS THERE. What the displacement MEANS — which parameter it
/// writes, whether that is a distance along a normal or a direction and a length — is a question
/// about the feature, and that lives with the dialog that owns it. Same division as the face
/// context menu.
/// </summary>
internal sealed partial class EffigyViewport
{
	/// <summary>
	/// Set by the dialog of a feature that consumes faces, for as long as that dialog is open.
	///
	/// The gate that keeps this parametric. Without it the arrows sit on every face a click selects,
	/// and dragging one writes a feature into the tree that nobody asked for.
	/// </summary>
	public bool FaceDragEnabled { get; set; }

	/// <summary>Raised once when a drag starts, before anything has moved.</summary>
	public Action FaceDragBegan { get; set; }

	/// <summary>
	/// Raised every frame the handle moves: the displacement accumulated since the drag started —
	/// not the per-frame delta — and the axis the handle is aligned to.
	///
	/// The total rather than something to integrate, because the consumer sets a parameter from it
	/// and a parameter is a value, not an increment. The axis comes along because a feature that
	/// stores one distance (an Extrude) needs to know what to measure it against, and by the time
	/// this fires the face has already moved away from the normal it started with.
	/// </summary>
	public Action<Vec3, Vec3> FaceDragMoved { get; set; }

	/// <summary>Raised when the button comes up.</summary>
	public Action FaceDragEnded { get; set; }

	/// <summary>True while the handle is being dragged. Idle picking stands down for the duration,
	/// or the click that ends the drag would also re-pick whatever is under the cursor.</summary>
	public bool IsDraggingFace => _draggingFace;

	private bool _draggingFace;
	private Vector3 _faceDragAnchor;
	private Vector3 _faceDragDelta;
	private Vector3 _faceDragNormal = Vector3.Up;

	/// <summary>
	/// The handle for the current face selection, and the drag it reports.
	///
	/// Drawn only while a face-consuming tool is open and holding faces. A sketch being drawn, the
	/// sculpt brush and the bone tool all have a click of their own, and a set of arrows floating
	/// over the model while one of them is armed is an invitation to a click that will not do what
	/// it looks like.
	///
	/// The faces come from SelectedFaces — what the DIALOG is holding — rather than from the idle
	/// selection. They are the same faces most of the time, and the difference is the whole point:
	/// the handle answers the open feature, so it follows that feature's set.
	/// </summary>
	private void FaceDragFrame()
	{
		if ( !FaceDragEnabled || FaceDragMoved is null || SelectedFaces is not { Count: > 0 } )
		{
			EndFaceDrag();
			return;
		}

		if ( (IsSketching || IsSculpting || IsPainting || IsMaterialBrushing || IsNoting || BoneToolActive) && !_draggingFace )
		{
			EndFaceDrag();
			return;
		}

		// MID-DRAG THE FACE IS NOT WHERE ITS REFERENCE SAYS IT IS. A FaceRef resolves geometrically,
		// by plane and anchor point, and the face this one names is travelling several units away
		// from both while the button is held. Re-resolving every frame would eventually find nothing
		// — or worse, find a DIFFERENT face — and the drag would abort halfway or jump to the far
		// side of the part. So the axis is read once, when the drag starts, and held.
		if ( _draggingFace )
		{
			DrawFaceHandle( _faceDragAnchor + _faceDragDelta, _faceDragNormal );
			return;
		}

		if ( !TryFaceHandle( out var centre, out var normal ) )
		{
			EndFaceDrag();
			return;
		}

		DrawFaceHandle( centre, normal );
	}

	/// <summary>
	/// The arrow, and the drag it reports.
	///
	/// ANCHORED WHERE THE DRAG STARTED plus what has been dragged so far — never at the face's live
	/// centroid. The face moves as you drag it, so re-reading the centroid every frame would add that
	/// movement to the handle a second time and the arrow would run away from the cursor.
	///
	/// THE SCOPE CARRIES NO ROTATION, so the axis handed to Arrow is the world-space normal as it
	/// stands. That is why this no longer needs the Rotation.LookAt dance the three-axis version did:
	/// there is no basis to build when there is only one axis to point.
	///
	/// ONE ARROW STILL PUSHES BOTH WAYS. Dragging back past the tail gives a negative distance and
	/// the face goes in, which is what Onshape's push-pull does with the same single arrow. A second
	/// arrow along -normal would say so more loudly and would also double the hitboxes at the exact
	/// spot the user is aiming; not worth it.
	/// </summary>
	private void DrawFaceHandle( Vector3 origin, Vector3 normal )
	{
		using var scope = Gizmo.Scope( "face-drag", new Transform( origin ) );

		Gizmo.Hitbox.DepthBias = 0.01f;

		if ( Gizmo.Control.Arrow( "face-pull", normal, out var distance ) )
		{
			if ( !_draggingFace )
			{
				_draggingFace = true;
				_faceDragAnchor = origin;
				_faceDragNormal = normal;
				_faceDragDelta = Vector3.Zero;
				FaceDragBegan?.Invoke();
			}

			// Accumulated rather than assigned, for the same reason Position's displacement was:
			// these controls report the change since the last frame and return false on any frame
			// the value did not move (RigViewport.cs:1621). The axis is the one the drag STARTED
			// with, not the live normal, so the total stays a straight line even though the face
			// it was taken from is travelling.
			_faceDragDelta += _faceDragNormal * distance;

			FaceDragMoved.Invoke(
				new Vec3( _faceDragDelta.x, _faceDragDelta.y, _faceDragDelta.z ),
				new Vec3( _faceDragNormal.x, _faceDragNormal.y, _faceDragNormal.z ) );
			return;
		}

		EndFaceDrag();
	}

	private void EndFaceDrag()
	{
		if ( !_draggingFace )
			return;

		_draggingFace = false;
		_faceDragDelta = Vector3.Zero;
		FaceDragEnded?.Invoke();
	}

	/// <summary>
	/// Where the handle sits and which way it points: the centroid of the selected faces, and the
	/// average of their normals.
	///
	/// AVERAGED RATHER THAN TAKEN FROM THE FIRST, because a selection of several faces has no first
	/// in any sense the user would recognise — clicking the same two faces in the other order would
	/// point the arrow somewhere else. On the facing pair that Move Face exists for, the two normals
	/// cancel and there is no meaningful axis at all; that case falls back to the first face's normal
	/// so the arrow still points along the wall's thickness, which is the direction that means
	/// something there.
	/// </summary>
	private bool TryFaceHandle( out Vector3 centre, out Vector3 normal )
	{
		centre = Vector3.Zero;
		normal = Vector3.Up;

		var sum = Vec3.Zero;
		var normals = Vec3.Zero;
		var first = Vec3.Zero;
		var found = 0;

		foreach ( var reference in SelectedFaces )
		{
			if ( !FacePlane.TryResolveFace( _displayBodies, reference, out var body, out var index ) )
				continue;

			var face = body.Mesh.Faces[index];
			var n = body.Mesh.FaceNormal( face );

			sum += body.Mesh.FaceCentroid( face );
			normals += n;

			if ( found == 0 )
				first = n;

			found++;
		}

		if ( found == 0 )
			return false;

		var mid = sum / found;
		var axis = normals.LengthSquared > 1e-6f ? normals.Normal : first;

		centre = new Vector3( mid.x, mid.y, mid.z );
		normal = new Vector3( axis.x, axis.y, axis.z );

		return true;
	}
}