Code/Building/LakeOutline.cs

Geometry utilities for lake/river junctions. Defines LakeOpening and LakeJet structs and builds lake outlines with openings, flares into river rows, sea-mouth outlines, and helper math for bezier flares and edge parameterization.

File Access
using System;
using System.Collections.Generic;
using Sandbox;

namespace RedSnail.WaterTool2;

/// <summary>
/// Where a river opens into a lake (or out of it): where round the shore, how wide the river is and how far the shore
/// flares into it, and the river's own row where the lake hands over to it. The flares are part of the lake's outline,
/// so the lake's surface fills the corners between the shore and the river, and its outline runs across the river's
/// row point for point.
/// </summary>
internal sealed class LakeOpening
{
	public int NodeId;
	public WaterRiver River;

	/// <summary>
	/// The river runs into the lake (otherwise it runs out of it).
	/// </summary>
	public bool Inflow;

	/// <summary>
	/// The middle of the opening, as a distance round the shore.
	/// </summary>
	public float At;

	public float HalfWidth;
	public float Flare;

	/// <summary>
	/// The river's row where the lake hands over to it, in the order the shore runs.
	/// </summary>
	public RiverRow Row;

	/// <summary>
	/// The way the river heads away from the lake at its row, level.
	/// </summary>
	public Vector3 Away;

	/// <summary>
	/// How much shore the opening and its flares take either side of its middle.
	/// </summary>
	public float Reach => HalfWidth + Flare;
}



/// <summary>
/// How a river's water spreads into a lake: a jet that leaves the river's mouth, widens and slows as it goes, so a river
/// flowing in pushes a plume of current out into the lake and the water by a lake's outlet is drawn towards it.
/// </summary>
internal readonly struct LakeJet
{
	/// <summary>
	/// How fast the jet widens: this much either side for every unit it travels.
	/// </summary>
	private const float Spread = 0.3f;

	public readonly Vector2 Mouth;
	public readonly Vector2 Inward;
	public readonly Vector2 Across;
	public readonly float HalfWidth;
	public readonly Vector2 Velocity;
	public readonly float Foam;
	public readonly float Length;



	public LakeJet(Vector2 _Mouth, Vector2 _Inward, float _HalfWidth, Vector2 _Velocity, float _Foam, float _Length)
	{
		Mouth = _Mouth;
		Inward = _Inward;
		Across = new Vector2(-_Inward.y, _Inward.x);
		HalfWidth = MathF.Max(1.0f, _HalfWidth);
		Velocity = _Velocity;
		Foam = _Foam;
		Length = MathF.Max(1.0f, _Length);
	}



	public Vector2 Evaluate(Vector2 _Point, out float _Foam)
	{
		float strength = Strength(_Point, out float half);

		_Foam = Foam * strength;

		// As the jet widens the same water spreads over more of the lake, so it slows.
		return Velocity * (strength * HalfWidth / half);
	}



	/// <summary>
	/// How much of the river's own water is at a point: all of it in the jet's middle at its mouth, fading out across it
	/// and along it.
	/// </summary>
	public float Presence(Vector2 _Point)
	{
		return Strength(_Point, out _);
	}



	private float Strength(Vector2 _Point, out float _Half)
	{
		Vector2 offset = _Point - Mouth;
		float along = MathF.Max(0.0f, Vector2.Dot(offset, Inward));
		float side = MathF.Abs(Vector2.Dot(offset, Across));

		_Half = HalfWidth + along * Spread;

		// Full strength across the middle of the jet, fading out past its edges.
		float profile = 1.0f - SmoothStep(0.7f, 1.4f, side / _Half);
		float decay = MathF.Exp(-along / Length);

		return profile * decay;
	}



	public static float SmoothStep(float _From, float _To, float _Value)
	{
		float t = Math.Clamp((_Value - _From) / (_To - _From), 0.0f, 1.0f);

		return t * t * (3.0f - 2.0f * t);
	}
}



