Visualize to-be-drawn shape

main
copygirl 2 months ago
parent 3fa91b5954
commit f7a9dfa2f1
  1. 412
      addons/terrain-editing/TerrainEditingControls+Editing.cs
  2. 26
      addons/terrain-editing/TerrainEditingControls.tscn
  3. 5
      terrain/Terrain.cs
  4. 10
      terrain/Tile.cs

@ -1,194 +1,191 @@
public partial class TerrainEditingControls public partial class TerrainEditingControls
{ {
bool _unhandledMotion = false; // Used to detect when mouse moves off the terrain. Terrain _currentTerrain = null;
TilePos? _tileHover = null; // Position of currently hovered tile.
bool _isSelecting = false; // Whether left mouse is held down to select tiles.
(TilePos, TilePos)? _selection = null;
Material _editToolMaterial; Material _editToolMaterial;
public override void _EnterTree() public override void _EnterTree()
{ {
_editToolMaterial = new StandardMaterial3D { _editToolMaterial = new StandardMaterial3D {
VertexColorUseAsAlbedo = true, AlbedoColor = Colors.Blue,
ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded, ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded,
DepthDrawMode = BaseMaterial3D.DepthDrawModeEnum.Disabled, DepthDrawMode = BaseMaterial3D.DepthDrawModeEnum.Disabled,
NoDepthTest = true, NoDepthTest = true,
}; };
} }
public override void _ExitTree()
=> ClearEditToolMesh();
public override void _Input(InputEvent ev) public override void _Input(InputEvent ev)
{ {
if (GetTerrain() is not Terrain terrain) return; var viewport = !Engine.IsEditorHint() ? GetViewport()
: EditorInterface.Singleton.GetEditorViewport3D();
if (Engine.IsEditorHint()) { if (Engine.IsEditorHint()) {
// Make sure to transform the input event to the 3D scene's viewport. // Make sure to transform the input event to the 3D scene's viewport.
var viewport = EditorInterface.Singleton.GetEditorViewport3D();
var container = viewport.GetParent<SubViewportContainer>(); var container = viewport.GetParent<SubViewportContainer>();
ev = ev.XformedBy(container.GetGlobalTransform().AffineInverse()); ev = ev.XformedBy(container.GetGlobalTransform().AffineInverse());
if (ev is InputEventMouse m) m.GlobalPosition = m.Position; if (ev is InputEventMouse m) m.GlobalPosition = m.Position;
} }
if (_isSelecting && (ev is InputEventMouseButton { ButtonIndex: MouseButton.Left, Pressed: false })) { if (ev is InputEventMouse mouse) {
_isSelecting = false; if (viewport.GetVisibleRect().HasPoint(mouse.Position)
GetViewport().SetInputAsHandled(); && (RayCastTerrain(mouse) is var (terrain, position))) {
return; var (tile, corner) = FindClosestTile(terrain, position);
}
var drawSize = (ToolShape == ToolShape.Corner) ? 1 : DrawSize;
if ((ev is InputEventMouseButton { ButtonIndex: var wheel, Pressed: var pressed, ShiftPressed: true }) var isEven = (drawSize % 2) == 0;
&& (wheel is MouseButton.WheelUp or MouseButton.WheelDown) && (_selection != null)) var radius = FloorToInt(drawSize / 2.0f);
{
// NOTE: Potential bug in the Godot editor? // Offset tile position by corner.
// Does it zoom both when mouse wheel is "pressed" and "released"? if (isEven) tile = corner switch {
// Because just cancelling one of them still causes zooming to occur. Corner.TopLeft => new(tile.X + 0, tile.Y + 0),
GetViewport().SetInputAsHandled(); Corner.TopRight => new(tile.X + 1, tile.Y + 0),
if (!pressed) return; Corner.BottomRight => new(tile.X + 1, tile.Y + 1),
Corner.BottomLeft => new(tile.X + 0, tile.Y + 1),
const float AdjustHeight = 0.5f; _ => throw new InvalidOperationException(),
var amount = (wheel == MouseButton.WheelUp) };
? AdjustHeight : -AdjustHeight;
var selection = TileRegion.From(_selection.Value);
// Raise connected corners.
foreach (var innerCorner in Enum.GetValues<Corner>()) {
var outerCorner = innerCorner.GetOpposite();
var innerPos = selection.GetTileFor(innerCorner);
var outerPos = innerPos.GetNeighbor(innerCorner);
var outerTile = terrain.GetTile(outerPos);
var innerHeight = terrain.GetTile(innerPos).Height[innerCorner];
var outerHeight = outerTile.Height[outerCorner];
if (IsEqualApprox(outerHeight, innerHeight)) {
outerTile.Height[outerCorner] = innerHeight + amount;
terrain.SetTile(outerPos, outerTile);
}
}
// Raise connected sides.
foreach (var side in Enum.GetValues<Side>()) {
foreach (var innerPos in selection.GetTilesFor(side)) {
var outerPos = innerPos.GetNeighbor(side);
var innerTile = terrain.GetTile(innerPos);
var outerTile = terrain.GetTile(outerPos);
var (innerCorner1, innerCorner2) = side.GetCorners();
var (outerCorner1, outerCorner2) = side.GetOpposite().GetCorners();
var changed = false;
var matchingCorners = new[]{ (innerCorner1, outerCorner1), (innerCorner2, outerCorner2) };
foreach (var (innerCorner, outerCorner) in matchingCorners) {
var innerHeight = innerTile.Height[innerCorner];
var outerHeight = outerTile.Height[outerCorner];
if (IsEqualApprox(outerHeight, innerHeight)) {
outerTile.Height[outerCorner] = innerHeight + amount;
changed = true;
}
}
if (changed) terrain.SetTile(outerPos, outerTile);
}
}
// Raise selected tiles themselves.
foreach (var pos in selection.GetAllTiles()) {
var tile = terrain.GetTile(pos);
tile.Height.Adjust(amount);
terrain.SetTile(pos, tile);
}
terrain.UpdateMeshAndShape();
terrain.NotifyPropertyListChanged();
return;
}
if ((ev is InputEventMouseButton { ButtonIndex: var wheel2, Pressed: var pressed2, CtrlPressed: true })
&& (wheel2 is MouseButton.WheelUp or MouseButton.WheelDown) && (_selection != null))
{
GetViewport().SetInputAsHandled();
if (!pressed2) return;
var amount = (wheel2 == MouseButton.WheelUp) ? 1 : -1;
var selection = TileRegion.From(_selection.Value);
foreach (var pos in selection.GetAllTiles()) {
var tile = terrain.GetTile(pos);
tile.TexturePrimary = PosMod(tile.TexturePrimary + amount, 4);
terrain.SetTile(pos, tile);
}
terrain.UpdateMeshAndShape(); IEnumerable<TilePos> GetTilesInSquare() {
terrain.NotifyPropertyListChanged(); var minX = tile.X - radius;
return; var minY = tile.Y - radius;
var maxX = tile.X + radius - (isEven ? 1 : 0);
var maxY = tile.Y + radius - (isEven ? 1 : 0);
for (var x = minX; x <= maxX; x++)
for (var y = minY; y <= maxY; y++)
yield return new(x, y);
} }
if (ev is InputEventMouseMotion) IEnumerable<TilePos> GetTilesInRadius() {
_unhandledMotion = true; var center = isEven
? new Vector2(tile.X , tile.Y )
if ((ev is InputEventMouse mouse) && (!Engine.IsEditorHint() || : new Vector2(tile.X + 0.5f, tile.Y + 0.5f);
EditorInterface.Singleton.GetEditorViewport3D() var distanceSqr = Pow(isEven ? radius - 0.25f : radius + 0.25f, 2);
.GetVisibleRect().HasPoint(mouse.Position))) return GetTilesInSquare()
OnInputRayCastTerrain(terrain, mouse); .Where(tile => center.DistanceSquaredTo(
new Vector2(tile.X + 0.5f, tile.Y + 0.5f)) < distanceSqr);
} }
void OnInputRayCastTerrain(Terrain terrain, InputEventMouse ev) UpdateEditToolMesh(terrain, ToolShape switch {
{ // TODO: Edit corner, not full tile.
// Ray is cast from the editor camera's view. ToolShape.Corner => [tile],
var camera = EditorInterface.Singleton.GetEditorViewport3D().GetCamera3D(); ToolShape.Circle => GetTilesInRadius(),
var from = camera.ProjectRayOrigin(ev.Position); ToolShape.Square => GetTilesInSquare(),
var to = from + camera.ProjectRayNormal(ev.Position) * camera.Far; _ => throw new InvalidOperationException(),
});
// Actual collision is done in the edited scene though. } else {
var root = (Node3D)EditorInterface.Singleton.GetEditedSceneRoot(); ClearEditToolMesh();
var space = root.GetWorld3D().DirectSpaceState;
var query = PhysicsRayQueryParameters3D.Create(from, to);
var result = space.IntersectRay(query);
var collider = (GodotObject)result.GetValueOrDefault("collider");
if (collider == terrain) {
var pos = (Vector3)result["position"];
// var normal = (Vector3)result["normal"];
// var shape = (int)result["shape"];
OnTerrainInput(terrain, ev, pos);
}
} }
void OnTerrainInput(Terrain terrain, InputEvent ev, Vector3 position)
{
if (terrain == null) return;
var localPos = terrain.ToLocal(position);
var tilePos = terrain.ToTilePos(localPos);
if (ev is InputEventMouseButton { ButtonIndex: MouseButton.Left, Pressed: true }) {
_selection = new(tilePos, tilePos);
_isSelecting = true;
GetViewport().SetInputAsHandled();
return;
} }
if (ev is InputEventMouseMotion) { // if (_isSelecting && (ev is InputEventMouseButton { ButtonIndex: MouseButton.Left, Pressed: false })) {
_unhandledMotion = false; // _isSelecting = false;
_tileHover = tilePos; // GetViewport().SetInputAsHandled();
if (_isSelecting) _selection = _selection.Value with { Item2 = tilePos }; // return;
} // }
// if ((ev is InputEventMouseButton { ButtonIndex: var wheel, Pressed: var pressed, ShiftPressed: true })
// && (wheel is MouseButton.WheelUp or MouseButton.WheelDown) && (_selection != null))
// {
// // NOTE: Potential bug in the Godot editor?
// // Does it zoom both when mouse wheel is "pressed" and "released"?
// // Because just cancelling one of them still causes zooming to occur.
// GetViewport().SetInputAsHandled();
// if (!pressed) return;
// const float AdjustHeight = 0.5f;
// var amount = (wheel == MouseButton.WheelUp)
// ? AdjustHeight : -AdjustHeight;
// var selection = TileRegion.From(_selection.Value);
// // Raise connected corners.
// foreach (var innerCorner in Enum.GetValues<Corner>()) {
// var outerCorner = innerCorner.GetOpposite();
// var innerPos = selection.GetTileFor(innerCorner);
// var outerPos = innerPos.GetNeighbor(innerCorner);
// var outerTile = terrain.GetTile(outerPos);
// var innerHeight = terrain.GetTile(innerPos).Height[innerCorner];
// var outerHeight = outerTile.Height[outerCorner];
// if (IsEqualApprox(outerHeight, innerHeight)) {
// outerTile.Height[outerCorner] = innerHeight + amount;
// terrain.SetTile(outerPos, outerTile);
// }
// }
// // Raise connected sides.
// foreach (var side in Enum.GetValues<Side>()) {
// foreach (var innerPos in selection.GetTilesFor(side)) {
// var outerPos = innerPos.GetNeighbor(side);
// var innerTile = terrain.GetTile(innerPos);
// var outerTile = terrain.GetTile(outerPos);
// var (innerCorner1, innerCorner2) = side.GetCorners();
// var (outerCorner1, outerCorner2) = side.GetOpposite().GetCorners();
// var changed = false;
// var matchingCorners = new[]{ (innerCorner1, outerCorner1), (innerCorner2, outerCorner2) };
// foreach (var (innerCorner, outerCorner) in matchingCorners) {
// var innerHeight = innerTile.Height[innerCorner];
// var outerHeight = outerTile.Height[outerCorner];
// if (IsEqualApprox(outerHeight, innerHeight)) {
// outerTile.Height[outerCorner] = innerHeight + amount;
// changed = true;
// }
// }
// if (changed) terrain.SetTile(outerPos, outerTile);
// }
// }
// // Raise selected tiles themselves.
// foreach (var pos in selection.GetAllTiles()) {
// var tile = terrain.GetTile(pos);
// tile.Height.Adjust(amount);
// terrain.SetTile(pos, tile);
// }
// terrain.UpdateMeshAndShape();
// terrain.NotifyPropertyListChanged();
// return;
// }
// if ((ev is InputEventMouseButton { ButtonIndex: var wheel2, Pressed: var pressed2, CtrlPressed: true })
// && (wheel2 is MouseButton.WheelUp or MouseButton.WheelDown) && (_selection != null))
// {
// GetViewport().SetInputAsHandled();
// if (!pressed2) return;
// var amount = (wheel2 == MouseButton.WheelUp) ? 1 : -1;
// var selection = TileRegion.From(_selection.Value);
// foreach (var pos in selection.GetAllTiles()) {
// var tile = terrain.GetTile(pos);
// tile.TexturePrimary = PosMod(tile.TexturePrimary + amount, 4);
// terrain.SetTile(pos, tile);
// }
// terrain.UpdateMeshAndShape();
// terrain.NotifyPropertyListChanged();
// return;
// }
// if (ev is InputEventMouseMotion)
// _unhandledMotion = true;
// if ((ev is InputEventMouse mouse) && viewport.GetVisibleRect().HasPoint(mouse.Position))
// OnInputRayCastTerrain(terrain, mouse);
} }
public override void _Process(double delta) void UpdateEditToolMesh(Terrain terrain, IEnumerable<TilePos> tiles)
{ {
if (GetTerrain() is not Terrain terrain) { if (terrain != _currentTerrain) ClearEditToolMesh();
_tileHover = null; _currentTerrain = terrain;
_selection = null;
_isSelecting = false;
terrain = null;
}
if (_unhandledMotion)
_tileHover = null;
if ((_tileHover != null) || (_selection != null)) {
var mesh = terrain.GetOrCreateMesh("EditToolMesh"); var mesh = terrain.GetOrCreateMesh("EditToolMesh");
mesh.ClearSurfaces(); mesh.ClearSurfaces();
mesh.SurfaceBegin(Mesh.PrimitiveType.Lines); mesh.SurfaceBegin(Mesh.PrimitiveType.Lines);
@ -198,85 +195,64 @@ public partial class TerrainEditingControls
mesh.SurfaceAddVertex(end); mesh.SurfaceAddVertex(end);
} }
void AddQuad(Vector3 topLeft, Vector3 topRight, void AddQuad(Vector3 topLeft , Vector3 topRight,
Vector3 bottomLeft, Vector3 bottomRight) { Vector3 bottomRight, Vector3 bottomLeft) {
AddLine(topLeft , topRight ); AddLine(topLeft , topRight );
AddLine(topRight , bottomRight); AddLine(topRight , bottomRight);
AddLine(bottomRight, bottomLeft ); AddLine(bottomRight, bottomLeft );
AddLine(bottomLeft , topLeft ); AddLine(bottomLeft , topLeft );
} }
if (_tileHover is TilePos hover) { foreach (var tile in tiles) {
var corners = terrain.GetTileCornerPositions(hover); var (topLeft, topRight, bottomRight, bottomLeft)
var margin = 0.1f; = terrain.GetTileCornerPositions(tile);
mesh.SurfaceSetColor(Colors.Black); AddQuad(topLeft, topRight, bottomRight, bottomLeft);
AddQuad(corners.TopLeft + new Vector3(-margin, 0, -margin),
corners.TopRight + new Vector3(+margin, 0, -margin),
corners.BottomLeft + new Vector3(-margin, 0, +margin),
corners.BottomRight + new Vector3(+margin, 0, +margin));
}
mesh.SurfaceSetColor(Colors.Blue);
if (_selection is (TilePos, TilePos) selection)
foreach (var pos in TileRegion.From(selection).GetAllTiles()) {
var corners = terrain.GetTileCornerPositions(pos);
AddQuad(corners.TopLeft, corners.TopRight, corners.BottomLeft, corners.BottomRight);
} }
mesh.SurfaceEnd(); mesh.SurfaceEnd();
mesh.SurfaceSetMaterial(0, _editToolMaterial); mesh.SurfaceSetMaterial(0, _editToolMaterial);
} else {
var meshInstance = (MeshInstance3D)GetNodeOrNull("EditToolMesh");
var mesh = (ImmediateMesh)meshInstance?.Mesh;
mesh?.ClearSurfaces();
}
} }
public Terrain GetTerrain() => EditorInterface.Singleton.GetSelection() void ClearEditToolMesh()
.GetSelectedNodes().OfType<Terrain>().FirstOrDefault(); => _currentTerrain?.GetNodeOrNull("EditToolMesh")?.QueueFree();
(Terrain Terrain, Vector3 Position)? RayCastTerrain(InputEventMouse ev)
readonly record struct TileRegion(int Left, int Top, int Right, int Bottom)
{ {
public TilePos TopLeft => new(Left , Top); // Ray is cast from the editor camera's view.
public TilePos TopRight => new(Right, Top); var camera = EditorInterface.Singleton.GetEditorViewport3D().GetCamera3D();
public TilePos BottomRight => new(Right, Bottom); var from = camera.ProjectRayOrigin(ev.Position);
public TilePos BottomLeft => new(Left , Bottom); var to = from + camera.ProjectRayNormal(ev.Position) * camera.Far;
public int Width => Right - Left + 1;
public int Height => Bottom - Top + 1;
public static TileRegion From((TilePos, TilePos) selection)
=> From(selection.Item1, selection.Item2);
public static TileRegion From(TilePos a, TilePos b)
=> new(Min(a.X, b.X), Min(a.Y, b.Y), Max(a.X, b.X), Max(a.Y, b.Y));
public TilePos GetTileFor(Corner corner)
=> corner switch {
Corner.TopLeft => TopLeft,
Corner.TopRight => TopRight,
Corner.BottomRight => BottomRight,
Corner.BottomLeft => BottomLeft,
_ => throw new ArgumentException($"Invalid Corner value '{corner}'", nameof(corner)),
};
public IEnumerable<TilePos> GetTilesFor(Side side) // Actual collision is done in the edited scene though.
{ var root = (Node3D)EditorInterface.Singleton.GetEditedSceneRoot();
var (left, top, right, bottom) = this; var space = root.GetWorld3D().DirectSpaceState;
return side switch { var query = PhysicsRayQueryParameters3D.Create(from, to);
Side.Left => Enumerable.Range(Top, Height).Select(y => new TilePos(left, y)),
Side.Top => Enumerable.Range(Left, Width).Select(x => new TilePos(x, top)), var result = space.IntersectRay(query);
Side.Right => Enumerable.Range(Top, Height).Select(y => new TilePos(right, y)), var collider = (GodotObject)result.GetValueOrDefault("collider");
Side.Bottom => Enumerable.Range(Left, Width).Select(x => new TilePos(x, bottom)), return (collider is Terrain terrain)
_ => throw new ArgumentException($"Invalid Side value '{side}'", nameof(side)), ? (terrain, (Vector3)result["position"])
}; : null;
} }
public IEnumerable<TilePos> GetAllTiles() static (TilePos, Corner) FindClosestTile(Terrain terrain, Vector3 position)
{ {
for (var x = Left; x <= Right; x++) var local = terrain.ToLocal(position);
for (var y = Top; y <= Bottom; y++)
yield return new(x, y); var tileX = local.X / terrain.TileSize + 0.5 + terrain.Size.X / 2;
} var tileY = local.Z / terrain.TileSize + 0.5 + terrain.Size.Y / 2;
var tile = new TilePos(FloorToInt(tileX), FloorToInt(tileY));
var cornerX = RoundToInt(PosMod(tileX, 1));
var cornerY = RoundToInt(PosMod(tileY, 1));
var corner = (cornerX, cornerY) switch {
(0, 0) => Corner.TopLeft,
(1, 0) => Corner.TopRight,
(1, 1) => Corner.BottomRight,
(0, 1) => Corner.BottomLeft,
_ => throw new InvalidOperationException(),
};
return (tile, corner);
} }
} }

