Editor/WaterEditTool.cs

Editor tool and sidebar widget for editing a WaterNetwork in the editor. Provides selection, dragging, gizmo movement, tangent handle editing, node/rivers operations (insert, delete, reverse, join/detach openings), drawing hover/selection gizmos and a sidebar UI to edit properties with undo scoping.

File Access
using System;
using System.Linq;
using Editor;
using Sandbox;

namespace RedSnail.WaterTool2.Editor;

/// <summary>
/// Which frame a node's position is shown and edited in.
/// </summary>
public enum WaterNodeSpace
{
	/// <summary>
	/// The network's own axes.
	/// </summary>
	Global,

	/// <summary>
	/// The node's river: X along it, Y across it. Height stays height either way.
	/// </summary>
	Local
}

/// <summary>
/// Select and edit what is already there: click a node or a river to select it, drag a node across the ground (it
/// keeps its height above the ground) or use its move gizmo, drag a node's tangent handles to reshape the rivers through
/// it, Ctrl+click a river to insert a node, Delete to remove the selection, and turn a river round from the sidebar.
/// </summary>
[Title("Edit")]
[Icon("edit")]
[Group("1")]
[Order(1)]
public class WaterEditTool : EditorTool
{
	private enum DragKind
	{
		None,
		Node,
		Handle,
		Gizmo
	}

	private const float MinEndDistance = 25.0f;

	public int SelectedNodeId { get; private set; } = -1;

	public int SelectedRiverId { get; private set; } = -1;

	/// <summary>
	/// Bumped whenever the selection changes, so the sidebar knows to rebuild itself.
	/// </summary>
	public int SelectionVersion { get; private set; }

	public WaterNetwork Network => Root?.Network;

	public WaterNode SelectedNode => Network.IsValid() ? Network.FindNode(SelectedNodeId) : null;

	public WaterRiver SelectedRiver => Network.IsValid() ? Network.FindRiver(SelectedRiverId) : null;

	[Property, Title("Space")] public WaterNodeSpace NodeSpace { get; set; } = WaterNodeSpace.Global;

	/// <summary>
	/// The selected node's position, in whichever frame <see cref="NodeSpace"/> asks for. Its height is the water's
	/// height there.
	/// </summary>
	[Property, Title("Position")]
	public Vector3 NodePosition
	{
		get
		{
			var network = Network;

			if (!network.IsValid() || SelectedNode is not { } node)
				return Vector3.Zero;

			return NodeSpace == WaterNodeSpace.Local ? NodeAxes(network, node).Inverse * node.Position : node.Position;
		}
		set
		{
			var network = Network;

			if (!network.IsValid() || SelectedNode is not { } node)
				return;

			network.MoveNode(node.Id, NodeSpace == WaterNodeSpace.Local ? NodeAxes(network, node) * value : value);
		}
	}

	/// <summary>
	/// How wide the river is at the selected node: setting it sets the end of every river there.
	/// </summary>
	[Property, Title("Width"), Range(WaterRiver.MinWidth, 4000.0f)]
	public float NodeWidth
	{
		get
		{
			var network = Network;

			if (!network.IsValid() || SelectedNode is not { } node)
				return 0.0f;

			var rivers = network.GetRiversAt(node.Id);

			return rivers.Count == 0 ? 0.0f : rivers.Average(river => river.WidthAt(node.Id));
		}
		set
		{
			var network = Network;

			if (!network.IsValid() || SelectedNode is not { } node)
				return;

			foreach (var river in network.GetRiversAt(node.Id))
			{
				if (river.StartNodeId == node.Id)
					river.StartWidth = value;

				if (river.EndNodeId == node.Id)
					river.EndWidth = value;
			}

			network.MarkDirty();
		}
	}

	private WaterNetworkTool Root => Manager?.CurrentTool as WaterNetworkTool;

	private DragKind m_Drag;
	private int m_DragRiverId = -1;
	private float m_DragHeightAboveGround;
	private Vector3 m_DragStartWorld;
	private Vector3 m_MoveDelta;
	private IDisposable m_UndoScope;



