Editor/WaterRiverTool.cs

Editor tool for drawing rivers in the scene editor. Handles placing nodes, splitting rivers, opening into lakes/sea, snapping/angle alignment, preview rendering and undo grouping, plus a small sidebar widget for properties and shortcuts.

File AccessNetworking
using System;
using System.Collections.Generic;
using Editor;
using Sandbox;

namespace RedSnail.WaterTool2.Editor;

/// <summary>
/// Which way the water of a river being drawn runs.
/// </summary>
public enum WaterFlowDirection
{
	/// <summary>
	/// Downhill. On level ground: on from a river being extended, into whatever the river was started from (a branch
	/// drawn off a river is a tributary, a river drawn off a lake runs into it), and the way it is drawn otherwise.
	/// </summary>
	Auto,

	/// <summary>
	/// The way the river is drawn: from the first node to the last.
	/// </summary>
	Downstream,

	/// <summary>
	/// Against the way the river is drawn: from the last node back to the first.
	/// </summary>
	Upstream
}

/// <summary>
/// Click on the ground to lay river nodes; consecutive clicks are joined by rivers. Clicking an existing node continues
/// from it or joins the river to it; clicking on an existing river splits it there and does the same, which is how
/// branches are drawn off rivers and rivers are made to flow into each other. Clicking on a lake opens the river into
/// it; clicking on the sea opens it into the sea through the nearest coast. Right-click or Escape ends the river.
/// </summary>
[Title("River")]
[Icon("waves")]
[Group("1")]
[Order(0)]
public class WaterRiverTool : EditorTool
{
	/// <summary>
	/// A right button release this close to where it was pressed is a click; anything further is the camera orbiting.
	/// </summary>
	private const float ClickDragTolerance = 4.0f;

	/// <summary>
	/// A split this close to a river's end reuses the end node instead of leaving a sliver of river behind.
	/// </summary>
	private const float MinEndDistance = 25.0f;

	/// <summary>
	/// How far the snapped direction is drawn out as a guide while placing.
	/// </summary>
	private const float GuideLength = 4000.0f;

	/// <summary>
	/// What the cursor is over this frame and where the next node would go.
	/// </summary>
	private readonly struct Target
	{
		public readonly Vector3 World;
		public readonly WaterNode Node;
		public readonly WaterRiverPick? River;

		/// <summary>
		/// The lake the river would open into, or the coast it would open into the sea through.
		/// </summary>
		public readonly WaterLake Lake;

		/// <summary>
		/// The cursor is out on the sea: <see cref="Lake"/> is the coast nearest to it.
		/// </summary>
		public readonly bool Sea;

		public Target(Vector3 _World, WaterNode _Node, WaterRiverPick? _River, WaterLake _Lake = null, bool _Sea = false)
		{
			World = _World;
			Node = _Node;
			River = _River;
			Lake = _Lake;
			Sea = _Sea;
		}
	}

	[Property, Title("Width"), Range(32.0f, 4000.0f)] public float Width { get; set; } = 400.0f;

	/// <summary>
	/// How fast new rivers run on level ground, in units per second.
	/// </summary>
	[Property, Title("Speed"), Range(0.0f, 1000.0f)] public float Speed { get; set; } = 120.0f;

	/// <summary>
	/// Wave strength of new rivers, relative to the network's wave profile.
	/// </summary>
	[Property, Title("Waves"), Range(0.0f, 2.0f)] public float Waves { get; set; } = 0.35f;

	[Property, Title("Depth"), Range(10.0f, 2000.0f)] public float Depth { get; set; } = 250.0f;

	/// <summary>
	/// Which way the water of the river being drawn runs.
	/// </summary>
	[Property, Title("Flow")] public WaterFlowDirection Flow { get; set; } = WaterFlowDirection.Auto;

	/// <summary>
	/// How far above the ground clicked on the water's surface is placed.
	/// </summary>
	[Property, Title("Height Offset"), Range(-200.0f, 200.0f)] public float HeightOffset { get; set; } = 0.0f;

