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.
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;
}
}