From 4e9cf595ff8db99cb8d1fc4adcd9a1f532bbbcd4 Mon Sep 17 00:00:00 2001 From: Layla Date: Wed, 10 Dec 2025 16:27:44 +0000 Subject: [PATCH] mapping tool mesh selection mode (#3588) --- .../Scene/Components/Mesh/MeshComponent.cs | 2 +- .../Code/Scene/Mesh/MoveModes/MoveMode.cs | 68 ---- .../Code/Scene/Mesh/MoveModes/PivotMode.cs | 2 +- .../Code/Scene/Mesh/MoveModes/PositionMode.cs | 17 +- .../Code/Scene/Mesh/MoveModes/RotateMode.cs | 21 +- .../Code/Scene/Mesh/MoveModes/ScaleMode.cs | 26 +- .../Code/Scene/Mesh/Tools/EdgeTool.UI.cs | 2 +- .../Code/Scene/Mesh/Tools/FaceTool.UI.cs | 2 +- .../Code/Scene/Mesh/Tools/MeshSelection.UI.cs | 181 +++++++++ .../Code/Scene/Mesh/Tools/MeshSelection.cs | 368 ++++++++++++++++++ .../Code/Scene/Mesh/Tools/MeshTool.UI.cs | 3 + .../tools/Code/Scene/Mesh/Tools/MeshTool.cs | 10 +- .../Code/Scene/Mesh/Tools/MoveModeToolBar.cs | 8 +- .../Code/Scene/Mesh/Tools/SelectionTool.cs | 176 ++++++++- .../Code/Scene/Mesh/Tools/TextureTool.UI.cs | 2 +- .../Code/Scene/Mesh/Tools/VertexTool.UI.cs | 53 +-- .../Scene/ObjectTool/RotationEditorTool.cs | 2 +- .../Code/Scene/ObjectTool/ScaleEditorTool.cs | 2 +- .../Code/Scene/SceneView/SceneViewWidget.cs | 4 +- 19 files changed, 777 insertions(+), 172 deletions(-) create mode 100644 game/addons/tools/Code/Scene/Mesh/Tools/MeshSelection.UI.cs create mode 100644 game/addons/tools/Code/Scene/Mesh/Tools/MeshSelection.cs diff --git a/engine/Sandbox.Engine/Scene/Components/Mesh/MeshComponent.cs b/engine/Sandbox.Engine/Scene/Components/Mesh/MeshComponent.cs index 4513961b..08b22536 100644 --- a/engine/Sandbox.Engine/Scene/Components/Mesh/MeshComponent.cs +++ b/engine/Sandbox.Engine/Scene/Components/Mesh/MeshComponent.cs @@ -18,7 +18,7 @@ public sealed class MeshComponent : Collider, ExecuteInEditor, ITintable, IMater Hull } - [Property, Order( 0 )] + [Property, Hide] public PolygonMesh Mesh { get; diff --git a/game/addons/tools/Code/Scene/Mesh/MoveModes/MoveMode.cs b/game/addons/tools/Code/Scene/Mesh/MoveModes/MoveMode.cs index ccddd925..a977dab0 100644 --- a/game/addons/tools/Code/Scene/Mesh/MoveModes/MoveMode.cs +++ b/game/addons/tools/Code/Scene/Mesh/MoveModes/MoveMode.cs @@ -6,80 +6,12 @@ namespace Editor.MeshEditor; /// public abstract class MoveMode { - protected IReadOnlyDictionary TransformVertices => _transformVertices; - - private readonly Dictionary _transformVertices = []; - private List _transformFaces; - private IDisposable _undoScope; - public void Update( SelectionTool tool ) { - if ( !tool.Selection.OfType().Any() ) - return; - OnUpdate( tool ); } protected virtual void OnUpdate( SelectionTool tool ) { } - - protected void StartDrag( SelectionTool tool ) - { - if ( _transformVertices.Count != 0 ) - return; - - var components = tool.Selection.OfType() - .Select( x => x.Component ) - .Distinct(); - - _undoScope ??= SceneEditorSession.Active.UndoScope( $"{(Gizmo.IsShiftPressed ? "Extrude" : "Move")} Selection" ) - .WithComponentChanges( components ) - .Push(); - - if ( Gizmo.IsShiftPressed ) - { - _transformFaces = tool.ExtrudeSelection(); - } - - foreach ( var vertex in tool.VertexSelection ) - { - _transformVertices[vertex] = vertex.PositionWorld; - } - } - - protected void UpdateDrag() - { - if ( _transformFaces is not null ) - { - foreach ( var group in _transformFaces.GroupBy( x => x.Component ) ) - { - var mesh = group.Key.Mesh; - var faces = group.Select( x => x.Handle ).ToArray(); - - foreach ( var face in faces ) - { - mesh.TextureAlignToGrid( mesh.Transform, face ); - } - } - } - - var meshes = TransformVertices - .Select( x => x.Key.Component.Mesh ) - .Distinct(); - - foreach ( var mesh in meshes ) - { - mesh.ComputeFaceTextureCoordinatesFromParameters(); - } - } - - protected void EndDrag() - { - _transformVertices.Clear(); - _transformFaces = null; - - _undoScope?.Dispose(); - _undoScope = null; - } } diff --git a/game/addons/tools/Code/Scene/Mesh/MoveModes/PivotMode.cs b/game/addons/tools/Code/Scene/Mesh/MoveModes/PivotMode.cs index 62b22733..764758cb 100644 --- a/game/addons/tools/Code/Scene/Mesh/MoveModes/PivotMode.cs +++ b/game/addons/tools/Code/Scene/Mesh/MoveModes/PivotMode.cs @@ -17,7 +17,7 @@ public sealed class PivotMode : MoveMode { var origin = tool.Pivot; - if ( !Gizmo.Pressed.Any && Gizmo.HasMouseFocus ) + if ( !Gizmo.Pressed.Any ) { _pivot = origin; _basis = tool.CalculateSelectionBasis(); diff --git a/game/addons/tools/Code/Scene/Mesh/MoveModes/PositionMode.cs b/game/addons/tools/Code/Scene/Mesh/MoveModes/PositionMode.cs index 84fa7943..d24a3c5a 100644 --- a/game/addons/tools/Code/Scene/Mesh/MoveModes/PositionMode.cs +++ b/game/addons/tools/Code/Scene/Mesh/MoveModes/PositionMode.cs @@ -20,9 +20,9 @@ public sealed class PositionMode : MoveMode { var origin = tool.Pivot; - if ( !Gizmo.Pressed.Any && Gizmo.HasMouseFocus ) + if ( !Gizmo.Pressed.Any ) { - EndDrag(); + tool.EndDrag(); _basis = tool.CalculateSelectionBasis(); _origin = origin; @@ -45,16 +45,9 @@ public sealed class PositionMode : MoveMode moveDelta -= _origin; - StartDrag( tool ); - - foreach ( var entry in TransformVertices ) - { - var position = entry.Value + moveDelta; - var transform = entry.Key.Transform; - entry.Key.Component.Mesh.SetVertexPosition( entry.Key.Handle, transform.PointToLocal( position ) ); - } - - UpdateDrag(); + tool.StartDrag(); + tool.Translate( moveDelta ); + tool.UpdateDrag(); } } } diff --git a/game/addons/tools/Code/Scene/Mesh/MoveModes/RotateMode.cs b/game/addons/tools/Code/Scene/Mesh/MoveModes/RotateMode.cs index 2f6c0692..ba91cc1c 100644 --- a/game/addons/tools/Code/Scene/Mesh/MoveModes/RotateMode.cs +++ b/game/addons/tools/Code/Scene/Mesh/MoveModes/RotateMode.cs @@ -18,9 +18,9 @@ public sealed class RotateMode : MoveMode protected override void OnUpdate( SelectionTool tool ) { - if ( !Gizmo.Pressed.Any && Gizmo.HasMouseFocus ) + if ( !Gizmo.Pressed.Any ) { - EndDrag(); + tool.EndDrag(); _moveDelta = default; _basis = tool.CalculateSelectionBasis(); @@ -35,22 +35,11 @@ public sealed class RotateMode : MoveMode { _moveDelta += angleDelta; - StartDrag( tool ); - var snapDelta = Gizmo.Snap( _moveDelta, _moveDelta ); - foreach ( var entry in TransformVertices ) - { - var rotation = _basis * snapDelta * _basis.Inverse; - var position = entry.Value - _origin; - position *= rotation; - position += _origin; - - var transform = entry.Key.Transform; - entry.Key.Component.Mesh.SetVertexPosition( entry.Key.Handle, transform.PointToLocal( position ) ); - } - - UpdateDrag(); + tool.StartDrag(); + tool.Rotate( _origin, _basis, snapDelta ); + tool.UpdateDrag(); } } } diff --git a/game/addons/tools/Code/Scene/Mesh/MoveModes/ScaleMode.cs b/game/addons/tools/Code/Scene/Mesh/MoveModes/ScaleMode.cs index 2168ca2a..c4fed77c 100644 --- a/game/addons/tools/Code/Scene/Mesh/MoveModes/ScaleMode.cs +++ b/game/addons/tools/Code/Scene/Mesh/MoveModes/ScaleMode.cs @@ -19,16 +19,13 @@ public sealed class ScaleMode : MoveMode protected override void OnUpdate( SelectionTool tool ) { - if ( !Gizmo.Pressed.Any && Gizmo.HasMouseFocus ) + if ( !Gizmo.Pressed.Any ) { - EndDrag(); + tool.EndDrag(); _moveDelta = default; _basis = tool.CalculateSelectionBasis(); - - var bounds = BBox.FromPoints( tool.VertexSelection - .Select( x => _basis.Inverse * x.PositionWorld ) ); - + var bounds = tool.CalculateLocalBounds(); _size = bounds.Size; _origin = tool.Pivot; @@ -52,20 +49,9 @@ public sealed class ScaleMode : MoveMode _size.z != 0 ? size.z / _size.z : 1 ); - StartDrag( tool ); - - foreach ( var entry in TransformVertices ) - { - var position = (entry.Value - _origin) * _basis.Inverse; - position *= scale; - position *= _basis; - position += _origin; - - var transform = entry.Key.Transform; - entry.Key.Component.Mesh.SetVertexPosition( entry.Key.Handle, transform.PointToLocal( position ) ); - } - - UpdateDrag(); + tool.StartDrag(); + tool.Scale( _origin, _basis, scale ); + tool.UpdateDrag(); } } } diff --git a/game/addons/tools/Code/Scene/Mesh/Tools/EdgeTool.UI.cs b/game/addons/tools/Code/Scene/Mesh/Tools/EdgeTool.UI.cs index f667100a..3cc7a982 100644 --- a/game/addons/tools/Code/Scene/Mesh/Tools/EdgeTool.UI.cs +++ b/game/addons/tools/Code/Scene/Mesh/Tools/EdgeTool.UI.cs @@ -299,7 +299,7 @@ partial class EdgeTool } } - [Shortcut( "editor.delete", "DEL", typeof( SceneViewportWidget ) )] + [Shortcut( "editor.delete", "DEL", typeof( SceneDock ) )] private void DeleteSelection() { var groups = _edges.GroupBy( face => face.Component ); diff --git a/game/addons/tools/Code/Scene/Mesh/Tools/FaceTool.UI.cs b/game/addons/tools/Code/Scene/Mesh/Tools/FaceTool.UI.cs index 366a06f5..baad64a2 100644 --- a/game/addons/tools/Code/Scene/Mesh/Tools/FaceTool.UI.cs +++ b/game/addons/tools/Code/Scene/Mesh/Tools/FaceTool.UI.cs @@ -112,7 +112,7 @@ partial class FaceTool } } - [Shortcut( "editor.delete", "DEL", typeof( SceneViewportWidget ) )] + [Shortcut( "editor.delete", "DEL", typeof( SceneDock ) )] private void DeleteSelection() { var groups = _faces.GroupBy( face => face.Component ); diff --git a/game/addons/tools/Code/Scene/Mesh/Tools/MeshSelection.UI.cs b/game/addons/tools/Code/Scene/Mesh/Tools/MeshSelection.UI.cs new file mode 100644 index 00000000..98ec7772 --- /dev/null +++ b/game/addons/tools/Code/Scene/Mesh/Tools/MeshSelection.UI.cs @@ -0,0 +1,181 @@ + +namespace Editor.MeshEditor; + +partial class MeshSelection +{ + public override Widget CreateToolSidebar() + { + return new MeshSelectionWidget( GetSerializedSelection(), this ); + } + + public class MeshSelectionWidget : ToolSidebarWidget + { + readonly MeshComponent[] _meshes; + readonly MeshSelection _tool; + + public MeshSelectionWidget( SerializedObject so, MeshSelection tool ) : base() + { + _tool = tool; + + AddTitle( "Mesh Mode", "layers" ); + + _meshes = so.Targets.OfType() + .Select( x => x.GetComponent() ) + .Where( x => x.IsValid() ) + .ToArray(); + + { + var group = AddGroup( "Move Mode" ); + var row = group.AddRow(); + row.Spacing = 8; + tool.Tool.CreateMoveModeButtons( row ); + } + + { + var group = AddGroup( "Operations" ); + + var grid = Layout.Row(); + grid.Spacing = 4; + + CreateButton( "Set Origin To Pivot", "gps_fixed", "mesh.set-origin-to-pivot", SetOriginToPivot, _meshes.Length > 0, grid ); + CreateButton( "Center Origin", "center_focus_strong", "mesh.center-origin", CenterOrigin, _meshes.Length > 0, grid ); + CreateButton( "Bake Scale", "straighten", "mesh.bake-scale", BakeScale, _meshes.Length > 0, grid ); + + grid.AddStretchCell(); + + group.Add( grid ); + } + + { + var group = AddGroup( "Pivot" ); + + var grid = Layout.Row(); + grid.Spacing = 4; + + CreateButton( "Previous", "chevron_left", "mesh.previous-pivot", PreviousPivot, _meshes.Length > 0, grid ); + CreateButton( "Next", "chevron_right", "mesh.next-pivot", NextPivot, _meshes.Length > 0, grid ); + CreateButton( "Clear", "restart_alt", "mesh.clear-pivot", ClearPivot, _meshes.Length > 0, grid ); + CreateButton( "World Origin", "language", "mesh.zero-pivot", ZeroPivot, _meshes.Length > 0, grid ); + + grid.AddStretchCell(); + + group.Add( grid ); + } + + Layout.AddStretchCell(); + } + + [Shortcut( "mesh.previous-pivot", "N+MWheelDn", typeof( SceneDock ) )] + public void PreviousPivot() => _tool.PreviousPivot(); + + [Shortcut( "mesh.next-pivot", "N+MWheelUp", typeof( SceneDock ) )] + public void NextPivot() => _tool.NextPivot(); + + [Shortcut( "mesh.clear-pivot", "Home", typeof( SceneDock ) )] + public void ClearPivot() => _tool.ClearPivot(); + + [Shortcut( "mesh.zero-pivot", "Ctrl+End", typeof( SceneDock ) )] + public void ZeroPivot() => _tool.ZeroPivot(); + + [Shortcut( "mesh.set-origin-to-pivot", "Ctrl+D", typeof( SceneDock ) )] + public void SetOriginToPivot() + { + using var scope = SceneEditorSession.Scope(); + + using ( SceneEditorSession.Active.UndoScope( "Set Origin To Pivot" ) + .WithGameObjectChanges( _meshes.Select( x => x.GameObject ), GameObjectUndoFlags.Properties ) + .WithComponentChanges( _meshes ) + .Push() ) + { + foreach ( var mesh in _meshes ) + { + SetMeshOrigin( mesh, _tool.Pivot ); + } + } + } + + [Shortcut( "mesh.center-origin", "End", typeof( SceneDock ) )] + public void CenterOrigin() + { + using var scope = SceneEditorSession.Scope(); + + using ( SceneEditorSession.Active.UndoScope( "Center Origin" ) + .WithGameObjectChanges( _meshes.Select( x => x.GameObject ), GameObjectUndoFlags.Properties ) + .WithComponentChanges( _meshes ) + .Push() ) + { + foreach ( var mesh in _meshes ) + { + CenterMeshOrigin( mesh ); + } + } + + _tool.ClearPivot(); + } + + public void BakeScale() + { + using var scope = SceneEditorSession.Scope(); + + using ( SceneEditorSession.Active.UndoScope( "Bake Scale" ) + .WithGameObjectChanges( _meshes.Select( x => x.GameObject ), GameObjectUndoFlags.Properties ) + .WithComponentChanges( _meshes ) + .Push() ) + { + foreach ( var mesh in _meshes ) + { + BakeScale( mesh ); + } + } + } + + static void CenterMeshOrigin( MeshComponent meshComponent ) + { + if ( !meshComponent.IsValid() ) return; + + var mesh = meshComponent.Mesh; + if ( mesh is null ) return; + + var children = meshComponent.GameObject.Children + .Select( x => (GameObject: x, Transform: x.WorldTransform) ) + .ToArray(); + + var world = meshComponent.WorldTransform; + var bounds = mesh.CalculateBounds( world ); + var center = bounds.Center; + var localCenter = world.PointToLocal( center ); + meshComponent.WorldPosition = center; + meshComponent.Mesh.ApplyTransform( new Transform( -localCenter ) ); + meshComponent.RebuildMesh(); + + foreach ( var child in children ) + { + child.GameObject.WorldTransform = child.Transform; + } + } + + static void SetMeshOrigin( MeshComponent meshComponent, Vector3 origin ) + { + if ( !meshComponent.IsValid() ) return; + + var mesh = meshComponent.Mesh; + if ( mesh is null ) return; + + var world = meshComponent.WorldTransform; + var localCenter = world.PointToLocal( origin ); + meshComponent.WorldPosition = origin; + meshComponent.Mesh.ApplyTransform( new Transform( -localCenter ) ); + meshComponent.RebuildMesh(); + } + + static void BakeScale( MeshComponent meshComponent ) + { + if ( !meshComponent.IsValid() ) return; + + var scale = meshComponent.WorldScale; + meshComponent.WorldScale = 1.0f; + meshComponent.Mesh.Scale( scale ); + meshComponent.RebuildMesh(); + } + } +} diff --git a/game/addons/tools/Code/Scene/Mesh/Tools/MeshSelection.cs b/game/addons/tools/Code/Scene/Mesh/Tools/MeshSelection.cs new file mode 100644 index 00000000..72beb55b --- /dev/null +++ b/game/addons/tools/Code/Scene/Mesh/Tools/MeshSelection.cs @@ -0,0 +1,368 @@ + +namespace Editor.MeshEditor; + +/// +/// Select and edit mesh objects. +/// +[Title( "Mesh Selection" )] +[Icon( "layers" )] +[Alias( "tools.mesh-selection" )] +[Group( "5" )] +public sealed partial class MeshSelection( MeshTool tool ) : SelectionTool +{ + public MeshTool Tool { get; private init; } = tool; + + readonly Dictionary _startPoints = []; + IDisposable _undoScope; + + MeshComponent[] _meshes = []; + + public override void StartDrag() + { + if ( _startPoints.Count > 0 ) return; + if ( _meshes.Length == 0 ) return; + if ( _meshes.Any( x => !x.IsValid() ) ) return; + + if ( Gizmo.IsShiftPressed ) + { + _undoScope ??= SceneEditorSession.Active.UndoScope( "Duplicate Object(s)" ) + .WithGameObjectCreations() + .Push(); + + DuplicateSelection(); + OnSelectionChanged(); + } + else + { + _undoScope ??= SceneEditorSession.Active.UndoScope( "Transform Object(s)" ) + .WithGameObjectChanges( _meshes.Select( x => x.GameObject ), GameObjectUndoFlags.Properties ) + .Push(); + } + + foreach ( var mesh in _meshes ) + { + _startPoints[mesh.GameObject] = mesh.WorldTransform; + } + } + + public override void EndDrag() + { + _startPoints.Clear(); + + _undoScope?.Dispose(); + _undoScope = null; + } + + public override void Translate( Vector3 delta ) + { + foreach ( var entry in _startPoints ) + { + entry.Key.WorldPosition = entry.Value.Position + delta; + } + } + + public override void Rotate( Vector3 origin, Rotation basis, Rotation delta ) + { + foreach ( var entry in _startPoints ) + { + var rot = basis * delta * basis.Inverse; + var position = entry.Value.Position - origin; + position *= rot; + position += origin; + rot *= entry.Value.Rotation; + var scale = entry.Value.Scale; + entry.Key.WorldTransform = new Transform( position, rot, scale ); + } + } + + public override void Scale( Vector3 origin, Rotation basis, Vector3 deltaScale ) + { + foreach ( var entry in _startPoints ) + { + var position = entry.Value.Position - origin; + position *= basis.Inverse; + position *= deltaScale; + position *= basis; + position += origin; + + var scale = entry.Value.Scale * deltaScale; + + entry.Key.WorldTransform = new Transform( + position, + entry.Value.Rotation, + scale + ); + } + } + + public override BBox CalculateLocalBounds() + { + return CalculateSelectionBounds(); + } + + public override Rotation CalculateSelectionBasis() + { + if ( Gizmo.Settings.GlobalSpace ) return Rotation.Identity; + + var mesh = _meshes.FirstOrDefault(); + return mesh.IsValid() ? mesh.WorldRotation : Rotation.Identity; + } + + public override void OnEnabled() + { + Selection.Clear(); + OnSelectionChanged(); + + var undo = SceneEditorSession.Active.UndoSystem; + undo.OnUndo += OnUndoRedo; + undo.OnRedo += OnUndoRedo; + } + + public override void OnDisabled() + { + var undo = SceneEditorSession.Active.UndoSystem; + undo.OnUndo -= OnUndoRedo; + undo.OnRedo -= OnUndoRedo; + } + + void OnUndoRedo( object _ ) + { + OnSelectionChanged(); + } + + public override void OnUpdate() + { + UpdateMoveMode(); + UpdateHovered(); + UpdateSelectionMode(); + DrawBounds(); + } + + void UpdateMoveMode() + { + if ( Tool is null ) return; + if ( Tool.MoveMode is null ) return; + if ( _meshes.Length == 0 ) return; + if ( _meshes.Any( x => !x.IsValid() ) ) return; + + Tool.MoveMode.Update( this ); + } + + BBox CalculateSelectionBounds() + { + var meshes = _meshes.Where( x => x.IsValid() && x.Model.IsValid() ); + return BBox.FromBoxes( meshes.Select( x => x.Model.Bounds.Transform( x.WorldTransform ) ) ); + } + + public override void OnSelectionChanged() + { + _meshes = Selection.OfType() + .Select( x => x.GetComponent() ) + .Where( x => x.IsValid() ) + .ToArray(); + + ClearPivot(); + } + + void UpdateSelectionMode() + { + if ( !Gizmo.HasMouseFocus ) return; + + if ( Gizmo.WasLeftMouseReleased && !Gizmo.Pressed.Any && !IsBoxSelecting ) + { + using ( Scene.Editor?.UndoScope( "Deselect all" ).Push() ) + { + EditorScene.Selection.Clear(); + } + } + } + + void UpdateHovered() + { + if ( IsBoxSelecting ) return; + + var tr = MeshTrace.Run(); + + if ( !tr.Hit ) return; + if ( tr.Component is not MeshComponent component ) return; + + using ( Gizmo.ObjectScope( tr.GameObject, tr.GameObject.WorldTransform ) ) + { + Gizmo.Hitbox.DepthBias = 1; + Gizmo.Hitbox.TrySetHovered( tr.Distance ); + + if ( !Gizmo.IsHovered ) return; + + if ( component.IsValid() && component.Model.IsValid() && !Selection.Contains( tr.GameObject ) ) + { + Gizmo.Draw.Color = Gizmo.Colors.Active.WithAlpha( MathF.Sin( RealTime.Now * 20.0f ).Remap( -1, 1, 0.3f, 0.8f ) ); + Gizmo.Draw.LineBBox( component.Model.Bounds ); + } + } + + if ( Gizmo.WasLeftMousePressed ) + { + Select( tr.GameObject ); + } + } + + void Select( GameObject element ) + { + bool ctrl = Application.KeyboardModifiers.HasFlag( KeyboardModifiers.Ctrl ); + bool shift = Application.KeyboardModifiers.HasFlag( KeyboardModifiers.Shift ); + bool contains = Selection.Contains( element ); + + if ( shift && contains ) return; + + using ( Scene.Editor?.UndoScope( "Select Mesh" ).Push() ) + { + if ( ctrl ) + { + if ( contains ) Selection.Remove( element ); + else Selection.Add( element ); + } + else if ( shift ) + { + Selection.Add( element ); + } + else + { + Selection.Set( element ); + } + } + } + + protected override void OnBoxSelect( Frustum frustum, Rect screenRect, bool isFinal ) + { + var selection = new HashSet(); + var previous = new HashSet(); + + bool fullyInside = true; + bool removing = Gizmo.IsCtrlPressed; + + foreach ( var mr in Scene.GetAllComponents() ) + { + var bounds = mr.GetWorldBounds(); + if ( !frustum.IsInside( bounds, !fullyInside ) ) + { + previous.Add( mr.GameObject ); + continue; + } + + selection.Add( mr.GameObject ); + } + + foreach ( var selectedObj in selection ) + { + if ( !removing ) + { + if ( Selection.Contains( selectedObj ) ) continue; + + Selection.Add( selectedObj ); + } + else + { + if ( !Selection.Contains( selectedObj ) ) continue; + + Selection.Remove( selectedObj ); + } + } + + foreach ( var removed in previous ) + { + if ( removing ) + { + Selection.Add( removed ); + } + else + { + Selection.Remove( removed ); + } + } + } + + private void DrawBounds() + { + using ( Gizmo.Scope( "Bounds" ) ) + { + Gizmo.Draw.IgnoreDepth = true; + Gizmo.Draw.Color = Color.White; + Gizmo.Draw.LineThickness = 4; + + var box = CalculateSelectionBounds(); + var textSize = 22 * Gizmo.Settings.GizmoScale * Application.DpiScale; + + Gizmo.Draw.Color = Gizmo.Colors.Active.WithAlpha( 0.5f ); + Gizmo.Draw.LineThickness = 1; + Gizmo.Draw.LineBBox( box ); + + Gizmo.Draw.LineThickness = 2; + Gizmo.Draw.Color = Gizmo.Colors.Left; + if ( box.Size.y > 0.01f ) + Gizmo.Draw.ScreenText( $"L: {box.Size.y:0.#}", box.Maxs.WithY( box.Center.y ), Vector2.Up * 32, size: textSize ); + Gizmo.Draw.Line( box.Maxs.WithY( box.Mins.y ), box.Maxs.WithY( box.Maxs.y ) ); + Gizmo.Draw.Color = Gizmo.Colors.Forward; + if ( box.Size.x > 0.01f ) + Gizmo.Draw.ScreenText( $"W: {box.Size.x:0.#}", box.Maxs.WithX( box.Center.x ), Vector2.Up * 32, size: textSize ); + Gizmo.Draw.Line( box.Maxs.WithX( box.Mins.x ), box.Maxs.WithX( box.Maxs.x ) ); + Gizmo.Draw.Color = Gizmo.Colors.Up; + if ( box.Size.z > 0.01f ) + Gizmo.Draw.ScreenText( $"H: {box.Size.z:0.#}", box.Maxs.WithZ( box.Center.z ), Vector2.Up * 32, size: textSize ); + Gizmo.Draw.Line( box.Maxs.WithZ( box.Mins.z ), box.Maxs.WithZ( box.Maxs.z ) ); + } + } + + public override bool HasBoxSelectionMode() => true; + + static IReadOnlyList GetPivots( BBox box ) + { + var mins = box.Mins; + var maxs = box.Maxs; + var center = box.Center; + + return + [ + new Vector3( mins.x, mins.y, mins.z ), + new Vector3( maxs.x, mins.y, mins.z ), + new Vector3( mins.x, maxs.y, mins.z ), + new Vector3( maxs.x, maxs.y, mins.z ), + + new Vector3( mins.x, mins.y, maxs.z ), + new Vector3( maxs.x, mins.y, maxs.z ), + new Vector3( mins.x, maxs.y, maxs.z ), + new Vector3( maxs.x, maxs.y, maxs.z ), + + new Vector3( center.x, center.y, mins.z ), + new Vector3( center.x, center.y, maxs.z ), + ]; + } + + int _pivotIndex = 0; + + void StepPivot( int direction ) + { + var box = CalculateSelectionBounds(); + if ( box.Size.Length <= 0 ) return; + + var pivots = GetPivots( box ); + + _pivotIndex = (_pivotIndex + direction + pivots.Count) % pivots.Count; + Pivot = pivots[_pivotIndex]; + } + + public void PreviousPivot() => StepPivot( -1 ); + public void NextPivot() => StepPivot( 1 ); + + public void ClearPivot() + { + var mesh = _meshes.FirstOrDefault(); + Pivot = mesh.IsValid() ? mesh.WorldPosition : default; + _pivotIndex = 0; + } + + public void ZeroPivot() + { + Pivot = default; + _pivotIndex = 0; + } +} diff --git a/game/addons/tools/Code/Scene/Mesh/Tools/MeshTool.UI.cs b/game/addons/tools/Code/Scene/Mesh/Tools/MeshTool.UI.cs index 1528206b..f1c5b621 100644 --- a/game/addons/tools/Code/Scene/Mesh/Tools/MeshTool.UI.cs +++ b/game/addons/tools/Code/Scene/Mesh/Tools/MeshTool.UI.cs @@ -35,4 +35,7 @@ file class MeshToolShortcutsWidget : Widget [Shortcut( "tools.texture-tool", "4", typeof( SceneDock ) )] public void ActivateTextureTool() => EditorToolManager.SetSubTool( nameof( TextureTool ) ); + + [Shortcut( "tools.mesh-selection", "5", typeof( SceneDock ) )] + public void ActivateMeshSelection() => EditorToolManager.SetSubTool( nameof( MeshSelection ) ); } diff --git a/game/addons/tools/Code/Scene/Mesh/Tools/MeshTool.cs b/game/addons/tools/Code/Scene/Mesh/Tools/MeshTool.cs index 7d1a6e34..657b8378 100644 --- a/game/addons/tools/Code/Scene/Mesh/Tools/MeshTool.cs +++ b/game/addons/tools/Code/Scene/Mesh/Tools/MeshTool.cs @@ -12,11 +12,12 @@ public partial class MeshTool : EditorTool { public Material ActiveMaterial { get; set; } = Material.Load( "materials/dev/reflectivity_30.vmat" ); - public MoveMode CurrentMoveMode { get; set; } + public MoveMode MoveMode { get; set; } public override IEnumerable GetSubtools() { yield return new BlockTool( this ); + yield return new MeshSelection( this ); yield return new VertexTool( this ); yield return new EdgeTool( this ); yield return new FaceTool( this ); @@ -32,7 +33,12 @@ public partial class MeshTool : EditorTool Selection.Clear(); - CurrentMoveMode = EditorTypeLibrary.Create( "PositionMode" ); + MoveMode = EditorTypeLibrary.Create( "PositionMode" ); + } + + public override void OnSelectionChanged() + { + CurrentTool?.OnSelectionChanged(); } [Shortcut( "tools.mesh-tool", "m", typeof( SceneDock ) )] diff --git a/game/addons/tools/Code/Scene/Mesh/Tools/MoveModeToolBar.cs b/game/addons/tools/Code/Scene/Mesh/Tools/MoveModeToolBar.cs index 46554398..85acdd03 100644 --- a/game/addons/tools/Code/Scene/Mesh/Tools/MoveModeToolBar.cs +++ b/game/addons/tools/Code/Scene/Mesh/Tools/MoveModeToolBar.cs @@ -20,7 +20,7 @@ class MoveModeToolBar : Widget void SetMode( string id ) { - _tool.CurrentMoveMode = EditorTypeLibrary.Create( id ); + _tool.MoveMode = EditorTypeLibrary.Create( id ); Update(); } @@ -72,9 +72,9 @@ file class MoveModeButton : Widget public void Activate() { - if ( _type.TargetType == _tool.CurrentMoveMode?.GetType() ) return; + if ( _type.TargetType == _tool.MoveMode?.GetType() ) return; - _tool.CurrentMoveMode = _type.Create(); + _tool.MoveMode = _type.Create(); } protected override void OnPaint() @@ -82,7 +82,7 @@ file class MoveModeButton : Widget Paint.Antialiasing = true; Paint.TextAntialiasing = true; - if ( _type.TargetType == _tool.CurrentMoveMode?.GetType() ) + if ( _type.TargetType == _tool.MoveMode?.GetType() ) { Paint.ClearPen(); Paint.SetBrush( Theme.Blue ); diff --git a/game/addons/tools/Code/Scene/Mesh/Tools/SelectionTool.cs b/game/addons/tools/Code/Scene/Mesh/Tools/SelectionTool.cs index 4dfe6dd0..4653b1d9 100644 --- a/game/addons/tools/Code/Scene/Mesh/Tools/SelectionTool.cs +++ b/game/addons/tools/Code/Scene/Mesh/Tools/SelectionTool.cs @@ -5,23 +5,50 @@ public abstract class SelectionTool : EditorTool { public Vector3 Pivot { get; set; } - public HashSet VertexSelection { get; init; } = []; - public virtual Rotation CalculateSelectionBasis() { return Rotation.Identity; } - public virtual List ExtrudeSelection( Vector3 delta = default ) + public virtual BBox CalculateLocalBounds() + { + return default; + } + + public virtual void StartDrag() + { + } + + public virtual void UpdateDrag() + { + } + + public virtual void EndDrag() + { + } + + public virtual void Translate( Vector3 delta ) + { + } + + public virtual void Rotate( Vector3 origin, Rotation basis, Rotation delta ) + { + } + + public virtual void Scale( Vector3 origin, Rotation basis, Vector3 scale ) { - return []; } } -public abstract class SelectionTool( MeshTool tool ) : SelectionTool +public abstract class SelectionTool( MeshTool tool ) : SelectionTool where T : IMeshElement { protected MeshTool Tool { get; private init; } = tool; + readonly HashSet _vertexSelection = []; + readonly Dictionary _transformVertices = []; + List _transformFaces; + IDisposable _undoScope; + protected virtual bool HasMoveMode => true; public static Vector2 RayScreenPosition => SceneViewportWidget.MousePosition; @@ -37,7 +64,49 @@ public abstract class SelectionTool( MeshTool tool ) : SelectionTool public virtual bool DrawVertices => false; - protected IDisposable _undoScope; + public override void Translate( Vector3 delta ) + { + foreach ( var entry in _transformVertices ) + { + var position = entry.Value + delta; + var transform = entry.Key.Transform; + entry.Key.Component.Mesh.SetVertexPosition( entry.Key.Handle, transform.PointToLocal( position ) ); + } + } + + public override void Rotate( Vector3 origin, Rotation basis, Rotation delta ) + { + foreach ( var entry in _transformVertices ) + { + var rotation = basis * delta * basis.Inverse; + var position = entry.Value - origin; + position *= rotation; + position += origin; + + var transform = entry.Key.Transform; + entry.Key.Component.Mesh.SetVertexPosition( entry.Key.Handle, transform.PointToLocal( position ) ); + } + } + + public override void Scale( Vector3 origin, Rotation basis, Vector3 scale ) + { + foreach ( var entry in _transformVertices ) + { + var position = (entry.Value - origin) * basis.Inverse; + position *= scale; + position *= basis; + position += origin; + + var transform = entry.Key.Transform; + entry.Key.Component.Mesh.SetVertexPosition( entry.Key.Handle, transform.PointToLocal( position ) ); + } + } + + public override BBox CalculateLocalBounds() + { + return BBox.FromPoints( _vertexSelection + .Select( x => CalculateSelectionBasis().Inverse * x.PositionWorld ) ); + } public override void OnEnabled() { @@ -54,10 +123,7 @@ public abstract class SelectionTool( MeshTool tool ) : SelectionTool public override void OnUpdate() { - if ( HasMoveMode ) - { - Tool.CurrentMoveMode?.Update( this ); - } + UpdateMoveMode(); if ( Gizmo.WasLeftMouseReleased && !Gizmo.Pressed.Any && Gizmo.Pressed.CursorDelta.Length < 1 ) { @@ -95,6 +161,16 @@ public abstract class SelectionTool( MeshTool tool ) : SelectionTool DrawSelection(); } + void UpdateMoveMode() + { + if ( !HasMoveMode ) return; + if ( Tool is null ) return; + if ( Tool.MoveMode is null ) return; + if ( !Selection.OfType().Any() ) return; + + Tool.MoveMode.Update( this ); + } + void SelectElements() { var elements = Selection.OfType().ToArray(); @@ -204,6 +280,11 @@ public abstract class SelectionTool( MeshTool tool ) : SelectionTool } } + public virtual List ExtrudeSelection( Vector3 delta = default ) + { + return []; + } + private void UpdateNudge() { if ( Gizmo.Pressed.Any || !Application.FocusWidget.IsValid() ) @@ -241,7 +322,7 @@ public abstract class SelectionTool( MeshTool tool ) : SelectionTool } else { - foreach ( var vertex in VertexSelection ) + foreach ( var vertex in _vertexSelection ) { var transform = vertex.Transform; var position = vertex.Component.Mesh.GetVertexPosition( vertex.Handle ); @@ -255,7 +336,7 @@ public abstract class SelectionTool( MeshTool tool ) : SelectionTool public BBox CalculateSelectionBounds() { - return BBox.FromPoints( VertexSelection + return BBox.FromPoints( _vertexSelection .Where( x => x.IsValid() ) .Select( x => x.Transform.PointToWorld( x.Component.Mesh.GetVertexPosition( x.Handle ) ) ) ); } @@ -268,27 +349,27 @@ public abstract class SelectionTool( MeshTool tool ) : SelectionTool public void CalculateSelectionVertices() { - VertexSelection.Clear(); + _vertexSelection.Clear(); foreach ( var face in Selection.OfType() ) { foreach ( var vertex in face.Component.Mesh.GetFaceVertices( face.Handle ) .Select( i => new MeshVertex( face.Component, i ) ) ) { - VertexSelection.Add( vertex ); + _vertexSelection.Add( vertex ); } } foreach ( var vertex in Selection.OfType() ) { - VertexSelection.Add( vertex ); + _vertexSelection.Add( vertex ); } foreach ( var edge in Selection.OfType() ) { edge.Component.Mesh.GetEdgeVertices( edge.Handle, out var hVertexA, out var hVertexB ); - VertexSelection.Add( new MeshVertex( edge.Component, hVertexA ) ); - VertexSelection.Add( new MeshVertex( edge.Component, hVertexB ) ); + _vertexSelection.Add( new MeshVertex( edge.Component, hVertexA ) ); + _vertexSelection.Add( new MeshVertex( edge.Component, hVertexB ) ); } _meshSelectionDirty = false; @@ -370,6 +451,65 @@ public abstract class SelectionTool( MeshTool tool ) : SelectionTool } } + public override void StartDrag() + { + if ( _transformVertices.Count != 0 ) + return; + + var components = Selection.OfType() + .Select( x => x.Component ) + .Distinct(); + + _undoScope ??= SceneEditorSession.Active.UndoScope( $"{(Gizmo.IsShiftPressed ? "Extrude" : "Move")} Selection" ) + .WithComponentChanges( components ) + .Push(); + + if ( Gizmo.IsShiftPressed ) + { + _transformFaces = ExtrudeSelection(); + } + + foreach ( var vertex in _vertexSelection ) + { + _transformVertices[vertex] = vertex.PositionWorld; + } + } + + public override void UpdateDrag() + { + if ( _transformFaces is not null ) + { + foreach ( var group in _transformFaces.GroupBy( x => x.Component ) ) + { + var mesh = group.Key.Mesh; + var faces = group.Select( x => x.Handle ).ToArray(); + + foreach ( var face in faces ) + { + mesh.TextureAlignToGrid( mesh.Transform, face ); + } + } + } + + var meshes = _transformVertices + .Select( x => x.Key.Component.Mesh ) + .Distinct(); + + foreach ( var mesh in meshes ) + { + mesh.ComputeFaceTextureCoordinatesFromParameters(); + } + } + + public override void EndDrag() + { + _transformVertices.Clear(); + _transformFaces = null; + + _undoScope?.Dispose(); + _undoScope = null; + } + public MeshVertex GetClosestVertex( int radius ) { var point = RayScreenPosition; @@ -515,6 +655,7 @@ public abstract class SelectionTool( MeshTool tool ) : SelectionTool return orientation; } + [SkipHotload] private static readonly Vector3[] FaceNormals = { new( 0, 0, 1 ), @@ -525,6 +666,7 @@ public abstract class SelectionTool( MeshTool tool ) : SelectionTool new( 1, 0, 0 ), }; + [SkipHotload] private static readonly Vector3[] FaceDownVectors = { new( 0, -1, 0 ), diff --git a/game/addons/tools/Code/Scene/Mesh/Tools/TextureTool.UI.cs b/game/addons/tools/Code/Scene/Mesh/Tools/TextureTool.UI.cs index 2f0e7a16..4665c6d5 100644 --- a/game/addons/tools/Code/Scene/Mesh/Tools/TextureTool.UI.cs +++ b/game/addons/tools/Code/Scene/Mesh/Tools/TextureTool.UI.cs @@ -137,7 +137,7 @@ partial class TextureTool } } - [Shortcut( "editor.delete", "DEL", typeof( SceneViewportWidget ) )] + [Shortcut( "editor.delete", "DEL", typeof( SceneDock ) )] private void DeleteSelection() { var groups = _faces.GroupBy( face => face.Component ); diff --git a/game/addons/tools/Code/Scene/Mesh/Tools/VertexTool.UI.cs b/game/addons/tools/Code/Scene/Mesh/Tools/VertexTool.UI.cs index e94dfacc..e58facba 100644 --- a/game/addons/tools/Code/Scene/Mesh/Tools/VertexTool.UI.cs +++ b/game/addons/tools/Code/Scene/Mesh/Tools/VertexTool.UI.cs @@ -56,42 +56,47 @@ partial class VertexTool _components = _vertexGroups.Select( x => x.Key ).ToList(); { - var row = new Widget { Layout = Layout.Row() }; - row.Layout.Spacing = 4; + var group = AddGroup( "Operations" ); - CreateButton( "Merge", "merge", "mesh.merge", Merge, _vertices.Length > 1, row.Layout ); + { + var row = new Widget { Layout = Layout.Row() }; + row.Layout.Spacing = 4; - var mergeObject = mergeProperties.GetSerialized(); - var range = ControlWidget.Create( mergeObject.GetProperty( nameof( MergeProperties.Range ) ) ); - var distance = ControlWidget.Create( mergeObject.GetProperty( nameof( MergeProperties.Distance ) ) ); - distance.HorizontalSizeMode = SizeMode.Expand; + CreateButton( "Merge", "merge", "mesh.merge", Merge, _vertices.Length > 1, row.Layout ); - range.FixedHeight = Theme.ControlHeight; - distance.FixedHeight = Theme.ControlHeight; + var mergeObject = mergeProperties.GetSerialized(); + var range = ControlWidget.Create( mergeObject.GetProperty( nameof( MergeProperties.Range ) ) ); + var distance = ControlWidget.Create( mergeObject.GetProperty( nameof( MergeProperties.Distance ) ) ); + distance.HorizontalSizeMode = SizeMode.Expand; - row.Layout.Add( range ); - row.Layout.Add( distance ); + range.FixedHeight = Theme.ControlHeight; + distance.FixedHeight = Theme.ControlHeight; - Layout.Add( row ); - } - { - var row = new Widget { Layout = Layout.Row() }; - row.Layout.Spacing = 4; + row.Layout.Add( range ); + row.Layout.Add( distance ); - CreateButton( "Snap To Vertex", "gps_fixed", "mesh.snap_to_vertex", SnapToVertex, _vertices.Length > 1, row.Layout ); - CreateButton( "Weld UVs", "scatter_plot", "mesh.vertex-weld-uvs", WeldUVs, _vertices.Length > 0, row.Layout ); - CreateButton( "Bevel", "straighten", "mesh.bevel", Bevel, _vertices.Length > 0, row.Layout ); - CreateButton( "Connect", "link", "mesh.connect", Connect, _vertices.Length > 1, row.Layout ); + group.Add( row ); + } - row.Layout.AddStretchCell(); + { + var row = new Widget { Layout = Layout.Row() }; + row.Layout.Spacing = 4; - Layout.Add( row ); + CreateButton( "Snap To Vertex", "gps_fixed", "mesh.snap_to_vertex", SnapToVertex, _vertices.Length > 1, row.Layout ); + CreateButton( "Weld UVs", "scatter_plot", "mesh.vertex-weld-uvs", WeldUVs, _vertices.Length > 0, row.Layout ); + CreateButton( "Bevel", "straighten", "mesh.bevel", Bevel, _vertices.Length > 0, row.Layout ); + CreateButton( "Connect", "link", "mesh.connect", Connect, _vertices.Length > 1, row.Layout ); + + row.Layout.AddStretchCell(); + + group.Add( row ); + } } Layout.AddStretchCell(); } - [Shortcut( "mesh.connect", "V", typeof( SceneViewportWidget ) )] + [Shortcut( "mesh.connect", "V", typeof( SceneDock ) )] private void Connect() { if ( _vertices.Length < 2 ) @@ -273,7 +278,7 @@ partial class VertexTool } } - [Shortcut( "editor.delete", "DEL", typeof( SceneViewportWidget ) )] + [Shortcut( "editor.delete", "DEL", typeof( SceneDock ) )] private void DeleteSelection() { var groups = _vertices.GroupBy( face => face.Component ); diff --git a/game/addons/tools/Code/Scene/ObjectTool/RotationEditorTool.cs b/game/addons/tools/Code/Scene/ObjectTool/RotationEditorTool.cs index 062607f5..454e2042 100644 --- a/game/addons/tools/Code/Scene/ObjectTool/RotationEditorTool.cs +++ b/game/addons/tools/Code/Scene/ObjectTool/RotationEditorTool.cs @@ -110,7 +110,7 @@ public class RotationEditorTool : EditorTool } - [Shortcut( "tools.rotate-tool", "e", typeof( SceneViewportWidget ) )] + [Shortcut( "tools.rotate-tool", "e", typeof( SceneDock ) )] public static void ActivateSubTool() { if ( !(EditorToolManager.CurrentModeName == nameof( ObjectEditorTool ) || EditorToolManager.CurrentModeName == "object") ) return; diff --git a/game/addons/tools/Code/Scene/ObjectTool/ScaleEditorTool.cs b/game/addons/tools/Code/Scene/ObjectTool/ScaleEditorTool.cs index 6c3cc963..de831b2a 100644 --- a/game/addons/tools/Code/Scene/ObjectTool/ScaleEditorTool.cs +++ b/game/addons/tools/Code/Scene/ObjectTool/ScaleEditorTool.cs @@ -55,7 +55,7 @@ public class ScaleEditorTool : EditorTool } - [Shortcut( "tools.scale-tool", "r", typeof( SceneViewportWidget ) )] + [Shortcut( "tools.scale-tool", "r", typeof( SceneDock ) )] public static void ActivateSubTool() { if ( !(EditorToolManager.CurrentModeName == nameof( ObjectEditorTool ) || EditorToolManager.CurrentModeName == "object") ) return; diff --git a/game/addons/tools/Code/Scene/SceneView/SceneViewWidget.cs b/game/addons/tools/Code/Scene/SceneView/SceneViewWidget.cs index cd8dd1d7..0ecec0f9 100644 --- a/game/addons/tools/Code/Scene/SceneView/SceneViewWidget.cs +++ b/game/addons/tools/Code/Scene/SceneView/SceneViewWidget.cs @@ -305,7 +305,7 @@ file class ViewportToolBar : Widget void OnToolChanged() { // Prevent flicker when changing tools - using var x = SuspendUpdates.For( GetAncestor() ); + using var x = SuspendUpdates.For( this ); var rootTool = SceneViewWidget.Current?.Tools.CurrentTool; var subTool = SceneViewWidget.Current?.Tools.CurrentSubTool; @@ -327,7 +327,7 @@ file class ViewportToolBar : Widget if ( toolWidget.IsValid() ) { - var scroller = new ScrollArea( this ); + var scroller = new ScrollArea( null ); scroller.FixedWidth = 240; toolWidget.FixedWidth = 240; scroller.HorizontalSizeMode = SizeMode.Flexible;