	/// <summary>
	/// The node's own axes in the network's space: forward along the river through it, laid flat so Z stays height.
	/// </summary>
	private static Rotation NodeAxes(WaterNetwork _Network, WaterNode _Node)
	{
		var river = _Network.GetRiversAt(_Node.Id).FirstOrDefault();

		if (river is null)
			return Rotation.Identity;

		Vector3 handle = WaterNetwork.GetHandle(river, _Node.Id);
		Vector3 forward = (river.EndNodeId == _Node.Id ? -handle : handle).WithZ(0.0f);

		return forward.IsNearZeroLength ? Rotation.Identity : Rotation.LookAt(forward.Normal, Vector3.Up);
	}



	public override void OnDisabled()
	{
		EndDrag();
	}



	public override Widget CreateToolSidebar()
	{
		return new WaterEditToolWidget(this);
	}



	public override void OnUpdate()
	{
		var network = Network;

		if (!network.IsValid())
		{
			EndDrag();
			ClearSelection();
			return;
		}

		// Undo can remove whatever we had selected.
		if (SelectedNodeId >= 0 && network.FindNode(SelectedNodeId) is null)
			ClearSelection();

		if (SelectedRiverId >= 0 && network.FindRiver(SelectedRiverId) is null)
			ClearSelection();

		if (m_Drag is DragKind.Node or DragKind.Handle)
		{
			UpdateDirectDrag(network);
			DrawSelection(network);
			return;
		}

		bool canPick = Gizmo.HasMouseFocus && !Gizmo.HasHovered && !Gizmo.Pressed.Any;
		WaterNode hoverNode = null;
		WaterRiverPick? hoverRiver = null;
		int hoverHandleRiver = -1;

		if (canPick)
		{
			var ray = Gizmo.CurrentRay;

			if (SelectedNode is { } selected)
				hoverHandleRiver = PickHandle(network, selected, ray);

			if (hoverHandleRiver < 0)
				hoverNode = WaterPicker.PickNode(network, ray);

			if (hoverHandleRiver < 0 && hoverNode is null)
				hoverRiver = WaterPicker.PickRiver(network, ray);
		}

		DrawHover(network, hoverNode, hoverRiver);
		DrawSelection(network, hoverHandleRiver);

		if (canPick && Gizmo.WasLeftMousePressed)
			OnPress(network, hoverNode, hoverRiver, hoverHandleRiver);

		if (SelectedNode is { } node)
			UpdateNodeGizmo(network, node);
	}



	public void Select(WaterNode _Node)
	{
		SetSelection(_Node?.Id ?? -1, -1);
	}



	public void Select(WaterRiver _River)
	{
		SetSelection(-1, _River?.Id ?? -1);
	}



	public void ClearSelection()
	{
		SetSelection(-1, -1);
	}



	private void SetSelection(int _NodeId, int _RiverId)
	{
		if (SelectedNodeId == _NodeId && SelectedRiverId == _RiverId)
			return;

		SelectedNodeId = _NodeId;
		SelectedRiverId = _RiverId;
		SelectionVersion++;
	}



	public void DeleteSelection()
	{
		var network = Network;

		if (!network.IsValid())
			return;

		if (SelectedNode is { } node)
		{
			// A node a river simply runs through is dissolved, so the river carries on in one piece; anything else (a
			// river end, a confluence) takes its rivers with it.
			bool passThrough = network.GetDegree(node.Id) == 2;

			using (SceneEditorSession.Active.UndoScope(passThrough ? "Dissolve Node" : "Delete Node").WithComponentChanges(network).Push())
			{
				if (!passThrough || network.DissolveNode(node.Id) is null)
					network.RemoveNode(node.Id);
			}
		}
		else if (SelectedRiver is { } river)
		{
			using (SceneEditorSession.Active.UndoScope("Delete River").WithComponentChanges(network).Push())
				network.RemoveRiver(river.Id);
		}

		ClearSelection();
	}



