Editor/EffigyEditor/EffigyWindow.Workspaces.cs
using Editor;
using Effigy;
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Marionette.EditorTools;

// ============================================================================
//  Workspaces — CAD, Sculpt, Paint, Rig.
//
//  WHY THIS FILE EXISTS AT ALL. Every toolset the editor grew went onto the one
//  stage bar, and the only thing that said which was showing was a word painted
//  small at the right of the tab row. Rig did not even get that: its tools lived
//  in a right-hand dock while every other toolset lived in the bar, so starting
//  a rig was a different KIND of action from starting a sculpt for no reason a
//  user could see. EffigyWorkspaceBar's header has the rest of that argument.
//
//  WHAT A WORKSPACE DRIVES. Three things, and the point of putting them together
//  is that they were previously three unrelated decisions made in three places:
//
//    1. THE STAGE BAR — which stage set is on it. Already existed as
//       EffigyBarMode; this file adds the fourth member and its stage set.
//    2. THE DOCKS — Materials matters in Paint, the rig tree matters in Rig, and
//       neither wants to be open the rest of the time. Per-workspace, remembered.
//    3. THE VIEWPORT'S INPUT — which of its eleven partial files owns a click.
//       Enforced AT THE DOOR (see LeaveCurrentWorkspace) rather than by each
//       handler re-checking the others, which is what it grew into.
//
//  NO SEPARATE WINDOWS, AND NO SECOND VIEWPORT. That was the first idea and it
//  is the wrong one. Each viewport is a live scene render, so four of them is
//  four times the GPU for panes nobody is looking at; worse, one mesh behind four
//  cameras means four selections, four undo stacks and four answers to "which
//  body is active", and the undo snapshot in EffigyWindow is window-global. And
//  the pipeline is not actually linear — paint reveals a bad face and you go back
//  to re-extrude it — so separate windows would tax the move people make most.
//  One window, one document, four ways of looking at it.
//
//  THE WORKSPACE IS NOT STORED. It is derived from _barMode, which the enter and
//  finish paths already maintain and already treat as the truth. A second field
//  would be a second thing to keep in step, and the one bug this whole change is
//  meant to make impossible is chrome disagreeing with mode.
// ============================================================================

public sealed partial class EffigyWindow
{
	private EffigyWorkspaceBar _workspaceBar;

	/// <summary>The rig stage set, built once at startup alongside the other four.</summary>
	private List<EffigyStage> _rigStages;

	private EffigyStageTool _boneTool, _boneFromPartTool, _boneAssignTool, _boneMirrorTool, _boneDeleteTool;
	private EffigyStageTool _boneSoftTool, _softPreviewTool, _softRestTool;
	private EffigyStageTool _rigSubdivideTool, _rigRemeshTool;

	/// <summary>Which part-studio stage was last looked at in the Rig workspace, so leaving and
	/// coming back lands where you were — same courtesy _partStage does for CAD.</summary>
	private int _rigStage;

	/// <summary>
	/// Which workspace the window is in, derived rather than stored.
	///
	/// SKETCH READS AS CAD. A sketch is opened inside CAD and finished again — the stage bar goes
	/// somewhere else for the duration, but you did not leave the workspace, and lighting a
	/// different pill in the switcher while someone draws a rectangle would be the switcher lying
	/// about a thing it exists to state.
	/// </summary>
	private EffigyWorkspace CurrentWorkspace => BarMode switch
	{
		EffigyBarMode.Sculpt => EffigyWorkspace.Sculpt,
		EffigyBarMode.Paint => EffigyWorkspace.Paint,
		EffigyBarMode.Rig => EffigyWorkspace.Rig,
		_ => EffigyWorkspace.Cad,
	};

	/// <summary>
	/// The bar mode, and the one place that notices it changed.
	///
	/// A PROPERTY RATHER THAN THE FIELD IT WAS, because six different methods assign it and every
	/// one of them would otherwise have to remember to re-light the switcher and re-lay the docks
	/// afterwards. Five of them remembering and one forgetting is precisely the failure this
	/// change exists to remove, and "you must also call SyncWorkspace" is a rule that only holds
	/// until the next tool gets added. Here it cannot be forgotten because there is nowhere to
	/// forget it.
	/// </summary>
	private EffigyBarMode BarMode
	{
		get => _barMode;
		set
		{
			if ( _barMode == value )
				return;

			var before = CurrentWorkspace;

			_barMode = value;

			var after = CurrentWorkspace;

			SyncViewportMode();

			if ( before != after )
				ApplyWorkspaceDocks( before, after );

			if ( _workspaceBar is not null )
				_workspaceBar.Selected = after;
		}
	}

	private EffigyBarMode _barMode = EffigyBarMode.Part;

