mapping tool mesh selection mode (#3588)

This commit is contained in:
Layla
2025-12-10 16:27:44 +00:00
committed by GitHub
parent 5046b22eee
commit 4e9cf595ff
19 changed files with 777 additions and 172 deletions

View File

@@ -18,7 +18,7 @@ public sealed class MeshComponent : Collider, ExecuteInEditor, ITintable, IMater
Hull
}
[Property, Order( 0 )]
[Property, Hide]
public PolygonMesh Mesh
{
get;

View File

@@ -6,80 +6,12 @@ namespace Editor.MeshEditor;
/// </summary>
public abstract class MoveMode
{
protected IReadOnlyDictionary<MeshVertex, Vector3> TransformVertices => _transformVertices;
private readonly Dictionary<MeshVertex, Vector3> _transformVertices = [];
private List<MeshFace> _transformFaces;
private IDisposable _undoScope;
public void Update( SelectionTool tool )
{
if ( !tool.Selection.OfType<IMeshElement>().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<IMeshElement>()
.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;
}
}

View File

@@ -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();

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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 );

View File

@@ -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 );

View File

@@ -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<GameObject>()
.Select( x => x.GetComponent<MeshComponent>() )
.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();
}
}
}

View File

@@ -0,0 +1,368 @@

namespace Editor.MeshEditor;
/// <summary>
/// Select and edit mesh objects.
/// </summary>
[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<GameObject, Transform> _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<GameObject>()
.Select( x => x.GetComponent<MeshComponent>() )
.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<GameObject>();
var previous = new HashSet<GameObject>();
bool fullyInside = true;
bool removing = Gizmo.IsCtrlPressed;
foreach ( var mr in Scene.GetAllComponents<MeshComponent>() )
{
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<Vector3> 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;
}
}

View File

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

View File

@@ -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<EditorTool> 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<MoveMode>( "PositionMode" );
MoveMode = EditorTypeLibrary.Create<MoveMode>( "PositionMode" );
}
public override void OnSelectionChanged()
{
CurrentTool?.OnSelectionChanged();
}
[Shortcut( "tools.mesh-tool", "m", typeof( SceneDock ) )]

View File

@@ -20,7 +20,7 @@ class MoveModeToolBar : Widget
void SetMode( string id )
{
_tool.CurrentMoveMode = EditorTypeLibrary.Create<MoveMode>( id );
_tool.MoveMode = EditorTypeLibrary.Create<MoveMode>( 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<MoveMode>();
_tool.MoveMode = _type.Create<MoveMode>();
}
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 );

View File

@@ -5,23 +5,50 @@ public abstract class SelectionTool : EditorTool
{
public Vector3 Pivot { get; set; }
public HashSet<MeshVertex> VertexSelection { get; init; } = [];
public virtual Rotation CalculateSelectionBasis()
{
return Rotation.Identity;
}
public virtual List<MeshFace> 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<T>( MeshTool tool ) : SelectionTool
public abstract class SelectionTool<T>( MeshTool tool ) : SelectionTool where T : IMeshElement
{
protected MeshTool Tool { get; private init; } = tool;
readonly HashSet<MeshVertex> _vertexSelection = [];
readonly Dictionary<MeshVertex, Vector3> _transformVertices = [];
List<MeshFace> _transformFaces;
IDisposable _undoScope;
protected virtual bool HasMoveMode => true;
public static Vector2 RayScreenPosition => SceneViewportWidget.MousePosition;
@@ -37,7 +64,49 @@ public abstract class SelectionTool<T>( 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<T>( 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<T>( MeshTool tool ) : SelectionTool
DrawSelection();
}
void UpdateMoveMode()
{
if ( !HasMoveMode ) return;
if ( Tool is null ) return;
if ( Tool.MoveMode is null ) return;
if ( !Selection.OfType<IMeshElement>().Any() ) return;
Tool.MoveMode.Update( this );
}
void SelectElements()
{
var elements = Selection.OfType<T>().ToArray();
@@ -204,6 +280,11 @@ public abstract class SelectionTool<T>( MeshTool tool ) : SelectionTool
}
}
public virtual List<MeshFace> ExtrudeSelection( Vector3 delta = default )
{
return [];
}
private void UpdateNudge()
{
if ( Gizmo.Pressed.Any || !Application.FocusWidget.IsValid() )
@@ -241,7 +322,7 @@ public abstract class SelectionTool<T>( 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<T>( 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<T>( MeshTool tool ) : SelectionTool
public void CalculateSelectionVertices()
{
VertexSelection.Clear();
_vertexSelection.Clear();
foreach ( var face in Selection.OfType<MeshFace>() )
{
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<MeshVertex>() )
{
VertexSelection.Add( vertex );
_vertexSelection.Add( vertex );
}
foreach ( var edge in Selection.OfType<MeshEdge>() )
{
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<T>( MeshTool tool ) : SelectionTool
}
}
public override void StartDrag()
{
if ( _transformVertices.Count != 0 )
return;
var components = Selection.OfType<IMeshElement>()
.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<T>( MeshTool tool ) : SelectionTool
return orientation;
}
[SkipHotload]
private static readonly Vector3[] FaceNormals =
{
new( 0, 0, 1 ),
@@ -525,6 +666,7 @@ public abstract class SelectionTool<T>( MeshTool tool ) : SelectionTool
new( 1, 0, 0 ),
};
[SkipHotload]
private static readonly Vector3[] FaceDownVectors =
{
new( 0, -1, 0 ),

View File

@@ -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 );

View File

@@ -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 );

View File

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

View File

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

View File

@@ -305,7 +305,7 @@ file class ViewportToolBar : Widget
void OnToolChanged()
{
// Prevent flicker when changing tools
using var x = SuspendUpdates.For( GetAncestor<SceneViewWidget>() );
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;