	[Property, Title("Snap Radius"), Range(0.0f, 500.0f)] public float SnapRadius { get; set; } = 150.0f;

	/// <summary>
	/// Sharp corners give straight rivers meeting at an angle (canals); off, the river flows smoothly through every node.
	/// </summary>
	[Property, Title("Sharp Corners")] public bool SharpCorners { get; set; } = false;

	/// <summary>
	/// Snap the direction of the river being drawn to whole steps of <see cref="AngleStep"/>, measured from the world
	/// axes and from the rivers already at the node being extended. Holding Shift flips it for one placement.
	/// </summary>
	[Property, Title("Angle Snap")] public bool AngleSnap { get; set; } = false;

	[Property, Title("Snap Angle"), Range(1.0f, 90.0f)] public float AngleStep { get; set; } = 10.0f;

	private int m_LastNodeId = -1;
	private Vector2 m_RightPressPosition;

	/// <summary>
	/// Whether the river being drawn runs against the way it is drawn: decided with its first stretch, kept for the
	/// rest of it so its water runs one way all along.
	/// </summary>
	private bool? m_ChainUpstream;

	private float? m_SnappedAngle;
	private Vector3? m_AlignAnchor;

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

	public bool IsDrawing => m_LastNodeId >= 0;

	private bool SnapActive => AngleSnap != Gizmo.IsShiftPressed;



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



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



	public override void OnUpdate()
	{
		var root = Root;

		if (root is null)
			return;

		var network = root.Network;

		// Undo can pull the node we were extending from out from under us.
		if (IsDrawing && (!network.IsValid() || network.FindNode(m_LastNodeId) is null))
			EndChain();

		if (!Gizmo.HasMouseFocus)
			return;

		if (Gizmo.WasRightMousePressed)
			m_RightPressPosition = Gizmo.CursorPosition;

		if (Gizmo.WasRightMouseReleased && Vector2.Distance(Gizmo.CursorPosition, m_RightPressPosition) < ClickDragTolerance)
			EndChain();

		if (!TryGetCursorPosition(network, out Vector3 cursorWorld, out bool onGround))
			return;

		var target = ResolveTarget(network, cursorWorld, onGround);

		m_SnappedAngle = null;
		m_AlignAnchor = null;

		// Joining a node is explicit and wins. Otherwise the snap places the end: on bare ground, or on a river the
		// snapped line crosses, which is split right there so the join is straight.
		if (target.Node is null && target.Lake is null && IsDrawing && SnapActive && TrySnapAngle(network, cursorWorld, target.River, out Vector3 snappedWorld, out float angle, out WaterRiverPick? split))
		{
			if (split is { } pick)
			{
				target = new Target(snappedWorld, null, pick);
				m_SnappedAngle = angle;
			}
			else if (target.River is null)
			{
				var joined = network.FindNearestNode(network.WorldTransform.PointToLocal(snappedWorld), SnapRadius);

				if (joined is not null && joined.Id != m_LastNodeId)
					target = new Target(network.WorldTransform.PointToWorld(joined.Position), joined, null);
				else if (SeaTarget(network, snappedWorld) is { } sea)
					target = sea;
				else
					target = new Target(snappedWorld, null, null);

				m_SnappedAngle = angle;
			}
		}

		DrawPreview(network, target);

		if (Gizmo.WasLeftMousePressed)
			PlaceNode(root, target);
	}



	public void EndChain()
	{
		m_LastNodeId = -1;
		m_ChainUpstream = null;
	}