	/// <summary>
	/// Push the workspace down onto the viewport's input. In the rig workspace a click is a bone
	/// or a whole part (Assign / Bone from Part need both); CAD idle selection runs everywhere
	/// else. See EffigyViewport.RigMode.
	///
	/// NOT ONLY FROM THE BarMode SETTER, which is where it belongs and is not sufficient on its
	/// own: that setter returns early when the mode has not changed, so a hotload taken while the
	/// rig workspace was already open would leave a freshly-defaulted RigMode at false with nothing
	/// left to switch it back on - the workspace would look right and behave as though it were CAD.
	/// Called from RebuildStages for exactly that, and it is cheap enough to call from anywhere.
	/// </summary>
	private void SyncViewportMode()
	{
		if ( _viewport is not null )
			_viewport.RigMode = CurrentWorkspace == EffigyWorkspace.Rig;
	}

	// --- switching ------------------------------------------------------------------------------

	/// <summary>
	/// A workspace was asked for. This is the whole of what a pill click means.
	///
	/// ASKED FOR, NOT SET. Sculpt and Paint can both refuse at the door — a sculpt feature whose
	/// cage did not build, a body whose UVs cannot carry paint — and when they do, EnterSculpt and
	/// EnterPaint leave the bar mode alone and put the reason in the prompt. Because the switcher
	/// paints from CurrentWorkspace, which is derived from that mode, a refusal simply leaves the
	/// old pill lit. Nothing here has to know which refusals exist.
	/// </summary>
	private void SetWorkspace( EffigyWorkspace workspace )
	{
		if ( _viewport is null || _studio is null )
			return;

		// Re-clicking the workspace you are already in is not a no-op: it is the only way to ask
		// for the dock layout back after dragging it around, and it is what someone reaches for
		// when a panel has gone missing. Everything downstream is idempotent.
		if ( workspace == CurrentWorkspace )
		{
			ApplyWorkspaceDocks( workspace, workspace, force: true );
			return;
		}

		switch ( workspace )
		{
			case EffigyWorkspace.Cad:
				LeaveCurrentWorkspace();
				ShowPartStages( force: true );
				break;

			case EffigyWorkspace.Sculpt:
				EnterSculptWorkspace();
				break;

			case EffigyWorkspace.Paint:
				EnterPaintWorkspace();
				break;

			case EffigyWorkspace.Rig:
				EnterRig();
				break;
		}

		// The switcher is painted from CurrentWorkspace, and CurrentWorkspace only moved if the
		// entry above actually took. A refusal leaves the pill where it was, which is the honest
		// answer — see the summary.
		if ( _workspaceBar is not null )
			_workspaceBar.Selected = CurrentWorkspace;

		RefreshTutorial();
	}

	/// <summary>
	/// Close whatever session owns the pointer right now.
	///
	/// THE EXCLUSIVITY, IN ONE PLACE. Sketching, sculpting, painting and the bone tool all claim a
	/// left-click in the viewport, and each of the four entry points used to carry its own partial
	/// list of the other three to shut down first — EnterSculpt finished sketches and cancelled the
	/// bone tool, AddPaint finished sketches and sculpts, EnterPaint did both again, and nothing
	/// anywhere finished a paint before arming a bone. Four hand-maintained lists of three is four
	/// chances to miss one, and the one that was missed is exactly the pair that could both be live.
	///
	/// Every entry point now calls this instead. Adding a fifth pointer-owning mode later means
	/// adding one line here rather than finding four lists.
	/// </summary>
	private void LeaveCurrentWorkspace()
	{
		if ( _viewport is null )
			return;

		if ( _viewport.IsSketching )
			FinishSketch();

		if ( _viewport.IsSculpting )
			FinishSculpt();

		if ( _viewport.IsPainting )
			FinishPaint();

		// The material brush has no feature and no session to commit, so leaving it is only a
		// matter of disarming it - but it MUST be disarmed, or the ring keeps taking clicks in
		// whatever workspace you switched to.
		if ( _viewport.IsMaterialBrushing )
			LeaveMaterialBrush();

		if ( _viewport.IsWeightPainting )
			FinishWeightPaint();

		// The note pen (grease pencil) owns the click the same way every brush does, and it is the
		// one mode none of the entries below ever disarmed - so it stayed armed across a switch,
		// and the next sketch or sculpt also scribbled notes. Notes commit per stroke, so leaving
		// the pen is only a disarm, never a commit. UpdateNoteChecks puts the CAD bar's pen tick
		// back, so it is not still lit as armed when you come back.
		if ( _viewport.IsNoting )
		{
			_viewport.EndNotes();
			UpdateNoteChecks();
		}

		_rigPanel?.CancelBoneTool();

		// The soft preview solves against the rig every frame, and its whole point is the rig
		// workspace; leaving it would leave the bones sagging and swinging while you model. Stopped
		// rather than merely hidden - the bones snap back to their authored pose.
		if ( _viewport.SoftPreviewRunning )
			_viewport.StopSoftPreview();

		// The rig bar is the one mode with nothing to finish — no feature, no session, just a stage
		// set on the bar — so leaving it is only a matter of not still claiming to be in it. The
		// CAD stages are what everything falls back to.
		if ( BarMode == EffigyBarMode.Rig )
			BarMode = EffigyBarMode.Part;
	}

