Editor utility for picking water-related objects in the editor. It provides functions to pick nodes, rivers, lakes, lake control points and shore insertion locations by tracing rays against the network geometry and transformed world/local coordinates.
using System;
using Sandbox;
namespace RedSnail.WaterTool2.Editor;
/// <summary>
/// A river hit by the cursor: the river, where along it (<see cref="T"/>) and the local-space point on its curve.
/// </summary>
internal readonly record struct WaterRiverPick(WaterRiver River, float T, Vector3 LocalPoint);
/// <summary>
/// Cursor picking against the water graph, done against the network's own geometry so it works the same whether or
/// not the pieces under the cursor have been built yet.
/// </summary>
internal static class WaterPicker
{
/// <summary>
/// Handles keep a constant on-screen size, so their pick radius grows with distance from the camera.
/// </summary>
public static float NodeRadius(Vector3 _WorldPosition)
{
return MathF.Max(8.0f, Gizmo.Camera.Position.Distance(_WorldPosition) * 0.012f);
}
/// <summary>
/// The node whose handle the ray passes through, nearest to the camera first.
/// </summary>
public static WaterNode PickNode(WaterNetwork _Network, Ray _Ray, float _RadiusScale = 1.0f)
{
var transform = _Network.WorldTransform;
WaterNode best = null;
float bestDistance = float.MaxValue;
foreach (var node in _Network.Nodes)
{
Vector3 world = transform.PointToWorld(node.Position);
var sphere = new Sphere(world, NodeRadius(world) * _RadiusScale);
if (!sphere.Trace(_Ray, float.MaxValue, out float distance) || distance >= bestDistance)
continue;
best = node;
bestDistance = distance;
}
return best;
}
/// <summary>
/// The river under the cursor: the one whose surface (its curve widened to its width, plus a margin) comes closest
/// to the ray. Null when the ray misses every river.
/// </summary>
public static WaterRiverPick? PickRiver(WaterNetwork _Network, Ray _Ray, float _Margin = 0.0f)
{
var transform = _Network.WorldTransform;
var localRay = _Ray.ToLocal(transform);
WaterRiverPick? best = null;
float bestLateral = float.MaxValue;
foreach (var river in _Network.Rivers)
{
var table = _Network.GetCurveTable(river);
if (table is null)
continue;
float reach = _Network.WidthOf(river).MaxHalf + _Margin;
if (!table.Bounds.Grow(reach).Trace(localRay, float.MaxValue, out _))
continue;
Vector3 previous = transform.PointToWorld(table.SamplePosition(0));
for (int i = 1; i < table.SampleCount; i++)
{
Vector3 current = transform.PointToWorld(table.SamplePosition(i));
var line = new Line(previous, current);
if (line.ClosestPoint(_Ray, out Vector3 onLine, out Vector3 onRay))
{
float lateral = Vector3.DistanceBetween(onLine, onRay);
if (lateral <= reach && lateral < bestLateral)
{
float span = Vector3.DistanceBetween(previous, current);
float fraction = span > 0.0f ? Vector3.DistanceBetween(previous, onLine) / span : 0.0f;
float distance = table.SampleDistance(i - 1) + fraction * (table.SampleDistance(i) - table.SampleDistance(i - 1));
float t = table.DistanceToT(distance);
best = new WaterRiverPick(river, t, table.Curve.Evaluate(t));
bestLateral = lateral;
}
}
previous = current;
}
}
return best;
}
/// <summary>
/// The lake under the cursor: where the ray meets each lake's water level, inside its shore. The nearest hit wins.
/// The sea's coast is no lake (its water is outside it): see <see cref="PickSea"/>.
/// </summary>
public static WaterLake PickLake(WaterNetwork _Network, Ray _Ray, out Vector3 _LocalHit)
{
var transform = _Network.WorldTransform;
WaterLake best = null;
float bestDistance = float.MaxValue;
_LocalHit = default;
foreach (var lake in _Network.Lakes)
{
var shore = lake.IsCoast ? null : _Network.GetShore(lake);
if (shore is null)
continue;
var plane = new Plane(transform.PointToWorld(lake.Centre), transform.NormalToWorld(Vector3.Up));
if (plane.Trace(_Ray, true) is not Vector3 hit)
continue;
Vector3 local = transform.PointToLocal(hit);
if (!shore.Contains(local))
continue;
float distance = _Ray.Position.Distance(hit);
if (distance >= bestDistance)
continue;
best = lake;
bestDistance = distance;
_LocalHit = local;
}
return best;
}
/// <summary>
/// The coast a river would open into the sea through, when the cursor is over the sea: the ray reaches the sea's
/// level before the ground (or meets no ground at all), out on the water's side of the coast.
/// <paramref name="_LocalOnShore"/> is the point of the coast nearest to where the ray meets the sea. Null when the
/// sea is off, the cursor is over land, or no coast has been built for the sea to have an edge at.
/// </summary>
public static WaterLake PickSea(WaterNetwork _Network, Ray _Ray, Vector3? _GroundWorld, out Vector3 _LocalOnShore)
{
_LocalOnShore = default;
if (!_Network.SeaEnabled)
return null;
float level = _Network.WorldSeaLevel;
if (_GroundWorld is Vector3 ground && ground.z > level - 1.0f)
return null;
if (new Plane(new Vector3(0.0f, 0.0f, level), Vector3.Up).Trace(_Ray, true) is not Vector3 hit)
return null;
return _Network.NearestCoast(_Network.WorldTransform.PointToLocal(hit), out _LocalOnShore);
}
/// <summary>
/// The control point of a lake whose handle the ray passes through, or -1.
/// </summary>
public static int PickLakePoint(WaterNetwork _Network, WaterLake _Lake, Ray _Ray)
{
var transform = _Network.WorldTransform;
int best = -1;
float bestDistance = float.MaxValue;
for (int i = 0; i < _Lake.Points.Count; i++)
{
Vector3 world = transform.PointToWorld(_Lake.PointAt(i));
var sphere = new Sphere(world, NodeRadius(world) * 0.8f);
if (!sphere.Trace(_Ray, float.MaxValue, out float distance) || distance >= bestDistance)
continue;
best = i;
bestDistance = distance;
}
return best;
}
/// <summary>
/// Where on a lake's outline the cursor is, for inserting a point there: the edge of the control polygon nearest to
/// where the ray meets the lake's level, within a handle's reach of the shore. <paramref name="_InsertIndex"/> is
/// the index the new point takes.
/// </summary>
public static bool PickShore(WaterNetwork _Network, WaterLake _Lake, Ray _Ray, out int _InsertIndex, out Vector3 _LocalPoint)
{
_InsertIndex = -1;
_LocalPoint = default;
var shore = _Network.GetShore(_Lake);
if (shore is null || _Lake.Points.Count < 2)
return false;
var transform = _Network.WorldTransform;
var plane = new Plane(transform.PointToWorld(_Lake.Centre), transform.NormalToWorld(Vector3.Up));
if (plane.Trace(_Ray, true) is not Vector3 hit)
return false;
Vector3 local = transform.PointToLocal(hit);
shore.NearestDistance(local, out float offset);
if (offset > NodeRadius(hit) * 2.0f)
return false;
float best = float.MaxValue;
var point = new Vector2(local.x, local.y);
for (int i = 0; i < _Lake.Points.Count; i++)
{
Vector3 a = _Lake.Points[i];
Vector3 b = _Lake.Points[(i + 1) % _Lake.Points.Count];
var from = new Vector2(a.x, a.y);
var span = new Vector2(b.x, b.y) - from;
float lengthSquared = span.LengthSquared;
float t = lengthSquared > 0.0f ? Math.Clamp(Vector2.Dot(point - from, span) / lengthSquared, 0.0f, 1.0f) : 0.0f;
float distance = (from + span * t).Distance(point);
if (distance >= best)
continue;
best = distance;
_InsertIndex = i + 1;
}
_LocalPoint = local.WithZ(_Lake.Height);
return _InsertIndex >= 0;
}
}