Skip to content

Commit 1dec924

Browse files
Merge branch 'master' into bugfix/edge-hover-selection-fix
2 parents adff0cd + 8dfd61f commit 1dec924

File tree

5 files changed

+470
-47
lines changed

5 files changed

+470
-47
lines changed

CHANGELOG.md

Lines changed: 2 additions & 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-242] Fixed a bug where edges were being incorrectly selected when one or both vertices were behind the camera's near plane, causing flipped lines and inconsistent selection behavior.
13+
- [PBLD-226] Fixed a bug where ProBuilder faces could not selected when obscured by another GameObject
1314

1415
## [6.0.6] - 2025-07-01
1516

@@ -27,6 +28,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
2728
- [PBLD-222] Fixed crash by preventing user from probuilderizing a gameobject that has isPartOfStaticBatch set to true.
2829
- [PBLD-245] Fixed warnings about obsolete API usage when using Unity 6.2 and later. Updated the API usage where the alternatives were available in Unity 2022.3.
2930

31+
3032
## [6.0.5] - 2025-03-11
3133

3234
### Changes

Editor/EditorCore/EditorSceneViewPicker.cs

Lines changed: 79 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -406,28 +406,32 @@ internal static float MouseRayHitTest(
406406
return FaceRaycast(mousePosition, pickerOptions, allowUnselected, selection, 0, true);
407407
}
408408

409+
static List<(ProBuilderMesh mesh, Face face, float dist, int hash)> s_PbHits = new List<(ProBuilderMesh, Face, float, int)>();
410+
409411
static float FaceRaycast(Vector3 mousePosition,
410412
ScenePickerPreferences pickerOptions,
411413
bool allowUnselected,
412414
SceneSelection selection,
413415
int deepClickOffset = 0,
414416
bool isPreview = true)
415417
{
416-
GameObject pickedGo = null;
417-
ProBuilderMesh pickedPb = null;
418-
Face pickedFace = null;
419-
420-
int newHash = 0;
418+
GameObject candidateGo = null;
419+
ProBuilderMesh candidatePb = null;
420+
Face candidateFace = null;
421+
float candidateDistance = Mathf.Infinity;
422+
float resultDistance = Mathf.Infinity;
421423

422424
// If any event modifiers are engaged don't cycle the deep click
423-
EventModifiers em = Event.current.modifiers;
425+
EventModifiers em = EventModifiers.None;
426+
if (Event.current != null)
427+
em = Event.current.modifiers;
424428

425429
// Reset cycle if we used an event modifier previously.
426430
// Move state back to single selection.
427431
if ((em != EventModifiers.None) != s_AppendModifierPreviousState)
428432
{
429433
s_AppendModifierPreviousState = (em != EventModifiers.None);
430-
s_DeepSelectionPrevious = newHash;
434+
s_DeepSelectionPrevious = 0;
431435
}
432436

433437
if (isPreview || em != EventModifiers.None)
@@ -437,13 +441,11 @@ static float FaceRaycast(Vector3 mousePosition,
437441

438442
selection.Clear();
439443

440-
float distance = Mathf.Infinity;
441-
442-
for (int i = 0, next = 0, pickedCount = s_OverlappingGameObjects.Count; i < pickedCount; i++)
444+
// First, find all ProBuilder meshes and their hit faces under the mouse
445+
for (int i = 0, pickedCount = s_OverlappingGameObjects.Count; i < pickedCount; i++)
443446
{
444447
var go = s_OverlappingGameObjects[i];
445448
var mesh = go.GetComponent<ProBuilderMesh>();
446-
Face face = null;
447449

448450
if (mesh != null && (allowUnselected || MeshSelection.topInternal.Contains(mesh)))
449451
{
@@ -456,59 +458,91 @@ static float FaceRaycast(Vector3 mousePosition,
456458
Mathf.Infinity,
457459
pickerOptions.cullMode))
458460
{
459-
face = mesh.facesInternal[hit.face];
460-
distance = Vector2.SqrMagnitude(((Vector2)mousePosition) - HandleUtility.WorldToGUIPoint(mesh.transform.TransformPoint(hit.point)));
461+
Face face = mesh.facesInternal[hit.face];
462+
float dist = Vector2.SqrMagnitude(((Vector2)mousePosition) - HandleUtility.WorldToGUIPoint(mesh.transform.TransformPoint(hit.point)));
463+
s_PbHits.Add((mesh, face, dist, face.GetHashCode()));
461464
}
462465
}
466+
}
463467

464-
// pb_Face doesn't define GetHashCode, meaning it falls to object.GetHashCode (reference comparison)
465-
int hash = face == null ? go.GetHashCode() : face.GetHashCode();
468+
if (s_PbHits.Count > 0)
469+
{
470+
// Sort ProBuilder hits by distance (closest first)
471+
s_PbHits.Sort((a, b) => a.dist.CompareTo(b.dist));
466472

467-
if (s_DeepSelectionPrevious == hash)
468-
next = (i + (1 + deepClickOffset)) % pickedCount;
473+
int chosenIndex = 0;
469474

470-
if (next == i)
475+
// Apply deep click cycling logic only if it's an actual click and a previous selection exists
476+
if (!isPreview && s_DeepSelectionPrevious != 0)
471477
{
472-
pickedGo = go;
473-
pickedPb = mesh;
474-
pickedFace = face;
478+
int currentSelectionIndex = -1;
479+
// Find the index of the previously selected item (if it's still in the list)
480+
for (int i = 0; i < s_PbHits.Count; i++)
481+
{
482+
if (s_PbHits[i].hash == s_DeepSelectionPrevious)
483+
{
484+
currentSelectionIndex = i;
485+
break;
486+
}
487+
}
488+
489+
if (currentSelectionIndex != -1)
490+
{
491+
// Calculate the next index for cycling
492+
chosenIndex = (currentSelectionIndex + (1 + deepClickOffset)) % s_PbHits.Count;
493+
// Handle negative result from modulo for deepClickOffset = -1 if currentSelectionIndex is 0
494+
if (chosenIndex < 0) chosenIndex += s_PbHits.Count;
495+
}
496+
// If s_DeepSelectionPrevious was set but no matching PB hit is found in current list,
497+
// fall back to the closest one (chosenIndex remains 0)
498+
}
499+
// else for first click or if s_DeepSelectionPrevious is 0, chosenIndex remains 0 (selects closest)
475500

476-
newHash = hash;
501+
var selectedHit = s_PbHits[chosenIndex];
502+
candidateGo = selectedHit.mesh.gameObject;
503+
candidatePb = selectedHit.mesh;
504+
candidateFace = selectedHit.face;
505+
candidateDistance = selectedHit.dist;
477506

478-
// a prior hash was matched, this is the next. if
479-
// it's just the first iteration don't break (but do
480-
// set the default).
481-
if (next != 0)
482-
break;
507+
// Update s_DeepSelectionPrevious only for actual clicks after cycling/filtering (not hovers)
508+
if (!isPreview)
509+
{
510+
s_DeepSelectionPrevious = selectedHit.hash;
511+
}
512+
}
513+
else // No ProBuilder meshes were hit, fallback to standard GameObject picking
514+
{
515+
// This means the mouse is over a non-ProBuilder GameObject, or nothing at all.
516+
// We should still allow picking of that topmost non-ProBuilder GameObject.
517+
GameObject topmostGo = HandleUtility.PickGameObject(mousePosition, false);
518+
if (topmostGo != null)
519+
{
520+
candidateGo = topmostGo;
521+
candidateDistance = 0f; // Indicate a direct hit (distance not relevant for non-PB pick)
522+
s_DeepSelectionPrevious = 0; // Reset deep selection if a non-PB object is picked
483523
}
484524
}
485525

486-
if (!isPreview)
487-
s_DeepSelectionPrevious = newHash;
488-
489-
if (pickedGo != null)
526+
// Final selection update
527+
if (candidateGo != null)
490528
{
491-
Event.current.Use();
529+
Event.current?.Use();
530+
531+
selection.gameObject = candidateGo;
532+
resultDistance = Mathf.Sqrt(candidateDistance);
492533

493-
if (pickedPb != null)
534+
if (candidatePb != null)
494535
{
495-
if (pickedPb.selectable)
536+
if (candidatePb.selectable)
496537
{
497-
selection.gameObject = pickedGo;
498-
selection.mesh = pickedPb;
499-
selection.SetSingleFace(pickedFace);
500-
501-
return Mathf.Sqrt(distance);
538+
selection.mesh = candidatePb;
539+
selection.SetSingleFace(candidateFace);
502540
}
503541
}
504-
505-
// If clicked off a pb_Object but onto another gameobject, set the selection
506-
// and dip out.
507-
selection.gameObject = pickedGo;
508-
return Mathf.Sqrt(distance);
509542
}
510543

511-
return distance;
544+
s_PbHits.Clear();
545+
return resultDistance;
512546
}
513547

