Network/WaterNetwork.Tangents.cs

Part of the WaterNetwork class that computes and updates Bezier tangent handles for water-river connections. It refreshes tangents around a node, previews the curve for a potential new river, and computes automatic or straight handles based on neighbour geometry and lake outward vectors.

NetworkingFile Access
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace RedSnail.WaterTool2;

public sealed partial class WaterNetwork
{
	/// <summary>
	/// Recomputes the automatic tangent handles at a node and at each of its neighbours. A neighbour's smoothing
	/// direction runs through this node, so it has to follow whenever this node moves or gains a river.
	/// </summary>
	public void RefreshTangentsAround(int _NodeId)
	{
		RefreshTangentsAt(_NodeId);

		foreach (var river in GetRiversAt(_NodeId).ToList())
			RefreshTangentsAt(river.OtherNodeId(_NodeId));
	}



	private void RefreshTangentsAt(int _NodeId)
	{
		var node = FindNode(_NodeId);

		if (node is null || !node.AutoTangents)
			return;

		var rivers = GetRiversAt(_NodeId).ToList();

		foreach (var river in rivers)
		{
			var other = FindNode(river.OtherNodeId(_NodeId));

			if (other is null)
				continue;

			var neighbours = new List<Vector3>();

			foreach (var otherRiver in rivers)
			{
				if (otherRiver == river)
					continue;

				var neighbour = FindNode(otherRiver.OtherNodeId(_NodeId));

				if (neighbour is not null)
					neighbours.Add(neighbour.Position);
			}

			Vector3 handle = ComputeAutoHandle(node.Position, other.Position, neighbours);

			if (river.StartNodeId == _NodeId)
				river.StartTangent = handle;
			else
				river.EndTangent = handle;
		}
	}



	/// <summary>
	/// The curve a river from <paramref name="_FromNodeId"/> to <paramref name="_ToLocalPosition"/> would get, with
	/// <paramref name="_ToNodeId"/> naming the existing node at that position when the river joins one, and
	/// <paramref name="_Sharp"/> when the new node is going to be a sharp bend. Lets a tool preview exactly what
	/// <see cref="AddRiver"/> will build.
	/// </summary>
	public WaterCurve PreviewCurve(int _FromNodeId, Vector3 _ToLocalPosition, int _ToNodeId = -1, bool _Sharp = false)
	{
		var from = FindNode(_FromNodeId);

		if (from is null)
			return new WaterCurve(_ToLocalPosition, _ToLocalPosition, _ToLocalPosition, _ToLocalPosition);

		var to = _ToNodeId >= 0 ? FindNode(_ToNodeId) : null;

		Vector3 startHandle = from.AutoTangents && !_Sharp
			? ComputeAutoHandle(from.Position, _ToLocalPosition, GetNeighbourPositions(_FromNodeId))
			: StraightHandle(from.Position, _ToLocalPosition);

		Vector3 endHandle = to is not null && to.AutoTangents && !_Sharp
			? ComputeAutoHandle(_ToLocalPosition, from.Position, GetNeighbourPositions(_ToNodeId))
			: StraightHandle(_ToLocalPosition, from.Position);

		if (TryLakeOutward(_FromNodeId, out Vector3 fromOutward))
			startHandle = fromOutward * startHandle.Length;

		if (_ToNodeId >= 0 && TryLakeOutward(_ToNodeId, out Vector3 toOutward))
			endHandle = toOutward * endHandle.Length;

		return new WaterCurve(from.Position, from.Position + startHandle, _ToLocalPosition + endHandle, _ToLocalPosition);
	}



	private List<Vector3> GetNeighbourPositions(int _NodeId)
	{
		return GetRiversAt(_NodeId)
			.Select(river => FindNode(river.OtherNodeId(_NodeId)))
			.Where(node => node is not null)
			.Select(node => node.Position)
			.ToList();
	}



	/// <summary>
	/// The Bezier handle leaving a node toward one of its rivers. At a bend (exactly one other neighbour) it runs from
	/// that neighbour through the node, so the river flows through without a kink. River ends point straight at the far
	/// node, and so do the arms of a confluence. The handle spans a third of the chord, which reproduces a straight
	/// line exactly when the nodes are in line.
	/// </summary>
	private static Vector3 ComputeAutoHandle(Vector3 _NodePosition, Vector3 _TargetPosition, IReadOnlyList<Vector3> _OtherNeighbours)
	{
		Vector3 chord = _TargetPosition - _NodePosition;
		float length = chord.Length;

		if (length < 0.001f)
			return Vector3.Zero;

		Vector3 direction = chord / length;

		if (_OtherNeighbours.Count == 1)
		{
			Vector3 through = _TargetPosition - _OtherNeighbours[0];

			// A hairpin (both rivers leaving on the same side) would fold the curve back on itself; keep it straight.
			if (!through.IsNearZeroLength && Vector3.Dot(through, chord) > 0.0f)
				direction = through.Normal;
		}

		return direction * (length / 3.0f);
	}



	/// <summary>
	/// A handle pointing straight at the far node: a straight river when both ends have one.
	/// </summary>
	private static Vector3 StraightHandle(Vector3 _NodePosition, Vector3 _TargetPosition)
	{
		return (_TargetPosition - _NodePosition) / 3.0f;
	}
}