Editor/WaterLakeTool.cs

Editor tool for placing and editing water lakes in the scene. Handles creating freeform, rectangular or circular lakes, drawing previews, selecting lakes and shore points, moving points and whole lakes with gizmos, joining rivers to lakes, and provides a sidebar widget for editing properties with undo support.

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

namespace RedSnail.WaterTool2.Editor;

/// <summary>
/// How a new lake is laid out.
/// </summary>
public enum WaterLakeShape
{
	/// <summary>
	/// Click its shore point by point; click the first point again (or right-click) to close it.
	/// </summary>
	Freeform,

	/// <summary>
	/// Click one corner, then the opposite one.
	/// </summary>
	Rectangle,

	/// <summary>
	/// Click its middle, then how far out it reaches.
	/// </summary>
	Circle
}

/// <summary>
/// Lakes: lay one out on the ground (freeform, a rectangle or a circle), click one to select it, drag its shore points,
/// Ctrl+click its shore to add a point, move it (and raise or lower its water) with the gizmo. Clicking the loose end of
/// a river near a lake opens the river into it; rivers drawn onto a lake with the River tool open into it as well.
/// </summary>
[Title("Lake")]
[Icon("water_drop")]
[Group("1")]
[Order(2)]
public class WaterLakeTool : 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;

	[Property, Title("Shape")] public WaterLakeShape Shape { get; set; } = WaterLakeShape.Freeform;

	/// <summary>
	/// A freeform lake's shore runs smoothly through its points; off, it runs straight from point to point.
	/// </summary>
	[Property, Title("Smooth")] public bool Smooth { get; set; } = true;

	[Property, Title("Waves"), Range(0.0f, 2.0f)] public float Waves { get; set; } = 1.0f;

	[Property, Title("Depth"), Range(10.0f, 5000.0f)] public float Depth { get; set; } = 500.0f;

	/// <summary>
	/// How far above the ground first clicked the water level of a new lake is.
	/// </summary>
	[Property, Title("Height Offset"), Range(-200.0f, 200.0f)] public float HeightOffset { get; set; } = 0.0f;

	[Property, Title("Circle Points"), Range(5, 32)] public int CirclePoints { get; set { field = value.Clamp(5, 64); } } = 12;

	public int SelectedLakeId { get; private set; } = -1;

	/// <summary>
	/// Bumped whenever the selection changes, so the sidebar knows to rebuild itself.
	/// </summary>
	public int SelectionVersion { get; private set; }

	public WaterNetwork Network => Root?.Network;

	/// <summary>
	/// The selected lake. The sea's coast is not one of them: the Sea tool builds it.
	/// </summary>
	public WaterLake SelectedLake => Network.IsValid() && Network.FindLake(SelectedLakeId) is { IsCoast: false } lake ? lake : null;

	/// <summary>
	/// The selected lake's water level, in network space. Setting it raises or lowers the lake and its openings.
	/// </summary>
	[Property, Title("Water Level")]
	public float LakeHeight
	{
		get => SelectedLake?.Height ?? 0.0f;
		set
		{
			if (SelectedLake is { } lake)
				Network.SetLakeHeight(lake.Id, value);
		}
	}

	private WaterNetworkTool Root => Manager?.CurrentTool as WaterNetworkTool;

	// A lake being laid out: the points clicked so far (world space), or the first click of a rectangle or circle.
	private readonly List<Vector3> m_DrawPoints = new();
	private bool m_Drawing;
	private float m_DrawHeight;
	private Vector2 m_RightPressPosition;

	// The selected lake's points: the one picked (for Delete) and the one being dragged.
	private int m_SelectedPoint = -1;
	private int m_DragPoint = -1;

	// The move gizmo's drag.
	private bool m_Moving;
	private Vector3 m_MoveStartWorld;
	private Vector3 m_MoveDelta;
	private IDisposable m_UndoScope;



	public override void OnDisabled()
	{
		CancelDrawing();
		EndDrag();
	}



	public override Widget CreateToolSidebar()
	{
		return new WaterLakeToolWidget(this);
	}



	public override void OnUpdate()
	{
		var network = Network;

		if (SelectedLakeId >= 0 && SelectedLake is null)
			Select(null);

		if (network.IsValid())
			DrawLakes(network);

		if (Gizmo.WasRightMousePressed)
			m_RightPressPosition = Gizmo.CursorPosition;

		bool rightClick = Gizmo.WasRightMouseReleased && Vector2.Distance(Gizmo.CursorPosition, m_RightPressPosition) < ClickDragTolerance;

		if (m_Drawing)
		{
			UpdateDrawing(rightClick);
			return;
		}

		if (m_DragPoint >= 0)
		{
			UpdatePointDrag(network);
			return;
		}

		// The gizmo takes the cursor while it is hovered or held, so nothing below picks or deselects under it.
		if (Gizmo.HasMouseFocus && !Gizmo.HasHovered && !m_Moving)
			UpdatePicking(network);

		if (network.IsValid() && SelectedLake is { } lake)
			UpdateLakeGizmo(network, lake);
		else
			EndDrag();
	}



	public void Select(WaterLake _Lake)
	{
		int id = _Lake?.Id ?? -1;

		m_SelectedPoint = -1;

		if (id == SelectedLakeId)
			return;

		SelectedLakeId = id;
		SelectionVersion++;
	}



	/// <summary>
	/// Escape: drop the lake being laid out, else the picked point, else the lake.
	/// </summary>
	public void Cancel()
	{
		if (m_Drawing)
		{
			CancelDrawing();
			return;
		}

		if (m_SelectedPoint >= 0)
		{
			m_SelectedPoint = -1;
			return;
		}

		Select(null);
	}



	/// <summary>
	/// Delete: the picked shore point when there is one, else the whole lake.
	/// </summary>
	public void DeleteSelection()
	{
		var network = Network;

		if (!network.IsValid() || SelectedLake is not { } lake)
			return;

		if (m_SelectedPoint >= 0)
		{
			using (SceneEditorSession.Active.UndoScope("Remove Lake Point").WithComponentChanges(network).Push())
				network.RemoveLakePoint(lake.Id, m_SelectedPoint);

			m_SelectedPoint = -1;
			return;
		}

		using (SceneEditorSession.Active.UndoScope("Delete Lake").WithComponentChanges(network).Push())
			network.RemoveLake(lake.Id);

		Select(null);
	}



	/// <summary>
	/// The sidebar edited the selected lake directly; push it through the network so its openings and pieces follow.
	/// </summary>
	public void OnLakeEdited()
	{
		var network = Network;

		if (!network.IsValid())
			return;

		network.SnapOpenings(SelectedLake);
		network.MarkDirty();
	}



	/// <summary>
	/// Picking a point of the selected lake (and dragging it), adding one on its shore, joining a river end to a lake,
	/// selecting a lake, or starting a new one: whatever the cursor is over.
	/// </summary>
	private void UpdatePicking(WaterNetwork _Network)
	{
		var ray = Gizmo.CurrentRay;

		if (_Network.IsValid() && SelectedLake is { } selected)
		{
			int point = WaterPicker.PickLakePoint(_Network, selected, ray);

			if (point >= 0)
			{
				DrawPointHint(_Network, selected, point);

				if (Gizmo.WasLeftMousePressed)
				{
					m_SelectedPoint = point;
					m_DragPoint = point;
				}

				return;
			}

			if (Gizmo.IsCtrlPressed && WaterPicker.PickShore(_Network, selected, ray, out int insertIndex, out Vector3 shorePoint))
			{
				DrawInsertHint(_Network, shorePoint);

				if (Gizmo.WasLeftMousePressed)
				{
					using (SceneEditorSession.Active.UndoScope("Add Lake Point").WithComponentChanges(_Network).Push())
						_Network.InsertLakePoint(selected.Id, insertIndex, shorePoint);

					m_SelectedPoint = insertIndex;
					m_DragPoint = insertIndex;
				}

				return;
			}
		}

		if (_Network.IsValid() && FreeNodeUnderCursor(_Network) is { } node && LakeNear(_Network, node) is { } joinable)
		{
			DrawJoinHint(_Network, node, joinable);

			if (Gizmo.WasLeftMousePressed)
			{
				using (SceneEditorSession.Active.UndoScope("Open River Into Lake").WithComponentChanges(_Network).Push())
					_Network.JoinOpening(joinable.Id, node.Id);

				Select(joinable);
			}

			return;
		}

		var lake = _Network.IsValid() ? WaterPicker.PickLake(_Network, ray, out _) : null;

		if (lake is not null)
		{
			using (Gizmo.Scope("watertool2_lake_hover"))
			{
				Gizmo.Draw.IgnoreDepth = true;
				Gizmo.Draw.LineThickness = 2.0f;
				Gizmo.Draw.Color = Color.Cyan.WithAlpha(0.8f);
				WaterGizmos.DrawLake(_Network, lake, _Network.WorldTransform);
			}

			if (Gizmo.WasLeftMousePressed)
				Select(lake);

			return;
		}

		if (!Gizmo.WasLeftMousePressed)
			return;

		// Clicking away from everything with a lake selected just lets go of it; with nothing selected it starts a lake.
		if (SelectedLake is not null)
		{
			Select(null);
			return;
		}

		if (GroundUnderCursor(0.0f) is Vector3 ground)
		{
			m_Drawing = true;
			m_DrawHeight = ground.z + HeightOffset;
			m_DrawPoints.Clear();
			m_DrawPoints.Add(ground.WithZ(m_DrawHeight));
		}
	}



	/// <summary>
	/// Laying a lake out: freeform point by point, or a rectangle or a circle from its first click to the cursor.
	/// </summary>
	private void UpdateDrawing(bool _RightClick)
	{
		if (GroundUnderCursor(m_DrawHeight) is not Vector3 ground)
			return;

		Vector3 cursor = ground.WithZ(m_DrawHeight);

		if (Shape == WaterLakeShape.Freeform)
		{
			Vector3 first = m_DrawPoints[0];
			bool closing = m_DrawPoints.Count >= 3 && Vector3.DistanceBetween(cursor, first) < WaterPicker.NodeRadius(first) * 1.5f;

			DrawFreeformPreview(cursor, closing);

			if (_RightClick)
			{
				if (m_DrawPoints.Count >= 3)
					FinishLake(m_DrawPoints.ToList(), Smooth);
				else
					CancelDrawing();

				return;
			}

			if (!Gizmo.WasLeftMousePressed)
				return;

			if (closing)
			{
				FinishLake(m_DrawPoints.ToList(), Smooth);
				return;
			}

			if (Vector3.DistanceBetween(cursor, m_DrawPoints[^1]) > 1.0f)
				m_DrawPoints.Add(cursor);

			return;
		}

		var shape = ShapePoints(m_DrawPoints[0], cursor);

		using (Gizmo.Scope("watertool2_lake_draw"))
		{
			Gizmo.Draw.IgnoreDepth = true;
			Gizmo.Draw.LineThickness = 2.0f;
			Gizmo.Draw.Color = Color.Cyan.WithAlpha(0.9f);

			WaterGizmos.DrawPolyline(shape, Transform.Zero, true);

			var size = ShapeSize(m_DrawPoints[0], cursor);
			Gizmo.Draw.ScreenText(Shape == WaterLakeShape.Circle ? $"r {size.x:0}" : $"{size.x:0} x {size.y:0}", cursor, new Vector2(12.0f, -12.0f), size: 12.0f);
		}

		if (_RightClick)
		{
			CancelDrawing();
			return;
		}

		if (Gizmo.WasLeftMousePressed && ShapeSize(m_DrawPoints[0], cursor).x > 16.0f)
			FinishLake(shape, Shape == WaterLakeShape.Circle);
	}



	/// <summary>
	/// The shore points of a rectangle or circle laid from one point to another, in world space.
	/// </summary>
	private List<Vector3> ShapePoints(Vector3 _From, Vector3 _To)
	{
		var points = new List<Vector3>();

		if (Shape == WaterLakeShape.Circle)
		{
			float radius = MathF.Max(1.0f, new Vector2(_To.x - _From.x, _To.y - _From.y).Length);
			int count = Math.Max(5, CirclePoints);

			for (int i = 0; i < count; i++)
			{
				float angle = MathF.PI * 2.0f * i / count;
				points.Add(_From + new Vector3(MathF.Cos(angle), MathF.Sin(angle), 0.0f) * radius);
			}

			return points;
		}

		points.Add(new Vector3(_From.x, _From.y, _From.z));
		points.Add(new Vector3(_To.x, _From.y, _From.z));
		points.Add(new Vector3(_To.x, _To.y, _From.z));
		points.Add(new Vector3(_From.x, _To.y, _From.z));

		return points;
	}



	private Vector2 ShapeSize(Vector3 _From, Vector3 _To)
	{
		if (Shape == WaterLakeShape.Circle)
		{
			float radius = new Vector2(_To.x - _From.x, _To.y - _From.y).Length;
			return new Vector2(radius, radius);
		}

		return new Vector2(MathF.Abs(_To.x - _From.x), MathF.Abs(_To.y - _From.y));
	}



	private void FinishLake(List<Vector3> _WorldPoints, bool _Smooth)
	{
		var root = Root;

		if (root is null)
			return;

		var network = root.Network;
		bool createNetwork = !network.IsValid();
		var scope = SceneEditorSession.Active.UndoScope("Add Lake");

		if (createNetwork)
			scope.WithGameObjectCreations();
		else
			scope.WithComponentChanges(network);

		WaterLake lake;

		using (scope.Push())
		{
			if (createNetwork)
				network = root.CreateNetwork();

			var transform = network.WorldTransform;
			var local = _WorldPoints.Select(point => transform.PointToLocal(point)).ToList();

			lake = network.AddLake(local, transform.PointToLocal(_WorldPoints[0].WithZ(m_DrawHeight)).z, Waves, Depth, _Smooth);
		}

		CancelDrawing();
		Select(lake);
	}



	private void CancelDrawing()
	{
		m_Drawing = false;
		m_DrawPoints.Clear();
	}



	/// <summary>
	/// Dragging a shore point of the selected lake on its water level.
	/// </summary>
	private void UpdatePointDrag(WaterNetwork _Network)
	{
		if (!_Network.IsValid() || SelectedLake is not { } lake || m_DragPoint >= lake.Points.Count || !Gizmo.IsLeftMouseDown)
		{
			EndDrag();
			return;
		}

		DrawPointHint(_Network, lake, m_DragPoint);

		// A press that never travels is a click (it picks the point for Delete), not a drag.
		if (Vector2.Distance(Gizmo.CursorPosition, Gizmo.Pressed.CursorPosition) < 2.0f && m_UndoScope is null)
			return;

		var transform = _Network.WorldTransform;
		var plane = new Plane(transform.PointToWorld(lake.Centre), transform.NormalToWorld(Vector3.Up));

		if (plane.Trace(Gizmo.CurrentRay, true) is not Vector3 hit)
			return;

		m_UndoScope ??= SceneEditorSession.Active.UndoScope("Move Lake Point").WithComponentChanges(_Network).Push();
		_Network.SetLakePoint(lake.Id, m_DragPoint, transform.PointToLocal(hit));
	}



	/// <summary>
	/// The standard move gizmo in the middle of the selected lake: its centre square drags the lake across the ground,
	/// its arrows move it, the up arrow raising or lowering its water.
	/// </summary>
	private void UpdateLakeGizmo(WaterNetwork _Network, WaterLake _Lake)
	{
		var transform = _Network.WorldTransform;
		Vector3 centreWorld = transform.PointToWorld(_Lake.Centre);

		using (Gizmo.Scope("watertool2_lake_gizmo", new Transform(centreWorld)))
		{
			Gizmo.Hitbox.DepthBias = 0.01f;

			bool moved = Gizmo.Control.Position("position", Vector3.Zero, out Vector3 delta, Rotation.Identity);

			if (moved)
			{
				if (!m_Moving)
				{
					m_Moving = true;
					m_MoveStartWorld = centreWorld;
					m_MoveDelta = Vector3.Zero;
					m_UndoScope ??= SceneEditorSession.Active.UndoScope("Move Lake").WithComponentChanges(_Network).Push();
				}

				m_MoveDelta += delta;

				Vector3 target = Gizmo.Snap(m_MoveStartWorld, m_MoveDelta, Rotation.Identity);
				_Network.MoveLake(_Lake.Id, transform.PointToLocal(target));
			}
		}

		if (m_Moving && !Gizmo.IsLeftMouseDown)
			EndDrag();
	}



	private void EndDrag()
	{
		m_Moving = false;
		m_DragPoint = -1;

		m_UndoScope?.Dispose();
		m_UndoScope = null;
	}



	/// <summary>
	/// Where the cursor meets the ground; a level plane at the given height when there is nothing there.
	/// </summary>
	private Vector3? GroundUnderCursor(float _FallbackHeight)
	{
		var trace = Trace.Run();

		if (trace.Hit)
			return trace.EndPosition;

		var meshes = MeshTrace.Run();

		if (meshes.Hit)
			return meshes.EndPosition;

		return new Plane(new Vector3(0.0f, 0.0f, _FallbackHeight), Vector3.Up).Trace(Gizmo.CurrentRay, true);
	}



	/// <summary>
	/// A loose river end under the cursor: one river at it, and not already opening into a lake.
	/// </summary>
	private static WaterNode FreeNodeUnderCursor(WaterNetwork _Network)
	{
		var node = WaterPicker.PickNode(_Network, Gizmo.CurrentRay);

		if (node is null || _Network.GetDegree(node.Id) != 1 || _Network.LakeAtOpening(node.Id) is not null)
			return null;

		return node;
	}



	/// <summary>
	/// A lake near enough to a river's loose end to open the river into it: one the end lies in, or within a river's
	/// width of its shore.
	/// </summary>
	private static WaterLake LakeNear(WaterNetwork _Network, WaterNode _Node)
	{
		var river = _Network.GetRiversAt(_Node.Id).FirstOrDefault();
		float reach = MathF.Max(200.0f, river?.WidthAt(_Node.Id) ?? 0.0f);

		foreach (var lake in _Network.Lakes)
		{
			var shore = lake.IsCoast ? null : _Network.GetShore(lake);

			if (shore is null)
				continue;

			shore.NearestDistance(_Node.Position, out float offset);

			if (shore.Contains(_Node.Position) || offset <= reach)
				return lake;
		}

		return null;
	}



	private void DrawLakes(WaterNetwork _Network)
	{
		var transform = _Network.WorldTransform;

		using (Gizmo.Scope("watertool2_lakes"))
		{
			Gizmo.Draw.IgnoreDepth = true;

			foreach (var lake in _Network.Lakes)
			{
				if (lake.IsCoast)
					continue;

				bool selected = lake.Id == SelectedLakeId;

				Gizmo.Draw.LineThickness = selected ? 3.0f : 1.5f;
				Gizmo.Draw.Color = (selected ? Color.Orange : Color.White).WithAlpha(selected ? 0.9f : 0.4f);

				WaterGizmos.DrawLake(_Network, lake, transform);

				if (!selected)
					continue;

				// The control polygon, faint, and a handle on each point.
				var control = new List<Vector3>();

				for (int i = 0; i < lake.Points.Count; i++)
					control.Add(lake.PointAt(i));

				Gizmo.Draw.LineThickness = 1.0f;
				Gizmo.Draw.Color = Color.Orange.WithAlpha(0.3f);
				WaterGizmos.DrawPolyline(control, transform, true);

				for (int i = 0; i < control.Count; i++)
				{
					Vector3 world = transform.PointToWorld(control[i]);

					Gizmo.Draw.Color = i == m_SelectedPoint ? Color.Yellow : Color.Orange;
					Gizmo.Draw.SolidSphere(world, WaterPicker.NodeRadius(world) * 0.5f);
				}
			}
		}
	}



	private void DrawFreeformPreview(Vector3 _Cursor, bool _Closing)
	{
		using (Gizmo.Scope("watertool2_lake_draw"))
		{
			Gizmo.Draw.IgnoreDepth = true;
			Gizmo.Draw.LineThickness = 2.0f;
			Gizmo.Draw.Color = Color.Cyan.WithAlpha(0.9f);

			var points = new List<Vector3>(m_DrawPoints);

			if (!_Closing)
				points.Add(_Cursor);

			WaterGizmos.DrawPolyline(points, Transform.Zero, false);

			// The edge back to the first point, faint, so the lake's shape can be seen before it is closed.
			Gizmo.Draw.Color = Color.Cyan.WithAlpha(_Closing ? 0.9f : 0.3f);
			Gizmo.Draw.Line(points[^1], m_DrawPoints[0]);

			foreach (var point in m_DrawPoints)
			{
				Gizmo.Draw.Color = Color.Cyan;
				Gizmo.Draw.SolidSphere(point, WaterPicker.NodeRadius(point) * 0.4f);
			}

			if (_Closing)
			{
				Gizmo.Draw.Color = Color.Green;
				WaterGizmos.DrawNode(m_DrawPoints[0], true);
				Gizmo.Draw.ScreenText("Close the lake", m_DrawPoints[0], new Vector2(12.0f, -12.0f), size: 12.0f);
			}
		}
	}



	private static void DrawPointHint(WaterNetwork _Network, WaterLake _Lake, int _Point)
	{
		Vector3 world = _Network.WorldTransform.PointToWorld(_Lake.PointAt(_Point));

		using (Gizmo.Scope("watertool2_lake_point"))
		{
			Gizmo.Draw.IgnoreDepth = true;
			Gizmo.Draw.LineThickness = 2.0f;
			Gizmo.Draw.Color = Color.Yellow;
			WaterGizmos.DrawNode(world, false);
		}
	}



	private static void DrawInsertHint(WaterNetwork _Network, Vector3 _LocalPoint)
	{
		Vector3 world = _Network.WorldTransform.PointToWorld(_LocalPoint);

		using (Gizmo.Scope("watertool2_lake_insert"))
		{
			Gizmo.Draw.IgnoreDepth = true;
			Gizmo.Draw.Color = Color.Green;
			WaterGizmos.DrawNode(world, true);
			Gizmo.Draw.ScreenText("Add point", world, new Vector2(12.0f, -12.0f), size: 12.0f);
		}
	}



	private static void DrawJoinHint(WaterNetwork _Network, WaterNode _Node, WaterLake _Lake)
	{
		Vector3 world = _Network.WorldTransform.PointToWorld(_Node.Position);

		using (Gizmo.Scope("watertool2_lake_join"))
		{
			Gizmo.Draw.IgnoreDepth = true;
			Gizmo.Draw.LineThickness = 2.0f;
			Gizmo.Draw.Color = Color.Cyan.WithAlpha(0.9f);
			WaterGizmos.DrawNode(world, true);
			WaterGizmos.DrawLake(_Network, _Lake, _Network.WorldTransform);
			Gizmo.Draw.ScreenText($"Open into lake #{_Lake.Id}", world, new Vector2(12.0f, -12.0f), size: 12.0f);
		}
	}
}