514548
static float VertexRaycast(Vector3 mousePosition, ScenePickerPreferences pickerOptions, bool allowUnselected, SceneSelection selection)

Tests/Editor/Export/ExportObj.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ public static void ExportSingleCube_CreatesUnityReadableMeshFile(
8080

8181
Assume.That(imported, Is.Not.Null);
8282
Assert.That(imported.vertexCount, Is.GreaterThan(0));
83+
84+
Object.DestroyImmediate(cube.gameObject);
8385
}
8486

8587
[Test]
@@ -108,13 +110,17 @@ public static void ExportSingleCube_CreatesTextureFile()
108110
Assert.That(meshRenderer.sharedMaterials[0].mainTexture, Is.Not.Null);
109111
var mainTex = meshRenderer.sharedMaterials[0].mainTexture;
110112
Assert.That(AssetDatabase.GetAssetPath(mainTex), Is.EqualTo(TestUtility.temporarySavedAssetsDirectory+mainTex.name+".png"));
113+
114+
Object.DestroyImmediate(cube.gameObject);
111115
}
112116

113117
[Test]
114118
public static void ExportMultipleMeshes_CreatesModelWithTwoGroups()
115119
{
116-
var cube1 = new Model("Cube A", ShapeFactory.Instantiate<Cube>());
117-
var cube2 = new Model("Cube B", ShapeFactory.Instantiate<Cube>());
120+
var cubeA = ShapeFactory.Instantiate<Cube>();
121+
var cubeB = ShapeFactory.Instantiate<Cube>();
122+
var cube1 = new Model("Cube A", cubeA);
123+
var cube2 = new Model("Cube B", cubeB);
118124
string exportedPath = TestUtility.temporarySavedAssetsDirectory + "ObjGroup.obj";
119125

120126
UnityEditor.ProBuilder.Actions.ExportObj.DoExport(exportedPath, new Model[] { cube1, cube2 }, new ObjOptions()
@@ -133,5 +139,8 @@ public static void ExportMultipleMeshes_CreatesModelWithTwoGroups()
133139
var all = AssetDatabase.LoadAllAssetRepresentationsAtPath(exportedPath);
134140

135141
Assert.That(all.Count(x => x is Mesh), Is.EqualTo(2));
142+
143+
Object.DestroyImmediate(cubeA.gameObject);
144+
Object.DestroyImmediate(cubeB.gameObject);
136145
}
137146
}

0 commit comments

Comments
 (0)