	// --- entering sculpt and paint from the switcher ---------------------------------------------

	/// <summary>
	/// "Sculpt" as a workspace, given that sculpting is a thing you do to a FEATURE.
	///
	/// This is the one place the switcher is not a plain mode flip, and it is worth being explicit
	/// about why: there is no ambient state called sculpting. EnterSculpt needs a SculptFeature,
	/// which needs a body under it and a spot in the tree. So the pill has to resolve one, and the
	/// order it tries is the order that makes the fewest surprises:
	///
	///   1. The sculpt you were last in this session, if it is still in the tree. Coming back to
	///      where you were is what leaving and returning should mean.
	///   2. The only sculpt in the document, if there is exactly one. With one candidate there is
	///      nothing to choose and asking would be ceremony.
	///   3. Otherwise land on the workspace's own bar — Subdivide and Sculpt — which is where those
	///      tools live now that they have left CAD. The brushes arrive once a sculpt is open.
	///
	/// Deliberately NOT "the last sculpt in the tree" for case 2 when there are several: which of
	/// three sculpts you meant is a real question, and guessing at it silently rolls the model back
	/// to a different place than you expected. With several and no memory, the home bar is the
	/// answer that cannot be wrong about your intent.
	/// </summary>
	private void EnterSculptWorkspace()
	{
		LeaveCurrentWorkspace();

		var sculpts = _studio.Features.OfType<SculptFeature>().ToList();

		var target = _lastSculptFeature is not null && sculpts.Contains( _lastSculptFeature )
			? _lastSculptFeature
			: sculpts.Count == 1 ? sculpts[0] : null;

		if ( target is not null )
		{
			EnterSculpt( target );
			return;
		}

		// Nothing to re-enter: land on the sculpt workspace's own bar, where Subdivide and Sculpt
		// live. The brushes arrive once a sculpt is actually open, not with the workspace itself.
		ShowSculptHome();
	}

	/// <summary>"Paint" as a workspace. Same three-step resolution as sculpt, for the same reason —
	/// see EnterSculptWorkspace, which carries the argument for both.</summary>
	private void EnterPaintWorkspace()
	{
		LeaveCurrentWorkspace();

		var paints = _studio.Features.OfType<PaintFeature>().ToList();

		var target = _lastPaintFeature is not null && paints.Contains( _lastPaintFeature )
			? _lastPaintFeature
			: paints.Count == 1 ? paints[0] : null;

		if ( target is not null )
		{
			EnterPaint( target );
			return;
		}

		ShowPaintHome();
	}

	/// <summary>
	/// Land on the Sculpt workspace's own bar, with no feature open.
	///
	/// BarMode.Sculpt with nothing sculpting is a state that did not exist before the landing bars:
	/// the workspace now has two faces — this home (Subdivide and Sculpt) and the brushes, which
	/// <see cref="EnterSculpt"/> swaps to once a feature is open. RebuildStages tells the two apart
	/// by asking the viewport whether a sculpt is actually running.
	/// </summary>
	private void ShowSculptHome()
	{
		if ( _stageBar is null )
			return;

		BarMode = EffigyBarMode.Sculpt;

		_stageBar.Mode = "SCULPT";
		_stageBar.SetFinish( null, null );
		_stageBar.SetStages( _sculptHomeStages );

		SetPrompt( _studio.Bodies.Count == 0
			? "Sculpt needs a body — draw a sketch and extrude it, or add a primitive first."
			: "Sculpt: add a Subdivide to build a cage, or a Sculpt to brush detail onto one." );
	}

	/// <summary>The Paint workspace's home — UV Project and Paint — with no feature open.</summary>
	private void ShowPaintHome()
	{
		if ( _stageBar is null )
			return;

		BarMode = EffigyBarMode.Paint;

		_stageBar.Mode = "PAINT";
		_stageBar.SetFinish( null, null );
		_stageBar.SetStages( _paintHomeStages );

		SetPrompt( _studio.Bodies.Count == 0
			? "Paint needs a body — draw a sketch and extrude it, or add a primitive first."
			: "Paint: add a UV Project to unwrap the mesh, or a Paint to brush colour on." );
	}

	/// <summary>
	/// The sculpt and paint features last entered, so the switcher can come back to them.
	///
	/// SEPARATE FROM _sculptFeature and _paintFeature, which are cleared on Finish — those answer
	/// "what am I editing", this answers "what was I editing", and the switcher needs the second
	/// one precisely when the first is null. Held as a reference and checked against the tree
	/// before use, because Undo can take the feature away without telling anyone.
	/// </summary>
	private SculptFeature _lastSculptFeature;