	/// <summary>
	/// Nodes win over rivers, rivers over lakes, lakes over the sea, the sea over bare ground. A node counts as hit when
	/// its on-screen handle is under the cursor or it lies within the snap radius of the ground point.
	/// </summary>
	private Target ResolveTarget(WaterNetwork _Network, Vector3 _CursorWorld, bool _OnGround)
	{
		if (!_Network.IsValid())
			return new Target(_CursorWorld, null, null);

		var transform = _Network.WorldTransform;
		var node = WaterPicker.PickNode(_Network, Gizmo.CurrentRay);

		if (node is null && SnapRadius > 0.0f)
			node = _Network.FindNearestNode(transform.PointToLocal(_CursorWorld), SnapRadius);

		if (node is not null)
			return new Target(transform.PointToWorld(node.Position), node, null);

		var river = WaterPicker.PickRiver(_Network, Gizmo.CurrentRay);

		if (river is { } pick)
			return new Target(transform.PointToWorld(pick.LocalPoint), null, pick);

		// Over a lake: the river opens into it at the nearest point of its shore.
		if (WaterPicker.PickLake(_Network, Gizmo.CurrentRay, out Vector3 lakeHit) is { } lake && _Network.GetShore(lake) is { } shore)
		{
			shore.At(shore.NearestDistance(lakeHit), out Vector3 onShore, out _, out _);

			return new Target(transform.PointToWorld(onShore), null, null, lake);
		}

		// Over the sea: the river opens into it through the nearest point of the coast.
		if (WaterPicker.PickSea(_Network, Gizmo.CurrentRay, _OnGround ? _CursorWorld : (Vector3?)null, out Vector3 onCoast) is { } coast)
			return new Target(transform.PointToWorld(onCoast), null, null, coast, true);

		return new Target(_CursorWorld, null, null);
	}



	/// <summary>
	/// A point on the ground (snapped there) that lies under the sea: the river opens into the sea through the coast
	/// nearest to it. Null on land, or when the sea is off or has no coast.
	/// </summary>
	private static Target? SeaTarget(WaterNetwork _Network, Vector3 _GroundWorld)
	{
		if (!_Network.SeaEnabled || _GroundWorld.z > _Network.WorldSeaLevel - 1.0f)
			return null;

		var transform = _Network.WorldTransform;

		if (_Network.NearestCoast(transform.PointToLocal(_GroundWorld), out Vector3 onCoast) is not { } coast)
			return null;

		return new Target(transform.PointToWorld(onCoast), null, null, coast, true);
	}



	private void PlaceNode(WaterNetworkTool _Root, in Target _Target)
	{
		var session = SceneEditorSession.Active;
		var network = _Root.Network;
		bool createNetwork = !network.IsValid();

		// Picking up an existing node changes nothing in the graph (starting from it, clicking the node we are already
		// on, or joining one we are already connected to): just carry on from there.
		if (_Target.Node is { } existing && (!IsDrawing || existing.Id == m_LastNodeId || network.FindRiverBetween(m_LastNodeId, existing.Id) is not null))
		{
			if (existing.Id != m_LastNodeId)
				m_ChainUpstream = null;

			m_LastNodeId = existing.Id;
			return;
		}

		string undoName = _Target.River is not null
			? (IsDrawing ? "Join River" : "Branch River")
			: _Target.Sea ? "Open River Into Sea"
			: _Target.Lake is not null ? "Open River Into Lake"
			: (IsDrawing ? "Extend River" : "Start River");

		// A brand new network is undone by destroying its GameObject; an existing one by restoring its graph.
		var scope = session.UndoScope(undoName);

		if (createNetwork)
			scope.WithGameObjectCreations();
		else
			scope.WithComponentChanges(network);

		using (scope.Push())
		{
			if (createNetwork)
				network = _Root.CreateNetwork();

			var transform = network.WorldTransform;
			WaterNode node;
			bool isNew = false;

			if (_Target.Node is not null)
			{
				node = _Target.Node;
			}
			else if (_Target.River is { } pick)
			{
				node = network.SplitRiver(pick.River.Id, pick.T, MinEndDistance);
			}
			else if (_Target.Lake is { } lake)
			{
				// A lake, or a coast for the sea: an opening on its shore either way.
				node = network.AddOpening(lake.Id, transform.PointToLocal(_Target.World));
			}
			else
			{
				node = network.AddNode(transform.PointToLocal(_Target.World) + Vector3.Up * HeightOffset);
				isNew = true;
			}

			if (node is null)
				return;

			if (IsDrawing && node.Id != m_LastNodeId && network.FindNode(m_LastNodeId) is { } last)
			{
				bool upstream = m_ChainUpstream ?? ChooseDirection(network, last, node);

				// A sharp corner needs both ends of the new river to keep the straight handles it starts with.
				if (SharpCorners)
				{
					network.SetAutoTangents(m_LastNodeId, false, false);
					network.SetAutoTangents(node.Id, false, false);
				}

				var river = upstream
					? network.AddRiver(node.Id, m_LastNodeId, Width, Speed, Waves, Depth)
					: network.AddRiver(m_LastNodeId, node.Id, Width, Speed, Waves, Depth);

				if (river is not null)
					m_ChainUpstream = upstream;

				// A river that has run into a lake or the sea ends there: nothing else can join it at that end.
				if (river is not null && _Target.Lake is not null)
				{
					EndChain();
					return;
				}
			}

			// A corner node is a node whose handles all point straight at its neighbours.
			if (isNew && SharpCorners)
				network.SetAutoTangents(node.Id, false);

			m_LastNodeId = node.Id;
		}
	}



