diff --git a/game/addons/tools/Code/Editor/RectEditor/EdgeAwareFaceUnwrapper.cs b/game/addons/tools/Code/Editor/RectEditor/EdgeAwareFaceUnwrapper.cs index 8f72396c..0e48f822 100644 --- a/game/addons/tools/Code/Editor/RectEditor/EdgeAwareFaceUnwrapper.cs +++ b/game/addons/tools/Code/Editor/RectEditor/EdgeAwareFaceUnwrapper.cs @@ -14,11 +14,59 @@ internal class EdgeAwareFaceUnwrapper faces = meshFaces; } - public UnwrapResult UnwrapToSquare() + public UnwrapResult Unwrap( MappingMode mode ) { if ( faces.Length == 0 ) return new UnwrapResult(); + bool straighten = mode == MappingMode.UnwrapSquare; + + InitializeVertexMap(); + + var unwrappedUVs = new List( new Vector2[vertexPositions.Count] ); + var processedFaces = new HashSet(); + var faceQueue = new Queue(); + + if ( faces.Length > 0 && faces[0].IsValid ) + { + // Seed the unwrap with the first face + UnwrapFirstFace( faces[0], unwrappedUVs, straighten ); + processedFaces.Add( faces[0] ); + + for ( int i = 1; i < faces.Length; i++ ) + { + if ( faces[i].IsValid ) + faceQueue.Enqueue( faces[i] ); + } + } + + int maxAttempts = faces.Length * 3; + int attempts = 0; + + while ( faceQueue.Count > 0 && attempts < maxAttempts ) + { + var currentFace = faceQueue.Dequeue(); + attempts++; + + if ( processedFaces.Contains( currentFace ) ) + continue; + + if ( TryUnfoldFace( currentFace, processedFaces, unwrappedUVs, straighten ) ) + { + processedFaces.Add( currentFace ); + attempts = 0; + } + else if ( attempts < maxAttempts ) + { + faceQueue.Enqueue( currentFace ); + } + } + + return BuildResult( unwrappedUVs ); + } + + private void InitializeVertexMap() + { foreach ( var face in faces ) { if ( !face.IsValid ) @@ -41,45 +89,200 @@ internal class EdgeAwareFaceUnwrapper faceToVertexIndices[face] = indices; } + } - var unwrappedUVs = new List( new Vector2[vertexPositions.Count] ); - var processedFaces = new HashSet(); - var faceQueue = new Queue(); + private void UnwrapFirstFace( MeshFace face, List unwrappedUVs, bool straighten ) + { + if ( !faceToVertexIndices.TryGetValue( face, out var indices ) || indices.Count < 3 ) + return; - if ( faces.Length > 0 && faces[0].IsValid ) + if ( straighten && indices.Count == 4 ) { - UnwrapFirstFace( faces[0], unwrappedUVs ); - processedFaces.Add( faces[0] ); + var p0 = vertexPositions[indices[0]]; + var p1 = vertexPositions[indices[1]]; + var p3 = vertexPositions[indices[3]]; - for ( int i = 1; i < faces.Length; i++ ) + float width = p0.Distance( p1 ); + float height = p0.Distance( p3 ); + + unwrappedUVs[indices[0]] = new Vector2( 0, 0 ); + unwrappedUVs[indices[1]] = new Vector2( width, 0 ); + unwrappedUVs[indices[2]] = new Vector2( width, height ); + unwrappedUVs[indices[3]] = new Vector2( 0, height ); + } + else + { + var p0 = vertexPositions[indices[0]]; + var p1 = vertexPositions[indices[1]]; + var p2 = vertexPositions[indices[2]]; + + var uDir = (p1 - p0).Normal; + var normal = uDir.Cross( (p2 - p0).Normal ).Normal; + var vDir = normal.Cross( uDir ); + + foreach ( var idx in indices ) { - if ( faces[i].IsValid ) - faceQueue.Enqueue( faces[i] ); + var relative = vertexPositions[idx] - p0; + unwrappedUVs[idx] = new Vector2( relative.Dot( uDir ), relative.Dot( vDir ) ); + } + } + } + + private bool TryUnfoldFace( MeshFace currentFace, HashSet processedFaces, List unwrappedUVs, bool straighten ) + { + if ( !faceToVertexIndices.TryGetValue( currentFace, out var currentIndices ) ) + return false; + + foreach ( var processedFace in processedFaces ) + { + var sharedEdge = FindSharedVertices( currentFace, processedFace, unwrappedUVs ); + if ( sharedEdge.HasValue ) + { + UnfoldFaceAlongEdge( currentFace, currentIndices, sharedEdge.Value, unwrappedUVs, straighten ); + return true; } } - int maxAttempts = faces.Length * 3; - int attempts = 0; + return false; + } - while ( faceQueue.Count > 0 && attempts < maxAttempts ) + private void UnfoldFaceAlongEdge( MeshFace face, List faceIndices, (int idx1, int idx2, Vector2 uv1, Vector2 uv2) edge, List unwrappedUVs, bool straighten ) + { + unwrappedUVs[edge.idx1] = edge.uv1; + unwrappedUVs[edge.idx2] = edge.uv2; + + // SQUARE MODE. + if ( straighten && faceIndices.Count == 4 ) { - var currentFace = faceQueue.Dequeue(); - attempts++; + UnfoldQuadStraight( faceIndices, edge, unwrappedUVs ); + } + // CONFORMING MODE. + else + { + UnfoldGeometric( faceIndices, edge, unwrappedUVs ); + } + } - if ( processedFaces.Contains( currentFace ) ) + /// + /// Unfolds a quad by extruding the shared edge perpendicularly by the average length of the connecting sides. + /// This forces the UV strip to remain straight (grid-like) even if the 3D mesh curves. + /// + private void UnfoldQuadStraight( List faceIndices, (int idx1, int idx2, Vector2 uv1, Vector2 uv2) edge, List unwrappedUVs ) + { + var uvVec = edge.uv2 - edge.uv1; + var uvLen = uvVec.Length; + var uvNormal = uvLen > 0.000001f ? uvVec / uvLen : new Vector2( 1, 0 ); + var uvPerp = new Vector2( -uvNormal.y, uvNormal.x ); // 90 degrees Left + + int ptr1 = faceIndices.IndexOf( edge.idx1 ); + int ptr2 = faceIndices.IndexOf( edge.idx2 ); + + int connectedTo1; + int connectedTo2; + + if ( (ptr1 + 1) % 4 == ptr2 ) + { + connectedTo2 = faceIndices[(ptr2 + 1) % 4]; + connectedTo1 = faceIndices[(ptr1 + 3) % 4]; + } + else + { + connectedTo1 = faceIndices[(ptr1 + 1) % 4]; + connectedTo2 = faceIndices[(ptr2 + 3) % 4]; + } + + float len1 = vertexPositions[edge.idx1].Distance( vertexPositions[connectedTo1] ); + float len2 = vertexPositions[edge.idx2].Distance( vertexPositions[connectedTo2] ); + float avgLen = (len1 + len2) * 0.5f; + + unwrappedUVs[connectedTo1] = edge.uv1 + uvPerp * avgLen; + unwrappedUVs[connectedTo2] = edge.uv2 + uvPerp * avgLen; + } + + /// + /// Unfolds a face by projecting its vertices onto a 2D plane defined by the shared edge. + /// This preserves the original geometric angles and shapes. + /// + private void UnfoldGeometric( List faceIndices, (int idx1, int idx2, Vector2 uv1, Vector2 uv2) edge, List unwrappedUVs ) + { + var pA = vertexPositions[edge.idx1]; + var pB = vertexPositions[edge.idx2]; + var edge3D = pB - pA; + var edge2D = edge.uv2 - edge.uv1; + + Vector3 pThird = Vector3.Zero; + foreach ( var idx in faceIndices ) + { + if ( idx != edge.idx1 && idx != edge.idx2 ) + { + pThird = vertexPositions[idx]; + break; + } + } + + var faceNormal = edge3D.Cross( pThird - pA ).Normal; + var localU = edge3D.Normal; + var localV = faceNormal.Cross( localU ); + + var edge2DDir = edge2D.Normal; + var edge2DPerp = new Vector2( -edge2DDir.y, edge2DDir.x ); + var scale = edge3D.Length > 0 ? edge2D.Length / edge3D.Length : 1.0f; + + foreach ( var idx in faceIndices ) + { + if ( idx == edge.idx1 || idx == edge.idx2 ) continue; - if ( TryUnfoldFace( currentFace, processedFaces, unwrappedUVs ) ) + var relative3D = vertexPositions[idx] - pA; + float u = relative3D.Dot( localU ); + float v = relative3D.Dot( localV ); + + unwrappedUVs[idx] = edge.uv1 + edge2DDir * u * scale + edge2DPerp * v * scale; + } + } + + private (int idx1, int idx2, Vector2 uv1, Vector2 uv2)? FindSharedVertices( MeshFace face1, MeshFace face2, List unwrappedUVs ) + { + if ( !faceToVertexIndices.TryGetValue( face1, out var indices1 ) || + !faceToVertexIndices.TryGetValue( face2, out var indices2 ) ) + return null; + + for ( int i = 0; i < indices1.Count; i++ ) + { + var idx1a = indices1[i]; + var idx1b = indices1[(i + 1) % indices1.Count]; + + var pos1a = vertexPositions[idx1a]; + var pos1b = vertexPositions[idx1b]; + + for ( int j = 0; j < indices2.Count; j++ ) { - processedFaces.Add( currentFace ); - attempts = 0; - } - else if ( attempts < maxAttempts ) - { - faceQueue.Enqueue( currentFace ); + var idx2a = indices2[j]; + var idx2b = indices2[(j + 1) % indices2.Count]; + + var pos2a = vertexPositions[idx2a]; + var pos2b = vertexPositions[idx2b]; + + const float tolerance = 0.001f; + bool matchForward = pos1a.Distance( pos2a ) < tolerance && pos1b.Distance( pos2b ) < tolerance; + bool matchReverse = pos1a.Distance( pos2b ) < tolerance && pos1b.Distance( pos2a ) < tolerance; + + if ( matchForward ) + { + return (idx1a, idx1b, unwrappedUVs[idx2a], unwrappedUVs[idx2b]); + } + if ( matchReverse ) + { + return (idx1a, idx1b, unwrappedUVs[idx2b], unwrappedUVs[idx2a]); + } } } + return null; + } + + private UnwrapResult BuildResult( List unwrappedUVs ) + { var finalFaceIndices = new List>(); foreach ( var face in faces ) { @@ -97,126 +300,6 @@ internal class EdgeAwareFaceUnwrapper }; } - private void UnwrapFirstFace( MeshFace face, List unwrappedUVs ) - { - if ( !faceToVertexIndices.TryGetValue( face, out var indices ) || indices.Count < 3 ) - return; - - var pos0 = vertexPositions[indices[0]]; - var pos1 = vertexPositions[indices[1]]; - var pos2 = vertexPositions[indices[2]]; - - var u = (pos1 - pos0).Normal; - var normal = u.Cross( (pos2 - pos0).Normal ).Normal; - var v = normal.Cross( u ); - - foreach ( var vertexIndex in indices ) - { - var pos = vertexPositions[vertexIndex]; - var relative = pos - pos0; - unwrappedUVs[vertexIndex] = new Vector2( relative.Dot( u ), relative.Dot( v ) ); - } - } - - private bool TryUnfoldFace( MeshFace currentFace, HashSet processedFaces, List unwrappedUVs ) - { - if ( !faceToVertexIndices.TryGetValue( currentFace, out var currentIndices ) ) - return false; - - foreach ( var processedFace in processedFaces ) - { - var sharedVertices = FindSharedVertices( currentFace, processedFace, unwrappedUVs ); - if ( sharedVertices.HasValue ) - { - UnfoldFaceAlongEdge( currentFace, currentIndices, sharedVertices.Value, unwrappedUVs ); - return true; - } - } - - return false; - } - - private (int idx1, int idx2, Vector2 uv1, Vector2 uv2)? FindSharedVertices( MeshFace face1, MeshFace face2, List unwrappedUVs ) - { - if ( !faceToVertexIndices.TryGetValue( face1, out var indices1 ) || - !faceToVertexIndices.TryGetValue( face2, out var indices2 ) ) - return null; - - for ( int i = 0; i < indices1.Count; i++ ) - { - var idx1a = indices1[i]; - var idx1b = indices1[(i + 1) % indices1.Count]; - - for ( int j = 0; j < indices2.Count; j++ ) - { - var idx2a = indices2[j]; - var idx2b = indices2[(j + 1) % indices2.Count]; - - var pos1a = vertexPositions[idx1a]; - var pos1b = vertexPositions[idx1b]; - var pos2a = vertexPositions[idx2a]; - var pos2b = vertexPositions[idx2b]; - - const float tolerance = 0.001f; - bool matchForward = pos1a.Distance( pos2a ) < tolerance && pos1b.Distance( pos2b ) < tolerance; - bool matchReverse = pos1a.Distance( pos2b ) < tolerance && pos1b.Distance( pos2a ) < tolerance; - - if ( matchForward || matchReverse ) - { - return matchForward - ? (idx1a, idx1b, unwrappedUVs[idx2a], unwrappedUVs[idx2b]) - : (idx1a, idx1b, unwrappedUVs[idx2b], unwrappedUVs[idx2a]); - } - } - } - - return null; - } - - private void UnfoldFaceAlongEdge( MeshFace face, List faceIndices, (int idx1, int idx2, Vector2 uv1, Vector2 uv2) sharedEdge, List unwrappedUVs ) - { - unwrappedUVs[sharedEdge.idx1] = sharedEdge.uv1; - unwrappedUVs[sharedEdge.idx2] = sharedEdge.uv2; - - var edge3DA = vertexPositions[sharedEdge.idx1]; - var edge3DB = vertexPositions[sharedEdge.idx2]; - var edge3D = edge3DB - edge3DA; - var edge2D = sharedEdge.uv2 - sharedEdge.uv1; - - Vector3 thirdVertex = Vector3.Zero; - foreach ( var idx in faceIndices ) - { - if ( idx != sharedEdge.idx1 && idx != sharedEdge.idx2 ) - { - thirdVertex = vertexPositions[idx]; - break; - } - } - - var faceNormal = edge3D.Cross( thirdVertex - edge3DA ).Normal; - var localU = edge3D.Normal; - var localV = faceNormal.Cross( localU ); - - foreach ( var idx in faceIndices ) - { - if ( idx == sharedEdge.idx1 || idx == sharedEdge.idx2 ) - continue; - - var pos3D = vertexPositions[idx]; - var relative3D = pos3D - edge3DA; - var localPos = new Vector2( relative3D.Dot( localU ), relative3D.Dot( localV ) ); - - var edgeLength2D = edge2D.Length; - var edgeLength3D = edge3D.Length; - var scale = edgeLength3D > 0 ? edgeLength2D / edgeLength3D : 1.0f; - - var edge2DDir = edge2D.Normal; - var edge2DPerp = new Vector2( -edge2DDir.y, edge2DDir.x ); - - unwrappedUVs[idx] = sharedEdge.uv1 + edge2DDir * localPos.x * scale + edge2DPerp * localPos.y * scale; - } - } - public class UnwrapResult { public List VertexPositions { get; set; } = new(); diff --git a/game/addons/tools/Code/Editor/RectEditor/FastTextureSettings.cs b/game/addons/tools/Code/Editor/RectEditor/FastTextureSettings.cs index 9841721e..c165b3d6 100644 --- a/game/addons/tools/Code/Editor/RectEditor/FastTextureSettings.cs +++ b/game/addons/tools/Code/Editor/RectEditor/FastTextureSettings.cs @@ -3,11 +3,19 @@ public enum MappingMode { /// - /// Unwrap the selected faces and attempt to fit them into the current rectangle + /// Unwrap the selected faces and attempt to fit them into the current rectangle. + /// Forces the faces into a grid-like alignment (straightens curves). /// [Icon( "auto_awesome_mosaic" )] UnwrapSquare, + /// + /// Unwrap the selected faces while maintaining their shape and angles. + /// Best for organic shapes or when distortion should be minimized. + /// + [Icon( "texture" )] + UnwrapConforming, + /// /// Planar Project the selected faces based on the current view. Can be selected again to update from a new view. /// @@ -30,10 +38,39 @@ public enum AlignmentMode VAxis } +public enum ScaleMode +{ + [Title( "Fit To Rectangle" ), Icon( "aspect_ratio" )] + Fit, + + [Title( "World Scale" ), Icon( "public" )] + WorldScale, + + [Title( "Tile U" ), Icon( "view_column" )] + TileU, + + [Title( "Tile V" ), Icon( "view_stream" )] + TileV +} + +public enum TileMode +{ + [Title( "Maintain Aspect" ), Icon( "crop_free" )] + MaintainAspect, + + [Title( "Repeat" ), Icon( "repeat" )] + Repeat +} + public class FastTextureSettings { [Hide] private MappingMode _mapping = MappingMode.UnwrapSquare; [Hide] private AlignmentMode _alignment = AlignmentMode.UAxis; + + [Hide] private ScaleMode _scaleMode = ScaleMode.Fit; + [Hide] private TileMode _tileMode = TileMode.MaintainAspect; + [Hide] private float _repeat = 1.0f; + [Hide] private bool _isTileView; [Hide] private bool _showRects; [Hide] private bool _isFlippedHorizontal; @@ -41,6 +78,14 @@ public class FastTextureSettings [Hide] private float _insetX; [Hide] private float _insetY; + [Hide] private Vector2 _savedRectMin = Vector2.Zero; + [Hide] private Vector2 _savedRectMax = Vector2.One; + + public FastTextureSettings() + { + Load(); + } + [Category( "UV Mapping" ), WideMode( HasLabel = false )] public MappingMode Mapping { @@ -125,7 +170,50 @@ public class FastTextureSettings } } + [Category( "Tiling" ), WideMode( HasLabel = false )] + public ScaleMode ScaleMode + { + get => _scaleMode; + set + { + if ( _scaleMode == value ) return; + _scaleMode = value; + OnSettingsChanged?.Invoke(); + } + } + + [Category( "Tiling" )] + [HideIf( nameof( ScaleMode ), ScaleMode.Fit )] + [WideMode( HasLabel = false )] + public TileMode TileMode + { + get => _tileMode; + set + { + if ( _tileMode == value ) return; + _tileMode = value; + OnSettingsChanged?.Invoke(); + } + } + + [Category( "Tiling" )] + [HideIf( nameof( ScaleMode ), ScaleMode.Fit )] + [WideMode] + [Range( 0.125f, 8.0f ), Step( 0.125f )] + public float Repeat + { + get => _repeat; + set + { + if ( _repeat == value ) return; + _repeat = value; + OnSettingsChanged?.Invoke(); + } + } + [Category( "Inset" )] + [WideMode( HasLabel = false )] + [Range( 0.0f, 32.0f ), Step( 1.0f )] public float InsetX { get => _insetX; @@ -140,6 +228,8 @@ public class FastTextureSettings } [Category( "Inset" )] + [WideMode( HasLabel = false )] + [Range( 0.0f, 32.0f ), Step( 1.0f )] public float InsetY { get => _insetY; @@ -153,6 +243,32 @@ public class FastTextureSettings } } + [Hide] + public Vector2 SavedRectMin + { + get => _savedRectMin; + set + { + if ( _savedRectMin != value ) + { + _savedRectMin = value; + } + } + } + + [Hide] + public Vector2 SavedRectMax + { + get => _savedRectMax; + set + { + if ( _savedRectMax != value ) + { + _savedRectMax = value; + } + } + } + [Hide] public bool IsPickingEdge { get; set; } @@ -161,10 +277,78 @@ public class FastTextureSettings { IsPickingEdge = !IsPickingEdge; } - [Hide] public Action OnMappingChanged { get; set; } [Hide] public Action OnSettingsChanged { get; set; } + + private struct FastTextureSettingsDto + { + public MappingMode Mapping { get; set; } + public AlignmentMode Alignment { get; set; } + public ScaleMode ScaleMode { get; set; } + public TileMode TileMode { get; set; } + public float Repeat { get; set; } + public bool IsTileView { get; set; } + public bool ShowRects { get; set; } + public bool IsFlippedHorizontal { get; set; } + public bool IsFlippedVertical { get; set; } + public float InsetX { get; set; } + public float InsetY { get; set; } + public Vector2 SavedRectMin { get; set; } + public Vector2 SavedRectMax { get; set; } + } + + public void Load() + { + var json = ProjectCookie.Get( "FastTexture.Settings", string.Empty ); + if ( string.IsNullOrWhiteSpace( json ) ) + return; + + try + { + var dto = JsonSerializer.Deserialize( json ); + + _mapping = dto.Mapping; + _alignment = dto.Alignment; + _scaleMode = dto.ScaleMode; + _tileMode = dto.TileMode; + _repeat = dto.Repeat; + _isTileView = dto.IsTileView; + _showRects = dto.ShowRects; + _isFlippedHorizontal = dto.IsFlippedHorizontal; + _isFlippedVertical = dto.IsFlippedVertical; + _insetX = dto.InsetX; + _insetY = dto.InsetY; + _savedRectMin = dto.SavedRectMin; + _savedRectMax = dto.SavedRectMax; + } + catch + { + } + } + + public void Save() + { + var dto = new FastTextureSettingsDto + { + Mapping = _mapping, + Alignment = _alignment, + ScaleMode = _scaleMode, + TileMode = _tileMode, + Repeat = _repeat, + IsTileView = _isTileView, + ShowRects = _showRects, + IsFlippedHorizontal = _isFlippedHorizontal, + IsFlippedVertical = _isFlippedVertical, + InsetX = _insetX, + InsetY = _insetY, + SavedRectMin = _savedRectMin, + SavedRectMax = _savedRectMax + }; + + var json = JsonSerializer.Serialize( dto ); + ProjectCookie.Set( "FastTexture.Settings", json ); + } } diff --git a/game/addons/tools/Code/Editor/RectEditor/FastTextureWindow.cs b/game/addons/tools/Code/Editor/RectEditor/FastTextureWindow.cs index 0ce8bb39..a3218a05 100644 --- a/game/addons/tools/Code/Editor/RectEditor/FastTextureWindow.cs +++ b/game/addons/tools/Code/Editor/RectEditor/FastTextureWindow.cs @@ -1,29 +1,41 @@ using Editor.MeshEditor; +using Sandbox; +using Sandbox.UI; namespace Editor.RectEditor; public class FastTextureWindow : Window { public MeshFace[] MeshFaces { get; private set; } + private List OriginalUVs { get; set; } = new(); + private List OriginalMaterials { get; set; } = new(); + public RectView _RectView { get; private set; } public FastTextureWindow() : base() { - Size = new Vector2( 900, 700 ); + Size = new Vector2( 700, 850 ); Settings.IsFastTextureTool = true; } protected override void BuildDock() { - DockManager.RegisterDockType( "Rect View", "space_dashboard", null, false ); - RectView = new RectView( this ); - DockManager.AddDock( null, RectView, DockArea.Right, DockManager.DockProperty.HideOnClose, 0.0f ); + _RectView = new RectView( this ); + _RectView.Layout = Layout.Column(); + _RectView.Layout.Margin = 0; + _RectView.Layout.Spacing = 0; - DockManager.RegisterDockType( "Properties", "edit", null, false ); - Properties = new Properties( this ); - UpdateProperties(); - DockManager.AddDock( null, Properties, DockArea.Left, DockManager.DockProperty.HideOnClose, 0.25f ); + var toolbar = new RectViewToolbar( this ); + _RectView.Layout.Add( toolbar ); + + _RectView.Layout.AddStretchCell(); + + DockManager.RegisterDockType( "Rect View", "space_dashboard", null, false ); + DockManager.AddDock( null, _RectView, DockArea.Right, DockManager.DockProperty.HideOnClose, 0.0f ); ToolBar.Visible = false; + MenuBar.Visible = false; + + RestoreDefaultDockLayout(); } public static void OpenWith( MeshFace[] faces, Material material = null ) @@ -41,10 +53,30 @@ public class FastTextureWindow : Window private void InitializeWithFaces( MeshFace[] faces, Material material ) { MeshFaces = faces; + + OriginalUVs.Clear(); + OriginalMaterials.Clear(); + + foreach ( var face in faces ) + { + OriginalUVs.Add( face.TextureCoordinates.ToArray() ); + OriginalMaterials.Add( face.Material ); + } + + if ( material != null ) + { + foreach ( var face in faces ) + { + face.Material = material; + } + } + + Settings.FastTextureSettings.Load(); + InitRectanglesFromMeshFaces(); Settings.ReferenceMaterial = material?.ResourcePath; - RectView.SetMaterial( material ); + _RectView?.SetMaterial( material ); } protected override void InitRectanglesFromMeshFaces() @@ -104,7 +136,7 @@ public class FastTextureWindow : Window { var material = asset?.LoadResource(); Settings.ReferenceMaterial = material?.ResourcePath; - RectView.SetMaterial( material ); + _RectView?.SetMaterial( material ); if ( MeshFaces != null ) { @@ -149,10 +181,464 @@ public class FastTextureWindow : Window [EditorEvent.Frame] private void OnFrame() { + if ( MeshFaces == null || SceneEditorSession.Active == null ) + return; + var selectedFaces = SceneEditorSession.Active.Selection.OfType().ToArray(); if ( selectedFaces.Length != MeshFaces.Length || !selectedFaces.All( x => MeshFaces.Contains( x ) ) ) { Close(); } } + + [Shortcut( "editor.cancel", "ESC" )] + public void Cancel() + { + if ( MeshFaces != null && OriginalUVs != null && MeshFaces.Length == OriginalUVs.Count ) + { + for ( int i = 0; i < MeshFaces.Length; i++ ) + { + MeshFaces[i].TextureCoordinates = OriginalUVs[i]; + + if ( i < OriginalMaterials.Count ) + { + MeshFaces[i].Material = OriginalMaterials[i]; + } + } + } + + MeshFaces = null; + + Close(); + } + + [Shortcut( "editor.confirm", "ENTER" )] + public void Confirm() + { + var meshRect = Document?.Rectangles.OfType().FirstOrDefault(); + if ( meshRect != null ) + { + Settings.FastTextureSettings.SavedRectMin = meshRect.Min; + Settings.FastTextureSettings.SavedRectMax = meshRect.Max; + } + + Settings.FastTextureSettings.Save(); + + Close(); + } + + [Shortcut( "editor.fasttexture.resetuvs", "Shift+R" )] + public void ResetUVs() + { + _RectView?.ResetUV(); + Update(); + } +} + +public class RectViewToolbar : Widget +{ + private FastTextureSettings Settings => Window?.Settings?.FastTextureSettings; + private FastTextureWindow Window; + private ScaleMode _lastScaleMode; + private TileMode _lastTiledMode; + + public RectViewToolbar( FastTextureWindow window ) : base( null ) + { + Window = window; + Layout = Layout.Row(); + Layout.Margin = 2; + Layout.Spacing = 2; + FixedHeight = 140; + + TransparentForMouseEvents = false; + SetModal( true ); + + OnPaintOverride = PaintMenuBackground; + + if ( Settings != null ) + { + _lastScaleMode = Settings.ScaleMode; + Settings.OnSettingsChanged += OnSettingsChanged; + Settings.OnMappingChanged += OnSettingsChanged; + } + + BuildUI(); + } + + public override void OnDestroyed() + { + base.OnDestroyed(); + if ( Settings != null ) + { + Settings.OnSettingsChanged -= OnSettingsChanged; + Settings.OnMappingChanged -= OnSettingsChanged; + } + } + + private void OnSettingsChanged() + { + if ( Settings.ScaleMode != _lastScaleMode ) + { + _lastScaleMode = Settings.ScaleMode; + BuildUI(); + } + + if ( Settings.TileMode != _lastTiledMode ) + { + _lastTiledMode = Settings.TileMode; + BuildUI(); + } + + Update(); + } + protected override void OnMousePress( MouseEvent e ) + { + base.OnMousePress( e ); + if ( !e.Accepted ) e.Accepted = true; + } + + protected override void OnMouseReleased( MouseEvent e ) + { + base.OnMouseReleased( e ); + if ( !e.Accepted ) e.Accepted = true; + } + private void BuildUI() + { + Layout.Clear( true ); + if ( Settings == null ) return; + + // --- Mapping & Alignment --- + { + var mappingCol = Layout.AddColumn(); + mappingCol.Margin = 2; + + AddGroup( mappingCol, "Mapping", layout => + { + var mappingSeg = new SegmentedControl(); + mappingSeg.AddOption( "Square", "auto_awesome_mosaic" ); + mappingSeg.AddOption( "Conform", "texture" ); + mappingSeg.AddOption( "Planar", "video_camera_back" ); + mappingSeg.AddOption( "Exist", "view_in_ar" ); + + mappingSeg.SelectedIndex = Settings.Mapping switch + { + MappingMode.UnwrapSquare => 0, + MappingMode.UnwrapConforming => 1, + MappingMode.Planar => 2, + MappingMode.UseExisting => 3, + _ => 0 + }; + + mappingSeg.OnSelectedChanged = ( val ) => + { + Settings.Mapping = val switch + { + "Square" => MappingMode.UnwrapSquare, + "Conform" => MappingMode.UnwrapConforming, + "Planar" => MappingMode.Planar, + "Exist" => MappingMode.UseExisting, + _ => Settings.Mapping + }; + }; + layout.Add( mappingSeg ); + } ); + + AddGroup( mappingCol, "Alignment", layout => + { + var alignCol = layout.AddColumn(); + alignCol.Spacing = 4; + + // Axis Segment + var alignSeg = new SegmentedControl(); + alignSeg.AddOption( "U Axis", "keyboard_tab" ); + alignSeg.AddOption( "V Axis", "vertical_align_bottom" ); + alignSeg.SelectedIndex = Settings.Alignment == AlignmentMode.UAxis ? 0 : 1; + alignSeg.OnSelectedChanged = ( val ) => + { + Settings.Alignment = val == "U Axis" ? AlignmentMode.UAxis : AlignmentMode.VAxis; + }; + alignCol.Add( alignSeg ); + + // Toggle Buttons + var toggleRow = alignCol.AddRow(); + toggleRow.Spacing = 2; + CreateModeButton( toggleRow, "swap_horiz", "Flip H", () => false, () => Settings.IsFlippedHorizontal = !Settings.IsFlippedHorizontal ); + CreateModeButton( toggleRow, "swap_vert", "Flip V", () => false, () => Settings.IsFlippedVertical = !Settings.IsFlippedVertical ); + CreateModeButton( toggleRow, "border_vertical", "Pick", () => Settings.IsPickingEdge, () => Settings.PickEdge() ); + } ); + } + + // --- Tiling & Scale --- + { + var tilingCol = Layout.AddColumn(); + tilingCol.Margin = 2; + + AddGroup( tilingCol, "Tiling / Scale", layout => + { + var scaleSeg = new SegmentedControl(); + scaleSeg.AddOption( "Fit", "aspect_ratio" ); + scaleSeg.AddOption( "World Scale", "public" ); + scaleSeg.AddOption( "Tile U", "view_column" ); + scaleSeg.AddOption( "Tile V", "view_stream" ); + + scaleSeg.SelectedIndex = Settings.ScaleMode switch + { + ScaleMode.Fit => 0, + ScaleMode.WorldScale => 1, + ScaleMode.TileU => 2, + ScaleMode.TileV => 3, + _ => 0 + }; + + scaleSeg.OnSelectedChanged = ( val ) => + { + Settings.ScaleMode = val switch + { + "Fit" => ScaleMode.Fit, + "World Scale" => ScaleMode.WorldScale, + "Tile U" => ScaleMode.TileU, + "Tile V" => ScaleMode.TileV, + _ => ScaleMode.Fit + }; + }; + layout.Add( scaleSeg ); + } ); + + AddGroup( tilingCol, "Repeat", layout => + { + var col = layout.AddColumn(); + col.Spacing = 4; + + // Repeat Mode Segment + var repeatSeg = new SegmentedControl(); + repeatSeg.Enabled = Settings.ScaleMode != ScaleMode.Fit || Settings.ScaleMode != ScaleMode.WorldScale; + repeatSeg.AddOption( "Aspect", "crop_free" ); + repeatSeg.AddOption( "Repeat", "repeat" ); + + repeatSeg.SelectedIndex = Settings.TileMode == TileMode.MaintainAspect ? 0 : 1; + repeatSeg.OnSelectedChanged = ( val ) => + { + Settings.TileMode = val == "Aspect" ? TileMode.MaintainAspect : TileMode.Repeat; + }; + + bool isFit = Settings.ScaleMode == ScaleMode.Fit || Settings.ScaleMode == ScaleMode.WorldScale; + repeatSeg.Enabled = !isFit; + + col.Add( repeatSeg ); + + // Repeat Slider + var so = Settings.GetSerialized(); + var repeatWidget = new FloatControlWidget( so.GetProperty( nameof( FastTextureSettings.Repeat ) ) ); + repeatWidget.Label = null; + repeatWidget.FixedHeight = Theme.RowHeight; + repeatWidget.Icon = "settings_ethernet"; + repeatWidget.HighlightColor = Theme.Blue; + repeatWidget.MinimumWidth = 160; + + repeatWidget.Enabled = !isFit && Settings.TileMode == TileMode.Repeat; + + col.Add( repeatWidget ); + } ); + } + + // --- Inset --- + { + var insetCol = Layout.AddColumn(); + insetCol.Margin = 2; + AddGroupToLayout( insetCol, "Inset", layout => + { + var col = layout.AddColumn(); + var so = Settings.GetSerialized(); + + var xInset = col.Add( new FloatControlWidget( so.GetProperty( nameof( FastTextureSettings.InsetX ) ) ) ); + xInset.FixedHeight = Theme.RowHeight; + xInset.HighlightColor = Theme.Red; + xInset.Label = null; + xInset.Icon = "swap_horiz"; + xInset.MinimumWidth = 160; + xInset.Enabled = Settings.ScaleMode != ScaleMode.WorldScale; + + var yInset = col.Add( new FloatControlWidget( so.GetProperty( nameof( FastTextureSettings.InsetY ) ) ) ); + yInset.FixedHeight = Theme.RowHeight; + yInset.HighlightColor = Theme.Green; + yInset.Label = null; + yInset.Icon = "import_export"; + yInset.MinimumWidth = 160; + yInset.Enabled = Settings.ScaleMode != ScaleMode.WorldScale; + } ); + } + + // --- View / Actions --- + { + var viewCol = Layout.AddColumn(); + viewCol.Margin = 2; + + AddGroup( viewCol, "View Options", layout => + { + CreateModeButton( layout, "grid_4x4", "Toggle Tile View", () => Settings.IsTileView, () => Settings.IsTileView = !Settings.IsTileView ); + CreateModeButton( layout, "rectangle", "Show Atlas Rects", () => Settings.ShowRects, () => Settings.ShowRects = !Settings.ShowRects ); + } ); + + AddGroup( viewCol, "Actions", layout => + { + CreateModeButton( layout, "undo", "Reset UVs (Shift+R)", () => false, () => Window.ResetUVs() ); + CreateModeButton( layout, "fit_screen", "Focus (F)", () => false, () => Window._RectView?.FocusOnUV() ); + } ); + } + + Layout.AddStretchCell(); + } + bool PaintMenuBackground() + { + Paint.SetBrushAndPen( Theme.TabBackground ); + Paint.DrawRect( Paint.LocalRect, 0 ); + return true; + } + + private void AddGroup( Layout parent, string title, Action build ) + { + AddGroupToLayout( parent, title, build ); + } + + private void AddGroupToLayout( Layout parentLayout, string title, Action build ) + { + var groupContainer = new Widget( this ); + groupContainer.Layout = Layout.Column(); + + groupContainer.MaximumHeight = 164; + groupContainer.MaximumWidth = 268; + + var group = new Widget( groupContainer ); + group.Layout = Layout.Row(); + group.Layout.Spacing = 4; + group.Layout.Margin = new Margin( 12, 16, 12, 12 ); + group.OnPaintOverride += () => + { + var controlRect = Paint.LocalRect; + controlRect.Top += 6; + controlRect = controlRect.Shrink( 0, 0, 1, 1 ); + + Paint.Antialiasing = true; + Paint.TextAntialiasing = true; + Paint.SetBrushAndPen( Theme.Text.WithAlpha( 0.01f ), Theme.Text.WithAlpha( 0.1f ) ); + Paint.DrawRect( controlRect, 4 ); + + Paint.SetPen( Theme.TextControl.WithAlpha( 0.6f ) ); + Paint.SetDefaultFont( 7, 500 ); + Paint.DrawText( new Vector2( 12, 0 ), title ); + return true; + }; + + build( group.Layout ); + groupContainer.Layout.Add( group ); + + parentLayout.Add( groupContainer ); + } + + private Widget CreateModeButton( Layout layout, string Icon, string text, Func isActive, Action onClick ) + { + var btn = new ToolbarIcon( Icon, text, isActive, onClick ); + layout.Add( btn ); + return btn; // Return the button + } +} + +public class ToolbarIcon : Widget +{ + private string _icon; + private Func _isActive; + private Action _onClick; + + // Track if we are holding the mouse down + private bool _isPressed; + + public ToolbarIcon( string icon, string tooltip, Func isActive, Action onClick ) : base( null ) + { + _icon = icon; + _isActive = isActive; + _onClick = onClick; + ToolTip = tooltip; + + FixedSize = 24; + Cursor = CursorShape.Finger; + } + + protected override void OnMousePress( MouseEvent e ) + { + base.OnMousePress( e ); + + if ( e.LeftMouseButton ) + { + _isPressed = true; + _onClick?.Invoke(); + Update(); + } + } + + protected override void OnMouseReleased( MouseEvent e ) + { + base.OnMouseReleased( e ); + + if ( _isPressed ) + { + _isPressed = false; + Update(); + } + } + + protected override void OnMouseLeave() + { + base.OnMouseLeave(); + + if ( _isPressed ) + { + _isPressed = false; + Update(); + } + } + + protected override void OnPaint() + { + Paint.Antialiasing = true; + Paint.TextAntialiasing = true; + + // 1. Is it toggled on? (Like the "Tile View" button) + bool isToggled = _isActive != null && _isActive(); + + // 2. Is the user holding it down right now? + bool isClicking = _isPressed && IsUnderMouse; + + // 3. Show Blue if EITHER is true + bool showBlue = (isToggled || isClicking) && Enabled; + + if ( showBlue ) + { + Paint.ClearPen(); + Paint.SetBrush( Theme.Blue ); + Paint.DrawRect( LocalRect.Shrink( 2 ), 4 ); + Paint.SetPen( Theme.TextButton ); + } + else if ( !Enabled ) + { + Paint.ClearPen(); + Paint.SetBrush( Color.White.WithAlpha( 0.02f ) ); + Paint.DrawRect( LocalRect.Shrink( 2 ), 4 ); + Paint.SetPen( Theme.TextLight.WithAlpha( 0.4f ) ); + Cursor = CursorShape.Arrow; + } + else if ( IsUnderMouse ) + { + Paint.ClearPen(); + Paint.SetBrush( Color.White.WithAlpha( 0.05f ) ); + Paint.DrawRect( LocalRect.Shrink( 2 ), 4 ); + Paint.SetPen( Theme.TextLight ); + } + else + { + Paint.ClearPen(); + Paint.SetPen( Theme.TextLight.WithAlpha( 0.8f ) ); + } + + Paint.DrawIcon( LocalRect, _icon, 16, TextFlag.Center ); + } } diff --git a/game/addons/tools/Code/Editor/RectEditor/MeshRectangle.cs b/game/addons/tools/Code/Editor/RectEditor/MeshRectangle.cs index 0f2e4778..c0ff32dc 100644 --- a/game/addons/tools/Code/Editor/RectEditor/MeshRectangle.cs +++ b/game/addons/tools/Code/Editor/RectEditor/MeshRectangle.cs @@ -38,6 +38,9 @@ public partial class Document [Hide, JsonIgnore] public (int vertexA, int vertexB) HoveredEdge { get; set; } = (-1, -1); + [Hide, JsonIgnore] + public List UnwrappedVertexPositionsWorldSpace { get; set; } = new(); + public MeshRectangle( Window window ) : base( window ) { } @@ -46,6 +49,13 @@ public partial class Document { MeshFaces = meshFaces; StoreOriginalUVs(); + + var settings = Session?.Settings?.FastTextureSettings; + if ( settings != null && settings.SavedRectMin != settings.SavedRectMax ) + { + Min = settings.SavedRectMin; + Max = settings.SavedRectMax; + } } public override void OnPaint( RectView view ) @@ -132,6 +142,16 @@ public partial class Document Max = max; } + private void SaveBoundsToSettings() + { + var settings = Session?.Settings?.FastTextureSettings; + if ( settings != null ) + { + settings.SavedRectMin = Min; + settings.SavedRectMax = Max; + } + } + public void ApplyMapping( FastTextureSettings settings, bool resetBoundsFromUseExisting = false ) { if ( MeshFaces == null || MeshFaces.Length == 0 ) @@ -150,7 +170,8 @@ public partial class Document switch ( currentMapping ) { case MappingMode.UnwrapSquare: - BuildUnwrappedMeshWithSquareMapping(); + case MappingMode.UnwrapConforming: + BuildUnwrappedMesh( currentMapping ); break; case MappingMode.Planar: var cameraRot = SceneViewWidget.Current.LastSelectedViewportWidget.State.CameraRotation; @@ -179,6 +200,8 @@ public partial class Document Max = previousBounds.Max; } + SaveBoundsToSettings(); + PreviousMappingMode = currentMapping; } @@ -256,7 +279,7 @@ public partial class Document return (min, max); } - private void BuildUnwrappedMeshWithSquareMapping() + private void BuildUnwrappedMesh( MappingMode mode ) { if ( MeshFaces == null || MeshFaces.Length == 0 ) return; @@ -266,7 +289,7 @@ public partial class Document OriginalVertexPositions.Clear(); var unwrapper = new EdgeAwareFaceUnwrapper( MeshFaces ); - var result = unwrapper.UnwrapToSquare(); + var result = unwrapper.Unwrap( mode ); UnwrappedVertexPositions.AddRange( result.VertexPositions ); FaceVertexIndices.AddRange( result.FaceIndices ); @@ -429,6 +452,8 @@ public partial class Document if ( UnwrappedVertexPositions.Count == 0 ) return; + UnwrappedVertexPositionsWorldSpace = new List( UnwrappedVertexPositions ); + var min = UnwrappedVertexPositions[0]; var max = UnwrappedVertexPositions[0]; @@ -486,6 +511,50 @@ public partial class Document return lines; } + /// + /// Gets the material's world space mapping dimensions. + /// + private (float width, float height) GetMaterialWorldScale() + { + var material = Material.Load( Session?.Settings.ReferenceMaterial ); + if ( material == null ) + return (512.0f, 512.0f); + + var worldMappingWidth = material.Attributes.GetInt( "WorldMappingWidth", 0 ); + var worldMappingHeight = material.Attributes.GetInt( "WorldMappingHeight", 0 ); + + var texture = material.FirstTexture; + if ( texture == null ) + { + if ( worldMappingWidth > 0 && worldMappingHeight > 0 ) + return (worldMappingWidth, worldMappingHeight); + return (512.0f, 512.0f); + } + + var textureWidth = texture.Size.x; + var textureHeight = texture.Size.y; + + float mappingWidth; + float mappingHeight; + + if ( worldMappingWidth > 0 ) + mappingWidth = worldMappingWidth; + else + mappingWidth = 0.25f * textureWidth; + + if ( worldMappingHeight > 0 ) + mappingHeight = worldMappingHeight; + else + mappingHeight = 0.25f * textureHeight; + + if ( mappingWidth <= 0 ) + mappingWidth = 512.0f; + if ( mappingHeight <= 0 ) + mappingHeight = 512.0f; + + return (mappingWidth, mappingHeight); + } + /// /// Transforms unwrapped vertex positions so they are relative to the current rectangle bounds /// @@ -507,6 +576,66 @@ public partial class Document settings.InsetY / imageSize.y ); + float tileU = 1.0f; + float tileV = 1.0f; + + if ( settings.ScaleMode == ScaleMode.WorldScale ) + { + var worldSpacePositions = UnwrappedVertexPositionsWorldSpace.Count > 0 + ? UnwrappedVertexPositionsWorldSpace + : UnwrappedVertexPositions; + + var minUV = worldSpacePositions[0]; + foreach ( var pos in worldSpacePositions ) + { + minUV = Vector2.Min( minUV, pos ); + } + + var (mappingWidth, mappingHeight) = GetMaterialWorldScale(); + + foreach ( var pos in worldSpacePositions ) + { + var scaledX = ((pos.x - minUV.x) / mappingWidth) + Min.x; + var scaledY = ((pos.y - minUV.y) / mappingHeight) + Min.y; + + transformedPositions.Add( new Vector2( scaledX, scaledY ) ); + } + + return transformedPositions; + } + + if ( settings.ScaleMode != ScaleMode.Fit ) + { + if ( settings.TileMode == TileMode.Repeat ) + { + if ( settings.ScaleMode == ScaleMode.TileU ) + tileU = settings.Repeat; + else if ( settings.ScaleMode == ScaleMode.TileV ) + tileV = settings.Repeat; + } + else if ( settings.TileMode == TileMode.MaintainAspect ) + { + var meshWidth = MathF.Max( unwrappedSize.x, 0.001f ); + var meshHeight = MathF.Max( unwrappedSize.y, 0.001f ); + var meshAspect = meshWidth / meshHeight; + + var rectPixelWidth = MathF.Max( rectSize.x * imageSize.x, 0.001f ); + var rectPixelHeight = MathF.Max( rectSize.y * imageSize.y, 0.001f ); + var rectAspect = rectPixelWidth / rectPixelHeight; + + if ( settings.ScaleMode == ScaleMode.TileU ) + { + tileU = meshAspect / rectAspect; + tileV = 1.0f; + } + else if ( settings.ScaleMode == ScaleMode.TileV ) + { + tileU = 1.0f; + tileV = rectAspect / meshAspect; + } + } + } + foreach ( var pos in UnwrappedVertexPositions ) { Vector2 relativePos; @@ -515,6 +644,9 @@ public partial class Document { var normalized = (pos - unwrappedMin) / unwrappedSize; + normalized.x *= tileU; + normalized.y *= tileV; + var insetMin = Min + insetUV; var insetSize = rectSize - insetUV * 2; relativePos = insetMin + normalized * insetSize; diff --git a/game/addons/tools/Code/Editor/RectEditor/RectView.cs b/game/addons/tools/Code/Editor/RectEditor/RectView.cs index 8a5faaa3..c0be8238 100644 --- a/game/addons/tools/Code/Editor/RectEditor/RectView.cs +++ b/game/addons/tools/Code/Editor/RectEditor/RectView.cs @@ -52,6 +52,65 @@ public class RectView : Widget ResetView(); } + protected override void OnKeyPress( KeyEvent e ) + { + base.OnKeyPress( e ); + + if ( e.Accepted ) return; + + var direction = Vector2.Zero; + + if ( e.Key == KeyCode.Left ) direction = new Vector2( -1, 0 ); + if ( e.Key == KeyCode.Right ) direction = new Vector2( 1, 0 ); + if ( e.Key == KeyCode.Up ) direction = new Vector2( 0, -1 ); + if ( e.Key == KeyCode.Down ) direction = new Vector2( 0, 1 ); + + if ( direction != Vector2.Zero ) + { + Nudge( direction ); + e.Accepted = true; + } + } + + private void Nudge( Vector2 direction ) + { + var gridCountX = GetGridCountX(); + var gridCountY = GetGridCountY(); + var step = new Vector2( 1.0f / gridCountX, 1.0f / gridCountY ); + var delta = direction * step; + + if ( Session.Settings.IsFastTextureTool ) + { + var meshRect = Document.Rectangles.OfType().FirstOrDefault(); + if ( meshRect != null ) + { + Session.ExecuteUndoableAction( "Nudge UVs", () => + { + meshRect.Min += delta; + meshRect.Max += delta; + meshRect.ApplyMapping( Session.Settings.FastTextureSettings, false ); + Document.Modified = true; + Document.OnModified?.Invoke(); + } ); + Update(); + } + } + else if ( Document.SelectedRectangles.Count > 0 ) + { + Session.ExecuteUndoableAction( "Nudge Rectangles", () => + { + foreach ( var rect in Document.SelectedRectangles ) + { + rect.Min += delta; + rect.Max += delta; + } + Document.Modified = true; + Document.OnModified?.Invoke(); + } ); + Update(); + } + } + private Vector2 SnapUVToGrid( Vector2 uv ) { var gridCountX = GetGridCountX(); @@ -65,7 +124,7 @@ public class RectView : Widget private Vector2 PixelToUV_OnGrid( Vector2 vPixel ) { - if ( Session.GridEnabled ) return SnapUVToGrid( PixelToUV( vPixel ) ); + if ( !Application.KeyboardModifiers.HasFlag( KeyboardModifiers.Ctrl ) ) return SnapUVToGrid( PixelToUV( vPixel ) ); return PixelToUV( vPixel ); } @@ -79,6 +138,22 @@ public class RectView : Widget NewRect = new Rect( min, max - min ); + if ( Session.Settings.IsFastTextureTool ) + { + var meshRect = Document.Rectangles.OfType().FirstOrDefault(); + if ( meshRect != null ) + { + meshRect.Min = NewRect.TopLeft; + meshRect.Max = NewRect.BottomRight; + + // Re-apply mapping so the UVs deform to the new shape immediately + meshRect.ApplyMapping( Session.Settings.FastTextureSettings, false ); + + Document.Modified = true; + Document.OnModified?.Invoke(); + } + } + Update(); } @@ -339,6 +414,9 @@ public class RectView : Widget { var m = new ContextMenu( this ); + if ( Session.Settings.IsFastTextureTool ) + return; + m.AddOption( "Delete Rectangle", "delete", () => Session.ExecuteUndoableAction( "Delete Rectangle", () => Document.DeleteRectangles( [rectangle] ) ) ); m.OpenAtCursor(); @@ -605,7 +683,7 @@ public class RectView : Widget } else { - Cursor = CursorShape.Arrow; + Cursor = CursorShape.Cross; } } else @@ -670,7 +748,7 @@ public class RectView : Widget foreach ( var rectangle in Document.SelectedRectangles ) { - Paint.SetBrush( Color.Yellow.WithAlpha( 0.2f ) ); + Paint.SetBrush( Color.White.WithAlpha( 0.1f ) ); Paint.SetPen( new Color32( 255, 255, 0 ), 3 ); DrawRectangle( rectangle, corner: (rectangle == rectangleUnderCursor && Document.SelectedRectangles.Count < 2) ? HoveredCorner : 0 ); } @@ -846,7 +924,11 @@ public class RectView : Widget return; if ( UvAssetRects.Count == 0 ) + { + ResetUV(); return; + } + var index = GetAssetRectIndexAtUV( uv ); if ( index < 0 ) @@ -873,6 +955,36 @@ public class RectView : Widget } ); } + public void ResetUV() + { + Session.ExecuteUndoableAction( "Apply UV From Asset Rect", () => + { + var meshRect = Document.Rectangles + .OfType() + .FirstOrDefault(); + if ( meshRect == null ) + return; + meshRect.Min = Vector2.Zero; + meshRect.Max = Vector2.One; + Document.Modified = true; + Document.OnModified?.Invoke(); + } ); + } + + public void FocusOnUV() + { + //Focus the view on the selected rectangle UVs + var selectedRect = Document.SelectedRectangles.FirstOrDefault(); + if ( selectedRect is null ) + return; + var rectCenter = selectedRect.Max + selectedRect.Min; + rectCenter *= 0.5f; + PanOffset = rectCenter - (0.5f / ZoomLevel); + UpdateViewRect(); + Update(); + + } + private Rect GetDrawRect() { const int marigin = 16; diff --git a/game/addons/tools/Code/Scene/Mesh/Tools/SelectionTool.cs b/game/addons/tools/Code/Scene/Mesh/Tools/SelectionTool.cs index 1212b9bc..0fb85a0e 100644 --- a/game/addons/tools/Code/Scene/Mesh/Tools/SelectionTool.cs +++ b/game/addons/tools/Code/Scene/Mesh/Tools/SelectionTool.cs @@ -311,7 +311,7 @@ public abstract class SelectionTool( MeshTool tool ) : SelectionTool where T private void UpdateNudge() { - if ( Gizmo.Pressed.Any || !Application.FocusWidget.IsValid() ) + if ( Gizmo.Pressed.Any || !Application.FocusWidget.IsValid() || !Gizmo.HasMouseFocus ) return; var keyUp = Application.IsKeyDown( KeyCode.Up );