/// <summary>
/// A lake's outline with its openings cut in: the shore, and at every river that meets it a flare out to the river's
/// row, the row itself point for point, and a flare back to the shore.
/// </summary>
internal static class LakeOutline
{
	/// <summary>
	/// Flares are rounded in about this many steps.
	/// </summary>
	private const int FlareSteps = 6;



	public static List<OutlineVertex> Build(LakeShore _Shore, IReadOnlyList<LakeOpening> _Openings, int _FirstRowId)
	{
		var outline = new List<OutlineVertex>();

		if (_Openings.Count == 0)
		{
			foreach (var point in _Shore.Points)
				outline.Add(OutlineVertex.Free(point));

			return SurfaceMesher.RemoveDuplicates(outline);
		}

		int count = _Openings.Count;

		for (int i = 0; i < count; i++)
		{
			var opening = _Openings[i];
			var previous = _Openings[(i + count - 1) % count];

			float leaves = opening.At - opening.Reach;
			float returns = opening.At + opening.Reach;

			// The shore from where the previous opening came back to it, to where this one leaves it.
			foreach (var point in _Shore.Between(previous.At + previous.Reach, leaves))
				outline.Add(OutlineVertex.Free(point));

			_Shore.At(leaves, out Vector3 leavesPoint, out _, out Vector3 leavesDirection);
			_Shore.At(returns, out Vector3 returnsPoint, out _, out Vector3 returnsDirection);

			var row = opening.Row;

			// Round into the river, across its row, and back out to the shore on the other side.
			AddFlare(outline, leavesPoint, leavesDirection, row.Points[0], opening.Away);

			for (int k = 0; k < row.Count; k++)
				outline.Add(OutlineVertex.OnRow(row, k, _FirstRowId + i));

			AddFlare(outline, row.Points[^1], -opening.Away, returnsPoint, returnsDirection);
		}

		return SurfaceMesher.RemoveDuplicates(outline);
	}



	/// <summary>
	/// The outline of a river's mouth into the sea: the coast either side of the opening, out to where it leaves the
	/// block of sea the mouth is built in, with the opening cut in as a lake's (flares and the river's row); then the
	/// block's edge out at sea (<paramref name="_Edge"/>, its points counter-clockwise, on the grid the sea is drawn on)
	/// back to where the coast came in. The water is inside, as in a lake's outline. Null when the coast never leaves the
	/// block, or the opening's flares don't fit inside it.
	/// </summary>
	public static List<OutlineVertex> BuildMouth(LakeShore _Shore, LakeOpening _Opening, Func<Vector3, bool> _Inside, IReadOnlyList<Vector3> _Edge)
	{
		float leaves = _Opening.At - _Opening.Reach;
		float returns = _Opening.At + _Opening.Reach;

		if (_Edge.Count < 4)
			return null;

		if (!_Shore.TryLeave(returns, true, _Inside, out float to) || !_Shore.TryLeave(leaves, false, _Inside, out float from))
			return null;

		// The two walks must not meet round the far side of a small coast.
		if (to - from >= _Shore.Length - 1.0f)
			return null;

		var outline = new List<OutlineVertex>();

		foreach (var point in _Shore.Between(from, leaves))
			outline.Add(OutlineVertex.Free(point));

		_Shore.At(leaves, out Vector3 leavesPoint, out _, out Vector3 leavesDirection);
		_Shore.At(returns, out Vector3 returnsPoint, out _, out Vector3 returnsDirection);

		var row = _Opening.Row;

		AddFlare(outline, leavesPoint, leavesDirection, row.Points[0], _Opening.Away);

		for (int k = 0; k < row.Count; k++)
			outline.Add(OutlineVertex.OnRow(row, k, 0));

		AddFlare(outline, row.Points[^1], -_Opening.Away, returnsPoint, returnsDirection);

		foreach (var point in _Shore.Between(returns, to))
			outline.Add(OutlineVertex.Free(point));

		// Out at sea: along the block's edge (the water is on the left of the coast, so counter-clockwise) from where the
		// coast left it back to where it came in.
		_Shore.At(to, out Vector3 end, out _, out _);
		_Shore.At(from, out Vector3 start, out _, out _);

		var lengths = new float[_Edge.Count + 1];

		for (int k = 0; k < _Edge.Count; k++)
			lengths[k + 1] = lengths[k] + (_Edge[(k + 1) % _Edge.Count] - _Edge[k]).WithZ(0.0f).Length;

		float perimeter = lengths[^1];
		float leaveAt = EdgeParameter(_Edge, lengths, end);
		float target = Wrap(EdgeParameter(_Edge, lengths, start) - leaveAt, perimeter);

		// The edge's points in the order the walk meets them.
		int first = 0;
		float firstAhead = float.MaxValue;

		for (int k = 0; k < _Edge.Count; k++)
		{
			float ahead = Wrap(lengths[k] - leaveAt, perimeter);

			if (ahead > 0.01f && ahead < firstAhead)
			{
				first = k;
				firstAhead = ahead;
			}
		}

		for (int step = 0; step < _Edge.Count; step++)
		{
			int k = (first + step) % _Edge.Count;
			float ahead = Wrap(lengths[k] - leaveAt, perimeter);

			if (ahead <= 0.01f || ahead >= target - 0.01f)
				break;

			outline.Add(OutlineVertex.OnSeaEdge(_Edge[k]));
		}

		return SurfaceMesher.RemoveDuplicates(outline);
	}