	/// <summary>
	/// Turns the selected river round, or with <paramref name="_WholeRun"/> every river it carries on into through
	/// plain bends.
	/// </summary>
	public void ReverseSelection(bool _WholeRun)
	{
		var network = Network;

		if (!network.IsValid() || SelectedRiver is not { } river)
			return;

		using (SceneEditorSession.Active.UndoScope("Reverse River").WithComponentChanges(network).Push())
		{
			if (_WholeRun)
				network.ReverseRun(river.Id);
			else
				network.ReverseRiver(river.Id);
		}

		SelectionVersion++;
	}



	/// <summary>
	/// Unhooks the selected node from the lake it opens into, or the sea.
	/// </summary>
	public void DetachSelection()
	{
		var network = Network;

		if (!network.IsValid() || SelectedNode is not { } node || network.LakeAtOpening(node.Id) is not { } lake)
			return;

		using (SceneEditorSession.Active.UndoScope(lake.IsCoast ? "Detach From Sea" : "Detach From Lake").WithComponentChanges(network).Push())
			network.RemoveOpening(node.Id);

		SelectionVersion++;
	}



	/// <summary>
	/// Opens the selected river end into the sea at the nearest point of the coast: the node moves onto it.
	/// </summary>
	public void JoinSeaSelection()
	{
		var network = Network;

		if (!network.IsValid() || SelectedNode is not { } node || network.NearestCoast(node.Position, out _, true) is not { } coast)
			return;

		using (SceneEditorSession.Active.UndoScope("Open River Into Sea").WithComponentChanges(network).Push())
			network.JoinOpening(coast.Id, node.Id);

		SelectionVersion++;
	}



	/// <summary>
	/// Sidebar edits of the selected node land on the node directly; this pushes them through the network so tangents
	/// and pieces follow.
	/// </summary>
	public void OnNodeEdited(WaterNode _Node, string _PropertyName)
	{
		var network = Network;

		if (!network.IsValid() || _Node is null)
			return;

		if (_PropertyName == nameof(WaterNode.AutoTangents))
		{
			network.SetAutoTangents(_Node.Id, _Node.AutoTangents);
			return;
		}

		network.MarkDirty();
	}



	public void OnRiverEdited()
	{
		Network?.MarkDirty();
	}



	private void OnPress(WaterNetwork _Network, WaterNode _HoverNode, WaterRiverPick? _HoverRiver, int _HoverHandleRiver)
	{
		if (_HoverHandleRiver >= 0)
		{
			m_Drag = DragKind.Handle;
			m_DragRiverId = _HoverHandleRiver;
			return;
		}

		if (_HoverNode is not null)
		{
			Select(_HoverNode);

			m_Drag = DragKind.Node;
			m_DragStartWorld = _Network.WorldTransform.PointToWorld(_HoverNode.Position);

			// The node keeps its height above the ground as it is dragged, so a river set into its bed stays there.
			m_DragHeightAboveGround = GroundBelow(m_DragStartWorld) is float ground ? m_DragStartWorld.z - ground : 0.0f;
			return;
		}

		if (_HoverRiver is { } pick)
		{
			if (Gizmo.IsCtrlPressed)
			{
				WaterNode inserted;

				using (SceneEditorSession.Active.UndoScope("Insert Node").WithComponentChanges(_Network).Push())
					inserted = _Network.SplitRiver(pick.River.Id, pick.T, MinEndDistance);

				Select(inserted);
			}
			else
			{
				Select(pick.River);
			}

			return;
		}

		ClearSelection();
	}



