Skip to content

Commit a4904b5

Browse files
authored
Merge pull request #10028 from Zee2/constraints-priority
Execution order/priority system for TransformConstraint
2 parents 7982594 + 8b60580 commit a4904b5

File tree

9 files changed

+237
-26
lines changed

9 files changed

+237
-26
lines changed

Assets/MRTK/SDK/Editor/Inspectors/UX/Constraints/ConstraintManagerInspector.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ private static EntryAction RenderManualConstraintItem(SerializedProperty constra
6666

6767
using (new EditorGUILayout.HorizontalScope())
6868
{
69-
EditorGUILayout.LabelField(constraint.GetType().Name, GUILayout.ExpandWidth(true));
69+
EditorGUILayout.LabelField($"Priority {(constraint as TransformConstraint).ExecutionPriority}: {constraint.GetType().Name}", GUILayout.ExpandWidth(true));
7070

7171
if (canRemove)
7272
{
@@ -107,7 +107,7 @@ private void RenderAutoConstraintMenu()
107107
using (new EditorGUILayout.HorizontalScope())
108108
{
109109
string constraintName = constraint.GetType().Name;
110-
EditorGUILayout.LabelField(constraintName);
110+
EditorGUILayout.LabelField($"Priority {(constraint as TransformConstraint).ExecutionPriority}: {constraint.GetType().Name}");
111111
if (GUILayout.Button("Go to component"))
112112
{
113113
Highlighter.Highlight("Inspector", $"{ObjectNames.NicifyVariableName(constraintName)} (Script)");

Assets/MRTK/SDK/Features/Input/Handlers/Constraints/ConstraintManager.cs

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public List<TransformConstraint> SelectedConstraints
4242
get => selectedConstraints;
4343
}
4444

45-
private HashSet<TransformConstraint> constraints = new HashSet<TransformConstraint>();
45+
private List<TransformConstraint> constraints = new List<TransformConstraint>();
4646
private MixedRealityTransform initialWorldPose;
4747

4848
/// <summary>
@@ -56,7 +56,7 @@ public bool AddConstraintToManualSelection(TransformConstraint constraint)
5656
var existingConstraint = selectedConstraints.Find(t => t == constraint);
5757
if (existingConstraint == null)
5858
{
59-
selectedConstraints.Add(constraint);
59+
ConstraintUtils.AddWithPriority(ref selectedConstraints, constraint, new ConstraintExecOrderComparer());
6060
}
6161

6262
return existingConstraint == null;
@@ -95,6 +95,15 @@ public void Initialize(MixedRealityTransform worldPose)
9595
}
9696
}
9797

98+
/// <summary>
99+
/// Re-sort list of constraints. Triggered by constraints
100+
/// when their execution order is modified at runtime.
101+
/// </summary>
102+
internal void RefreshPriorities()
103+
{
104+
constraints.Sort(new ConstraintExecOrderComparer());
105+
}
106+
98107
/// <summary>
99108
/// Registering of a constraint during runtime. This method gets called by the constraint
100109
/// components to auto register in their OnEnable method.
@@ -105,7 +114,7 @@ internal void AutoRegisterConstraint(TransformConstraint constraint)
105114
// add to auto component list
106115
if (constraint.isActiveAndEnabled)
107116
{
108-
constraints.Add(constraint);
117+
ConstraintUtils.AddWithPriority(ref constraints, constraint, new ConstraintExecOrderComparer());
109118
constraint.Initialize(initialWorldPose);
110119
}
111120
}
@@ -127,7 +136,7 @@ protected void Awake()
127136
{
128137
if (constraint.isActiveAndEnabled)
129138
{
130-
constraints.Add(constraint);
139+
ConstraintUtils.AddWithPriority(ref constraints, constraint, new ConstraintExecOrderComparer());
131140
}
132141
}
133142
}
@@ -137,30 +146,20 @@ private void ApplyConstraintsForType(ref MixedRealityTransform transform, bool i
137146
ManipulationHandFlags handMode = isOneHanded ? ManipulationHandFlags.OneHanded : ManipulationHandFlags.TwoHanded;
138147
ManipulationProximityFlags proximityMode = isNear ? ManipulationProximityFlags.Near : ManipulationProximityFlags.Far;
139148

140-
if (autoConstraintSelection)
149+
foreach (var constraint in constraints)
141150
{
142-
foreach (var constraint in constraints)
151+
// If on manual mode, filter executed constraints by which have been manually selected
152+
if (!autoConstraintSelection && !selectedConstraints.Contains(constraint))
143153
{
144-
if (constraint.isActiveAndEnabled &&
145-
constraint.ConstraintType == transformType &&
146-
constraint.HandType.HasFlag(handMode) &&
147-
constraint.ProximityType.HasFlag(proximityMode))
148-
{
149-
constraint.ApplyConstraint(ref transform);
150-
}
154+
continue;
151155
}
152-
}
153-
else
154-
{
155-
foreach (var constraint in selectedConstraints)
156+
157+
if (constraint.isActiveAndEnabled &&
158+
constraint.ConstraintType == transformType &&
159+
constraint.HandType.HasFlag(handMode) &&
160+
constraint.ProximityType.HasFlag(proximityMode))
156161
{
157-
if (constraint.isActiveAndEnabled &&
158-
constraint.ConstraintType == transformType &&
159-
constraint.HandType.HasFlag(handMode) &&
160-
constraint.ProximityType.HasFlag(proximityMode))
161-
{
162-
constraint.ApplyConstraint(ref transform);
163-
}
162+
constraint.ApplyConstraint(ref transform);
164163
}
165164
}
166165
}

