Skip to content

Commit 4226148

Browse files
Fixed issue with edge selection. (#625)
* Fixed issue with edge selection. * add changelog * Update following PR comments
1 parent 131323b commit 4226148

File tree

3 files changed

+129
-38
lines changed

3 files changed

+129
-38
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1010
### Fixed
1111

1212
- [PBLD-240] Fixed a bug where buttons for "Create Cube" and "Create PolyShape" appeared incorrectly on Light theme.
13+
- [PBLD-258] Fixed an bug where clicking a highlighted edge might select a hidden edge instead.
1314

1415
## [6.0.7] - 2025-08-28
1516

Editor/EditorCore/EditorSceneViewPicker.cs

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -639,13 +639,38 @@ static float EdgeRaycast(Vector3 mousePosition, ScenePickerPreferences pickerPre
639639
bool highlightedEdgeExists = ProBuilderEditor.instance.hovering.edges.Count > 0;
640640

641641
// if there is an edge already highlighted, we don't want to select a different game object
642-
if (!highlightedEdgeExists)
642+
if (highlightedEdgeExists)
643643
{
644-
selection.gameObject = HandleUtility.PickGameObject(mousePosition, false);
644+
ProBuilderMesh hoveredMeshFromHighlight = ProBuilderEditor.instance.hovering.mesh;
645+
if (hoveredMeshFromHighlight != null && ProBuilderEditor.instance.hovering.edges.Count > 0)
646+
{
647+
Edge highlightedEdge = ProBuilderEditor.instance.hovering.edges[0]; // Assuming single edge hover
648+
649+
// Calculate the screen distance for this specific highlighted edge.
650+
Vector3[] positions = hoveredMeshFromHighlight.positionsInternal;
651+
Vector3 worldPosA = hoveredMeshFromHighlight.transform.TransformPoint(positions[highlightedEdge.a]);
652+
Vector3 worldPosB = hoveredMeshFromHighlight.transform.TransformPoint(positions[highlightedEdge.b]);
653+
654+
if (ProcessEdgePoints(Camera.current, worldPosA, worldPosB, out Vector3 guiPointA, out Vector3 guiPointB))
655+
{
656+
float distToHighlightedEdge = HandleUtility.DistancePointLine(mousePosition, guiPointA, guiPointB);
657+
658+
// If the highlighted edge is within the maximum picking distance, select it and return early.
659+
if (distToHighlightedEdge <= ScenePickerPreferences.maxPointerDistance)
660+
{
661+
selection.gameObject = hoveredMeshFromHighlight.gameObject;
662+
selection.mesh = hoveredMeshFromHighlight;
663+
selection.SetSingleEdge(highlightedEdge);
664+
return distToHighlightedEdge;
665+
}
666+
}
667+
}
645668
}
646-
var hoveredMesh = selection.gameObject != null ? selection.gameObject.GetComponent<ProBuilderMesh>() : null;
647669

648670
float bestDistance = Mathf.Infinity;
671+
selection.gameObject = HandleUtility.PickGameObject(mousePosition, false);
672+
var hoveredMesh = selection.gameObject != null ? selection.gameObject.GetComponent<ProBuilderMesh>() : null;
673+
649674
bool hoveredIsInSelection = MeshSelection.topInternal.Contains(hoveredMesh);
650675

651676
if (hoveredMesh != null && (allowUnselected || hoveredIsInSelection))
@@ -659,7 +684,8 @@ static float EdgeRaycast(Vector3 mousePosition, ScenePickerPreferences pickerPre
659684
selection.SetSingleEdge(tup.edge);
660685
bestDistance = tup.distance;
661686

662-
// If the nearest edge was acquired by a raycast, then the distance to mesh is 0f.
687+
// If the nearest edge was acquired by a raycast on an already selected mesh,
688+
// return early to prioritize it.
663689
if (hoveredIsInSelection)
664690
return tup.distance;
665691
}

Tests/Editor/Picking/EdgePickerTests.cs

Lines changed: 98 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections;
23
using System.Linq;
34
using UnityEngine;
45
using UObject = UnityEngine.Object;
@@ -25,13 +26,15 @@ public void Setup()
2526
window.Focus();
2627

2728
m_Camera = new GameObject("TestCamera", typeof(Camera)).GetComponent<Camera>();
28-
m_Camera.transform.position = new Vector3(0, 0, -10);
29+
m_Camera.transform.position = new Vector3(0, 3, 0);
2930
m_Camera.transform.LookAt(Vector3.zero);
3031
m_Camera.orthographic = false;
3132
m_Camera.cullingMask = ~0;
33+
m_Camera.fieldOfView = 60f;
34+
MatchSceneViewToCamera(m_Camera);
3235

33-
m_Mesh = ShapeFactory.Instantiate<UnityEngine.ProBuilder.Shapes.Plane>();
34-
m_Mesh.name = "TestPlane";
36+
m_Mesh = ShapeFactory.Instantiate<UnityEngine.ProBuilder.Shapes.Cube>();
37+
m_Mesh.name = "TestCube";
3538
m_Mesh.transform.position = Vector3.zero;
3639
m_Mesh.transform.rotation = Quaternion.identity;
3740
m_Mesh.Refresh();
@@ -83,23 +86,28 @@ private Event CreateMouseEvent(Vector3 mousePosition, EventType type = EventType
8386
return evt;
8487
}
8588

86-
[Test]
87-
public void EdgePicker_PicksVisibleEdge()
89+
[UnityTest]
90+
public IEnumerator EdgePicker_PicksVisibleEdge()
8891
{
89-
Edge edgeToPick = m_Mesh.facesInternal[0].edgesInternal[0];
92+
Edge edgeToPick = m_Mesh.facesInternal[4].edgesInternal[0];
9093

9194
Vector3 pA_world = m_Mesh.transform.TransformPoint(m_Mesh.positionsInternal[edgeToPick.a]);
9295
Vector3 pB_world = m_Mesh.transform.TransformPoint(m_Mesh.positionsInternal[edgeToPick.b]);
9396

9497
Vector3 centerOfEdge_world = (pA_world + pB_world) / 2f;
9598
Vector2 mousePos = UnityEditor.HandleUtility.WorldToGUIPoint(centerOfEdge_world);
9699

100+
yield return null;
101+
97102
UnityEngine.TestTools.LogAssert.ignoreFailingMessages = true;
98103
EditorSceneViewPicker.DoMouseClick(
99104
CreateMouseEvent(mousePos, EventType.MouseDown, EventModifiers.None),
100105
SelectMode.Edge,
101106
m_PickerPreferences);
102107

108+
109+
yield return null;
110+
103111
SceneSelection currentSelection = EditorSceneViewPicker.selection;
104112

105113
Assert.IsNotNull(currentSelection.mesh, "A mesh should be selected.");
@@ -109,8 +117,39 @@ public void EdgePicker_PicksVisibleEdge()
109117
Assert.AreEqual(edgeToPick, currentSelection.edges.First(), "The expected edge should be picked.");
110118
}
111119

112-
[Test]
113-
public void EdgePicker_DoesNotPickWhenNotHovering()
120+
[UnityTest]
121+
public IEnumerator EdgePicker_DoesntPickHiddenEdge()
122+
{
123+
Edge edgeToPick = m_Mesh.facesInternal[0].edgesInternal[0];
124+
125+
Vector3 pA_world = m_Mesh.transform.TransformPoint(m_Mesh.positionsInternal[edgeToPick.a]);
126+
Vector3 pB_world = m_Mesh.transform.TransformPoint(m_Mesh.positionsInternal[edgeToPick.b]);
127+
128+
Vector3 centerOfEdge_world = (pA_world + pB_world) / 2f;
129+
Vector2 mousePos = UnityEditor.HandleUtility.WorldToGUIPoint(centerOfEdge_world);
130+
131+
yield return null;
132+
133+
UnityEngine.TestTools.LogAssert.ignoreFailingMessages = true;
134+
EditorSceneViewPicker.DoMouseClick(
135+
CreateMouseEvent(mousePos, EventType.MouseDown, EventModifiers.None),
136+
SelectMode.Edge,
137+
m_PickerPreferences);
138+
139+
140+
yield return null;
141+
142+
SceneSelection currentSelection = EditorSceneViewPicker.selection;
143+
144+
Assert.IsNotNull(currentSelection.mesh, "A mesh should be selected.");
145+
Assert.AreEqual(m_Mesh, currentSelection.mesh, "The correct mesh should be selected.");
146+
Assert.IsNotNull(currentSelection.edges, "Edges collection should not be null.");
147+
Assert.AreEqual(1, currentSelection.edges.Count, "Exactly one edge should be selected.");
148+
Assert.AreEqual(m_Mesh.facesInternal[4].edgesInternal[0], currentSelection.edges.First(), "The expected edge should be picked.");
149+
}
150+
151+
[UnityTest]
152+
public IEnumerator EdgePicker_DoesNotPickWhenNotHovering()
114153
{
115154
Vector2 mousePos = new Vector2(Screen.width / 2f, Screen.height / 2f + 500f);
116155

@@ -120,31 +159,32 @@ public void EdgePicker_DoesNotPickWhenNotHovering()
120159
SelectMode.Edge,
121160
m_PickerPreferences);
122161

162+
yield return null;
163+
123164
SceneSelection currentSelection = EditorSceneViewPicker.selection;
124165

125166
Assert.IsNull(currentSelection.mesh, "No mesh should be selected.");
126167
Assert.IsEmpty(currentSelection.edges, "No edges should be selected.");
127168
}
128169

129-
[Test]
130-
public void EdgePicker_PicksCorrectEdgeWithMultipleOverlapping()
170+
[UnityTest]
171+
public IEnumerator EdgePicker_PicksCorrectEdgeWithMultipleOverlapping()
131172
{
132-
m_Camera.transform.position = new Vector3(0.5f, 0.5f, -10);
133-
m_Camera.transform.LookAt(Vector3.zero);
173+
MatchSceneViewToCamera(m_Camera);
134174

135-
ProBuilderMesh mesh2 = ShapeFactory.Instantiate<UnityEngine.ProBuilder.Shapes.Plane>();
136-
mesh2.name = "TestPlane2";
137-
mesh2.transform.position = new Vector3(0.1f, 0.1f, 0.5f);
175+
ProBuilderMesh mesh2 = ShapeFactory.Instantiate<UnityEngine.ProBuilder.Shapes.Cube>();
176+
mesh2.name = "TestCube2";
177+
mesh2.transform.position = new Vector3(0f, 1f, 0f);
138178
mesh2.Refresh();
139179

140180
var meshCollider2 = mesh2.gameObject.GetComponent<MeshCollider>();
141181
if (meshCollider2 == null) meshCollider2 = mesh2.gameObject.AddComponent<MeshCollider>();
142182
meshCollider2.sharedMesh = mesh2.mesh;
143183
meshCollider2.enabled = true;
144184

145-
MeshSelection.AddToSelection(mesh2.gameObject);
185+
yield return null;
146186

147-
Edge edge1 = m_Mesh.facesInternal[0].edgesInternal[0];
187+
Edge edge1 = m_Mesh.facesInternal[0].edgesInternal[3];
148188
Vector3 pA1_world = m_Mesh.transform.TransformPoint(m_Mesh.positionsInternal[edge1.a]);
149189
Vector3 pB1_world = m_Mesh.transform.TransformPoint(m_Mesh.positionsInternal[edge1.b]);
150190
Vector3 center1_world = (pA1_world + pB1_world) / 2f;
@@ -157,6 +197,8 @@ public void EdgePicker_PicksCorrectEdgeWithMultipleOverlapping()
157197
SelectMode.Edge,
158198
m_PickerPreferences);
159199

200+
yield return null;
201+
160202
SceneSelection currentSelection = EditorSceneViewPicker.selection;
161203

162204
Assert.IsNotNull(currentSelection.mesh, "An edge should be picked.");
@@ -168,49 +210,51 @@ public void EdgePicker_PicksCorrectEdgeWithMultipleOverlapping()
168210
UObject.DestroyImmediate(mesh2.gameObject);
169211
}
170212