	/// <summary>
	/// Which way the water of a river from one node to another should run: whatever the tool is set to, or in Auto
	/// downhill, and on level ground on from the river being extended, or into the water the river was started from.
	/// True when it runs from the new node back to the old one.
	/// </summary>
	private bool ChooseDirection(WaterNetwork _Network, WaterNode _From, WaterNode _To)
	{
		switch (Flow)
		{
			case WaterFlowDirection.Downstream:
				return false;

			case WaterFlowDirection.Upstream:
				return true;
		}

		float drop = _From.Position.z - _To.Position.z;

		if (MathF.Abs(drop) > 1.0f)
			return drop < 0.0f;

		// Started from a lake or the sea: the river runs into it.
		if (_Network.LakeAtOpening(_From.Id) is not null)
			return true;

		var rivers = _Network.GetRiversAt(_From.Id);

		// Extending a river from one of its ends carries it on the way it already runs.
		if (rivers.Count == 1)
			return rivers[0].StartNodeId == _From.Id;

		// Drawn off a river (a split) or a confluence: a tributary, running into it.
		if (rivers.Count >= 2)
			return true;

		return false;
	}



	/// <summary>
	/// Snaps the direction from the node being extended to the cursor onto the nearest of the snap angles: whole steps
	/// round from the world X axis, and from each river already leaving that node. The cursor's distance along the
	/// snapped direction is kept and the point is dropped back onto the ground there.
	/// </summary>
	private bool TrySnapAngle(WaterNetwork _Network, Vector3 _CursorWorld, WaterRiverPick? _Hovered, out Vector3 _SnappedWorld, out float _Angle, out WaterRiverPick? _Split)
	{
		_SnappedWorld = _CursorWorld;
		_Angle = 0.0f;
		_Split = null;

		if (!_Network.IsValid() || _Network.FindNode(m_LastNodeId) is not { } last)
			return false;

		var transform = _Network.WorldTransform;
		Vector3 startWorld = transform.PointToWorld(last.Position);
		var delta = new Vector2(_CursorWorld.x - startWorld.x, _CursorWorld.y - startWorld.y);

		if (delta.Length < 1.0f)
			return false;

		float step = MathF.Max(1.0f, AngleStep);
		float raw = MathF.Atan2(delta.y, delta.x).RadianToDegree();
		float best = SnapTo(raw, 0.0f, step);

		foreach (var river in _Network.GetRiversAt(last.Id))
		{
			if (!_Network.TryGetCurve(river, out var curve))
				continue;

			Vector3 tangent = river.StartNodeId == last.Id ? curve.Tangent(0.0f) : -curve.Tangent(1.0f);
			Vector3 worldTangent = transform.NormalToWorld(tangent);
			var flat = new Vector2(worldTangent.x, worldTangent.y);

			if (flat.Length < 0.001f)
				continue;

			float reference = MathF.Atan2(flat.y, flat.x).RadianToDegree();

			foreach (float candidate in new[] { SnapTo(raw, reference, step), SnapTo(raw, reference + 180.0f, step) })
			{
				if (MathF.Abs(AngleDifference(candidate, raw)) < MathF.Abs(AngleDifference(best, raw)))
					best = candidate;
			}
		}

		var direction = new Vector2(MathF.Cos(best.DegreeToRadian()), MathF.Sin(best.DegreeToRadian()));
		var start = new Vector2(startWorld.x, startWorld.y);
		float length = MathF.Max(1.0f, Vector2.Dot(delta, direction));
		float tolerance = AlignTolerance(_CursorWorld);

		_Angle = NormalizeAngle(best);

		if (TryCrossRiver(_Network, start, direction, length, tolerance, _Hovered, out WaterRiverPick pick, out Vector3 crossingWorld))
		{
			_SnappedWorld = crossingWorld;
			_Split = pick;
			m_AlignAnchor = null;

			return true;
		}

		if (TryAlign(_Network, last, start, direction, length, tolerance, out float aligned, out Vector3 anchor))
		{
			length = aligned;
			m_AlignAnchor = anchor;
		}

		Vector2 snapped = start + direction * length;

		_SnappedWorld = new Vector3(snapped.x, snapped.y, GroundHeightAt(snapped, _CursorWorld.z) + HeightOffset);

		return true;
	}