file class WaterLakeToolWidget : ToolSidebarWidget
{
	private readonly WaterLakeTool m_Tool;
	private int m_ShownVersion = -1;
	private IDisposable m_UndoScope;



	public WaterLakeToolWidget(WaterLakeTool _Tool)
	{
		m_Tool = _Tool;

		Rebuild();
	}



	[EditorEvent.Frame]
	public void Frame()
	{
		if (m_Tool.SelectionVersion != m_ShownVersion)
			Rebuild();
	}



	private void Rebuild()
	{
		m_ShownVersion = m_Tool.SelectionVersion;

		Layout.Clear(true);
		AddTitle("Lake", "water_drop");

		var tool = m_Tool.GetSerialized();
		var defaults = AddGroup("New Lakes");

		SidebarRows.Add(defaults, tool.GetProperty(nameof(WaterLakeTool.Shape)));
		SidebarRows.Add(defaults, tool.GetProperty(nameof(WaterLakeTool.Smooth)));
		SidebarRows.Add(defaults, tool.GetProperty(nameof(WaterLakeTool.Waves)));
		SidebarRows.Add(defaults, tool.GetProperty(nameof(WaterLakeTool.Depth)));
		SidebarRows.Add(defaults, tool.GetProperty(nameof(WaterLakeTool.HeightOffset)));
		SidebarRows.Add(defaults, tool.GetProperty(nameof(WaterLakeTool.CirclePoints)));

		var network = m_Tool.Network;

		if (network.IsValid() && m_Tool.SelectedLake is { } lake)
		{
			var group = AddGroup($"Lake #{lake.Id}");
			var serialized = lake.GetSerialized();

			serialized.OnPropertyStartEdit += _ => BeginEdit("Edit Lake");
			serialized.OnPropertyFinishEdit += _ => EndEdit();
			serialized.OnPropertyChanged += _ => m_Tool.OnLakeEdited();

			// The water level is the tool's (it moves the openings with it), edited like the lake's own properties.
			var level = m_Tool.GetSerialized();

			level.OnPropertyStartEdit += _ => BeginEdit("Edit Lake");
			level.OnPropertyFinishEdit += _ => EndEdit();

			SidebarRows.Add(group, level.GetProperty(nameof(WaterLakeTool.LakeHeight)));
			SidebarRows.Add(group, serialized.GetProperty(nameof(WaterLake.Smooth)));
			SidebarRows.Add(group, serialized.GetProperty(nameof(WaterLake.Waves)));
			SidebarRows.Add(group, serialized.GetProperty(nameof(WaterLake.Depth)));
			SidebarRows.Add(group, serialized.GetProperty(nameof(WaterLake.EntranceRadius)));

			group.Add(new Label($"Shore points: {lake.Points.Count}"));
			group.Add(new Label($"Rivers opening into it: {lake.Openings.Count}"));

			var row = group.AddRow();
			row.Spacing = 4;
			row.Add(new Button("Delete Lake", "delete") { Clicked = m_Tool.DeleteSelection });
		}
		else
		{
			AddGroup("Lake").Add(new Label("Click the ground to lay out a lake, or click a lake to edit it. Click the loose end of a river near a lake to open the river into it.") { WordWrap = true });
		}

		AddShortcuts(
			("Lay Out A Lake", "LMB on ground"),
			("Close Freeform Lake", "LMB on first point / RMB"),
			("Select Lake", "LMB on lake"),
			("Move Shore Point", "LMB Drag on point"),
			("Add Shore Point", "CTRL + LMB on shore"),
			("Open River Into Lake", "LMB on river end"),
			("Delete Point / Lake", "DEL"),
			("Cancel / Deselect", "ESC")
		);

		Layout.AddStretchCell();
	}



	private void BeginEdit(string _Name)
	{
		var network = m_Tool.Network;

		if (!network.IsValid())
			return;

		m_UndoScope ??= SceneEditorSession.Active.UndoScope(_Name).WithComponentChanges(network).Push();
	}



	private void EndEdit()
	{
		m_UndoScope?.Dispose();
		m_UndoScope = null;
	}



	[Shortcut("watertool2.lake-delete", "DEL", typeof(SceneViewWidget))]
	private void DeleteSelection()
	{
		m_Tool.DeleteSelection();
	}



	[Shortcut("watertool2.lake-cancel", "ESC", typeof(SceneViewWidget))]
	private void Cancel()
	{
		m_Tool.Cancel();
	}
}