Editor tool and sidebar widget for editing a WaterNetwork in the editor. Provides selection, dragging, gizmo movement, tangent handle editing, node/rivers operations (insert, delete, reverse, join/detach openings), drawing hover/selection gizmos and a sidebar UI to edit properties with undo scoping.
using System;
using System.Linq;
using Editor;
using Sandbox;
namespace RedSnail.WaterTool2.Editor;
/// <summary>
/// Which frame a node's position is shown and edited in.
/// </summary>
public enum WaterNodeSpace
{
/// <summary>
/// The network's own axes.
/// </summary>
Global,
/// <summary>
/// The node's river: X along it, Y across it. Height stays height either way.
/// </summary>
Local
}
/// <summary>
/// Select and edit what is already there: click a node or a river to select it, drag a node across the ground (it
/// keeps its height above the ground) or use its move gizmo, drag a node's tangent handles to reshape the rivers through
/// it, Ctrl+click a river to insert a node, Delete to remove the selection, and turn a river round from the sidebar.
/// </summary>
[Title("Edit")]
[Icon("edit")]
[Group("1")]
[Order(1)]
public class WaterEditTool : EditorTool
{
private enum DragKind
{
None,
Node,
Handle,
Gizmo
}
private const float MinEndDistance = 25.0f;
public int SelectedNodeId { get; private set; } = -1;
public int SelectedRiverId { 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;
public WaterNode SelectedNode => Network.IsValid() ? Network.FindNode(SelectedNodeId) : null;
public WaterRiver SelectedRiver => Network.IsValid() ? Network.FindRiver(SelectedRiverId) : null;
[Property, Title("Space")] public WaterNodeSpace NodeSpace { get; set; } = WaterNodeSpace.Global;
/// <summary>
/// The selected node's position, in whichever frame <see cref="NodeSpace"/> asks for. Its height is the water's
/// height there.
/// </summary>
[Property, Title("Position")]
public Vector3 NodePosition
{
get
{
var network = Network;
if (!network.IsValid() || SelectedNode is not { } node)
return Vector3.Zero;
return NodeSpace == WaterNodeSpace.Local ? NodeAxes(network, node).Inverse * node.Position : node.Position;
}
set
{
var network = Network;
if (!network.IsValid() || SelectedNode is not { } node)
return;
network.MoveNode(node.Id, NodeSpace == WaterNodeSpace.Local ? NodeAxes(network, node) * value : value);
}
}
/// <summary>
/// How wide the river is at the selected node: setting it sets the end of every river there.
/// </summary>
[Property, Title("Width"), Range(WaterRiver.MinWidth, 4000.0f)]
public float NodeWidth
{
get
{
var network = Network;
if (!network.IsValid() || SelectedNode is not { } node)
return 0.0f;
var rivers = network.GetRiversAt(node.Id);
return rivers.Count == 0 ? 0.0f : rivers.Average(river => river.WidthAt(node.Id));
}
set
{
var network = Network;
if (!network.IsValid() || SelectedNode is not { } node)
return;
foreach (var river in network.GetRiversAt(node.Id))
{
if (river.StartNodeId == node.Id)
river.StartWidth = value;
if (river.EndNodeId == node.Id)
river.EndWidth = value;
}
network.MarkDirty();
}
}
private WaterNetworkTool Root => Manager?.CurrentTool as WaterNetworkTool;
private DragKind m_Drag;
private int m_DragRiverId = -1;
private float m_DragHeightAboveGround;
private Vector3 m_DragStartWorld;
private Vector3 m_MoveDelta;
private IDisposable m_UndoScope;
/// <summary>
/// The node's own axes in the network's space: forward along the river through it, laid flat so Z stays height.
/// </summary>
private static Rotation NodeAxes(WaterNetwork _Network, WaterNode _Node)
{
var river = _Network.GetRiversAt(_Node.Id).FirstOrDefault();
if (river is null)
return Rotation.Identity;
Vector3 handle = WaterNetwork.GetHandle(river, _Node.Id);
Vector3 forward = (river.EndNodeId == _Node.Id ? -handle : handle).WithZ(0.0f);
return forward.IsNearZeroLength ? Rotation.Identity : Rotation.LookAt(forward.Normal, Vector3.Up);
}
public override void OnDisabled()
{
EndDrag();
}
public override Widget CreateToolSidebar()
{
return new WaterEditToolWidget(this);
}
public override void OnUpdate()
{
var network = Network;
if (!network.IsValid())
{
EndDrag();
ClearSelection();
return;
}
// Undo can remove whatever we had selected.
if (SelectedNodeId >= 0 && network.FindNode(SelectedNodeId) is null)
ClearSelection();
if (SelectedRiverId >= 0 && network.FindRiver(SelectedRiverId) is null)
ClearSelection();
if (m_Drag is DragKind.Node or DragKind.Handle)
{
UpdateDirectDrag(network);
DrawSelection(network);
return;
}
bool canPick = Gizmo.HasMouseFocus && !Gizmo.HasHovered && !Gizmo.Pressed.Any;
WaterNode hoverNode = null;
WaterRiverPick? hoverRiver = null;
int hoverHandleRiver = -1;
if (canPick)
{
var ray = Gizmo.CurrentRay;
if (SelectedNode is { } selected)
hoverHandleRiver = PickHandle(network, selected, ray);
if (hoverHandleRiver < 0)
hoverNode = WaterPicker.PickNode(network, ray);
if (hoverHandleRiver < 0 && hoverNode is null)
hoverRiver = WaterPicker.PickRiver(network, ray);
}
DrawHover(network, hoverNode, hoverRiver);
DrawSelection(network, hoverHandleRiver);
if (canPick && Gizmo.WasLeftMousePressed)
OnPress(network, hoverNode, hoverRiver, hoverHandleRiver);
if (SelectedNode is { } node)
UpdateNodeGizmo(network, node);
}
public void Select(WaterNode _Node)
{
SetSelection(_Node?.Id ?? -1, -1);
}
public void Select(WaterRiver _River)
{
SetSelection(-1, _River?.Id ?? -1);
}
public void ClearSelection()
{
SetSelection(-1, -1);
}
private void SetSelection(int _NodeId, int _RiverId)
{
if (SelectedNodeId == _NodeId && SelectedRiverId == _RiverId)
return;
SelectedNodeId = _NodeId;
SelectedRiverId = _RiverId;
SelectionVersion++;
}
public void DeleteSelection()
{
var network = Network;
if (!network.IsValid())
return;
if (SelectedNode is { } node)
{
// A node a river simply runs through is dissolved, so the river carries on in one piece; anything else (a
// river end, a confluence) takes its rivers with it.
bool passThrough = network.GetDegree(node.Id) == 2;
using (SceneEditorSession.Active.UndoScope(passThrough ? "Dissolve Node" : "Delete Node").WithComponentChanges(network).Push())
{
if (!passThrough || network.DissolveNode(node.Id) is null)
network.RemoveNode(node.Id);
}
}
else if (SelectedRiver is { } river)
{
using (SceneEditorSession.Active.UndoScope("Delete River").WithComponentChanges(network).Push())
network.RemoveRiver(river.Id);
}
ClearSelection();
}
/// <summary>
/// Turns the selected river round, or with <paramref name="_WholeRun"/> every river it carries on into through
/// plain bends.
/// </summary>
public void ReverseSelection(bool _WholeRun)
{
var network = Network;
if (!network.IsValid() || SelectedRiver is not { } river)
return;
using (SceneEditorSession.Active.UndoScope("Reverse River").WithComponentChanges(network).Push())
{
if (_WholeRun)
network.ReverseRun(river.Id);
else
network.ReverseRiver(river.Id);
}
SelectionVersion++;
}
/// <summary>
/// Unhooks the selected node from the lake it opens into, or the sea.
/// </summary>
public void DetachSelection()
{
var network = Network;
if (!network.IsValid() || SelectedNode is not { } node || network.LakeAtOpening(node.Id) is not { } lake)
return;
using (SceneEditorSession.Active.UndoScope(lake.IsCoast ? "Detach From Sea" : "Detach From Lake").WithComponentChanges(network).Push())
network.RemoveOpening(node.Id);
SelectionVersion++;
}
/// <summary>
/// Opens the selected river end into the sea at the nearest point of the coast: the node moves onto it.
/// </summary>
public void JoinSeaSelection()
{
var network = Network;
if (!network.IsValid() || SelectedNode is not { } node || network.NearestCoast(node.Position, out _, true) is not { } coast)
return;
using (SceneEditorSession.Active.UndoScope("Open River Into Sea").WithComponentChanges(network).Push())
network.JoinOpening(coast.Id, node.Id);
SelectionVersion++;
}
/// <summary>
/// Sidebar edits of the selected node land on the node directly; this pushes them through the network so tangents
/// and pieces follow.
/// </summary>
public void OnNodeEdited(WaterNode _Node, string _PropertyName)
{
var network = Network;
if (!network.IsValid() || _Node is null)
return;
if (_PropertyName == nameof(WaterNode.AutoTangents))
{
network.SetAutoTangents(_Node.Id, _Node.AutoTangents);
return;
}
network.MarkDirty();
}
public void OnRiverEdited()
{
Network?.MarkDirty();
}
private void OnPress(WaterNetwork _Network, WaterNode _HoverNode, WaterRiverPick? _HoverRiver, int _HoverHandleRiver)
{
if (_HoverHandleRiver >= 0)
{
m_Drag = DragKind.Handle;
m_DragRiverId = _HoverHandleRiver;
return;
}
if (_HoverNode is not null)
{
Select(_HoverNode);
m_Drag = DragKind.Node;
m_DragStartWorld = _Network.WorldTransform.PointToWorld(_HoverNode.Position);
// The node keeps its height above the ground as it is dragged, so a river set into its bed stays there.
m_DragHeightAboveGround = GroundBelow(m_DragStartWorld) is float ground ? m_DragStartWorld.z - ground : 0.0f;
return;
}
if (_HoverRiver is { } pick)
{
if (Gizmo.IsCtrlPressed)
{
WaterNode inserted;
using (SceneEditorSession.Active.UndoScope("Insert Node").WithComponentChanges(_Network).Push())
inserted = _Network.SplitRiver(pick.River.Id, pick.T, MinEndDistance);
Select(inserted);
}
else
{
Select(pick.River);
}
return;
}
ClearSelection();
}
/// <summary>
/// A node pressed directly slides across the ground under the cursor; a handle slides on the level plane through
/// its current position, so a river's fall is only changed on purpose (through the gizmo).
/// </summary>
private void UpdateDirectDrag(WaterNetwork _Network)
{
if (!Gizmo.IsLeftMouseDown)
{
EndDrag();
return;
}
// A press that never travels is a click, not a drag: no undo step for it.
if (Vector2.Distance(Gizmo.CursorPosition, Gizmo.Pressed.CursorPosition) < 2.0f && m_UndoScope is null)
return;
var transform = _Network.WorldTransform;
if (m_Drag == DragKind.Node && SelectedNode is { } node)
{
if (GroundHit(transform.PointToWorld(node.Position)) is not Vector3 hit)
return;
m_UndoScope ??= SceneEditorSession.Active.UndoScope("Move Node").WithComponentChanges(_Network).Push();
_Network.MoveNode(node.Id, transform.PointToLocal(hit + Vector3.Up * m_DragHeightAboveGround));
}
else if (m_Drag == DragKind.Handle && SelectedNode is { } handleNode && _Network.FindRiver(m_DragRiverId) is { } river)
{
Vector3 handleWorld = transform.PointToWorld(handleNode.Position + WaterNetwork.GetHandle(river, handleNode.Id));
var plane = new Plane(handleWorld, Vector3.Up);
if (plane.Trace(Gizmo.CurrentRay, true) is not Vector3 hit)
return;
m_UndoScope ??= SceneEditorSession.Active.UndoScope("Move Tangent").WithComponentChanges(_Network).Push();
// Alt breaks the mirror so a bend can be turned into a corner.
Vector3 handle = transform.PointToLocal(hit) - handleNode.Position;
_Network.SetHandle(river.Id, handleNode.Id, handle, !Gizmo.IsAltPressed);
}
else
{
EndDrag();
}
}
/// <summary>
/// The standard move gizmo on the selected node: its centre square drags across the ground, the arrows move on the
/// world axes, or in Local space along and across the river through the node.
/// </summary>
private void UpdateNodeGizmo(WaterNetwork _Network, WaterNode _Node)
{
if (m_Drag is DragKind.Node or DragKind.Handle)
return;
var transform = _Network.WorldTransform;
Vector3 nodeWorld = transform.PointToWorld(_Node.Position);
var handleRotation = Gizmo.Settings.GlobalSpace ? Rotation.Identity : NodeFrame(_Network, _Node);
using (Gizmo.Scope("watertool2_node_gizmo", new Transform(nodeWorld)))
{
Gizmo.Hitbox.DepthBias = 0.01f;
Vector3? surfaceHit = null;
bool moved = Gizmo.Control.Position("position", Vector3.Zero, out Vector3 delta, handleRotation, centerRaycast: () =>
{
surfaceHit = GroundHit(nodeWorld);
return surfaceHit;
});
if (moved)
{
if (m_Drag != DragKind.Gizmo)
{
m_Drag = DragKind.Gizmo;
m_DragStartWorld = nodeWorld;
m_MoveDelta = Vector3.Zero;
m_DragHeightAboveGround = GroundBelow(nodeWorld) is float ground ? nodeWorld.z - ground : 0.0f;
m_UndoScope ??= SceneEditorSession.Active.UndoScope("Move Node").WithComponentChanges(_Network).Push();
}
Vector3 target;
if (surfaceHit is Vector3 hit)
{
target = hit + Vector3.Up * m_DragHeightAboveGround;
}
else
{
m_MoveDelta += delta;
target = Gizmo.Snap(m_DragStartWorld, m_MoveDelta, handleRotation);
}
_Network.MoveNode(_Node.Id, transform.PointToLocal(target));
}
}
if (m_Drag == DragKind.Gizmo && !Gizmo.IsLeftMouseDown)
EndDrag();
}
private static Rotation NodeFrame(WaterNetwork _Network, WaterNode _Node)
{
var river = _Network.GetRiversAt(_Node.Id).FirstOrDefault();
if (river is null)
return Rotation.Identity;
Vector3 handle = WaterNetwork.GetHandle(river, _Node.Id);
Vector3 forward = _Network.WorldTransform.NormalToWorld(river.EndNodeId == _Node.Id ? -handle : handle);
if (forward.IsNearZeroLength || MathF.Abs(forward.Normal.z) > 0.99f)
return Rotation.Identity;
return Rotation.LookAt(forward.Normal, Vector3.Up);
}
private void EndDrag()
{
m_Drag = DragKind.None;
m_DragRiverId = -1;
m_UndoScope?.Dispose();
m_UndoScope = null;
}
/// <summary>
/// Where the cursor ray meets the ground; a level plane through the point being dragged when nothing is there.
/// </summary>
private Vector3? GroundHit(Vector3 _Fallback)
{
var trace = Trace.Run();
if (trace.Hit)
return trace.EndPosition;
return new Plane(_Fallback, Vector3.Up).Trace(Gizmo.CurrentRay, true);
}
/// <summary>
/// The ground straight under a point, if there is any.
/// </summary>
private float? GroundBelow(Vector3 _World)
{
var trace = Scene.Trace.Ray(new Ray(_World + Vector3.Up * 1.0f, Vector3.Down), 20000.0f).Run();
return trace.Hit ? trace.EndPosition.z : null;
}
/// <summary>
/// The river whose tangent handle at the selected node is under the ray, or -1.
/// </summary>
private static int PickHandle(WaterNetwork _Network, WaterNode _Node, Ray _Ray)
{
var transform = _Network.WorldTransform;
int best = -1;
float bestDistance = float.MaxValue;
foreach (var river in _Network.GetRiversAt(_Node.Id))
{
Vector3 world = transform.PointToWorld(_Node.Position + WaterNetwork.GetHandle(river, _Node.Id));
var sphere = new Sphere(world, WaterPicker.NodeRadius(world) * 0.7f);
if (!sphere.Trace(_Ray, float.MaxValue, out float distance) || distance >= bestDistance)
continue;
best = river.Id;
bestDistance = distance;
}
return best;
}
private static void DrawHover(WaterNetwork _Network, WaterNode _HoverNode, WaterRiverPick? _HoverRiver)
{
var transform = _Network.WorldTransform;
using (Gizmo.Scope("watertool2_hover"))
{
Gizmo.Draw.IgnoreDepth = true;
Gizmo.Draw.LineThickness = 2.0f;
Gizmo.Draw.Color = Color.Cyan.WithAlpha(0.8f);
if (_HoverNode is not null)
WaterGizmos.DrawNode(transform.PointToWorld(_HoverNode.Position), false);
if (_HoverRiver is { } pick && _Network.TryGetCurve(pick.River, out var curve))
{
WaterGizmos.DrawRiverOutline(curve, _Network.WidthOf(pick.River), transform);
// Ctrl turns the click into "insert a node here": show where.
if (Gizmo.IsCtrlPressed)
{
Gizmo.Draw.Color = Color.Orange;
WaterGizmos.DrawNode(transform.PointToWorld(pick.LocalPoint), true);
}
}
}
}
private void DrawSelection(WaterNetwork _Network, int _HoverHandleRiver = -1)
{
var transform = _Network.WorldTransform;
using (Gizmo.Scope("watertool2_selection"))
{
Gizmo.Draw.IgnoreDepth = true;
Gizmo.Draw.LineThickness = 2.0f;
if (SelectedRiver is { } river && _Network.TryGetCurve(river, out var curve))
{
Gizmo.Draw.Color = Color.Yellow;
WaterGizmos.DrawRiverOutline(curve, _Network.WidthOf(river), transform);
float size = MathF.Max(40.0f, _Network.WidthOf(river).MaxHalf * 0.6f);
WaterGizmos.DrawFlowArrows(curve, transform, size * 4.0f, size);
}
if (SelectedNode is not { } node)
return;
Vector3 nodeWorld = transform.PointToWorld(node.Position);
Gizmo.Draw.Color = Color.Yellow;
WaterGizmos.DrawNode(nodeWorld, true);
// Tangent handles: one per river leaving the node. A lake's (or the sea's) opening's handle is held square to
// the shore.
bool opening = _Network.LakeAtOpening(node.Id) is not null;
foreach (var attached in _Network.GetRiversAt(node.Id))
{
Vector3 handleWorld = transform.PointToWorld(node.Position + WaterNetwork.GetHandle(attached, node.Id));
bool active = attached.Id == _HoverHandleRiver || (m_Drag == DragKind.Handle && attached.Id == m_DragRiverId);
Gizmo.Draw.Color = node.AutoTangents || opening ? Color.White.WithAlpha(0.5f) : Color.White;
Gizmo.Draw.LineThickness = 1.0f;
Gizmo.Draw.Line(nodeWorld, handleWorld);
Gizmo.Draw.Color = active ? Color.Cyan : (node.AutoTangents ? Color.White.WithAlpha(0.6f) : Color.White);
Gizmo.Draw.SolidSphere(handleWorld, WaterPicker.NodeRadius(handleWorld) * 0.45f);
}
}
}
}
file class WaterEditToolWidget : ToolSidebarWidget
{
private readonly WaterEditTool m_Tool;
private int m_ShownVersion = -1;
private IDisposable m_UndoScope;
public WaterEditToolWidget(WaterEditTool _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("Edit Water", "edit");
var network = m_Tool.Network;
if (network.IsValid() && m_Tool.SelectedNode is { } node)
BuildNodeSection(network, node);
else if (network.IsValid() && m_Tool.SelectedRiver is { } river)
BuildRiverSection(network, river);
else
AddGroup("Selection").Add(new Label("Click a node or a river to select it.") { WordWrap = true });
AddShortcuts(
("Select", "LMB"),
("Drag Node / Tangent", "LMB Drag"),
("Break Tangent Mirror", "ALT + Drag"),
("Insert Node On River", "CTRL + LMB"),
("Delete Selection", "DEL"),
("Deselect", "ESC")
);
Layout.AddStretchCell();
}
private void BuildNodeSection(WaterNetwork _Network, WaterNode _Node)
{
var group = AddGroup($"Node #{_Node.Id}");
var serialized = _Node.GetSerialized();
serialized.OnPropertyStartEdit += _ => BeginEdit("Edit Node");
serialized.OnPropertyFinishEdit += _ => EndEdit();
serialized.OnPropertyChanged += property => m_Tool.OnNodeEdited(_Node, property?.Name);
// Position and width are read off the tool rather than the node: the position can be shown in the node's own
// frame, and the width belongs to the rivers at the node.
var tool = m_Tool.GetSerialized();
tool.OnPropertyStartEdit += _ => BeginEdit("Edit Node");
tool.OnPropertyFinishEdit += _ => EndEdit();
int degree = _Network.GetDegree(_Node.Id);
var lake = _Network.LakeAtOpening(_Node.Id);
SidebarRows.Add(group, tool.GetProperty(nameof(WaterEditTool.NodeSpace)));
SidebarRows.AddVector(group, tool.GetProperty(nameof(WaterEditTool.NodePosition)));
if (degree > 0)
SidebarRows.Add(group, tool.GetProperty(nameof(WaterEditTool.NodeWidth)));
SidebarRows.Add(group, serialized.GetProperty(nameof(WaterNode.AutoTangents)));
SidebarRows.Add(group, serialized.GetProperty(nameof(WaterNode.CornerRadius)));
string role = lake is { IsCoast: true } ? (_Network.SeaEnabled ? "Opens into the sea" : "Opens into the sea (the sea is off)")
: lake is not null ? $"Opens into lake #{lake.Id}"
: degree >= 3 ? $"Confluence of {degree} rivers"
: degree == 2 ? "Bend"
: degree == 1 ? "River end"
: "Unconnected";
group.Add(new Label(role));
var row = group.AddRow();
row.Spacing = 4;
row.Add(new Button("Delete Node", "delete") { Clicked = m_Tool.DeleteSelection });
if (lake is not null)
row.Add(new Button("Detach", "link_off") { Clicked = m_Tool.DetachSelection });
else if (degree == 1 && _Network.SeaEnabled && _Network.HasCoast)
row.Add(new Button("Join Sea", "sailing") { Clicked = m_Tool.JoinSeaSelection, ToolTip = "Open this river into the sea at the nearest point of the coast." });
}
private void BuildRiverSection(WaterNetwork _Network, WaterRiver _River)
{
var group = AddGroup($"River #{_River.Id}");
var serialized = _River.GetSerialized();
serialized.OnPropertyStartEdit += _ => BeginEdit("Edit River");
serialized.OnPropertyFinishEdit += _ => EndEdit();
serialized.OnPropertyChanged += _ => m_Tool.OnRiverEdited();
SidebarRows.Add(group, serialized.GetProperty(nameof(WaterRiver.StartWidth)));
SidebarRows.Add(group, serialized.GetProperty(nameof(WaterRiver.EndWidth)));
SidebarRows.Add(group, serialized.GetProperty(nameof(WaterRiver.Speed)));
SidebarRows.Add(group, serialized.GetProperty(nameof(WaterRiver.Waves)));
SidebarRows.Add(group, serialized.GetProperty(nameof(WaterRiver.Depth)));
float length = _Network.GetCurveTable(_River)?.Length ?? 0.0f;
group.Add(new Label($"Length: {length:0} units"));
if (_Network.TryGetSpeedRange(_River.Id, out float slowest, out float fastest))
group.Add(new Label($"Flows at {slowest:0} to {fastest:0} units/s"));
var flowRow = group.AddRow();
flowRow.Spacing = 4;
flowRow.Add(new Button("Reverse", "swap_horiz") { Clicked = () => m_Tool.ReverseSelection(false), ToolTip = "Turn this river round: its water flows the other way." });
flowRow.Add(new Button("Reverse All", "swap_calls") { Clicked = () => m_Tool.ReverseSelection(true), ToolTip = "Turn round every river this one carries on into through plain bends." });
var row = group.AddRow();
row.Spacing = 4;
row.Add(new Button("Delete River", "delete") { Clicked = m_Tool.DeleteSelection });
}
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.delete-selection", "DEL", typeof(SceneViewWidget))]
private void DeleteSelection()
{
m_Tool.DeleteSelection();
}
[Shortcut("watertool2.deselect", "ESC", typeof(SceneViewWidget))]
private void Deselect()
{
m_Tool.ClearSelection();
}
}