	/// <summary>
	/// Where the snapped line crosses a river's centreline near the cursor's distance along it (from much further off
	/// for the river under the cursor). The nearest such crossing wins.
	/// </summary>
	private static bool TryCrossRiver(WaterNetwork _Network, Vector2 _Start, Vector2 _Direction, float _Length, float _Tolerance, WaterRiverPick? _Hovered, out WaterRiverPick _Pick, out Vector3 _World)
	{
		var transform = _Network.WorldTransform;
		float bestError = float.MaxValue;
		bool found = false;

		_Pick = default;
		_World = default;

		foreach (var river in _Network.Rivers)
		{
			var table = _Network.GetCurveTable(river);

			if (table is null)
				continue;

			bool hovered = _Hovered is { } hover && hover.River.Id == river.Id;
			float allowed = hovered ? MathF.Max(_Tolerance, _Network.WidthOf(river).MaxHalf * 4.0f) : _Tolerance;
			Vector3 previousWorld = transform.PointToWorld(table.SamplePosition(0));

			for (int i = 1; i < table.SampleCount; i++)
			{
				Vector3 currentWorld = transform.PointToWorld(table.SamplePosition(i));
				var p = new Vector2(previousWorld.x, previousWorld.y);
				var edge = new Vector2(currentWorld.x - previousWorld.x, currentWorld.y - previousWorld.y);
				float denominator = Cross(_Direction, edge);

				if (MathF.Abs(denominator) > 0.000001f)
				{
					Vector2 offset = p - _Start;
					float t = Cross(offset, edge) / denominator;
					float u = Cross(offset, _Direction) / denominator;

					if (t >= 1.0f && u >= 0.0f && u <= 1.0f)
					{
						float error = MathF.Abs(t - _Length);

						if (error <= allowed && error < bestError)
						{
							float distance = table.SampleDistance(i - 1) + u * (table.SampleDistance(i) - table.SampleDistance(i - 1));
							float curveT = table.DistanceToT(distance);

							bestError = error;
							found = true;
							_Pick = new WaterRiverPick(river, curveT, table.Curve.Evaluate(curveT));
							_World = Vector3.Lerp(previousWorld, currentWorld, u);
						}
					}
				}

				previousWorld = currentWorld;
			}
		}

		return found;
	}



