mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-04-19 05:48:07 -04:00
This commit imports the C# engine code and game files, excluding C++ source code. [Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
868 lines
25 KiB
C#
868 lines
25 KiB
C#
using HalfEdgeMesh;
|
|
using Sandbox.UI;
|
|
using System.Text.Json.Nodes;
|
|
using System.Text.Json.Serialization;
|
|
|
|
namespace Editor.MeshEditor;
|
|
|
|
/// <summary>
|
|
/// References a face handle and the mesh component it belongs to.
|
|
/// </summary>
|
|
public struct MeshFace : IMeshElement
|
|
{
|
|
[Hide, JsonInclude] public MeshComponent Component { get; private set; }
|
|
[Hide, JsonIgnore] public readonly FaceHandle Handle => Component.IsValid() ? Component.Mesh.FaceHandleFromIndex( HandleIndex ) : default;
|
|
[Hide, JsonInclude] public int HandleIndex { get; set; }
|
|
|
|
[Hide, JsonIgnore] public readonly bool IsValid => Component.IsValid() && Handle.IsValid;
|
|
[Hide, JsonIgnore] public readonly Transform Transform => IsValid ? Component.WorldTransform : Transform.Zero;
|
|
|
|
[Hide, JsonIgnore] public readonly Vector3 Center => IsValid ? Component.Mesh.GetFaceCenter( Handle ) : Vector3.Zero;
|
|
|
|
public MeshFace( MeshComponent component, FaceHandle handle )
|
|
{
|
|
Component = component;
|
|
HandleIndex = handle.Index;
|
|
}
|
|
|
|
public readonly override int GetHashCode() => HashCode.Combine( Component, nameof( MeshFace ), Handle );
|
|
public override readonly string ToString() => IsValid ? $"{Component.GameObject.Name} Face {Handle}" : "Invalid Face";
|
|
|
|
[JsonIgnore]
|
|
[Title( "Shift" ), Group( "Texture State" )]
|
|
public readonly Vector2 TextureOffset
|
|
{
|
|
get => IsValid ? Component.Mesh.GetTextureOffset( Handle ) : default;
|
|
set => Component?.Mesh.SetTextureOffset( Handle, value );
|
|
}
|
|
|
|
[JsonIgnore]
|
|
[Title( "Scale" ), Group( "Texture State" )]
|
|
public readonly Vector2 TextureScale
|
|
{
|
|
get => IsValid ? Component.Mesh.GetTextureScale( Handle ) : default;
|
|
set => Component?.Mesh.SetTextureScale( Handle, value );
|
|
}
|
|
|
|
[JsonIgnore]
|
|
[Group( "Texture State" )]
|
|
public readonly Material Material
|
|
{
|
|
get => IsValid ? Component.Mesh.GetFaceMaterial( Handle ) : default;
|
|
set => Component?.Mesh.SetFaceMaterial( Handle, value );
|
|
}
|
|
|
|
[Hide]
|
|
public readonly MeshVertex GetClosestVertex( Vector2 point, float maxDistance )
|
|
{
|
|
if ( !IsValid )
|
|
return default;
|
|
|
|
var transform = Transform;
|
|
var minDistance = maxDistance;
|
|
var closestVertex = VertexHandle.Invalid;
|
|
|
|
foreach ( var vertex in Component.Mesh.GetFaceVertices( Handle ) )
|
|
{
|
|
var vertexPosition = transform.PointToWorld( Component.Mesh.GetVertexPosition( vertex ) );
|
|
var vertexCoord = Gizmo.Camera.ToScreen( vertexPosition );
|
|
var distance = vertexCoord.Distance( point );
|
|
if ( distance < minDistance )
|
|
{
|
|
minDistance = distance;
|
|
closestVertex = vertex;
|
|
}
|
|
}
|
|
|
|
return new MeshVertex( Component, closestVertex );
|
|
}
|
|
|
|
[Hide]
|
|
public readonly MeshEdge GetClosestEdge( Vector3 position, Vector2 point, float maxDistance )
|
|
{
|
|
if ( !IsValid )
|
|
return default;
|
|
|
|
if ( !Component.Mesh.GetFaceVerticesConnectedToFace( Handle, out var hEdges ) )
|
|
return default;
|
|
|
|
var transform = Transform;
|
|
var minDistance = maxDistance;
|
|
var hClosestEdge = HalfEdgeHandle.Invalid;
|
|
|
|
foreach ( var hEdge in hEdges )
|
|
{
|
|
var line = Component.Mesh.GetEdgeLine( hEdge );
|
|
line = new Line( transform.PointToWorld( line.Start ), transform.PointToWorld( line.End ) );
|
|
var closestPoint = line.ClosestPoint( position );
|
|
var pointCoord = Gizmo.Camera.ToScreen( closestPoint );
|
|
var distance = pointCoord.Distance( point );
|
|
if ( distance < minDistance )
|
|
{
|
|
minDistance = distance;
|
|
hClosestEdge = hEdge;
|
|
}
|
|
}
|
|
|
|
if ( !hClosestEdge.IsValid )
|
|
return default;
|
|
|
|
var hOppositeEdge = Component.Mesh.GetOppositeHalfEdge( hClosestEdge );
|
|
if ( hClosestEdge.Index < hOppositeEdge.Index )
|
|
{
|
|
return new MeshEdge( Component, hClosestEdge );
|
|
}
|
|
else
|
|
{
|
|
return new MeshEdge( Component, hOppositeEdge );
|
|
}
|
|
}
|
|
}
|
|
|
|
[Inspector( typeof( MeshFace ) )]
|
|
public class FaceInspector : InspectorWidget
|
|
{
|
|
private readonly MeshFace[] Faces;
|
|
private readonly List<IGrouping<MeshComponent, MeshFace>> FaceGroups;
|
|
private readonly List<MeshComponent> Components;
|
|
|
|
[Range( 0, 128, slider: false )]
|
|
private int FitX = 1;
|
|
|
|
[Range( 0, 128, slider: false )]
|
|
private int FitY = 1;
|
|
|
|
private bool TreatAsOne = false;
|
|
|
|
Widget OperationsBody;
|
|
|
|
[Range( 0, 64, slider: false ), Step( 1 )]
|
|
private Vector2Int NumCuts = 1;
|
|
|
|
private class TextureModifyButton : Widget
|
|
{
|
|
private readonly string Icon;
|
|
|
|
public TextureModifyButton( Widget parent, string tooltip, string icon, Action onClick ) : base( parent )
|
|
{
|
|
Icon = icon;
|
|
ToolTip = tooltip;
|
|
FixedSize = 26;
|
|
Cursor = CursorShape.Finger;
|
|
MouseClick = onClick;
|
|
}
|
|
|
|
protected override void OnPaint()
|
|
{
|
|
base.OnPaint();
|
|
|
|
Paint.ClearPen();
|
|
var bg = Theme.ControlBackground;
|
|
if ( Paint.HasMouseOver )
|
|
bg = bg.Lighten( 0.3f );
|
|
Paint.SetBrush( bg );
|
|
Paint.DrawRect( LocalRect, Theme.ControlRadius );
|
|
Paint.Draw( LocalRect.Shrink( 3 ), Icon );
|
|
}
|
|
}
|
|
|
|
private class GroupHeader : Widget
|
|
{
|
|
readonly Layout toggleLayout;
|
|
|
|
public GroupHeader( Widget parent, string name ) : base( parent )
|
|
{
|
|
Title = name;
|
|
FixedHeight = Theme.RowHeight;
|
|
VerticalSizeMode = SizeMode.CanGrow;
|
|
HorizontalSizeMode = SizeMode.Flexible;
|
|
Layout = Layout.Row();
|
|
Layout.Spacing = 5;
|
|
Layout.Margin = new Margin( 20, 0, 0, 0 );
|
|
|
|
toggleLayout = Layout.AddColumn();
|
|
|
|
Layout.AddStretchCell();
|
|
|
|
State = ProjectCookie.Get( $"{Title}.state", true );
|
|
}
|
|
|
|
public string Title { get; set; }
|
|
|
|
protected override void OnMousePress( MouseEvent e )
|
|
{
|
|
base.OnMousePress( e );
|
|
|
|
if ( e.Button == MouseButtons.Left )
|
|
{
|
|
Toggle();
|
|
}
|
|
}
|
|
|
|
protected override void OnDoubleClick( MouseEvent e )
|
|
{
|
|
e.Accepted = false;
|
|
}
|
|
|
|
protected override void OnPaint()
|
|
{
|
|
bool isChecked = true;
|
|
|
|
{
|
|
Paint.ClearPen();
|
|
Paint.SetBrush( Color.Black.WithAlpha( 0.16f ) );
|
|
Paint.DrawRect( LocalRect );
|
|
}
|
|
|
|
float spacing = 0;
|
|
|
|
Paint.ClearBrush();
|
|
Paint.Pen = Theme.Text.WithAlpha( Paint.HasMouseOver ? 0.7f : (isChecked ? 0.6f : 0.3f) );
|
|
|
|
Paint.SetDefaultFont( 10, weight: 500, sizeInPixels: true );
|
|
Paint.DrawText( LocalRect.Shrink( toggleLayout.OuterRect.Right + spacing, 0, 0, 0 ), Title, TextFlag.LeftCenter );
|
|
|
|
Paint.DrawIcon( LocalRect.Shrink( 0, 0 ), State ? "arrow_drop_down" : "arrow_right", 16, TextFlag.LeftCenter );
|
|
}
|
|
|
|
public bool State { get; private set; } = true;
|
|
public Action<bool> OnToggled;
|
|
|
|
public void Toggle()
|
|
{
|
|
State = !State;
|
|
OnToggled?.Invoke( State );
|
|
ProjectCookie.Set( $"{Title}.state", State );
|
|
}
|
|
}
|
|
|
|
public FaceInspector( SerializedObject so ) : base( so )
|
|
{
|
|
Faces = SerializedObject.Targets
|
|
.OfType<MeshFace>()
|
|
.ToArray();
|
|
|
|
FaceGroups = Faces.GroupBy( x => x.Component ).ToList();
|
|
Components = FaceGroups.Select( x => x.Key ).ToList();
|
|
|
|
Layout = Layout.Column();
|
|
|
|
var sheet = new ControlSheet();
|
|
Layout.Add( sheet );
|
|
|
|
sheet.AddObject( so );
|
|
|
|
{
|
|
var header = Layout.Add( new GroupHeader( this, "Modify Texture" ) );
|
|
|
|
var body = new Widget
|
|
{
|
|
VerticalSizeMode = SizeMode.CanGrow,
|
|
HorizontalSizeMode = SizeMode.Flexible,
|
|
};
|
|
|
|
var modifyLayout = Layout.Grid();
|
|
body.Layout = modifyLayout;
|
|
|
|
modifyLayout.Spacing = 6;
|
|
modifyLayout.Margin = new Margin( 20, 6, 20, 6 );
|
|
modifyLayout.AddCell( 0, 0, new Label.Small( "Align:" ) );
|
|
modifyLayout.AddCell( 1, 0, new TextureModifyButton( this, "Align to Grid", "hammer/texture_align_grid.png", AlignToGrid ) );
|
|
modifyLayout.AddCell( 2, 0, new TextureModifyButton( this, "Align to Face", "hammer/texture_align_face.png", AlignToFace ) );
|
|
modifyLayout.AddCell( 3, 0, new TextureModifyButton( this, "Align to View", "hammer/texture_align_view.png", AlignToView ) );
|
|
modifyLayout.SetColumnStretch( 0, 0, 0, 0, 0, 0, 1 );
|
|
|
|
modifyLayout.AddCell( 0, 1, new Label.Small( "Scale:" ) );
|
|
modifyLayout.AddCell( 1, 1, new TextureModifyButton( this, "Scale X Up", "hammer/texture_scale_up_x.png", () => DoScaleX( true ) ) );
|
|
modifyLayout.AddCell( 2, 1, new TextureModifyButton( this, "Scale X Down", "hammer/texture_scale_dn_x.png", () => DoScaleX( false ) ) );
|
|
modifyLayout.AddCell( 3, 1, new TextureModifyButton( this, "Scale Y Up", "hammer/texture_scale_up_y.png", () => DoScaleY( true ) ) );
|
|
modifyLayout.AddCell( 4, 1, new TextureModifyButton( this, "Scale Y Down", "hammer/texture_scale_dn_y.png", () => DoScaleY( false ) ) );
|
|
|
|
modifyLayout.AddCell( 0, 2, new Label.Small( "Shift:" ) );
|
|
modifyLayout.AddCell( 1, 2, new TextureModifyButton( this, "Shift Left", "hammer/texture_shift_left.png", () => DoShiftX( true ) ) );
|
|
modifyLayout.AddCell( 2, 2, new TextureModifyButton( this, "Shift Right", "hammer/texture_shift_right.png", () => DoShiftX( false ) ) );
|
|
modifyLayout.AddCell( 3, 2, new TextureModifyButton( this, "Shift Up", "hammer/texture_shift_up.png", () => DoShiftY( true ) ) );
|
|
modifyLayout.AddCell( 4, 2, new TextureModifyButton( this, "Shift Down", "hammer/texture_shift_down.png", () => DoShiftY( false ) ) );
|
|
|
|
modifyLayout.AddCell( 0, 3, new Label.Small( "Rotate:" ) );
|
|
modifyLayout.AddCell( 1, 3, new TextureModifyButton( this, "Rotate CW", "hammer/texture_rotate_cw.png", () => DoRotate( true ) ) );
|
|
modifyLayout.AddCell( 2, 3, new TextureModifyButton( this, "Rotate CCW", "hammer/texture_rotate_ccw.png", () => DoRotate( false ) ) );
|
|
|
|
modifyLayout.AddCell( 0, 4, new Label.Small( "Fit:" ) );
|
|
modifyLayout.AddCell( 1, 4, new TextureModifyButton( this, "Fit Both", "hammer/texture_fit_both.png", () => DoFit( FitX, FitY ) ) );
|
|
modifyLayout.AddCell( 2, 4, new TextureModifyButton( this, "Fit X", "hammer/texture_fit_x.png", () => DoFit( FitX, -1 ) ) );
|
|
modifyLayout.AddCell( 3, 4, new TextureModifyButton( this, "Fit Y", "hammer/texture_fit_y.png", () => DoFit( -1, FitY ) ) );
|
|
|
|
{
|
|
var cs = new ControlSheet();
|
|
cs.AddProperty( this, x => x.FitX );
|
|
cs.AddProperty( this, x => x.FitY );
|
|
modifyLayout.AddCell( 4, 4, cs, 3, 1 );
|
|
}
|
|
|
|
modifyLayout.AddCell( 0, 5, new Label.Small( "Justify:" ) );
|
|
modifyLayout.AddCell( 1, 5, new TextureModifyButton( this, "Justify Left", "hammer/texture_justify_l.png", () => DoJustify( PolygonMesh.TextureJustification.Left ) ) );
|
|
modifyLayout.AddCell( 2, 5, new TextureModifyButton( this, "Justify Right", "hammer/texture_justify_r.png", () => DoJustify( PolygonMesh.TextureJustification.Right ) ) );
|
|
modifyLayout.AddCell( 3, 5, new TextureModifyButton( this, "Justify Top", "hammer/texture_justify_t.png", () => DoJustify( PolygonMesh.TextureJustification.Top ) ) );
|
|
modifyLayout.AddCell( 4, 5, new TextureModifyButton( this, "Justify Bottom", "hammer/texture_justify_b.png", () => DoJustify( PolygonMesh.TextureJustification.Bottom ) ) );
|
|
modifyLayout.AddCell( 5, 5, new TextureModifyButton( this, "Justify Center", "hammer/texture_justify_c.png", () => DoJustify( PolygonMesh.TextureJustification.Center ) ) );
|
|
|
|
|
|
{
|
|
var cs = new ControlSheet();
|
|
cs.AddProperty( this, x => x.TreatAsOne );
|
|
modifyLayout.AddCell( 0, 6, cs, 6, 1, TextFlag.Center );
|
|
}
|
|
|
|
if ( !header.State )
|
|
{
|
|
body.Hide();
|
|
}
|
|
Layout.Add( body );
|
|
|
|
header.OnToggled += ( s ) =>
|
|
{
|
|
using var x = SuspendUpdates.For( Parent );
|
|
|
|
if ( !s )
|
|
{
|
|
body.Hide();
|
|
}
|
|
else
|
|
{
|
|
body.Show();
|
|
}
|
|
|
|
UpdateGeometry();
|
|
Parent?.UpdateGeometry();
|
|
};
|
|
}
|
|
|
|
{
|
|
var header = Layout.Add( new GroupHeader( this, "Operations" ) );
|
|
|
|
OperationsBody = new Widget
|
|
{
|
|
VerticalSizeMode = SizeMode.CanGrow,
|
|
HorizontalSizeMode = SizeMode.Flexible,
|
|
Layout = Layout.Column()
|
|
};
|
|
|
|
OperationsBody.Layout.Spacing = 6;
|
|
OperationsBody.Layout.Margin = 6;
|
|
|
|
CreateButton( "Extract Faces", "mesh.extract-faces", ExtractFaces );
|
|
CreateButton( "Detach Faces", "mesh.detach-faces", DetachFaces );
|
|
CreateButton( "Combine Faces", "mesh.combine-faces", CombineFaces );
|
|
CreateButton( "Collapse Faces", "mesh.collapse", Collapse );
|
|
CreateButton( "Remove Bad Faces", "mesh.remove-bad-faces", RemoveBadFaces );
|
|
CreateButton( "Flip All Faces", "mesh.flip-all-faces", FlipAllFaces );
|
|
|
|
OperationsBody.Layout.AddSeparator();
|
|
OperationsBody.Layout.Add( ControlWidget.Create( this.GetSerialized().GetProperty( nameof( NumCuts ) ) ) );
|
|
|
|
CreateButton( "Slice", "mesh.quad-slice", QuadSlice, Faces.Length > 0 );
|
|
|
|
if ( !header.State )
|
|
{
|
|
OperationsBody.Hide();
|
|
}
|
|
|
|
Layout.Add( OperationsBody );
|
|
|
|
header.OnToggled += ( s ) =>
|
|
{
|
|
using var x = SuspendUpdates.For( Parent );
|
|
|
|
if ( !s )
|
|
{
|
|
OperationsBody.Hide();
|
|
}
|
|
else
|
|
{
|
|
OperationsBody.Show();
|
|
}
|
|
|
|
UpdateGeometry();
|
|
Parent?.UpdateGeometry();
|
|
};
|
|
}
|
|
|
|
Layout.AddStretchCell();
|
|
}
|
|
|
|
void CreateButton( string text, string keybind, Action clicked, bool enabled = true )
|
|
{
|
|
var btn = OperationsBody.Layout.Add( new Button.Primary( text, this )
|
|
{
|
|
Enabled = enabled,
|
|
Clicked = clicked,
|
|
ToolTip = text
|
|
} );
|
|
|
|
|
|
if ( !string.IsNullOrEmpty( keybind ) )
|
|
{
|
|
btn.ToolTip = text + " [" + EditorShortcuts.GetKeys( keybind ) + "]";
|
|
}
|
|
}
|
|
|
|
[Shortcut( "mesh.collapse", "SHIFT+O", typeof( SceneViewportWidget ) )]
|
|
private void Collapse()
|
|
{
|
|
using var scope = SceneEditorSession.Scope();
|
|
|
|
using ( SceneEditorSession.Active.UndoScope( "Collapse Faces" )
|
|
.WithComponentChanges( Components )
|
|
.Push() )
|
|
{
|
|
var selection = SceneEditorSession.Active.Selection;
|
|
selection.Clear();
|
|
|
|
foreach ( var hFace in Faces )
|
|
{
|
|
if ( !hFace.IsValid )
|
|
continue;
|
|
|
|
hFace.Component.Mesh.CollapseFace( hFace.Handle, out _ );
|
|
}
|
|
}
|
|
}
|
|
|
|
[Shortcut( "mesh.remove-bad-faces", "", typeof( SceneViewportWidget ) )]
|
|
private void RemoveBadFaces()
|
|
{
|
|
using var scope = SceneEditorSession.Scope();
|
|
|
|
using ( SceneEditorSession.Active.UndoScope( "Remove Bad Faces" )
|
|
.WithComponentChanges( Components )
|
|
.Push() )
|
|
{
|
|
foreach ( var component in Components )
|
|
{
|
|
component.Mesh.RemoveBadFaces();
|
|
}
|
|
}
|
|
}
|
|
|
|
[Shortcut( "editor.delete", "DEL", typeof( SceneViewportWidget ) )]
|
|
private void DeleteSelection()
|
|
{
|
|
var groups = Faces.GroupBy( face => face.Component );
|
|
|
|
if ( !groups.Any() )
|
|
return;
|
|
|
|
var components = groups.Select( x => x.Key ).ToArray();
|
|
|
|
using ( SceneEditorSession.Active.UndoScope( "Delete Faces" ).WithComponentChanges( components ).Push() )
|
|
{
|
|
foreach ( var group in groups )
|
|
group.Key.Mesh.RemoveFaces( group.Select( x => x.Handle ) );
|
|
}
|
|
}
|
|
|
|
private void AlignToGrid()
|
|
{
|
|
using var scope = SceneEditorSession.Scope();
|
|
|
|
using ( SceneEditorSession.Active.UndoScope( "Align to Grid" )
|
|
.WithComponentChanges( Components )
|
|
.Push() )
|
|
{
|
|
foreach ( var face in Faces )
|
|
{
|
|
face.Component.Mesh.TextureAlignToGrid( face.Transform, face.Handle );
|
|
}
|
|
}
|
|
}
|
|
|
|
private void AlignToFace()
|
|
{
|
|
using var scope = SceneEditorSession.Scope();
|
|
|
|
using ( SceneEditorSession.Active.UndoScope( "Align to Face" )
|
|
.WithComponentChanges( Components )
|
|
.Push() )
|
|
{
|
|
foreach ( var face in Faces )
|
|
{
|
|
face.Component.Mesh.TextureAlignToFace( face.Transform, face.Handle );
|
|
}
|
|
}
|
|
}
|
|
|
|
private void AlignToView()
|
|
{
|
|
var sceneView = SceneViewportWidget.LastSelected;
|
|
if ( !sceneView.IsValid() )
|
|
return;
|
|
|
|
using var scope = SceneEditorSession.Scope();
|
|
|
|
var position = sceneView.State.CameraPosition;
|
|
var rotation = sceneView.State.CameraRotation;
|
|
var uAxis = rotation.Right;
|
|
var vAxis = rotation.Up;
|
|
var offset = new Vector2( uAxis.Dot( position ), vAxis.Dot( position ) );
|
|
|
|
using ( SceneEditorSession.Active.UndoScope( "Align to View" )
|
|
.WithComponentChanges( Components )
|
|
.Push() )
|
|
{
|
|
foreach ( var face in Faces )
|
|
{
|
|
face.Component.Mesh.SetFaceTextureParameters( face.Handle, offset, uAxis, vAxis );
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DoRotate( bool clockwise )
|
|
{
|
|
using var scope = SceneEditorSession.Scope();
|
|
|
|
var amount = EditorScene.GizmoSettings.AngleSpacing * (clockwise ? 1 : -1);
|
|
|
|
using ( SceneEditorSession.Active.UndoScope( "Rotate" )
|
|
.WithComponentChanges( Components )
|
|
.Push() )
|
|
{
|
|
foreach ( var face in Faces )
|
|
{
|
|
var mesh = face.Component.Mesh;
|
|
mesh.GetFaceTextureParameters( face.Handle, out var axisU, out var axisV, out var scale );
|
|
|
|
Vector3 newAxisU = (Vector3)axisU;
|
|
Vector3 newAxisV = (Vector3)axisV;
|
|
var axis = Vector3.Cross( newAxisU, newAxisV );
|
|
axis = axis.Normal;
|
|
|
|
var rotation = Rotation.FromAxis( axis, amount );
|
|
newAxisU *= rotation;
|
|
newAxisV *= rotation;
|
|
newAxisU = newAxisU.Normal;
|
|
newAxisV = newAxisV.Normal;
|
|
|
|
mesh.SetFaceTextureParameters( face.Handle, new Vector4( newAxisU, axisU.w ), new Vector4( newAxisV, axisV.w ), scale );
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DoShiftX( bool positive )
|
|
{
|
|
using var scope = SceneEditorSession.Scope();
|
|
|
|
var gridSpacing = EditorScene.GizmoSettings.GridSpacing;
|
|
|
|
using ( SceneEditorSession.Active.UndoScope( "Shift X" )
|
|
.WithComponentChanges( Components )
|
|
.Push() )
|
|
{
|
|
foreach ( var face in Faces )
|
|
{
|
|
var mesh = face.Component.Mesh;
|
|
var scale = mesh.GetTextureScale( face.Handle ).x;
|
|
scale = scale.AlmostEqual( 0.0f ) ? 0.25f : scale;
|
|
var amount = gridSpacing / scale;
|
|
var offset = mesh.GetTextureOffset( face.Handle );
|
|
offset = offset.WithX( offset.x + amount * (positive ? 1.0f : -1.0f) );
|
|
mesh.SetTextureOffset( face.Handle, offset );
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DoShiftY( bool positive )
|
|
{
|
|
using var scope = SceneEditorSession.Scope();
|
|
|
|
var gridSpacing = EditorScene.GizmoSettings.GridSpacing;
|
|
|
|
using ( SceneEditorSession.Active.UndoScope( "Shift Y" )
|
|
.WithComponentChanges( Components )
|
|
.Push() )
|
|
{
|
|
foreach ( var face in Faces )
|
|
{
|
|
var mesh = face.Component.Mesh;
|
|
var scale = mesh.GetTextureScale( face.Handle ).y;
|
|
scale = scale.AlmostEqual( 0.0f ) ? 0.25f : scale;
|
|
var amount = gridSpacing / scale;
|
|
var offset = mesh.GetTextureOffset( face.Handle );
|
|
offset = offset.WithY( offset.y + amount * (positive ? 1.0f : -1.0f) );
|
|
mesh.SetTextureOffset( face.Handle, offset );
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DoScaleX( bool positive )
|
|
{
|
|
using var scope = SceneEditorSession.Scope();
|
|
|
|
using ( SceneEditorSession.Active.UndoScope( "Scale X" )
|
|
.WithComponentChanges( Components )
|
|
.Push() )
|
|
{
|
|
foreach ( var face in Faces )
|
|
{
|
|
var mesh = face.Component.Mesh;
|
|
var scale = mesh.GetTextureScale( face.Handle );
|
|
scale = scale.WithX( scale.x * (positive ? 2.0f : 0.5f) );
|
|
mesh.SetTextureScale( face.Handle, scale );
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DoScaleY( bool positive )
|
|
{
|
|
using var scope = SceneEditorSession.Scope();
|
|
|
|
using ( SceneEditorSession.Active.UndoScope( "Scale Y" )
|
|
.WithComponentChanges( Components )
|
|
.Push() )
|
|
{
|
|
foreach ( var face in Faces )
|
|
{
|
|
var mesh = face.Component.Mesh;
|
|
var scale = mesh.GetTextureScale( face.Handle );
|
|
scale = scale.WithY( scale.y * (positive ? 2.0f : 0.5f) );
|
|
mesh.SetTextureScale( face.Handle, scale );
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DoJustify( PolygonMesh.TextureJustification justification )
|
|
{
|
|
using var scope = SceneEditorSession.Scope();
|
|
|
|
using ( SceneEditorSession.Active.UndoScope( "Justify" )
|
|
.WithComponentChanges( Components )
|
|
.Push() )
|
|
{
|
|
JustifyTexturesForFaceSelection( justification );
|
|
|
|
foreach ( var group in FaceGroups )
|
|
{
|
|
var mesh = group.Key.Mesh;
|
|
mesh.ComputeFaceTextureCoordinatesFromParameters( group.Select( x => x.Handle ) );
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DoFit( int repeatX, int repeatY )
|
|
{
|
|
using var scope = SceneEditorSession.Scope();
|
|
|
|
var justification = PolygonMesh.TextureJustification.Fit;
|
|
if ( repeatX == -1 ) justification = PolygonMesh.TextureJustification.FitY;
|
|
else if ( repeatY == -1 ) justification = PolygonMesh.TextureJustification.FitX;
|
|
|
|
using ( SceneEditorSession.Active.UndoScope( "Fit" )
|
|
.WithComponentChanges( Components )
|
|
.Push() )
|
|
{
|
|
JustifyTexturesForFaceSelection( justification );
|
|
|
|
if ( repeatX > 0 || repeatY > 0 )
|
|
{
|
|
foreach ( var face in Faces )
|
|
{
|
|
var mesh = face.Component.Mesh;
|
|
var scale = mesh.GetTextureScale( face.Handle );
|
|
|
|
if ( repeatX > 0 )
|
|
scale.x /= repeatX;
|
|
|
|
if ( repeatY > 0 )
|
|
scale.y /= repeatY;
|
|
|
|
mesh.SetTextureScale( face.Handle, scale );
|
|
}
|
|
}
|
|
|
|
if ( repeatX != -1 )
|
|
JustifyTexturesForFaceSelection( PolygonMesh.TextureJustification.Left );
|
|
|
|
if ( repeatY != -1 )
|
|
JustifyTexturesForFaceSelection( PolygonMesh.TextureJustification.Top );
|
|
|
|
foreach ( var group in FaceGroups )
|
|
{
|
|
var mesh = group.Key.Mesh;
|
|
mesh.ComputeFaceTextureCoordinatesFromParameters( group.Select( x => x.Handle ) );
|
|
}
|
|
}
|
|
}
|
|
|
|
private void JustifyTexturesForFaceSelection( PolygonMesh.TextureJustification justification )
|
|
{
|
|
PolygonMesh.FaceExtents extents = null;
|
|
|
|
if ( TreatAsOne )
|
|
{
|
|
extents = new PolygonMesh.FaceExtents();
|
|
|
|
foreach ( var group in FaceGroups )
|
|
{
|
|
var mesh = group.Key.Mesh;
|
|
mesh.UnionExtentsForFaces( group.Select( x => x.Handle ), mesh.Transform, extents );
|
|
}
|
|
}
|
|
|
|
foreach ( var group in FaceGroups )
|
|
{
|
|
var mesh = group.Key.Mesh;
|
|
mesh.JustifyFaceTextureParameters( group.Select( x => x.Handle ), justification, extents );
|
|
}
|
|
}
|
|
|
|
[Shortcut( "mesh.extract-faces", "ALT+N", typeof( SceneViewportWidget ) )]
|
|
private void ExtractFaces()
|
|
{
|
|
using var scope = SceneEditorSession.Scope();
|
|
|
|
var options = new GameObject.SerializeOptions();
|
|
var gameObjects = Components.Select( x => x.GameObject );
|
|
|
|
using ( SceneEditorSession.Active.UndoScope( "Extract Faces" )
|
|
.WithComponentChanges( Components )
|
|
.WithGameObjectDestructions( gameObjects )
|
|
.WithGameObjectCreations()
|
|
.Push() )
|
|
{
|
|
var selection = SceneEditorSession.Active.Selection;
|
|
selection.Clear();
|
|
|
|
foreach ( var group in FaceGroups )
|
|
{
|
|
var entry = group.Key.GameObject;
|
|
var json = group.Key.Serialize( options );
|
|
SceneUtility.MakeIdGuidsUnique( json as JsonObject );
|
|
|
|
var go = new GameObject( entry.Name );
|
|
go.WorldTransform = entry.WorldTransform;
|
|
go.MakeNameUnique();
|
|
|
|
entry.AddSibling( go, false );
|
|
|
|
var newMeshComponent = go.Components.Create<MeshComponent>( true );
|
|
newMeshComponent.DeserializeImmediately( json as JsonObject );
|
|
var newMesh = newMeshComponent.Mesh;
|
|
|
|
var faceIndices = group.Select( x => x.Handle.Index ).ToArray();
|
|
var facesToRemove = newMesh.FaceHandles
|
|
.Where( f => !faceIndices.Contains( f.Index ) )
|
|
.ToArray();
|
|
|
|
newMesh.RemoveFaces( facesToRemove );
|
|
|
|
var transform = go.WorldTransform;
|
|
var newBounds = newMesh.CalculateBounds( transform );
|
|
var newTransfrom = transform.WithPosition( newBounds.Center );
|
|
newMesh.ApplyTransform( new Transform( transform.Rotation.Inverse * (transform.Position - newTransfrom.Position) ) );
|
|
go.WorldTransform = newTransfrom;
|
|
newMeshComponent.RebuildMesh();
|
|
|
|
foreach ( var hFace in newMesh.FaceHandles )
|
|
selection.Add( new MeshFace( newMeshComponent, hFace ) );
|
|
|
|
var mesh = group.Key.Mesh;
|
|
var faces = group.Select( x => x.Handle );
|
|
|
|
if ( faces.Count() == mesh.FaceHandles.Count() )
|
|
{
|
|
entry.Destroy();
|
|
}
|
|
else
|
|
{
|
|
mesh.RemoveFaces( faces );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
[Shortcut( "mesh.detach-faces", "N", typeof( SceneViewportWidget ) )]
|
|
private void DetachFaces()
|
|
{
|
|
using var scope = SceneEditorSession.Scope();
|
|
|
|
using ( SceneEditorSession.Active.UndoScope( "Detach Faces" )
|
|
.WithComponentChanges( Components )
|
|
.Push() )
|
|
{
|
|
var selection = SceneEditorSession.Active.Selection;
|
|
selection.Clear();
|
|
|
|
foreach ( var group in FaceGroups )
|
|
{
|
|
group.Key.Mesh.DetachFaces( group.Select( x => x.Handle ).ToArray(), out var newFaces );
|
|
foreach ( var hFace in newFaces )
|
|
selection.Add( new MeshFace( group.Key, hFace ) );
|
|
}
|
|
}
|
|
}
|
|
|
|
[Shortcut( "mesh.combine-faces", "Backspace", typeof( SceneViewportWidget ) )]
|
|
private void CombineFaces()
|
|
{
|
|
using var scope = SceneEditorSession.Scope();
|
|
|
|
using ( SceneEditorSession.Active.UndoScope( "Combine Faces" )
|
|
.WithComponentChanges( Components )
|
|
.Push() )
|
|
{
|
|
var selection = SceneEditorSession.Active.Selection;
|
|
selection.Clear();
|
|
|
|
foreach ( var group in FaceGroups )
|
|
{
|
|
var mesh = group.Key.Mesh;
|
|
mesh.CombineFaces( group.Select( x => x.Handle ).ToArray() );
|
|
mesh.ComputeFaceTextureCoordinatesFromParameters();
|
|
}
|
|
}
|
|
}
|
|
|
|
[Shortcut( "mesh.flip-all-faces", "F", typeof( SceneViewportWidget ) )]
|
|
private void FlipAllFaces()
|
|
{
|
|
using var scope = SceneEditorSession.Scope();
|
|
|
|
using ( SceneEditorSession.Active.UndoScope( "Flip All Faces" )
|
|
.WithComponentChanges( Components )
|
|
.Push() )
|
|
{
|
|
foreach ( var component in Components )
|
|
{
|
|
component.Mesh.FlipAllFaces();
|
|
}
|
|
}
|
|
}
|
|
|
|
[Shortcut( "mesh.quad-slice", "CTRL+D", typeof( SceneViewportWidget ) )]
|
|
private void QuadSlice()
|
|
{
|
|
using var scope = SceneEditorSession.Scope();
|
|
|
|
using ( SceneEditorSession.Active.UndoScope( "Quad Slice" )
|
|
.WithComponentChanges( Components )
|
|
.Push() )
|
|
{
|
|
var selection = SceneEditorSession.Active.Selection;
|
|
selection.Clear();
|
|
|
|
foreach ( var group in FaceGroups )
|
|
{
|
|
var mesh = group.Key.Mesh;
|
|
var newFaces = new List<FaceHandle>();
|
|
mesh.QuadSliceFaces( group.Select( x => x.Handle ).ToArray(), NumCuts.x, NumCuts.y, 60.0f, newFaces );
|
|
mesh.ComputeFaceTextureCoordinatesFromParameters(); // TODO: Shouldn't be needed, something in quad slice isn't computing these
|
|
|
|
foreach ( var hFace in newFaces )
|
|
{
|
|
selection.Add( new MeshFace( group.Key, hFace ) );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|