	/// <summary>
	/// A node pressed directly slides across the ground under the cursor; a handle slides on the level plane through
	/// its current position, so a river's fall is only changed on purpose (through the gizmo).
	/// </summary>
	private void UpdateDirectDrag(WaterNetwork _Network)
	{
		if (!Gizmo.IsLeftMouseDown)
		{
			EndDrag();
			return;
		}

		// A press that never travels is a click, not a drag: no undo step for it.
		if (Vector2.Distance(Gizmo.CursorPosition, Gizmo.Pressed.CursorPosition) < 2.0f && m_UndoScope is null)
			return;

		var transform = _Network.WorldTransform;

		if (m_Drag == DragKind.Node && SelectedNode is { } node)
		{
			if (GroundHit(transform.PointToWorld(node.Position)) is not Vector3 hit)
				return;

			m_UndoScope ??= SceneEditorSession.Active.UndoScope("Move Node").WithComponentChanges(_Network).Push();
			_Network.MoveNode(node.Id, transform.PointToLocal(hit + Vector3.Up * m_DragHeightAboveGround));
		}
		else if (m_Drag == DragKind.Handle && SelectedNode is { } handleNode && _Network.FindRiver(m_DragRiverId) is { } river)
		{
			Vector3 handleWorld = transform.PointToWorld(handleNode.Position + WaterNetwork.GetHandle(river, handleNode.Id));
			var plane = new Plane(handleWorld, Vector3.Up);

			if (plane.Trace(Gizmo.CurrentRay, true) is not Vector3 hit)
				return;

			m_UndoScope ??= SceneEditorSession.Active.UndoScope("Move Tangent").WithComponentChanges(_Network).Push();

			// Alt breaks the mirror so a bend can be turned into a corner.
			Vector3 handle = transform.PointToLocal(hit) - handleNode.Position;
			_Network.SetHandle(river.Id, handleNode.Id, handle, !Gizmo.IsAltPressed);
		}
		else
		{
			EndDrag();
		}
	}



	/// <summary>
	/// The standard move gizmo on the selected node: its centre square drags across the ground, the arrows move on the
	/// world axes, or in Local space along and across the river through the node.
	/// </summary>
	private void UpdateNodeGizmo(WaterNetwork _Network, WaterNode _Node)
	{
		if (m_Drag is DragKind.Node or DragKind.Handle)
			return;

		var transform = _Network.WorldTransform;
		Vector3 nodeWorld = transform.PointToWorld(_Node.Position);
		var handleRotation = Gizmo.Settings.GlobalSpace ? Rotation.Identity : NodeFrame(_Network, _Node);

		using (Gizmo.Scope("watertool2_node_gizmo", new Transform(nodeWorld)))
		{
			Gizmo.Hitbox.DepthBias = 0.01f;

			Vector3? surfaceHit = null;

			bool moved = Gizmo.Control.Position("position", Vector3.Zero, out Vector3 delta, handleRotation, centerRaycast: () =>
			{
				surfaceHit = GroundHit(nodeWorld);
				return surfaceHit;
			});

			if (moved)
			{
				if (m_Drag != DragKind.Gizmo)
				{
					m_Drag = DragKind.Gizmo;
					m_DragStartWorld = nodeWorld;
					m_MoveDelta = Vector3.Zero;
					m_DragHeightAboveGround = GroundBelow(nodeWorld) is float ground ? nodeWorld.z - ground : 0.0f;
					m_UndoScope ??= SceneEditorSession.Active.UndoScope("Move Node").WithComponentChanges(_Network).Push();
				}

				Vector3 target;

				if (surfaceHit is Vector3 hit)
				{
					target = hit + Vector3.Up * m_DragHeightAboveGround;
				}
				else
				{
					m_MoveDelta += delta;
					target = Gizmo.Snap(m_DragStartWorld, m_MoveDelta, handleRotation);
				}

				_Network.MoveNode(_Node.Id, transform.PointToLocal(target));
			}
		}

		if (m_Drag == DragKind.Gizmo && !Gizmo.IsLeftMouseDown)
			EndDrag();
	}



