Editor/WaterSeaTool.cs

Editor tool and sidebar UI for the Sea feature of a water system. WaterSeaTool manages detection, building and clearing of coastline data in the WaterNetwork, draws gizmos for coastlines each frame, and exposes actions; WaterSeaToolWidget is the editor sidebar that shows and edits WaterNetwork properties, tracks undo scopes, and updates stats.

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

namespace RedSnail.WaterTool2.Editor;

/// <summary>
/// The sea: its level and waves, and the coast, built at the press of a button by scanning the terrain (and the
/// colliders or models the coast is detected from) for where the land rises out of the sea. Round the land, out to the
/// coast distance, the coast zone is the sea's water as a surface of its own, which rivers open into as they open into a
/// lake; past it the infinite sea takes over, masked out over the zone and the land. Rivers are opened into the sea with
/// the River tool (click on the sea) or the Edit tool (Join Sea).
/// </summary>
[Title("Sea")]
[Icon("sailing")]
[Group("1")]
[Order(3)]
public class WaterSeaTool : EditorTool
{
	/// <summary>
	/// Bumped whenever the sidebar has something new to show.
	/// </summary>
	public int Version { get; private set; }

	public WaterNetwork Network => Root?.Network;

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

	// What the sidebar last showed, so it rebuilds when that changes underneath it (undo, the inspector).
	private WaterNetwork m_ShownNetwork;
	private int m_ShownCoastlines = -1;
	private bool m_ShownStale;
	private string m_ShownReport;



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



	public override void OnUpdate()
	{
		var network = Network;
		int coastlines = network.IsValid() ? network.Lakes.Count(lake => lake.IsCoast) : -1;
		bool stale = network.IsValid() && network.IsCoastStale;
		string report = network.IsValid() ? network.CoastReport : null;

		if (network != m_ShownNetwork || coastlines != m_ShownCoastlines || stale != m_ShownStale || report != m_ShownReport)
		{
			m_ShownNetwork = network;
			m_ShownCoastlines = coastlines;
			m_ShownStale = stale;
			m_ShownReport = report;
			Version++;
		}

		if (network.IsValid())
			DrawCoast(network);
	}



	/// <summary>
	/// Scans the scene and lays the coast round the land, creating the network first when the scene has none.
	/// </summary>
	public void BuildCoast()
	{
		var root = Root;

		if (root is null)
			return;

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

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

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

			network.BuildCoast();
		}

		Version++;
	}



	public void ClearCoast()
	{
		var network = Network;

		if (!network.IsValid())
			return;

		using (SceneEditorSession.Active.UndoScope("Clear Coast").WithComponentChanges(network).Push())
			network.ClearCoast();

		Version++;
	}



	/// <summary>
	/// The coastlines, where the coast zone gives way to the infinite sea, and the blocks river mouths are built in.
	/// </summary>
	private static void DrawCoast(WaterNetwork _Network)
	{
		var transform = _Network.WorldTransform;

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

			foreach (var lake in _Network.Lakes)
			{
				if (lake.IsCoast)
					WaterGizmos.DrawLake(_Network, lake, transform);
			}

			Gizmo.Draw.LineThickness = 1.0f;
			Gizmo.Draw.Color = Color.Cyan.WithAlpha(0.5f);

			foreach (var (a, b) in _Network.CoastZoneOutline())
				Gizmo.Draw.Line(a, b);

			Gizmo.Draw.Color = Color.Orange.WithAlpha(0.6f);

			foreach (var block in _Network.RiverMouthBlocks())
				Gizmo.Draw.LineBBox(block);
		}
	}
}