	private PaintFeature _lastPaintFeature;

	// --- the rig workspace ------------------------------------------------------------------------

	/// <summary>
	/// The rig tools, moved out of the panel's header and onto the bar.
	///
	/// SAME ACTIONS, NOT A SECOND COPY. Every one of these calls straight into EffigyRigPanel,
	/// which still owns the skeleton, the pending chain and every refusal — see the block above
	/// CancelBoneTool there. The panel keeps its buttons too: a bar tool and a panel button running
	/// one method is one control reachable from wherever you are looking, and the panel is where
	/// you are looking when you have just clicked a bone in the tree.
	///
	/// THREE TOOLS AND TWO STAGES, not the four-stage set the other workspaces run to. There is no
	/// Pose stage because there is no posing yet — the gizmo in EffigyViewport drags a bone that
	/// already exists and that is all — and a tab with nothing behind it is the thing this editor's
	/// icon set already refuses to draw. Stages get added when tools do.
	/// </summary>
	private List<EffigyStage> BuildRigStages()
	{
		var bones = new EffigyStage { Name = "Bones" };

		_boneTool = new EffigyStageTool
		{
			Icon = EffigyIcon.Bone,
			Label = "Add Bone",
			Tip = "Click the model to place a bone. Click again to extend a chain from it. "
				+ "Select a bone first to branch a new chain from ITS tail.",
			Checkable = true,
			Clicked = () =>
			{
				_rigPanel?.ToggleBoneTool();
				UpdateRigChecks();
			},
		};

		bones.Add( _boneTool );

		_boneFromPartTool = new EffigyStageTool
		{
			Icon = EffigyIcon.Bone,
			Label = "Bone from Part",
			Tip = "Measure the selected part and add a bone down its longest axis, already pinned "
				+ "to it. Select a bone first to parent the new one.",
			Clicked = () =>
			{
				MakeBonesFromSelectedParts();
				UpdateRigChecks();
			},
		};

		bones.Add( _boneFromPartTool );

		_boneDeleteTool = new EffigyStageTool
		{
			Icon = EffigyIcon.CutTool,
			Label = "Delete",
			Tip = "Delete the selected bone. Its children re-parent to its parent.",
			// No RecordUndo here, unlike most destructive buttons in this window: DeleteBone fires
			// RigChanging on its way in, and that IS RecordUndo (wired in BuildDocks). Recording
			// again would put two identical snapshots on the stack and cost two Ctrl+Zs to undo one
			// delete. Same reason Mirror does not record either.
			Clicked = () =>
			{
				_rigPanel?.DeleteSelected();
				UpdateRigChecks();
			},
		};

		bones.Add( _boneDeleteTool );

		var bind = new EffigyStage { Name = "Bind" };

		_boneAssignTool = new EffigyStageTool
		{
			Icon = EffigyIcon.BoneBind,
			Label = "Assign Body",
			Tip = "Select a bone, select a part, then press this to pin them. "
				+ "With no part selected it arms click-to-assign in the viewport. "
				+ "Anything left unassigned falls back to the nearest bone.",
			Checkable = true,
			Clicked = () =>
			{
				_rigPanel?.ToggleAssignBodyTool();
				UpdateRigChecks();
			},
		};

		bind.Add( _boneAssignTool );

		_boneMirrorTool = new EffigyStageTool
		{
			Icon = EffigyIcon.Mirror,
			Label = "Mirror",
			Tip = "Mirror the selected bone and everything under it across the centre line, "
				+ "onto the same parent.",
			Clicked = () =>
			{
				_rigPanel?.MirrorSelected();
				UpdateRigChecks();
			},
		};

		bind.Add( _boneMirrorTool );

		// --- Soft ---
		//
		// The stage the kernel has been waiting for. SoftBone, SoftPose and SoftSolver have been
		// written, tested and shipped to the game assembly since before this bar existed, and
		// RigDiagnostics has been checking soft bones the whole time - it will tell you one has a
		// zero cone - while nothing in the editor could make a bone soft for it to complain about.
		//
		// THE NUMBERS ARE NOT HERE. Stiffness, damping, weight and cone are properties of ONE bone,
		// the way its head and tail are, so they live in the rig panel's inspector beside them
		// rather than on a bar that is about verbs. What the bar gets is the verb - make this bone
		// soft - and the two controls that only make sense while something is wobbling.
		var soft = new EffigyStage { Name = "Soft" };

		_boneSoftTool = new EffigyStageTool
		{
			Icon = EffigyIcon.BoneSoft,
			Label = "Make Soft",
			Tip = "Let the selected bone lag and swing behind the pose. "
				+ "Stiffness, damping, weight and cone are in the Rig panel under the bone's head and tail.",
			Checkable = true,
			Clicked = () =>
			{
				_rigPanel?.ToggleSelectedSoft();
				UpdateRigChecks();
			},
		};

		soft.Add( _boneSoftTool );

		_softPreviewTool = new EffigyStageTool
		{
			Icon = EffigyIcon.SoftPreview,
			Label = "Preview",
			Tip = "Run the soft-bone solver on the rig, so you can see the swing while you tune it. "
				+ "Drag the part around to push the bones.",
			Checkable = true,
			Clicked = () =>
			{
				ToggleSoftPreview();
				UpdateRigChecks();
			},
		};

		soft.Add( _softPreviewTool );

		_softRestTool = new EffigyStageTool
		{
			Icon = EffigyIcon.SoftRest,
			Label = "Rest",
			Tip = "Forget the motion and put every soft bone back on its pose. "
				+ "What you want after flinging the model about.",
			Clicked = () =>
			{
				_viewport?.RestSoftPreview();
				UpdateRigChecks();
			},
		};

		soft.Add( _softRestTool );

		var weights = new EffigyStage { Name = "Weights" };

		weights.Add( new EffigyStageTool
		{
			Icon = EffigyIcon.BoneBind,
			Label = "Paint Weights",
			Tip = "Paint which bone owns which part of the mesh. The ramp is a texture atlas, "
				+ "not vertex colour — pick a bone in the Rig tree first.",
			Checkable = true,
			Clicked = ToggleWeightPaint,
		} );

		// --- Mesh ---
		//
		// Subdivide, on the rig bar, because the mesh being too coarse to bend is a RIGGING
		// problem and it is discovered here — you paint a shoulder, drag the arm, and watch the
		// elbow crease into a hinge instead of a bend. The fix for that is more loops across the
		// joint, and until now the only door to one was the Sculpt workspace: a detour that reads
		// as "you are about to sculpt" when you are not, and that rolls the model back to a sculpt
		// feature on the way.
		//
		// SAME FEATURE, DIFFERENT DEFAULT. This adds the same SubdivideFeature the Sculpt bar
		// adds — one implementation, one tree row, undoable and editable like any other — but with
		// All Faces ticked, which is the linear form that adds density and leaves every vertex
		// where it is. The whole-body Catmull-Clark the sculpt bar defaults to SMOOTHS, pulling
		// the cage onto its limit surface: exactly right when you are about to sculpt the
		// silhouette, and exactly wrong when you have already bound bones to that silhouette and
		// only want it to bend. The tick is in the dialog either way, so the default is a starting
		// point rather than a decision taken away.
		var mesh = new EffigyStage { Name = "Mesh" };

		_rigSubdivideTool = new EffigyStageTool
		{
			Icon = EffigyIcon.Subdivide,
			Label = "Subdivide",
			Tip = "Add loops to the selected part so it bends instead of creasing. "
				+ "Adds a Subdivide set to All Faces — density without moving the shape.",
			Clicked = AddRigSubdivide,
		};

		mesh.Add( _rigSubdivideTool );

		// AND ITS OPPOSITE, on the same bar, because the rig workspace is where BOTH halves of "this
		// mesh is the wrong density" are discovered. Too coarse creases at the elbow; too dense and
		// the weight brush is a slideshow, auto-skin takes a minute, and the compile writes a model
		// nothing wants. The second is what an imported part is, every time.
		_rigRemeshTool = new EffigyStageTool
		{
			Icon = EffigyIcon.Remesh,
			Label = "Remesh",
			Tip = "Reduce the selected part to a triangle budget so it can be weighted and posed. "
				+ "Keeps the shape; returns triangles.",
			Clicked = AddRigRemesh,
		};

		mesh.Add( _rigRemeshTool );

		return new List<EffigyStage> { bones, bind, soft, weights, mesh };
	}

