mirror of
https://github.com/Facepunch/sbox-public.git
synced 2025-12-23 22:48:07 -05:00
Fast texture tool improvements (#3624)
https://files.facepunch.com/louie/1b1811b1/sbox-dev_wPVinlJTtE.png -Support Tiling in the fast texture tool. -Shortcuts. -Escaping out resets to orignal uv. -Unwrap Square better. -World Mapping. -Apply active material when entering fast texturing. -Clicking in the rect view with no rect file fills to the full uv. -Snapping toggle. -Reset uv and focus buttons. -New cleaner UI.
This commit is contained in:
@@ -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<Vector2>( new Vector2[vertexPositions.Count] );
|
||||
var processedFaces = new HashSet<MeshFace>();
|
||||
var faceQueue = new Queue<MeshFace>();
|
||||
|
||||
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<Vector2>( new Vector2[vertexPositions.Count] );
|
||||
var processedFaces = new HashSet<MeshFace>();
|
||||
var faceQueue = new Queue<MeshFace>();
|
||||
|
||||
if ( faces.Length > 0 && faces[0].IsValid )
|
||||
private void UnwrapFirstFace( MeshFace face, List<Vector2> unwrappedUVs, bool straighten )
|
||||
{
|
||||
UnwrapFirstFace( faces[0], unwrappedUVs );
|
||||
processedFaces.Add( faces[0] );
|
||||
if ( !faceToVertexIndices.TryGetValue( face, out var indices ) || indices.Count < 3 )
|
||||
return;
|
||||
|
||||
for ( int i = 1; i < faces.Length; i++ )
|
||||
if ( straighten && indices.Count == 4 )
|
||||
{
|
||||
if ( faces[i].IsValid )
|
||||
faceQueue.Enqueue( faces[i] );
|
||||
var p0 = vertexPositions[indices[0]];
|
||||
var p1 = vertexPositions[indices[1]];
|
||||
var p3 = vertexPositions[indices[3]];
|
||||
|
||||
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 )
|
||||
{
|
||||
var relative = vertexPositions[idx] - p0;
|
||||
unwrappedUVs[idx] = new Vector2( relative.Dot( uDir ), relative.Dot( vDir ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int maxAttempts = faces.Length * 3;
|
||||
int attempts = 0;
|
||||
|
||||
while ( faceQueue.Count > 0 && attempts < maxAttempts )
|
||||
private bool TryUnfoldFace( MeshFace currentFace, HashSet<MeshFace> processedFaces, List<Vector2> unwrappedUVs, bool straighten )
|
||||
{
|
||||
var currentFace = faceQueue.Dequeue();
|
||||
attempts++;
|
||||
if ( !faceToVertexIndices.TryGetValue( currentFace, out var currentIndices ) )
|
||||
return false;
|
||||
|
||||
if ( processedFaces.Contains( currentFace ) )
|
||||
foreach ( var processedFace in processedFaces )
|
||||
{
|
||||
var sharedEdge = FindSharedVertices( currentFace, processedFace, unwrappedUVs );
|
||||
if ( sharedEdge.HasValue )
|
||||
{
|
||||
UnfoldFaceAlongEdge( currentFace, currentIndices, sharedEdge.Value, unwrappedUVs, straighten );
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void UnfoldFaceAlongEdge( MeshFace face, List<int> faceIndices, (int idx1, int idx2, Vector2 uv1, Vector2 uv2) edge, List<Vector2> unwrappedUVs, bool straighten )
|
||||
{
|
||||
unwrappedUVs[edge.idx1] = edge.uv1;
|
||||
unwrappedUVs[edge.idx2] = edge.uv2;
|
||||
|
||||
// SQUARE MODE.
|
||||
if ( straighten && faceIndices.Count == 4 )
|
||||
{
|
||||
UnfoldQuadStraight( faceIndices, edge, unwrappedUVs );
|
||||
}
|
||||
// CONFORMING MODE.
|
||||
else
|
||||
{
|
||||
UnfoldGeometric( faceIndices, edge, unwrappedUVs );
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private void UnfoldQuadStraight( List<int> faceIndices, (int idx1, int idx2, Vector2 uv1, Vector2 uv2) edge, List<Vector2> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unfolds a face by projecting its vertices onto a 2D plane defined by the shared edge.
|
||||
/// This preserves the original geometric angles and shapes.
|
||||
/// </summary>
|
||||
private void UnfoldGeometric( List<int> faceIndices, (int idx1, int idx2, Vector2 uv1, Vector2 uv2) edge, List<Vector2> 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 ) )
|
||||
{
|
||||
processedFaces.Add( currentFace );
|
||||
attempts = 0;
|
||||
}
|
||||
else if ( attempts < maxAttempts )
|
||||
{
|
||||
faceQueue.Enqueue( currentFace );
|
||||
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<Vector2> 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++ )
|
||||
{
|
||||
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<Vector2> unwrappedUVs )
|
||||
{
|
||||
var finalFaceIndices = new List<List<int>>();
|
||||
foreach ( var face in faces )
|
||||
{
|
||||
@@ -97,126 +300,6 @@ internal class EdgeAwareFaceUnwrapper
|
||||
};
|
||||
}
|
||||
|
||||
private void UnwrapFirstFace( MeshFace face, List<Vector2> 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<MeshFace> processedFaces, List<Vector2> 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<Vector2> 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<int> faceIndices, (int idx1, int idx2, Vector2 uv1, Vector2 uv2) sharedEdge, List<Vector2> 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<Vector2> VertexPositions { get; set; } = new();
|
||||
|
||||
@@ -3,11 +3,19 @@
|
||||
public enum MappingMode
|
||||
{
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
[Icon( "auto_awesome_mosaic" )]
|
||||
UnwrapSquare,
|
||||
|
||||
/// <summary>
|
||||
/// Unwrap the selected faces while maintaining their shape and angles.
|
||||
/// Best for organic shapes or when distortion should be minimized.
|
||||
/// </summary>
|
||||
[Icon( "texture" )]
|
||||
UnwrapConforming,
|
||||
|
||||
/// <summary>
|
||||
/// Planar Project the selected faces based on the current view. Can be selected again to update from a new view.
|
||||
/// </summary>
|
||||
@@ -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<FastTextureSettingsDto>( 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 );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Vector2[]> OriginalUVs { get; set; } = new();
|
||||
private List<Material> 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<Material>();
|
||||
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<MeshFace>().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<Document.MeshRectangle>().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<Layout> build )
|
||||
{
|
||||
AddGroupToLayout( parent, title, build );
|
||||
}
|
||||
|
||||
private void AddGroupToLayout( Layout parentLayout, string title, Action<Layout> 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<bool> 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<bool> _isActive;
|
||||
private Action _onClick;
|
||||
|
||||
// Track if we are holding the mouse down
|
||||
private bool _isPressed;
|
||||
|
||||
public ToolbarIcon( string icon, string tooltip, Func<bool> 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 );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ public partial class Document
|
||||
[Hide, JsonIgnore]
|
||||
public (int vertexA, int vertexB) HoveredEdge { get; set; } = (-1, -1);
|
||||
|
||||
[Hide, JsonIgnore]
|
||||
public List<Vector2> 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<Vector2>( UnwrappedVertexPositions );
|
||||
|
||||
var min = UnwrappedVertexPositions[0];
|
||||
var max = UnwrappedVertexPositions[0];
|
||||
|
||||
@@ -486,6 +511,50 @@ public partial class Document
|
||||
return lines;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the material's world space mapping dimensions.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transforms unwrapped vertex positions so they are relative to the current rectangle bounds
|
||||
/// </summary>
|
||||
@@ -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;
|
||||
|
||||
@@ -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<Document.MeshRectangle>().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<Document.MeshRectangle>().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<Document.MeshRectangle>()
|
||||
.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;
|
||||
|
||||
@@ -311,7 +311,7 @@ public abstract class SelectionTool<T>( 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 );
|
||||
|
||||
Reference in New Issue
Block a user