file class WaterSeaToolWidget : ToolSidebarWidget
{
	private readonly WaterSeaTool m_Tool;
	private int m_ShownVersion = -1;
	private IDisposable m_UndoScope;
	private Label m_Stats;



	public WaterSeaToolWidget(WaterSeaTool _Tool)
	{
		m_Tool = _Tool;

		Rebuild();
	}



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

		// Kept up to date in place: rebuilding the sidebar under a property being dragged would drop the drag.
		var network = m_Tool.Network;

		if (m_Stats.IsValid() && network.IsValid())
		{
			string stats = network.SeaStats;

			if (m_Stats.Text != stats)
				m_Stats.Text = stats;
		}
	}



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

		Layout.Clear(true);
		m_Stats = null;
		AddTitle("Sea", "sailing");

		var network = m_Tool.Network;

		if (!network.IsValid())
		{
			var start = AddGroup("Sea");

			start.Add(new Label("Build the coast to start: the terrain is scanned, a coastline laid round the land, and the sea turned on.") { WordWrap = true });
			start.Add(new Button("Build Coast", "landscape") { Clicked = m_Tool.BuildCoast });

			Layout.AddStretchCell();
			return;
		}

		var serialized = network.GetSerialized();

		serialized.OnPropertyStartEdit += _ => BeginEdit("Edit Sea");
		serialized.OnPropertyFinishEdit += _ => EndEdit();

		var sea = AddGroup("Sea");

		SidebarRows.Add(sea, serialized.GetProperty(nameof(WaterNetwork.SeaEnabled)));
		SidebarRows.Add(sea, serialized.GetProperty(nameof(WaterNetwork.SeaLevel)));
		SidebarRows.AddWide(sea, serialized.GetProperty(nameof(WaterNetwork.SeaProfile)));
		SidebarRows.Add(sea, serialized.GetProperty(nameof(WaterNetwork.SeaWaves)));
		SidebarRows.Add(sea, serialized.GetProperty(nameof(WaterNetwork.SeaDepth)));

		var coast = AddGroup("Coast");

		SidebarRows.Add(coast, serialized.GetProperty(nameof(WaterNetwork.CoastDetection)));
		SidebarRows.Add(coast, serialized.GetProperty(nameof(WaterNetwork.CoastResolution)));
		SidebarRows.Add(coast, serialized.GetProperty(nameof(WaterNetwork.CoastHeightMargin)));
		SidebarRows.Add(coast, serialized.GetProperty(nameof(WaterNetwork.CoastDistance)));
		SidebarRows.Add(coast, serialized.GetProperty(nameof(WaterNetwork.EstuarySize)));
		SidebarRows.AddWide(coast, serialized.GetProperty(nameof(WaterNetwork.CoastIgnoreTags)));

		var buttons = coast.AddRow();
		buttons.Spacing = 4;
		buttons.Add(new Button("Build Coast", "landscape") { Clicked = m_Tool.BuildCoast, ToolTip = "Scan the terrain and lay the coast round the land, in place of the one there is." });
		buttons.Add(new Button("Clear", "delete") { Clicked = m_Tool.ClearCoast, ToolTip = "Take the coast away: the infinite sea is everywhere again." });

		int coastlines = network.Lakes.Count(lake => lake.IsCoast);

		coast.Add(new Label(coastlines == 0 ? "No coast yet: the infinite sea is everywhere." : $"Coastlines: {coastlines}") { WordWrap = true });

		if (!string.IsNullOrEmpty(network.CoastReport))
			coast.Add(new Label(network.CoastReport) { WordWrap = true });

		if (network.IsCoastStale)
			coast.Add(new Label("The sea level has moved since the coast was built: build it again.") { WordWrap = true });

		var horizon = AddGroup("Infinite Sea");

		SidebarRows.Add(horizon, serialized.GetProperty(nameof(WaterNetwork.SeaCellSize)));
		SidebarRows.Add(horizon, serialized.GetProperty(nameof(WaterNetwork.SeaRings)));
		SidebarRows.Add(horizon, serialized.GetProperty(nameof(WaterNetwork.SeaRingCells)));

		var drawing = AddGroup("Drawing");

		m_Stats = drawing.Add(new Label(network.SeaStats) { WordWrap = true });

		AddShortcuts(
			("Open A River Into The Sea", "River tool: LMB on sea"),
			("Open A River End Into The Sea", "Edit tool: Join Sea")
		);

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