	/// <summary>
	/// The Rig bar's Subdivide — a SubdivideFeature over the selected part, defaulted to density
	/// rather than smoothing. See the Mesh stage above for why the default differs from Sculpt's.
	///
	/// GOES THROUGH AddFeature like every other creation button, so the undo step, the rollback
	/// placement and the dialog are the ones the rest of the editor already agreed on. What it
	/// does NOT do is leave the rig workspace: the feature lands in the tree, the studio rebuilds
	/// under the skeleton, and the bones stay put because Subdivide replaces a body's mesh in
	/// place and never its id — which is the id the bone assignments are keyed on.
	/// </summary>
	private void AddRigSubdivide()
	{
		// AddFeature refuses a Subdivide with nothing picked rather than quietly densifying every
		// part in the document, and says so in the prompt. The bar can do better than a prompt
		// after the fact: the tool is locked until something is selected, the same way Bone from
		// Part is, so the refusal is visible before the click. Kept as a guard anyway — a stale
		// check is how a locked button gets clicked.
		if ( _viewport is null || _viewport.IdleFaces.Count == 0 && _viewport.IdleBodyIds.Count == 0 )
		{
			SetPrompt( "Subdivide needs to know which part — click one in the Parts list on the left, "
				+ "or pick faces in the viewport, then press Subdivide again." );
			return;
		}

		var feature = (SubdivideFeature)NewFeature( ToolKind.Subdivide, -1 );

		// Only when the selection is a whole part. A face pick is already the local, linear form —
		// AllFaces is not even read then — and ticking it would be writing a setting the feature
		// will ignore, which is worse than leaving it alone because the dialog would still show it.
		if ( _viewport.IdleFaces.Count == 0 )
			feature.AllFaces.Value = true;

		AddFeature( feature );
	}