Assets/MRTK/SDK/Features/Input/Handlers/Constraints/TransformConstraint.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,27 @@ public ManipulationProximityFlags ProximityType
4141
set => proximityType = value;
4242
}
4343

44+
[SerializeField]
45+
[Tooltip("Execution order priority of this constraint. Lower numbers will be executed before higher numbers.")]
46+
private int executionOrder = 0;
47+
48+
/// <summary>
49+
/// Execution order priority of this constraint. Lower numbers will be executed before higher numbers.
50+
/// </summary>
51+
public int ExecutionPriority
52+
{
53+
get => executionOrder;
54+
set {
55+
executionOrder = value;
56+
57+
// Notify all ConstraintManagers to re-sort these priorities.
58+
foreach(var mgr in gameObject.GetComponents<ConstraintManager>())
59+
{
60+
mgr.RefreshPriorities();
61+
}
62+
}
63+
}
64+
4465
protected MixedRealityTransform worldPoseOnManipulationStart;
4566

4667
public abstract TransformFlags ConstraintType { get; }
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using UnityEngine;
5+
using System.Collections.Generic;
6+
using Microsoft.MixedReality.Toolkit.UI;
7+
8+
namespace Microsoft.MixedReality.Toolkit.Utilities
9+
{
10+
/// <summary>
11+
/// Defines a comparer to sort TransformConstraints by their
12+
/// requested execution order, or any other priority
13+
/// mechanism that a subclass utilizes.
14+
/// </summary>
15+
internal class ConstraintExecOrderComparer : IComparer<TransformConstraint>
16+
{
17+
/// <returns>
18+
/// Returns < 0 if x should be executed first.
19+
/// Returns > 0 if y should be executed first.
20+
/// Returns = 0 if they are of equivalent execution priority.
21+
/// </returns>
22+
internal virtual int Compare(TransformConstraint x, TransformConstraint y)
23+
{
24+
return x.ExecutionPriority - y.ExecutionPriority;
25+
}
26+
}
27+
}