	private static Rotation NodeFrame(WaterNetwork _Network, WaterNode _Node)
	{
		var river = _Network.GetRiversAt(_Node.Id).FirstOrDefault();

		if (river is null)
			return Rotation.Identity;

		Vector3 handle = WaterNetwork.GetHandle(river, _Node.Id);
		Vector3 forward = _Network.WorldTransform.NormalToWorld(river.EndNodeId == _Node.Id ? -handle : handle);

		if (forward.IsNearZeroLength || MathF.Abs(forward.Normal.z) > 0.99f)
			return Rotation.Identity;

		return Rotation.LookAt(forward.Normal, Vector3.Up);
	}



	private void EndDrag()
	{
		m_Drag = DragKind.None;
		m_DragRiverId = -1;
		m_UndoScope?.Dispose();
		m_UndoScope = null;
	}



	/// <summary>
	/// Where the cursor ray meets the ground; a level plane through the point being dragged when nothing is there.
	/// </summary>
	private Vector3? GroundHit(Vector3 _Fallback)
	{
		var trace = Trace.Run();

		if (trace.Hit)
			return trace.EndPosition;

		return new Plane(_Fallback, Vector3.Up).Trace(Gizmo.CurrentRay, true);
	}



	/// <summary>
	/// The ground straight under a point, if there is any.
	/// </summary>
	private float? GroundBelow(Vector3 _World)
	{
		var trace = Scene.Trace.Ray(new Ray(_World + Vector3.Up * 1.0f, Vector3.Down), 20000.0f).Run();

		return trace.Hit ? trace.EndPosition.z : null;
	}



	/// <summary>
	/// The river whose tangent handle at the selected node is under the ray, or -1.
	/// </summary>
	private static int PickHandle(WaterNetwork _Network, WaterNode _Node, Ray _Ray)
	{
		var transform = _Network.WorldTransform;
		int best = -1;
		float bestDistance = float.MaxValue;

		foreach (var river in _Network.GetRiversAt(_Node.Id))
		{
			Vector3 world = transform.PointToWorld(_Node.Position + WaterNetwork.GetHandle(river, _Node.Id));
			var sphere = new Sphere(world, WaterPicker.NodeRadius(world) * 0.7f);

			if (!sphere.Trace(_Ray, float.MaxValue, out float distance) || distance >= bestDistance)
				continue;

			best = river.Id;
			bestDistance = distance;
		}

		return best;
	}



	private static void DrawHover(WaterNetwork _Network, WaterNode _HoverNode, WaterRiverPick? _HoverRiver)
	{
		var transform = _Network.WorldTransform;

		using (Gizmo.Scope("watertool2_hover"))
		{
			Gizmo.Draw.IgnoreDepth = true;
			Gizmo.Draw.LineThickness = 2.0f;
			Gizmo.Draw.Color = Color.Cyan.WithAlpha(0.8f);

			if (_HoverNode is not null)
				WaterGizmos.DrawNode(transform.PointToWorld(_HoverNode.Position), false);

			if (_HoverRiver is { } pick && _Network.TryGetCurve(pick.River, out var curve))
			{
				WaterGizmos.DrawRiverOutline(curve, _Network.WidthOf(pick.River), transform);

				// Ctrl turns the click into "insert a node here": show where.
				if (Gizmo.IsCtrlPressed)
				{
					Gizmo.Draw.Color = Color.Orange;
					WaterGizmos.DrawNode(transform.PointToWorld(pick.LocalPoint), true);
				}
			}
		}
	}