	/// <summary>
	/// How far round a closed edge (from its first point) the point of it nearest to another is.
	/// </summary>
	private static float EdgeParameter(IReadOnlyList<Vector3> _Edge, float[] _Lengths, Vector3 _Point)
	{
		float best = float.MaxValue;
		float parameter = 0.0f;
		var target = new Vector2(_Point.x, _Point.y);

		for (int k = 0; k < _Edge.Count; k++)
		{
			var from = new Vector2(_Edge[k].x, _Edge[k].y);
			var span = new Vector2(_Edge[(k + 1) % _Edge.Count].x, _Edge[(k + 1) % _Edge.Count].y) - from;
			float lengthSquared = span.LengthSquared;
			float t = lengthSquared > 0.0f ? Math.Clamp(Vector2.Dot(target - from, span) / lengthSquared, 0.0f, 1.0f) : 0.0f;
			float distance = (from + span * t).Distance(target);

			if (distance >= best)
				continue;

			best = distance;
			parameter = _Lengths[k] + MathF.Sqrt(lengthSquared) * t;
		}

		return parameter;
	}



	private static float Wrap(float _Value, float _Length)
	{
		if (_Length <= 0.0f)
			return 0.0f;

		float value = _Value % _Length;

		return value < 0.0f ? value + _Length : value;
	}



	/// <summary>
	/// The shore turning out into the side of a river (or back from it): a curve that leaves along one direction and
	/// arrives along another, its two ends left out (they are the shore's and the row's own points). The pulls are
	/// kept level, so the curve's height runs smoothly from one end to the other.
	/// </summary>
	private static void AddFlare(List<OutlineVertex> _Outline, Vector3 _From, Vector3 _FromDirection, Vector3 _To, Vector3 _ToDirection)
	{
		float span = _From.Distance(_To);

		if (span <= 0.01f)
			return;

		Vector3 fromDirection = Flat(_FromDirection);
		Vector3 toDirection = Flat(_ToDirection);

		// A quarter turn's worth of pull at each end, which is what makes the curve read as an arc.
		float pull = span * 0.39f;
		Vector3 controlFrom = _From + fromDirection * pull;
		Vector3 controlTo = _To - toDirection * pull;

		for (int i = 1; i < FlareSteps; i++)
		{
			float t = (float)i / FlareSteps;
			float u = 1.0f - t;

			Vector3 point = u * u * u * _From + 3.0f * u * u * t * controlFrom + 3.0f * u * t * t * controlTo + t * t * t * _To;

			_Outline.Add(OutlineVertex.Free(point));
		}
	}



	private static Vector3 Flat(Vector3 _Direction)
	{
		Vector3 flat = _Direction.WithZ(0.0f);

		return flat.IsNearZeroLength ? _Direction.Normal : flat.Normal;
	}
}