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