	/// <summary>
	/// The Rig bar's Remesh — a RemeshFeature over the selected part.
	///
	/// SAME SHAPE AS <see cref="AddRigSubdivide"/> and for the same reasons: through AddFeature so
	/// the undo step and the dialog are the shared ones, locked behind a selection so the refusal
	/// is visible before the click rather than as a prompt after it, and it does not leave the
	/// workspace, because Remesh replaces a body's mesh and never its id.
	///
	/// WHAT IT DOES NOT DO is pick a budget for you. A part with no bones on it yet has no obvious
	/// right answer and the dialog opens on the question — see RemeshFeature.Target for why the
	/// default is a percentage rather than a count.
	/// </summary>
	private void AddRigRemesh()
	{
		if ( _viewport is null || _viewport.IdleBodyIds.Count == 0 )
		{
			SetPrompt( "Remesh needs to know which part — click one in the Parts list on the left, "
				+ "or click the solid in the viewport, then press Remesh again." );
			return;
		}

		AddFeature( NewFeature( ToolKind.Remesh, -1 ) );
	}

	/// <summary>
	/// Start or stop the soft-bone preview, and say so in the prompt.
	///
	/// The prompt is doing real work here rather than narrating: the preview is driven by gravity
	/// and by the pose gizmo and by nothing else, which is the right design (see
	/// EffigyViewport.SoftPreview.cs) but is not guessable from a button called Preview. A rig that
	/// sags an inch and stops looks like a broken preview until you know that settling is the
	/// point.
	/// </summary>
	private void ToggleWeightPaint()
	{
		if ( _viewport is { IsWeightPainting: true } )
		{
			FinishWeightPaint();
			return;
		}

		EnterWeightPaint();
	}

	private void EnterWeightPaint()
	{
		if ( _viewport is null || _studio is null || _rigPanel is not { HasBones: true } rig )
		{
			SetPrompt( "Paint Weights needs a skeleton — place a bone first." );
			return;
		}

		if ( !rig.HasSelectedBone )
		{
			SetPrompt( "Pick a bone in the Rig tree, then press Paint Weights." );
			return;
		}

		BarMode = EffigyBarMode.Rig;

		var (mesh, ranges) = _studio.ToMeshWithBodies();

		if ( !NormalBake.Measure( mesh ).CanBake )
			UVUnwrap.Unwrap( mesh );

		var weights = SkinBinder.BindBodies( mesh, ranges, rig.BodyBoneMap, rig.Skeleton );
		weights = SkinBinder.SmoothWeights( mesh, weights );

		if ( _studio.WeightPaint is { Count: > 0 } layer && layer.CanApply( mesh, out _ ) )
			layer.Apply( mesh, weights, rig.Skeleton, out _ );

		var session = new WeightPaintSession( mesh, weights, rig.Skeleton, _studio.WeightPaint )
		{
			Bone = rig.SelectedBoneIndex,
			Radius = 0.25f,
		};
		session.Radius = session.SuggestedRadius;

		_viewport.BeginWeightPaint( session );
		_weightBar?.Bind( session, rig.SelectedBoneName );
		SetPrompt( $"Painting {rig.SelectedBoneName}. The ramp is a texture, not vertex colour. Drag on the mesh." );
	}

	private void FinishWeightPaint()
	{
		if ( _viewport is null || !_viewport.IsWeightPainting )
			return;

		if ( _viewport.WeightSession is not null )
			_studio.WeightPaint = _viewport.WeightSession.Layer;

		_viewport.EndWeightPaint();
		_weightBar?.Bind( null, null );
		RebuildStudio();
	}

	private void ToggleSoftPreview()
	{
		if ( _viewport is null )
			return;

		SetPrompt( _viewport.ToggleSoftPreview()
			? "Soft preview: gravity is pulling the soft bones. Drag a bone to make the ones below it swing. "
				+ "Rest puts them back."
			: "" );
	}