	private void DrawSelection(WaterNetwork _Network, int _HoverHandleRiver = -1)
	{
		var transform = _Network.WorldTransform;

		using (Gizmo.Scope("watertool2_selection"))
		{
			Gizmo.Draw.IgnoreDepth = true;
			Gizmo.Draw.LineThickness = 2.0f;

			if (SelectedRiver is { } river && _Network.TryGetCurve(river, out var curve))
			{
				Gizmo.Draw.Color = Color.Yellow;
				WaterGizmos.DrawRiverOutline(curve, _Network.WidthOf(river), transform);

				float size = MathF.Max(40.0f, _Network.WidthOf(river).MaxHalf * 0.6f);
				WaterGizmos.DrawFlowArrows(curve, transform, size * 4.0f, size);
			}

			if (SelectedNode is not { } node)
				return;

			Vector3 nodeWorld = transform.PointToWorld(node.Position);

			Gizmo.Draw.Color = Color.Yellow;
			WaterGizmos.DrawNode(nodeWorld, true);

			// Tangent handles: one per river leaving the node. A lake's (or the sea's) opening's handle is held square to
			// the shore.
			bool opening = _Network.LakeAtOpening(node.Id) is not null;

			foreach (var attached in _Network.GetRiversAt(node.Id))
			{
				Vector3 handleWorld = transform.PointToWorld(node.Position + WaterNetwork.GetHandle(attached, node.Id));
				bool active = attached.Id == _HoverHandleRiver || (m_Drag == DragKind.Handle && attached.Id == m_DragRiverId);

				Gizmo.Draw.Color = node.AutoTangents || opening ? Color.White.WithAlpha(0.5f) : Color.White;
				Gizmo.Draw.LineThickness = 1.0f;
				Gizmo.Draw.Line(nodeWorld, handleWorld);

				Gizmo.Draw.Color = active ? Color.Cyan : (node.AutoTangents ? Color.White.WithAlpha(0.6f) : Color.White);
				Gizmo.Draw.SolidSphere(handleWorld, WaterPicker.NodeRadius(handleWorld) * 0.45f);
			}
		}
	}
}