@ -78,7 +78,7 @@ horizontal_alignment = 1
custom_minimum_size = Vector2(0, 80) custom_minimum_size = Vector2(0, 80)
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 1 size_flags_horizontal = 1
min_value = -16.0 min_value = -8.0
max_value = -1.0 max_value = -1.0
value = -1.0 value = -1.0
@ -87,6 +87,12 @@ layout_mode = 2
[node name="Grass" type="Button" parent="."] [node name="Grass" type="Button" parent="."]
layout_mode = 2 layout_mode = 2
theme_override_colors/icon_normal_color = Color(1, 1, 1, 1)
theme_override_colors/icon_focus_color = Color(1, 1, 1, 1)
theme_override_colors/icon_pressed_color = Color(1, 1, 1, 1)
theme_override_colors/icon_hover_color = Color(1, 1, 1, 1)
theme_override_colors/icon_hover_pressed_color = Color(1, 1, 1, 1)
theme_override_colors/icon_disabled_color = Color(0.753984, 0.753984, 0.753984, 0.501961)
theme_override_constants/icon_max_width = 16 theme_override_constants/icon_max_width = 16
disabled = true disabled = true
toggle_mode = true toggle_mode = true
@ -94,6 +100,12 @@ icon = SubResource("ImageTexture_btyvd")
[node name="Dirt" type="Button" parent="."] [node name="Dirt" type="Button" parent="."]
layout_mode = 2 layout_mode = 2
theme_override_colors/icon_normal_color = Color(1, 1, 1, 1)
theme_override_colors/icon_focus_color = Color(1, 1, 1, 1)
theme_override_colors/icon_pressed_color = Color(1, 1, 1, 1)
theme_override_colors/icon_hover_color = Color(1, 1, 1, 1)
theme_override_colors/icon_hover_pressed_color = Color(1, 1, 1, 1)
theme_override_colors/icon_disabled_color = Color(0.753984, 0.753984, 0.753984, 0.501961)
theme_override_constants/icon_max_width = 16 theme_override_constants/icon_max_width = 16
disabled = true disabled = true
toggle_mode = true toggle_mode = true
@ -103,6 +115,12 @@ flat = true
[node name="Rock" type="Button" parent="."] [node name="Rock" type="Button" parent="."]
layout_mode = 2 layout_mode = 2
theme_override_colors/icon_normal_color = Color(1, 1, 1, 1)
theme_override_colors/icon_focus_color = Color(1, 1, 1, 1)
theme_override_colors/icon_pressed_color = Color(1, 1, 1, 1)
theme_override_colors/icon_hover_color = Color(1, 1, 1, 1)
theme_override_colors/icon_hover_pressed_color = Color(1, 1, 1, 1)
theme_override_colors/icon_disabled_color = Color(0.753984, 0.753984, 0.753984, 0.501961)
theme_override_constants/icon_max_width = 16 theme_override_constants/icon_max_width = 16
disabled = true disabled = true
toggle_mode = true toggle_mode = true
@ -112,6 +130,12 @@ flat = true
[node name="Sand" type="Button" parent="."] [node name="Sand" type="Button" parent="."]
layout_mode = 2 layout_mode = 2
theme_override_colors/icon_normal_color = Color(1, 1, 1, 1)
theme_override_colors/icon_focus_color = Color(1, 1, 1, 1)
theme_override_colors/icon_pressed_color = Color(1, 1, 1, 1)
theme_override_colors/icon_hover_color = Color(1, 1, 1, 1)
theme_override_colors/icon_hover_pressed_color = Color(1, 1, 1, 1)
theme_override_colors/icon_disabled_color = Color(0.753984, 0.753984, 0.753984, 0.501961)
theme_override_constants/icon_max_width = 16 theme_override_constants/icon_max_width = 16
disabled = true disabled = true
toggle_mode = true toggle_mode = true