171-
[Test]
172-
public void EdgePicker_PicksClippedEdge_OnePointBehindCamera()
213+
[UnityTest]
214+
public IEnumerator EdgePicker_PicksClippedEdge_OnePointBehindCamera()
173215
{
174-
175-
m_Camera.nearClipPlane = 0.1f;
176-
m_Camera.transform.position = new Vector3(0, 0, -0.5f);
177-
m_Camera.transform.LookAt(Vector3.zero);
178-
179-
Edge targetEdge = m_Mesh.facesInternal[0].edgesInternal[0];
180-
181-
Vector3[] currentLocalPositions = m_Mesh.positionsInternal;
182-
currentLocalPositions[targetEdge.a] = new Vector3(currentLocalPositions[targetEdge.a].x, currentLocalPositions[targetEdge.a].y, -0.6f);
183-
216+
m_Mesh.transform.position = new Vector3(0, 0, 0);
217+
m_Mesh.transform.localScale = new Vector3(1, 30, 1);
184218
m_Mesh.Rebuild();
185219

186220
m_Mesh.GetComponent<MeshCollider>().sharedMesh = m_Mesh.mesh;
187221
m_Mesh.GetComponent<MeshCollider>().enabled = true;
188222

189-
Vector3 testPointOnClippedEdge_world = m_Mesh.transform.TransformPoint(-0.5f, 0, -0.3f);
190-
Vector2 mousePos = UnityEditor.HandleUtility.WorldToGUIPoint(testPointOnClippedEdge_world);
223+
yield return null;
224+
225+
Edge edgeToTest = m_Mesh.facesInternal[3].edgesInternal[2];
226+
Vector3 pA_world = m_Mesh.transform.TransformPoint(m_Mesh.positionsInternal[edgeToTest.a]);
227+
Vector3 pB_world = m_Mesh.transform.TransformPoint(m_Mesh.positionsInternal[edgeToTest.b]);
228+
229+
Vector3 centerOfEdge_world = (pA_world + pB_world) / 2f;
230+
Vector2 mousePos = UnityEditor.HandleUtility.WorldToGUIPoint(centerOfEdge_world);
191231

192232
UnityEngine.TestTools.LogAssert.ignoreFailingMessages = true;
193233
EditorSceneViewPicker.DoMouseClick(
194234
CreateMouseEvent(mousePos, EventType.MouseDown, EventModifiers.None),
195235
SelectMode.Edge,
196236
m_PickerPreferences);
197237

238+
yield return null;
239+
198240
SceneSelection currentSelection = EditorSceneViewPicker.selection;
199241

200242
Assert.IsNotNull(currentSelection.mesh, "Clipped edge should be picked.");
201243
Assert.AreEqual(m_Mesh, currentSelection.mesh, "The correct mesh should be selected.");
202244
Assert.IsNotNull(currentSelection.edges, "Edges collection should not be null.");
203245
Assert.AreEqual(1, currentSelection.edges.Count, "Exactly one edge should be selected.");
204-
Assert.AreEqual(targetEdge, currentSelection.edges.First(), "The expected clipped edge should be picked.");
246+
Assert.AreEqual(edgeToTest, currentSelection.edges.First(), "The expected clipped edge should be picked.");
205247
}
206248