	/// <summary>
	/// With the direction fixed, snaps the distance along it to where it crosses a guide line through another node
	/// (square to the river being drawn, along the world axes, or along the rivers at that node).
	/// </summary>
	private static bool TryAlign(WaterNetwork _Network, WaterNode _Last, Vector2 _Start, Vector2 _Direction, float _Length, float _Tolerance, out float _AlignedLength, out Vector3 _AnchorWorld)
	{
		var transform = _Network.WorldTransform;
		var perpendicular = new Vector2(-_Direction.y, _Direction.x);
		float bestError = _Tolerance;
		bool found = false;

		_AlignedLength = _Length;
		_AnchorWorld = default;

		foreach (var node in _Network.Nodes)
		{
			if (node.Id == _Last.Id)
				continue;

			Vector3 world = transform.PointToWorld(node.Position);
			var anchor = new Vector2(world.x, world.y);

			foreach (Vector2 guide in GuidesAt(_Network, node, perpendicular))
			{
				float denominator = Cross(_Direction, guide);

				if (MathF.Abs(denominator) < 0.05f)
					continue;

				float crossing = Cross(anchor - _Start, guide) / denominator;

				if (crossing < 1.0f)
					continue;

				float error = MathF.Abs(crossing - _Length);

				if (error >= bestError)
					continue;

				bestError = error;
				_AlignedLength = crossing;
				_AnchorWorld = world;
				found = true;
			}
		}

		return found;
	}



	private static IEnumerable<Vector2> GuidesAt(WaterNetwork _Network, WaterNode _Node, Vector2 _Perpendicular)
	{
		yield return _Perpendicular;
		yield return new Vector2(1.0f, 0.0f);
		yield return new Vector2(0.0f, 1.0f);

		var transform = _Network.WorldTransform;

		foreach (var river in _Network.GetRiversAt(_Node.Id))
		{
			if (!_Network.TryGetCurve(river, out var curve))
				continue;

			Vector3 tangent = river.StartNodeId == _Node.Id ? curve.Tangent(0.0f) : -curve.Tangent(1.0f);
			Vector3 worldTangent = transform.NormalToWorld(tangent);
			var flat = new Vector2(worldTangent.x, worldTangent.y);

			if (flat.Length > 0.001f)
				yield return flat.Normal;
		}
	}



	private static float AlignTolerance(Vector3 _World)
	{
		return MathF.Max(25.0f, Gizmo.Camera.Position.Distance(_World) * 0.025f);
	}



	private static float Cross(Vector2 _A, Vector2 _B)
	{
		return _A.x * _B.y - _A.y * _B.x;
	}



	private static float SnapTo(float _Angle, float _Reference, float _Step)
	{
		return _Reference + MathF.Round(AngleDifference(_Angle, _Reference) / _Step) * _Step;
	}



	private static float AngleDifference(float _To, float _From)
	{
		float difference = (_To - _From) % 360.0f;

		if (difference > 180.0f)
			difference -= 360.0f;
		else if (difference <= -180.0f)
			difference += 360.0f;

		return difference;
	}



	private static float NormalizeAngle(float _Angle)
	{
		float normalized = _Angle % 360.0f;

		return normalized < 0.0f ? normalized + 360.0f : normalized;
	}



	/// <summary>
	/// The ground height at a point in plan, from a trace straight down through it; the fallback height when there is
	/// nothing there.
	/// </summary>
	private float GroundHeightAt(Vector2 _Point, float _Fallback)
	{
		const float reach = 20000.0f;

		var ray = new Ray(new Vector3(_Point.x, _Point.y, _Fallback + reach * 0.5f), Vector3.Down);
		var physics = Scene.Trace.Ray(ray, reach).Run();

		if (physics.Hit)
			return physics.EndPosition.z;

		var meshes = Scene.Trace.Ray(ray, reach).UseRenderMeshes(true, EditorPreferences.BackfaceSelection).WithoutTags("hidden").UsePhysicsWorld(false).Run();

		return meshes.Hit ? meshes.EndPosition.z : _Fallback;
	}