Assets/MRTK/SDK/Features/Utilities/ConstraintExecOrderComparer.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using UnityEngine;
5+
using System.Collections.Generic;
6+
using Microsoft.MixedReality.Toolkit.UI;
7+
8+
namespace Microsoft.MixedReality.Toolkit.Utilities
9+
{
10+
/// <summary>
11+
/// Utilities for the management of constraints.
12+
/// </summary>
13+
internal static class ConstraintUtils
14+
{
15+
/// <summary>
16+
/// Adds a constraint to the specified already-sorted list of constraints, maintaining
17+
/// execution priority order. SortedSet is not used, as equal priorities
18+
/// break duplicate-checking with SortedSet, as well as SortedSet not being
19+
/// able to handle runtime-changing exec priorities.
20+
/// </summary>
21+
/// <param name="constraintList">Sorted list of existing priorites</param>
22+
/// <param name="constraint">Constraint to add</param>
23+
/// <param name="comparer">ConstraintExecOrderComparer for comparing two constraint priorities</param>
24+
internal static void AddWithPriority(ref List<TransformConstraint> constraintList, TransformConstraint constraint, ConstraintExecOrderComparer comparer)
25+
{
26+
if (constraintList.Contains(constraint))
27+
{
28+
return;
29+
}
30+
31+
if (constraintList.Count == 0 || comparer.Compare(constraintList[constraintList.Count-1], constraint) < 0)
32+
{
33+
constraintList.Add(constraint);
34+
return;
35+
}
36+
else if (comparer.Compare(constraintList[0], constraint) > 0)
37+
{
38+
constraintList.Insert(0, constraint);
39+
return;
40+
}
41+
else
42+
{
43+
int idx = constraintList.BinarySearch(constraint, comparer);
44+
if (idx < 0)
45+
{
46+
// idx will be the two's complement of the index of the
47+
// next element that is "larger" than the given constraint.
48+
idx = ~idx;
49+
}
50+
constraintList.Insert(idx, constraint);
51+
}
52+
}
53+
}
54+
}

Assets/MRTK/SDK/Features/Utilities/ConstraintUtils.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Assets/MRTK/SDK/StandardAssets/Prefabs/SceneDescriptionPanelRev.prefab

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,9 @@ MonoBehaviour:
818818
oneHandRotationModeNear: 1
819819
oneHandRotationModeFar: 1
820820
releaseBehavior: -1
821+
transformSmoothingLogicType:
822+
reference: Microsoft.MixedReality.Toolkit.Utilities.DefaultTransformSmoothingLogic,
823+
Microsoft.MixedReality.Toolkit.SDK
821824
smoothingFar: 1
822825
smoothingNear: 1
823826
moveLerpTime: 0.000001
@@ -900,6 +903,7 @@ MonoBehaviour:
900903
m_EditorClassIdentifier:
901904
handType: 1
902905
proximityType: 3
906+
executionOrder: 0
903907
faceAway: 1
904908
--- !u!114 &134786571603963941
905909
MonoBehaviour:
@@ -915,6 +919,7 @@ MonoBehaviour:
915919
m_EditorClassIdentifier:
916920
handType: 3
917921
proximityType: 3
922+
executionOrder: 1
918923
constraintOnRotation: 5
919924
useLocalSpaceForConstraint: 0
920925
--- !u!114 &6165489573220541460

Assets/MRTK/Tests/PlayModeTests/ConstraintTests.cs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -929,6 +929,89 @@ public IEnumerator CombinedConstraintFarNear()
929929
TestUtilities.AssertAboutEqual(originalPosition, testObject.transform.position, "Position should be equal for far interaction");
930930
}
931931