207-
[Test]
208-
public void EdgePicker_DoesNotPickEdge_BothPointsBehindCamera()
249+
[UnityTest]
250+
public IEnumerator EdgePicker_DoesNotPickEdge_BothPointsBehindCamera()
209251
{
210252
m_Camera.nearClipPlane = 0.1f;
211253
m_Camera.transform.position = new Vector3(0, 0, -0.5f);
212254
m_Camera.transform.LookAt(Vector3.zero);
213255

256+
MatchSceneViewToCamera(m_Camera);
257+
214258
m_Mesh.transform.position = new Vector3(0, 0, -1.0f);
215259
m_Mesh.Refresh();
216260

@@ -224,16 +268,36 @@ public void EdgePicker_DoesNotPickEdge_BothPointsBehindCamera()
224268
Vector3 centerOfEdge_world = (pA_world + pB_world) / 2f;
225269
Vector2 mousePos = UnityEditor.HandleUtility.WorldToGUIPoint(centerOfEdge_world);
226270

271+
yield return null;
272+
227273
UnityEngine.TestTools.LogAssert.Expect("Handles.GetClosestPickingID called outside an editor OnGUI");
228274
UnityEngine.TestTools.LogAssert.Expect("Assertion failed on expression: 'device.IsInsideFrame()'");
229275
EditorSceneViewPicker.DoMouseClick(
230276
CreateMouseEvent(mousePos, EventType.MouseDown, EventModifiers.None),
231277
SelectMode.Edge,
232278
m_PickerPreferences);
233279

280+
yield return null;
281+
234282
SceneSelection currentSelection = EditorSceneViewPicker.selection;
235283

236284
Assert.IsNull(currentSelection.mesh, "No mesh should be selected.");
237285
Assert.IsEmpty(currentSelection.edges, "No edges should be selected.");
238286
}
287+
288+
private void MatchSceneViewToCamera(Camera cam)
289+
{
290+
var sceneView = SceneView.lastActiveSceneView;
291+
if (sceneView == null)
292+
sceneView = EditorWindow.GetWindow<SceneView>();
293+
294+
sceneView.in2DMode = false;
295+
sceneView.orthographic = cam.orthographic;
296+
sceneView.size = cam.orthographicSize;
297+
sceneView.pivot = cam.transform.position + cam.transform.forward * 10f; // pivot is center of view
298+
sceneView.rotation = cam.transform.rotation;
299+
sceneView.cameraSettings.fieldOfView = cam.fieldOfView;
300+
301+
sceneView.Repaint();
302+
}
239303
}

0 commit comments

Comments
 (0)