From 5a174253deeb39e069493d05d1d4847702a116b6 Mon Sep 17 00:00:00 2001 From: varinotmUnity Date: Fri, 14 Nov 2025 13:38:48 -0500 Subject: [PATCH 1/2] Fixed issue when selecting a face that was grouped in the window, and then moving it. this was due to bad handle position set, which would then overwrite the uv center badly. this only happens in the editor, not with the API --- Editor/EditorCore/UVEditor.cs | 16 +-- Tests/Editor/Editor/UVEditorTest.cs | 190 +++++++++++++++++++++++++++- 2 files changed, 195 insertions(+), 11 deletions(-) diff --git a/Editor/EditorCore/UVEditor.cs b/Editor/EditorCore/UVEditor.cs index 605eddf6b..8833ef0c5 100644 --- a/Editor/EditorCore/UVEditor.cs +++ b/Editor/EditorCore/UVEditor.cs @@ -92,14 +92,11 @@ static Color DRAG_BOX_COLOR static readonly Color SELECTED_COLOR_MANUAL = new Color(1f, .68f, 0f, .39f); static readonly Color SELECTED_COLOR_AUTO = new Color(0f, .785f, 1f, .39f); -#if UNITY_STANDALONE_OSX - public bool ControlKey { get { return Event.current.modifiers == EventModifiers.Command; } } - #else public bool ControlKey { - get { return Event.current.modifiers == EventModifiers.Control; } + get { return EditorGUI.actionKey; } } -#endif + public bool ShiftKey { get { return Event.current.modifiers == EventModifiers.Shift; } @@ -547,13 +544,17 @@ internal void OnBeginUVModification() GUI.FocusControl(string.Empty); bool update = false; + Vector2 originalHandlePosition = handlePosition; + // Make sure all TextureGroups are auto-selected for (int i = 0; i < selection.Length; i++) { if (selection[i].selectedFaceCount > 0) { int fc = selection[i].selectedFaceCount; - selection[i].SetSelectedFaces(SelectTextureGroups(selection[i], selection[i].selectedFacesInternal)); + selection[i].SetSelectedFaces( + SelectTextureGroups(selection[i], selection[i].selectedFacesInternal) + ); // kinda lame... this will cause setSelectedUVsWithSceneView to be called again. if (fc != selection[i].selectedFaceCount) @@ -567,9 +568,8 @@ internal void OnBeginUVModification() if (update) { // UpdateSelection clears handlePosition - Vector2 storedHandlePosition = handlePosition; ProBuilderEditor.Refresh(); - SetHandlePosition(storedHandlePosition, true); + SetHandlePosition(originalHandlePosition, true); } CopySelectionUVs(out uv_origins); diff --git a/Tests/Editor/Editor/UVEditorTest.cs b/Tests/Editor/Editor/UVEditorTest.cs index 49a685ada..4f6578573 100644 --- a/Tests/Editor/Editor/UVEditorTest.cs +++ b/Tests/Editor/Editor/UVEditorTest.cs @@ -9,7 +9,8 @@ using UnityEngine.ProBuilder.Shapes; using EditorUtility = UnityEditor.ProBuilder.EditorUtility; -public class UVEditorWindow +[TestFixture] +public class UVEditorTests { ProBuilderMesh m_cube; @@ -18,7 +19,7 @@ public void Setup() { m_cube = ShapeFactory.Instantiate(); EditorUtility.InitObject(m_cube); - // Unsure UV bounds origin is not at (0,0) lower left + // Unsure UV bounds origin is not at (0,0) lower left foreach (var face in m_cube.facesInternal) face.uv = new AutoUnwrapSettings(face.uv) { anchor = AutoUnwrapSettings.Anchor.UpperLeft, offset = new Vector2(-0.5f, -0.5f) }; m_cube.RefreshUV(m_cube.faces); @@ -31,8 +32,29 @@ public void Setup() [TearDown] public void Cleanup() { + // Close the UV Editor window first + if (UVEditor.instance != null) + { + UVEditor.instance.Close(); + } + + // Clear ProBuilder selections + MeshSelection.ClearElementSelection(); + Selection.activeGameObject = null; + + // Reset tool context ToolManager.SetActiveContext(); - UObject.DestroyImmediate(m_cube.gameObject); + + // Destroy the cube + if (m_cube != null && m_cube.gameObject != null) + { + UObject.DestroyImmediate(m_cube.gameObject); + } + + m_cube = null; + + // Clear undo to prevent resurrection + Undo.ClearAll(); } [Test] @@ -103,4 +125,166 @@ public void Manual_PlanarProjection() minimalUV = UVEditor.instance.UVSelectionMinimalUV(); Assert.That(minimalUV, Is.EqualTo(UVEditor.LowerLeft)); } + + /// + /// Test that moving a single unconnected face doesn't cause distortion + /// + [Test] + public void MoveSingleFace_PreservesRelativePositions() + { + // Setup: One face in manual UV mode, NOT in a texture group + var face0 = m_cube.facesInternal[0]; + + face0.manualUV = false; + face0.textureGroup = -1; // No texture group (isolated face) + + m_cube.ToMesh(); + m_cube.Refresh(); + + // Select the face + MeshSelection.SetSelection(m_cube.gameObject); + m_cube.SetSelectedFaces(new Face[] { face0 }); + MeshSelection.OnObjectSelectionChanged(); + + // Capture initial UV positions + var face0InitialUVs = UnityEngine.ProBuilder.ArrayUtility.ValuesWithIndexes( + m_cube.texturesInternal, face0.distinctIndexesInternal); + + // Calculate initial relative offsets within the face + Vector2[] face0InitialOffsets = new Vector2[face0InitialUVs.Length]; + for (int i = 0; i < face0InitialUVs.Length; i++) + face0InitialOffsets[i] = face0InitialUVs[i] - face0InitialUVs[0]; + + // Simulate a move operation + Vector2 moveDelta = new Vector2(0.1f, 0.2f); + UVEditor.instance.SceneMoveTool(moveDelta); + + // Get final UV positions + var face0FinalUVs = UnityEngine.ProBuilder.ArrayUtility.ValuesWithIndexes( + m_cube.texturesInternal, face0.distinctIndexesInternal); + + // TEST 1: Verify each vertex moved by the delta + for (int i = 0; i < face0InitialUVs.Length; i++) + { + Assert.That(face0FinalUVs[i].x, Is.EqualTo(face0InitialUVs[i].x + moveDelta.x).Within(0.0001f), + $"Vertex {i} X should move by delta"); + Assert.That(face0FinalUVs[i].y, Is.EqualTo(face0InitialUVs[i].y + moveDelta.y).Within(0.0001f), + $"Vertex {i} Y should move by delta"); + } + + // TEST 2: Verify relative offsets within the face are preserved (no distortion) + for (int i = 0; i < face0FinalUVs.Length; i++) + { + Vector2 finalOffset = face0FinalUVs[i] - face0FinalUVs[0]; + Assert.That(finalOffset.x, Is.EqualTo(face0InitialOffsets[i].x).Within(0.0001f), + $"Vertex {i} relative X offset changed - face was distorted!"); + Assert.That(finalOffset.y, Is.EqualTo(face0InitialOffsets[i].y).Within(0.0001f), + $"Vertex {i} relative Y offset changed - face was distorted!"); + } + + // Cleanup + UVEditor.instance.OnFinishUVModification(); + } + + [Test] + public void MoveConnectedFaces_PreservesRelativePositions() + { + // Setup: Two faces in the same texture group (Auto UV mode) + var face0 = m_cube.facesInternal[0]; + var face1 = m_cube.facesInternal[1]; + + face0.manualUV = false; + face1.manualUV = false; + face0.textureGroup = 1; + face1.textureGroup = 1; + + m_cube.ToMesh(); + m_cube.Refresh(); + + // Select ONLY face0 + MeshSelection.SetSelection(m_cube.gameObject); + m_cube.SetSelectedFaces(new Face[] { face0 }); + MeshSelection.OnObjectSelectionChanged(); + + // Capture initial UV positions of BOTH faces + var face0InitialUVs = UnityEngine.ProBuilder.ArrayUtility.ValuesWithIndexes( + m_cube.texturesInternal, face0.distinctIndexesInternal); + var face1InitialUVs = UnityEngine.ProBuilder.ArrayUtility.ValuesWithIndexes( + m_cube.texturesInternal, face1.distinctIndexesInternal); + + // Calculate initial relative offsets within each face + Vector2[] face0InitialOffsets = new Vector2[face0InitialUVs.Length]; + for (int i = 0; i < face0InitialUVs.Length; i++) + face0InitialOffsets[i] = face0InitialUVs[i] - face0InitialUVs[0]; + + Vector2[] face1InitialOffsets = new Vector2[face1InitialUVs.Length]; + for (int i = 0; i < face1InitialUVs.Length; i++) + face1InitialOffsets[i] = face1InitialUVs[i] - face1InitialUVs[0]; + + // Calculate initial distance between the two faces + Vector2 face0InitialCenter = Bounds2D.Center(face0InitialUVs); + Vector2 face1InitialCenter = Bounds2D.Center(face1InitialUVs); + Vector2 initialCenterDistance = face1InitialCenter - face0InitialCenter; + + // Simulate a move operation + Vector2 moveDelta = new Vector2(0.1f, 0.2f); + UVEditor.instance.SceneMoveTool(moveDelta); + + // Get final UV positions + var face0FinalUVs = UnityEngine.ProBuilder.ArrayUtility.ValuesWithIndexes( + m_cube.texturesInternal, face0.distinctIndexesInternal); + var face1FinalUVs = UnityEngine.ProBuilder.ArrayUtility.ValuesWithIndexes( + m_cube.texturesInternal, face1.distinctIndexesInternal); + + // TEST 1: Verify each vertex in face0 moved by the delta + for (int i = 0; i < face0InitialUVs.Length; i++) + { + Assert.That(face0FinalUVs[i].x, Is.EqualTo(face0InitialUVs[i].x + moveDelta.x).Within(0.0001f), + $"Face0 vertex {i} X should move by delta"); + Assert.That(face0FinalUVs[i].y, Is.EqualTo(face0InitialUVs[i].y + moveDelta.y).Within(0.0001f), + $"Face0 vertex {i} Y should move by delta"); + } + + // TEST 2: Verify each vertex in face1 moved by the delta (auto-selected) + for (int i = 0; i < face1InitialUVs.Length; i++) + { + Assert.That(face1FinalUVs[i].x, Is.EqualTo(face1InitialUVs[i].x + moveDelta.x).Within(0.0001f), + $"Face1 vertex {i} X should move by delta"); + Assert.That(face1FinalUVs[i].y, Is.EqualTo(face1InitialUVs[i].y + moveDelta.y).Within(0.0001f), + $"Face1 vertex {i} Y should move by delta"); + } + + // TEST 3: Verify relative offsets within face0 are preserved (no distortion) + for (int i = 0; i < face0FinalUVs.Length; i++) + { + Vector2 finalOffset = face0FinalUVs[i] - face0FinalUVs[0]; + Assert.That(finalOffset.x, Is.EqualTo(face0InitialOffsets[i].x).Within(0.0001f), + $"Face0 vertex {i} relative X offset changed - face was distorted!"); + Assert.That(finalOffset.y, Is.EqualTo(face0InitialOffsets[i].y).Within(0.0001f), + $"Face0 vertex {i} relative Y offset changed - face was distorted!"); + } + + // TEST 4: Verify relative offsets within face1 are preserved (no distortion) + for (int i = 0; i < face1FinalUVs.Length; i++) + { + Vector2 finalOffset = face1FinalUVs[i] - face1FinalUVs[0]; + Assert.That(finalOffset.x, Is.EqualTo(face1InitialOffsets[i].x).Within(0.0001f), + $"Face1 vertex {i} relative X offset changed - face was distorted!"); + Assert.That(finalOffset.y, Is.EqualTo(face1InitialOffsets[i].y).Within(0.0001f), + $"Face1 vertex {i} relative Y offset changed - face was distorted!"); + } + + // TEST 5: Verify the distance between face centers is preserved + Vector2 face0FinalCenter = Bounds2D.Center(face0FinalUVs); + Vector2 face1FinalCenter = Bounds2D.Center(face1FinalUVs); + Vector2 finalCenterDistance = face1FinalCenter - face0FinalCenter; + + Assert.That(finalCenterDistance.x, Is.EqualTo(initialCenterDistance.x).Within(0.0001f), + "Distance between face centers X changed - faces were recentered!"); + Assert.That(finalCenterDistance.y, Is.EqualTo(initialCenterDistance.y).Within(0.0001f), + "Distance between face centers Y changed - faces were recentered!"); + + // Cleanup + UVEditor.instance.OnFinishUVModification(); + } } From 3b61463de269b80512bb01890b4da48cfa6cb1e6 Mon Sep 17 00:00:00 2001 From: varinotmUnity Date: Fri, 14 Nov 2025 13:46:10 -0500 Subject: [PATCH 2/2] add changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22b6dba09..138b875c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - [PBLD-240] Fixed a bug where buttons for "Create Cube" and "Create PolyShape" appeared incorrectly on Light theme. - [PBLD-258] Fixed an bug where clicking a highlighted edge might select a hidden edge instead. - [PBLD-262] Fixed a bug in the deep cycling of face selection where faces from hidden meshes would get prioritized +- [PBLD-276] Fixed a bug where dragging a single face from a connected texture group in the UV Editor caused visual distortion during the drag operation ## [6.0.7] - 2025-08-28