932+
/// <summary>
933+
/// Tests that multiple constraints obey their relative execution order priorities
934+
/// </summary>
935+
[UnityTest]
936+
public IEnumerator ConstraintExecutionOrder()
937+
{
938+
TestUtilities.PlayspaceToArbitraryPose();
939+
940+
var testObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
941+
testObject.transform.localScale = Vector3.one * 0.2f;
942+
Vector3 originalPosition = TestUtilities.PositionRelativeToPlayspace(Vector3.forward);
943+
testObject.transform.position = originalPosition;
944+
Quaternion originalRotation = Quaternion.identity;
945+
testObject.transform.rotation = originalRotation;
946+
var manipHandler = testObject.AddComponent<ObjectManipulator>();
947+
manipHandler.HostTransform = testObject.transform;
948+
manipHandler.SmoothingFar = false;
949+
manipHandler.SmoothingNear = false;
950+
manipHandler.OneHandRotationModeFar = ObjectManipulator.RotateInOneHandType.RotateAboutGrabPoint;
951+
952+
// Add every-axis rotation constraint
953+
var rotateConstraint = manipHandler.EnsureComponent<RotationAxisConstraint>();
954+
rotateConstraint.UseLocalSpaceForConstraint = false;
955+
rotateConstraint.ConstraintOnRotation = AxisFlags.XAxis | AxisFlags.YAxis | AxisFlags.ZAxis;
956+
rotateConstraint.ProximityType = ManipulationProximityFlags.Far;
957+
958+
// Add a face user constraint.
959+
var faceUserConstraint = manipHandler.EnsureComponent<FaceUserConstraint>();
960+
faceUserConstraint.FaceAway = true;
961+
faceUserConstraint.ProximityType = ManipulationProximityFlags.Far;
962+
963+
// First, we'll test the rotate constraint executing first.
964+
// Expected behavior: cube will still rotate to face user, because
965+
// the FaceUserConstraint will execute second.
966+
rotateConstraint.ExecutionPriority = 0;
967+
faceUserConstraint.ExecutionPriority = 1;
968+
yield return null;
969+
970+
const int numHandSteps = 1;
971+
972+
TestHand rightHand = new TestHand(Handedness.Right);
973+
974+
yield return rightHand.Show(TestUtilities.PositionRelativeToPlayspace(new Vector3(0.05f, -0.1f, 0.45f)));
975+
yield return null;
976+
977+
// Pinch and rotate hand.
978+
yield return rightHand.SetGesture(ArticulatedHandPose.GestureId.Pinch);
979+
yield return null;
980+
yield return rightHand.SetRotation(Quaternion.Euler(0, 45, 0), numHandSteps);
981+
yield return null;
982+
983+
// With this execution order, the faceUser constraint should still be setting the cube's rotation.
984+
TestUtilities.AssertAboutEqual(testObject.transform.forward.normalized, (testObject.transform.position - CameraCache.Main.transform.position).normalized, "Cube should have faced away from user.");
985+
986+
// Reset hand position + gesture
987+
yield return rightHand.SetGesture(ArticulatedHandPose.GestureId.Open);
988+
yield return null;
989+
yield return rightHand.SetRotation(Quaternion.identity, numHandSteps);
990+
yield return null;
991+
992+
// Reset cube position+rotation
993+
testObject.transform.position = originalPosition;
994+
testObject.transform.rotation = originalRotation;
995+
996+
// Now, we'll switch the execution order of the two constraints.
997+
// With the rotation constraint executing *after* the faceUser constraint,
998+
// the rotation of the cube should now be properly locked.
999+
1000+
// This also tests whether setting these priorities
1001+
// properly re-sorts the ConstraintManager's priority list.
1002+
rotateConstraint.ExecutionPriority = 1;
1003+
faceUserConstraint.ExecutionPriority = 0;
1004+
1005+
// Pinch and rotate hand.
1006+
yield return rightHand.SetGesture(ArticulatedHandPose.GestureId.Pinch);
1007+
yield return null;
1008+
yield return rightHand.SetRotation(Quaternion.Euler(0, 45, 0), numHandSteps);
1009+
yield return null;
1010+
1011+
// With this execution order, the rotation constraint should be properly constraining rotation.
1012+
TestUtilities.AssertAboutEqual(testObject.transform.rotation, originalRotation, "Cube should not have been able to rotate.");
1013+
}
1014+
9321015
/// <summary>
9331016
/// Tests that FaceUserConstraint updates the rotation to face the user
9341017
/// </summary>

0 commit comments

Comments
 (0)