	/// <summary>
	/// Push the panel's state onto the bar.
	///
	/// The panel is the one that knows, and it changes its mind without being asked — Escape closes
	/// a chain, clicking a different bone in the tree disarms an assign — so this is wired to its
	/// ToolStateChanged as well as being called after every tool click. A tick that can go stale
	/// on the two commonest gestures in the workspace is worse than no tick.
	/// </summary>
	private void UpdateRigChecks()
	{
		if ( _rigPanel is null )
			return;

		var hasBone = _rigPanel.HasSelectedBone;

		if ( _boneTool is not null )
		{
			_boneTool.Checked = _rigPanel.BoneToolActive;

			// The panel's own button rewrites itself to "Branch from 'upper_arm'" when a bone is
			// selected, because that is the gesture nobody discovers on their own. Worth carrying
			// onto the bar for the same reason — but on the TIP rather than the label, which has a
			// measured button around it that would jump width on every selection change.
			_boneTool.Tip = hasBone
				? $"Click the model to extend a new chain from '{_rigPanel.SelectedBoneName}'. Escape when done."
				: "Click the model to place a bone. Click again to extend the chain. "
					+ "Select a bone first to branch from its tail.";
		}

		var hasParts = _viewport is { IdleBodyIds.Count: > 0 };

		if ( _boneFromPartTool is not null )
		{
			_boneFromPartTool.Enabled = hasParts;
			_boneFromPartTool.DisabledReason = hasParts
				? null
				: "Select a part first — in the Parts list or the viewport";
		}

		// A face pick counts here where it does not for Bone from Part: Subdivide takes faces as
		// well as bodies, and picking the four faces across an elbow is the precise version of what
		// this button is for. Measuring a bone out of a face selection is not a thing.
		var hasGeometry = hasParts || _viewport is { IdleFaces.Count: > 0 };

		if ( _rigSubdivideTool is not null )
		{
			_rigSubdivideTool.Enabled = hasGeometry;
			_rigSubdivideTool.DisabledReason = hasGeometry
				? null
				: "Select a part first — in the Parts list, or pick faces in the viewport";
		}

		// Remesh keys off hasParts, not hasGeometry. A face pick names its body and Subdivide takes
		// that as a valid target, but there is no per-face Remesh — the quadrics are summed over the
		// whole surface and a boundary drawn through a face selection would be preserved as a border,
		// which is the opposite of what picking a region would mean.
		if ( _rigRemeshTool is not null )
		{
			_rigRemeshTool.Enabled = hasParts;
			_rigRemeshTool.DisabledReason = hasParts
				? null
				: "Select a part first — in the Parts list, or click the solid in the viewport";
		}

		if ( _boneAssignTool is not null )
		{
			_boneAssignTool.Checked = _rigPanel.AssigningBody;
			_boneAssignTool.Tip = hasBone && hasParts
				? $"Pin the selected part(s) to '{_rigPanel.SelectedBoneName}'."
				: hasBone
					? "Click bodies in the viewport to pin them to the selected bone. "
						+ "Anything left unassigned falls back to the nearest bone."
					: "Select a bone, select a part, then press Assign.";
		}

		if ( _boneSoftTool is not null )
			_boneSoftTool.Checked = _rigPanel.SelectedBoneIsSoft;

		if ( _softPreviewTool is not null )
			_softPreviewTool.Checked = _viewport?.SoftPreviewRunning ?? false;

		// Preview and Rest are about the rig as a whole rather than the selection, so they follow a
		// different rule from the three below: something to simulate, not something selected. A rig
		// with no soft bones would preview a skeleton that cannot move, which is a button that
		// appears to do nothing.
		var anySoft = _rigPanel.SoftBoneCount > 0;

		foreach ( var tool in new[] { _softPreviewTool, _softRestTool } )
		{
			if ( tool is null )
				continue;

			tool.Enabled = anySoft;
			tool.DisabledReason = anySoft ? null : "Nothing is soft yet - select a bone and press Make Soft";
		}

		// Assign, Mirror and Delete all act on "the selected bone" and do nothing without one. The
		// lock reason is the tooltip, the same contract the CAD stages' starter lock uses.
		foreach ( var tool in new[] { _boneAssignTool, _boneMirrorTool, _boneDeleteTool, _boneSoftTool } )
		{
			if ( tool is null )
				continue;

			tool.Enabled = hasBone;
			tool.DisabledReason = hasBone ? null : "Select a bone first — in the viewport or the Rig panel";
		}

		_stageBar?.Refresh();
	}