@ -33,11 +33,6 @@ public partial class Terrain
=> (pos.X >= 0) && (pos.X < Size.X) => (pos.X >= 0) && (pos.X < Size.X)
&& (pos.Y >= 0) && (pos.Y < Size.Y); && (pos.Y >= 0) && (pos.Y < Size.Y);
/// <summary> Transforms a 3D position local to the equivalent tile position. </summary>
public TilePos ToTilePos(Vector3 localPos)
=> new(RoundToInt(localPos.X / TileSize + Size.X / 2.0f),
RoundToInt(localPos.Z / TileSize + Size.Y / 2.0f));
public void UpdateMeshAndShape() public void UpdateMeshAndShape()
{ {

@ -106,6 +106,16 @@ public struct Corners<T>(T topLeft, T topRight, T bottomRight, T bottomLeft)
default: throw new ArgumentException($"Invalid Corner value '{corner}'", nameof(corner)); default: throw new ArgumentException($"Invalid Corner value '{corner}'", nameof(corner));
} } } }
} }
public readonly void Deconstruct(
out T topLeft , out T topRight,
out T bottomRight, out T bottomLeft)
{
topLeft = TopLeft;
topRight = TopRight;
bottomRight = BottomRight;
bottomLeft = BottomLeft;
}
} }
public static class CornersExtensions public static class CornersExtensions

Loading…
Cancel
Save