file class WaterEditToolWidget : ToolSidebarWidget
{
	private readonly WaterEditTool m_Tool;
	private int m_ShownVersion = -1;
	private IDisposable m_UndoScope;



	public WaterEditToolWidget(WaterEditTool _Tool)
	{
		m_Tool = _Tool;

		Rebuild();
	}



	[EditorEvent.Frame]
	public void Frame()
	{
		if (m_Tool.SelectionVersion != m_ShownVersion)
			Rebuild();
	}



	private void Rebuild()
	{
		m_ShownVersion = m_Tool.SelectionVersion;

		Layout.Clear(true);
		AddTitle("Edit Water", "edit");

		var network = m_Tool.Network;

		if (network.IsValid() && m_Tool.SelectedNode is { } node)
			BuildNodeSection(network, node);
		else if (network.IsValid() && m_Tool.SelectedRiver is { } river)
			BuildRiverSection(network, river);
		else
			AddGroup("Selection").Add(new Label("Click a node or a river to select it.") { WordWrap = true });

		AddShortcuts(
			("Select", "LMB"),
			("Drag Node / Tangent", "LMB Drag"),
			("Break Tangent Mirror", "ALT + Drag"),
			("Insert Node On River", "CTRL + LMB"),
			("Delete Selection", "DEL"),
			("Deselect", "ESC")
		);

		Layout.AddStretchCell();
	}



	private void BuildNodeSection(WaterNetwork _Network, WaterNode _Node)
	{
		var group = AddGroup($"Node #{_Node.Id}");
		var serialized = _Node.GetSerialized();

		serialized.OnPropertyStartEdit += _ => BeginEdit("Edit Node");
		serialized.OnPropertyFinishEdit += _ => EndEdit();
		serialized.OnPropertyChanged += property => m_Tool.OnNodeEdited(_Node, property?.Name);

		// Position and width are read off the tool rather than the node: the position can be shown in the node's own
		// frame, and the width belongs to the rivers at the node.
		var tool = m_Tool.GetSerialized();

		tool.OnPropertyStartEdit += _ => BeginEdit("Edit Node");
		tool.OnPropertyFinishEdit += _ => EndEdit();

		int degree = _Network.GetDegree(_Node.Id);
		var lake = _Network.LakeAtOpening(_Node.Id);

		SidebarRows.Add(group, tool.GetProperty(nameof(WaterEditTool.NodeSpace)));
		SidebarRows.AddVector(group, tool.GetProperty(nameof(WaterEditTool.NodePosition)));

		if (degree > 0)
			SidebarRows.Add(group, tool.GetProperty(nameof(WaterEditTool.NodeWidth)));

		SidebarRows.Add(group, serialized.GetProperty(nameof(WaterNode.AutoTangents)));
		SidebarRows.Add(group, serialized.GetProperty(nameof(WaterNode.CornerRadius)));

		string role = lake is { IsCoast: true } ? (_Network.SeaEnabled ? "Opens into the sea" : "Opens into the sea (the sea is off)")
			: lake is not null ? $"Opens into lake #{lake.Id}"
			: degree >= 3 ? $"Confluence of {degree} rivers"
			: degree == 2 ? "Bend"
			: degree == 1 ? "River end"
			: "Unconnected";

		group.Add(new Label(role));

		var row = group.AddRow();
		row.Spacing = 4;
		row.Add(new Button("Delete Node", "delete") { Clicked = m_Tool.DeleteSelection });

		if (lake is not null)
			row.Add(new Button("Detach", "link_off") { Clicked = m_Tool.DetachSelection });
		else if (degree == 1 && _Network.SeaEnabled && _Network.HasCoast)
			row.Add(new Button("Join Sea", "sailing") { Clicked = m_Tool.JoinSeaSelection, ToolTip = "Open this river into the sea at the nearest point of the coast." });
	}



	private void BuildRiverSection(WaterNetwork _Network, WaterRiver _River)
	{
		var group = AddGroup($"River #{_River.Id}");
		var serialized = _River.GetSerialized();

		serialized.OnPropertyStartEdit += _ => BeginEdit("Edit River");
		serialized.OnPropertyFinishEdit += _ => EndEdit();
		serialized.OnPropertyChanged += _ => m_Tool.OnRiverEdited();

		SidebarRows.Add(group, serialized.GetProperty(nameof(WaterRiver.StartWidth)));
		SidebarRows.Add(group, serialized.GetProperty(nameof(WaterRiver.EndWidth)));
		SidebarRows.Add(group, serialized.GetProperty(nameof(WaterRiver.Speed)));
		SidebarRows.Add(group, serialized.GetProperty(nameof(WaterRiver.Waves)));
		SidebarRows.Add(group, serialized.GetProperty(nameof(WaterRiver.Depth)));

		float length = _Network.GetCurveTable(_River)?.Length ?? 0.0f;
		group.Add(new Label($"Length: {length:0} units"));

		if (_Network.TryGetSpeedRange(_River.Id, out float slowest, out float fastest))
			group.Add(new Label($"Flows at {slowest:0} to {fastest:0} units/s"));

		var flowRow = group.AddRow();
		flowRow.Spacing = 4;
		flowRow.Add(new Button("Reverse", "swap_horiz") { Clicked = () => m_Tool.ReverseSelection(false), ToolTip = "Turn this river round: its water flows the other way." });
		flowRow.Add(new Button("Reverse All", "swap_calls") { Clicked = () => m_Tool.ReverseSelection(true), ToolTip = "Turn round every river this one carries on into through plain bends." });

		var row = group.AddRow();
		row.Spacing = 4;
		row.Add(new Button("Delete River", "delete") { Clicked = m_Tool.DeleteSelection });
	}



	private void BeginEdit(string _Name)
	{
		var network = m_Tool.Network;

		if (!network.IsValid())
			return;

		m_UndoScope ??= SceneEditorSession.Active.UndoScope(_Name).WithComponentChanges(network).Push();
	}



	private void EndEdit()
	{
		m_UndoScope?.Dispose();
		m_UndoScope = null;
	}



	[Shortcut("watertool2.delete-selection", "DEL", typeof(SceneViewWidget))]
	private void DeleteSelection()
	{
		m_Tool.DeleteSelection();
	}



	[Shortcut("watertool2.deselect", "ESC", typeof(SceneViewWidget))]
	private void Deselect()
	{
		m_Tool.ClearSelection();
	}
}