using Sandbox;
namespace Editor.MeshEditor;
///
/// Base class for vertex, edge and face tools.
///
public abstract class BaseMeshTool : EditorTool
{
public SelectionSystem MeshSelection => Selection;
public HashSet VertexSelection { get; init; } = new();
public static Vector2 RayScreenPosition => SceneViewportWidget.MousePosition;
public static bool IsMultiSelecting => Application.KeyboardModifiers.HasFlag( KeyboardModifiers.Ctrl ) ||
Application.KeyboardModifiers.HasFlag( KeyboardModifiers.Shift );
public Vector3 Pivot { get; set; }
private bool _meshSelectionDirty;
private bool _nudge;
private bool _invertSelection;
private MeshComponent _hoverMesh;
protected IDisposable _undoScope;
protected virtual bool DrawVertices => false;
public override IEnumerable GetSubtools()
{
yield return new PositionTool( this );
yield return new RotateTool( this );
yield return new ScaleTool( this );
yield return new PivotTool( this );
}
public override void OnEnabled()
{
base.OnEnabled();
AllowGameObjectSelection = false;
Selection.Clear();
MeshSelection.OnItemAdded += OnMeshSelectionChanged;
MeshSelection.OnItemRemoved += OnMeshSelectionChanged;
SceneEditorSession.Active.UndoSystem.OnUndo += ( _ ) => OnMeshSelectionChanged();
SceneEditorSession.Active.UndoSystem.OnRedo += ( _ ) => OnMeshSelectionChanged();
}
public override void OnDisabled()
{
base.OnDisabled();
var selectedObjects = GetSelectedObjects();
Selection.Clear();
foreach ( var go in selectedObjects )
{
if ( !go.IsValid() )
continue;
Selection.Add( go );
}
}
public override void OnUpdate()
{
base.OnUpdate();
if ( Gizmo.WasLeftMouseReleased && !Gizmo.Pressed.Any && Gizmo.Pressed.CursorDelta.Length < 1 )
{
Gizmo.Select();
}
var removeList = GetInvalidSelection().ToList();
foreach ( var s in removeList )
MeshSelection.Remove( s );
if ( Application.IsKeyDown( KeyCode.I ) )
{
if ( !_invertSelection && Gizmo.IsCtrlPressed )
{
InvertSelection();
}
_invertSelection = true;
}
else
{
_invertSelection = false;
}
UpdateNudge();
if ( _meshSelectionDirty )
{
CalculateSelectionVertices();
OnMeshSelectionChanged();
}
DrawSelection();
}
private void InvertSelection()
{
if ( !MeshSelection.Any() )
return;
var newSelection = GetAllSelectedElements()
.Except( MeshSelection )
.ToArray();
MeshSelection.Clear();
foreach ( var element in newSelection )
MeshSelection.Add( element );
}
protected virtual IEnumerable GetAllSelectedElements()
{
return Enumerable.Empty();
}
private void DrawSelection()
{
var face = TraceFace();
if ( face.IsValid() )
_hoverMesh = face.Component;
if ( _hoverMesh.IsValid() )
DrawMesh( _hoverMesh );
foreach ( var group in MeshSelection.OfType()
.GroupBy( x => x.Component ) )
{
var component = group.Key;
if ( !component.IsValid() )
continue;
if ( component == _hoverMesh )
continue;
DrawMesh( component );
}
}
protected void DrawMesh( MeshComponent mesh )
{
using ( Gizmo.ObjectScope( mesh.GameObject, mesh.WorldTransform ) )
{
var color = new Color( 0.3137f, 0.7843f, 1.0f, 0.5f );
if ( DrawVertices )
{
using ( Gizmo.Scope( "Vertices" ) )
{
Gizmo.Draw.Color = color;
foreach ( var v in mesh.Mesh.GetVertexPositions() )
{
Gizmo.Draw.Sprite( v, 8, null, false );
}
}
}
using ( Gizmo.Scope( "Edges" ) )
{
Gizmo.Draw.Color = color;
Gizmo.Draw.LineThickness = 2;
foreach ( var v in mesh.Mesh.GetEdges() )
{
Gizmo.Draw.Line( v );
}
}
}
}
private void UpdateNudge()
{
if ( Gizmo.Pressed.Any || !Application.FocusWidget.IsValid() )
return;
var keyUp = Application.IsKeyDown( KeyCode.Up );
var keyDown = Application.IsKeyDown( KeyCode.Down );
var keyLeft = Application.IsKeyDown( KeyCode.Left );
var keyRight = Application.IsKeyDown( KeyCode.Right );
if ( !keyUp && !keyDown && !keyLeft && !keyRight )
{
_nudge = false;
_undoScope?.Dispose();
_undoScope = null;
return;
}
if ( _nudge )
return;
var basis = CalculateSelectionBasis();
var direction = new Vector2( keyLeft ? 1 : keyRight ? -1 : 0, keyUp ? 1 : keyDown ? -1 : 0 );
var delta = Gizmo.Nudge( basis, direction );
var components = MeshSelection.OfType().Select( x => x.Component );
_undoScope ??= SceneEditorSession.Active.UndoScope( "Nudge Vertices" ).WithComponentChanges( components ).Push();
if ( Gizmo.IsShiftPressed )
{
ExtrudeSelection( delta );
}
else
{
foreach ( var vertex in VertexSelection )
{
var transform = vertex.Transform;
var position = vertex.Component.Mesh.GetVertexPosition( vertex.Handle );
position = transform.PointToWorld( position ) + delta;
vertex.Component.Mesh.SetVertexPosition( vertex.Handle, transform.PointToLocal( position ) );
}
}
_nudge = true;
}
public virtual List ExtrudeSelection( Vector3 delta = default )
{
return default;
}
public override void OnSelectionChanged()
{
base.OnSelectionChanged();
if ( Selection.OfType().Any() )
{
EditorToolManager.CurrentModeName = "object";
}
}
public BBox CalculateSelectionBounds()
{
return BBox.FromPoints( VertexSelection
.Where( x => x.IsValid() )
.Select( x => x.Transform.PointToWorld( x.Component.Mesh.GetVertexPosition( x.Handle ) ) ) );
}
public virtual Rotation CalculateSelectionBasis()
{
return Rotation.Identity;
}
public virtual Vector3 CalculateSelectionOrigin()
{
var bounds = CalculateSelectionBounds();
return bounds.Center;
}
public void CalculateSelectionVertices()
{
VertexSelection.Clear();
foreach ( var face in MeshSelection.OfType() )
{
foreach ( var vertex in face.Component.Mesh.GetFaceVertices( face.Handle )
.Select( i => new MeshVertex( face.Component, i ) ) )
{
VertexSelection.Add( vertex );
}
}
foreach ( var vertex in MeshSelection.OfType() )
{
VertexSelection.Add( vertex );
}
foreach ( var edge in MeshSelection.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 ) );
}
_meshSelectionDirty = false;
}
private HashSet GetSelectedObjects()
{
var objects = new HashSet();
foreach ( var face in MeshSelection.OfType()
.Where( x => x.IsValid() ) )
{
objects.Add( face.GameObject );
}
return objects;
}
private IEnumerable GetInvalidSelection()
{
foreach ( var selection in MeshSelection.OfType()
.Where( x => !x.IsValid() || x.Scene != Scene ) )
{
yield return selection;
}
}
private void OnMeshSelectionChanged( object o )
{
_hoverMesh = null;
_meshSelectionDirty = true;
}
private void OnMeshSelectionChanged()
{
Pivot = CalculateSelectionOrigin();
}
protected void Select( IMeshElement element )
{
if ( Application.KeyboardModifiers.HasFlag( KeyboardModifiers.Ctrl ) )
{
if ( MeshSelection.Contains( element ) )
{
MeshSelection.Remove( element );
}
else
{
MeshSelection.Add( element );
}
return;
}
else if ( Application.KeyboardModifiers.HasFlag( KeyboardModifiers.Shift ) )
{
if ( !MeshSelection.Contains( element ) )
{
MeshSelection.Add( element );
}
return;
}
MeshSelection.Set( element );
}
protected void UpdateSelection( IMeshElement element )
{
if ( Gizmo.WasLeftMousePressed )
{
if ( element.IsValid() )
{
Select( element );
}
else if ( !IsMultiSelecting )
{
MeshSelection.Clear();
}
}
else if ( Gizmo.IsLeftMouseDown && Gizmo.CursorMoveDelta.LengthSquared > 0 && element.IsValid() )
{
if ( Application.KeyboardModifiers.HasFlag( KeyboardModifiers.Ctrl ) )
{
if ( MeshSelection.Contains( element ) )
MeshSelection.Remove( element );
}
else
{
if ( !MeshSelection.Contains( element ) )
MeshSelection.Add( element );
}
}
}
protected MeshVertex GetClosestVertex( int radius )
{
var point = RayScreenPosition;
var bestFace = TraceFace( out var bestHitDistance );
var bestVertex = bestFace.GetClosestVertex( point, radius );
if ( bestFace.IsValid() && bestVertex.IsValid() )
return bestVertex;
var results = TraceFaces( radius, point );
foreach ( var result in results )
{
var face = result.MeshFace;
var hitDistance = result.Distance;
var vertex = face.GetClosestVertex( point, radius );
if ( !vertex.IsValid() )
continue;
if ( hitDistance < bestHitDistance || !bestFace.IsValid() )
{
bestHitDistance = hitDistance;
bestVertex = vertex;
bestFace = face;
}
}
return bestVertex;
}
protected MeshEdge GetClosestEdge( int radius )
{
var point = RayScreenPosition;
var bestFace = TraceFace( out var bestHitDistance );
var hitPosition = Gizmo.CurrentRay.Project( bestHitDistance );
var bestEdge = bestFace.GetClosestEdge( hitPosition, point, radius );
if ( bestFace.IsValid() && bestEdge.IsValid() )
return bestEdge;
var results = TraceFaces( radius, point );
foreach ( var result in results )
{
var face = result.MeshFace;
var hitDistance = result.Distance;
hitPosition = Gizmo.CurrentRay.Project( hitDistance );
var edge = face.GetClosestEdge( hitPosition, point, radius );
if ( !edge.IsValid() )
continue;
if ( hitDistance < bestHitDistance || !bestFace.IsValid() )
{
bestHitDistance = hitDistance;
bestEdge = edge;
bestFace = face;
}
}
return bestEdge;
}
private MeshFace TraceFace( out float distance )
{
distance = default;
var result = MeshTrace.Run();
if ( !result.Hit || result.Component is not MeshComponent component )
return default;
distance = result.Distance;
var face = component.Mesh.TriangleToFace( result.Triangle );
return new MeshFace( component, face );
}
public MeshFace TraceFace()
{
var result = MeshTrace.Run();
if ( !result.Hit || result.Component is not MeshComponent component )
return default;
var face = component.Mesh.TriangleToFace( result.Triangle );
return new MeshFace( component, face );
}
private struct MeshFaceTraceResult
{
public MeshFace MeshFace;
public float Distance;
}
private List TraceFaces( int radius, Vector2 point )
{
var rays = new List { Gizmo.CurrentRay };
for ( var ring = 1; ring < radius; ring++ )
{
rays.Add( Gizmo.Camera.GetRay( point + new Vector2( 0, ring ) ) );
rays.Add( Gizmo.Camera.GetRay( point + new Vector2( ring, 0 ) ) );
rays.Add( Gizmo.Camera.GetRay( point + new Vector2( 0, -ring ) ) );
rays.Add( Gizmo.Camera.GetRay( point + new Vector2( -ring, 0 ) ) );
}
var faces = new List();
var faceHash = new HashSet();
foreach ( var ray in rays )
{
var result = MeshTrace.Ray( ray, Gizmo.RayDepth ).Run();
if ( !result.Hit )
continue;
if ( result.Component is not MeshComponent component )
continue;
var face = component.Mesh.TriangleToFace( result.Triangle );
var faceElement = new MeshFace( component, face );
if ( faceHash.Add( faceElement ) )
faces.Add( new MeshFaceTraceResult { MeshFace = faceElement, Distance = result.Distance } );
}
return faces;
}
protected static Vector3 ComputeTextureVAxis( Vector3 normal ) => FaceDownVectors[GetOrientationForPlane( normal )];
private static int GetOrientationForPlane( Vector3 plane )
{
plane = plane.Normal;
var maxDot = 0.0f;
int orientation = 0;
for ( int i = 0; i < 6; i++ )
{
var dot = Vector3.Dot( plane, FaceNormals[i] );
if ( dot >= maxDot )
{
maxDot = dot;
orientation = i;
}
}
return orientation;
}
private static readonly Vector3[] FaceNormals =
{
new( 0, 0, 1 ),
new( 0, 0, -1 ),
new( 0, -1, 0 ),
new( 0, 1, 0 ),
new( -1, 0, 0 ),
new( 1, 0, 0 ),
};
private static readonly Vector3[] FaceDownVectors =
{
new( 0, -1, 0 ),
new( 0, -1, 0 ),
new( 0, 0, -1 ),
new( 0, 0, -1 ),
new( 0, 0, -1 ),
new( 0, 0, -1 ),
};
}