	/// <summary>
	/// Enter the rig workspace.
	///
	/// THE ONLY WORKSPACE WITH NOTHING TO OPEN. Sculpt and paint are scoped to a feature and roll
	/// the model back to it; a rig is one skeleton over the whole finished studio, owned by the
	/// panel for the life of the window. So this is a plain mode change — which is what makes it
	/// the odd one out in the other direction, and worth saying rather than leaving as an absence
	/// somebody later reads as a missing rollback.
	/// </summary>
	private void EnterRig()
	{
		if ( _viewport is null || _stageBar is null )
			return;

		LeaveCurrentWorkspace();

		// A dialog and the rig bar would be two things claiming the model at once, the same
		// argument EnterSculpt makes for closing it.
		_dialog?.Close();

		BarMode = EffigyBarMode.Rig;

		// Whatever was picked in CAD is still lit, and in a workspace where faces can no longer be
		// clicked a highlighted face is a selection you cannot clear by clicking off it. It is also
		// what the face-drag arrow hangs off, which is one more thing over the model competing for
		// a click meant for a bone.
		_viewport.ClearIdleSelection();

		_stageBar.Mode = "RIG";

		// NO FINISH BUTTON. Finish means "commit what you have been doing to the feature tree", and
		// there is no feature here to commit to — every bone edit already landed on the skeleton
		// when it was made. A green tick that only changed which tools were on screen would be
		// teaching the wrong thing about what green means in this editor.
		_stageBar.SetFinish( null, null );
		_stageBar.SetStages( _rigStages, _rigStage );

		UpdateRigChecks();

		SetPrompt( _rigPanel is { HasBones: true }
			? "Rig: click a bone to select it, or place more with Add Bone."
			: "Rig: press Add Bone and click the model to place your first bone." );
	}

	// --- dock layouts ----------------------------------------------------------------------------

	/// <summary>
	/// Which docks each workspace opens with.
	///
	/// A STARTING POINT, NOT A CAGE. Whatever you do to the docks while you are in a workspace is
	/// captured on the way out and restored on the way back (see ApplyWorkspaceDocks), so these
	/// values only decide what the FIRST visit looks like. A layout table that overrode the user
	/// every time would be the tool rearranging itself under someone who had already arranged it.
	///
	/// Tutorial and Console are deliberately absent. They are not about which part of the pipeline
	/// you are in — they are open because you opened them — so a workspace switch leaves them
	/// exactly as it found them.
	/// </summary>
	private static readonly Dictionary<EffigyWorkspace, (bool Features, bool Materials, bool Rig)> WorkspaceDocks = new()
	{
		// The feature tree is on in three of four: it is the document's history, and going back to
		// re-extrude something is the move that made a single window right in the first place.
		[EffigyWorkspace.Cad] = (Features: true, Materials: false, Rig: false),
		[EffigyWorkspace.Sculpt] = (Features: true, Materials: false, Rig: false),
		[EffigyWorkspace.Paint] = (Features: true, Materials: true, Rig: false),

		// Rig is the exception. The skeleton tree wants the right-hand side to itself, and by the
		// time you are placing bones the feature tree is history you are no longer editing.
		[EffigyWorkspace.Rig] = (Features: false, Materials: false, Rig: true),
	};

	/// <summary>What the docks looked like the last time each workspace was left.</summary>
	private readonly Dictionary<EffigyWorkspace, (bool Features, bool Materials, bool Rig)> _workspaceDocks = new();

	/// <summary>
	/// Capture the layout the old workspace is being left in, then lay out the new one.
	///
	/// The capture is what makes the table above a default rather than a rule: open the Materials
	/// browser while sculpting and it is still there next time you sculpt, without any of this
	/// having to know you did it.
	/// </summary>
	private void ApplyWorkspaceDocks( EffigyWorkspace from, EffigyWorkspace to, bool force = false )
	{
		if ( _rigPanel is null || _materialsPanel is null )
			return; // Still building. BuildDocks lays out the first workspace itself.

		if ( from != to )
			_workspaceDocks[from] = ReadDockState();

		var wanted = _workspaceDocks.TryGetValue( to, out var remembered ) && !force
			? remembered
			: WorkspaceDocks[to];

		SetDockIfChanged( "Features", wanted.Features );
		SetDockIfChanged( "Materials", wanted.Materials );
		SetDockIfChanged( "Rig", wanted.Rig );

		// The one dock a workspace is actually ABOUT gets raised, not merely opened — it may be
		// tabbed behind another on the same edge, and an open-but-hidden panel is the same as a
		// closed one to the person looking for it.
		switch ( to )
		{
			case EffigyWorkspace.Paint: DockManager.RaiseDock( "Materials" ); break;
			case EffigyWorkspace.Rig: DockManager.RaiseDock( "Rig" ); break;
		}

		// The View menu's ticks were written for the layout that just went away.
		SyncDockChecks();
	}

	private (bool Features, bool Materials, bool Rig) ReadDockState() =>
		(DockManager.IsDockOpen( "Features" ),
			DockManager.IsDockOpen( "Materials" ),
			DockManager.IsDockOpen( "Rig" ));

	/// <summary>Asked before set, because SetDockState on a dock already in that state still costs
	/// a relayout — and four of those per switch is a visible flicker on a window this size.
	/// </summary>
	private void SetDockIfChanged( string title, bool open )
	{
		if ( DockManager.IsDockOpen( title ) != open )
			DockManager.SetDockState( title, open );
	}
}