	/// <summary>
	/// Where the cursor is on the ground. With nothing there (<paramref name="_OnGround"/> false), on a level plane.
	/// </summary>
	private bool TryGetCursorPosition(WaterNetwork _Network, out Vector3 _WorldPosition, out bool _OnGround)
	{
		_OnGround = true;

		var physics = Trace.Run();

		if (physics.Hit)
		{
			_WorldPosition = physics.EndPosition;
			return true;
		}

		var meshes = MeshTrace.Run();

		if (meshes.Hit)
		{
			_WorldPosition = meshes.EndPosition;
			return true;
		}

		_OnGround = false;

		// Nothing under the cursor (an empty scene, or aiming past the terrain): a level plane through the node being
		// extended, so the river can still be drawn.
		float height = 0.0f;

		if (IsDrawing && _Network.IsValid() && _Network.FindNode(m_LastNodeId) is { } last)
			height = _Network.WorldTransform.PointToWorld(last.Position).z;

		var plane = new Plane(new Vector3(0.0f, 0.0f, height), Vector3.Up);
		var hit = plane.Trace(Gizmo.CurrentRay, true);

		_WorldPosition = hit ?? default;

		return hit.HasValue;
	}



	private void DrawPreview(WaterNetwork _Network, in Target _Target)
	{
		using (Gizmo.Scope("watertool2_river_draw"))
		{
			Gizmo.Draw.IgnoreDepth = true;
			Gizmo.Draw.LineThickness = 2.0f;

			if (_Target.Node is not null)
			{
				Gizmo.Draw.Color = Color.Green;
				WaterGizmos.DrawNode(_Target.World, true);
			}
			else if (_Target.River is { } pick)
			{
				// Hovering a river: show it, and where the new node would split it.
				Gizmo.Draw.Color = Color.Orange.WithAlpha(0.6f);

				if (_Network.TryGetCurve(pick.River, out var riverCurve))
					WaterGizmos.DrawRiverOutline(riverCurve, _Network.WidthOf(pick.River), _Network.WorldTransform);

				Gizmo.Draw.Color = Color.Orange;
				WaterGizmos.DrawNode(_Target.World, true);
			}
			else if (_Target.Lake is not null)
			{
				string text = _Target.Sea
					? (IsDrawing ? "Open into the sea" : "Start from the sea")
					: (IsDrawing ? "Open into lake" : "Start from lake");

				Gizmo.Draw.Color = Color.Cyan;
				WaterGizmos.DrawLake(_Network, _Target.Lake, _Network.WorldTransform);
				WaterGizmos.DrawNode(_Target.World, true);
				Gizmo.Draw.ScreenText(text, _Target.World, new Vector2(12.0f, -12.0f), size: 12.0f);
			}
			else
			{
				Gizmo.Draw.Color = Color.White;
				WaterGizmos.DrawNode(_Target.World, false);
			}

			if (!IsDrawing || !_Network.IsValid() || _Network.FindNode(m_LastNodeId) is not { } last)
				return;

			if (_Target.Node is not null && _Target.Node.Id == m_LastNodeId)
				return;

			var transform = _Network.WorldTransform;
			Vector3 targetLocal = _Target.Node?.Position ?? _Target.River?.LocalPoint ?? transform.PointToLocal(_Target.World);
			var curve = _Network.PreviewCurve(m_LastNodeId, targetLocal, _Target.Node?.Id ?? -1, SharpCorners);

			Gizmo.Draw.Color = Color.White.WithAlpha(0.9f);
			WaterGizmos.DrawRiverOutline(curve, new RiverWidth(Width, Width, curve.ApproximateLength()), transform);

			// Which way the water will run.
			bool upstream = m_ChainUpstream ?? PreviewDirection(_Network, last, targetLocal);
			var flowCurve = upstream ? new WaterCurve(curve.P3, curve.P2, curve.P1, curve.P0) : curve;
			float size = MathF.Max(40.0f, Width * 0.3f);

			Gizmo.Draw.Color = Color.Cyan;
			WaterGizmos.DrawFlowArrows(flowCurve, transform, size * 5.0f, size);

			if (m_SnappedAngle is { } angle)
			{
				Vector3 start = transform.PointToWorld(last.Position);
				var direction = new Vector3(MathF.Cos(angle.DegreeToRadian()), MathF.Sin(angle.DegreeToRadian()), 0.0f);

				Gizmo.Draw.LineThickness = 1.0f;
				Gizmo.Draw.Color = Color.Cyan.WithAlpha(0.5f);
				Gizmo.Draw.Line(start, start + direction * GuideLength);

				Gizmo.Draw.Color = Color.Cyan;
				Gizmo.Draw.ScreenText($"{angle:0}°", _Target.World, new Vector2(12.0f, -12.0f), size: 12.0f);
			}

			if (m_AlignAnchor is { } anchor)
			{
				Gizmo.Draw.LineThickness = 2.0f;
				Gizmo.Draw.Color = Color.Orange;
				Gizmo.Draw.Line(anchor, _Target.World);
				WaterGizmos.DrawNode(anchor, true);
			}
		}
	}



	/// <summary>
	/// The direction <see cref="ChooseDirection"/> would pick for a river to a point not placed yet.
	/// </summary>
	private bool PreviewDirection(WaterNetwork _Network, WaterNode _From, Vector3 _ToLocal)
	{
		var probe = new WaterNode { Id = -1, Position = _ToLocal };

		return ChooseDirection(_Network, _From, probe);
	}
}



file class WaterRiverToolWidget : ToolSidebarWidget
{
	private readonly WaterRiverTool m_Tool;



	public WaterRiverToolWidget(WaterRiverTool _Tool)
	{
		m_Tool = _Tool;

		AddTitle("Draw River", "waves");

		var serialized = _Tool.GetSerialized();
		var river = AddGroup("River");

		SidebarRows.Add(river, serialized.GetProperty(nameof(WaterRiverTool.Width)));
		SidebarRows.Add(river, serialized.GetProperty(nameof(WaterRiverTool.Speed)));
		SidebarRows.Add(river, serialized.GetProperty(nameof(WaterRiverTool.Waves)));
		SidebarRows.Add(river, serialized.GetProperty(nameof(WaterRiverTool.Depth)));
		SidebarRows.Add(river, serialized.GetProperty(nameof(WaterRiverTool.Flow)));

		var placing = AddGroup("Placing");

		SidebarRows.Add(placing, serialized.GetProperty(nameof(WaterRiverTool.HeightOffset)));
		SidebarRows.Add(placing, serialized.GetProperty(nameof(WaterRiverTool.SnapRadius)));
		SidebarRows.Add(placing, serialized.GetProperty(nameof(WaterRiverTool.SharpCorners)));
		SidebarRows.Add(placing, serialized.GetProperty(nameof(WaterRiverTool.AngleSnap)));
		SidebarRows.Add(placing, serialized.GetProperty(nameof(WaterRiverTool.AngleStep)));

		AddShortcuts(
			("Place Node", "LMB"),
			("Continue From / Join Node", "LMB on node"),
			("Branch From / Flow Into River", "LMB on river"),
			("Open Into Lake", "LMB on lake"),
			("Open Into Sea", "LMB on sea (needs a coast)"),
			("Snap Angle + Align With Nodes", "Hold SHIFT"),
			("End River", "RMB / ESC")
		);

		Layout.AddStretchCell();
	}



	[Shortcut("watertool2.river-end", "ESC", typeof(SceneViewWidget))]
	private void EndChain()
	{
		m_Tool.EndChain();
	}
}