Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Packages/com.orkunmanap.runtime-transform-handles/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- `Handle.Targets` (`IReadOnlyCollection<Transform>`) exposes the actual manipulated objects, and
`TransformGroup.Targets` / `TransformHandleManager.GetTargets(handle)` provide the same — there
was previously no public way to ask a handle which objects it controls.
- `Handle.Pivot` is the honest name for the manipulation pivot (the ghost transform the handle
moves around).

### Deprecated
- `Handle.Target` is now `[Obsolete]`. It returns the manipulation pivot (ghost), not the selected
object — a long-standing source of confusion. Use `Handle.Pivot` for the pivot or
`Handle.Targets` for the manipulated objects. The member keeps working until the next major.

## [3.2.0] - 2026-06-11

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,15 @@ Handles require a dedicated physics layer (default name `TransformHandle`). Crea
| `void ChangeHandleSpace(Handle, Space)` | Set a handle's space and re-center the ghost. |
| `void ChangeHandlePivot(TransformGroup, bool originToCenter)` | Toggle pivot vs. bounds-center origin. |
| `Camera MainCamera` | Camera used for raycasting (settable; falls back to `Camera.main`). |
| `IReadOnlyCollection<Transform> GetTargets(Handle)` | The objects a handle manipulates (live, read-only). |
| `TransformHandleSettings Settings` | Optional settings asset (overrides serialized defaults). |

### `Handle`

| Member | Description |
|--------|-------------|
| `Transform Target` | Manipulated transform (**read-only**; set via `CreateHandle`/`Enable`). |
| `IReadOnlyCollection<Transform> Targets` | The manipulated objects (**read-only**, live view). |
| `Transform Pivot` | The manipulation pivot — the ghost the handle moves around (**read-only**; not your object). |
| `Camera HandleCamera` | Camera for screen math (**read-only**; set when enabled). |
| `HandleType Type` | Property; assigning rebuilds the child handles. |
| `HandleAxes Axes` | Property; assigning rebuilds the child handles. |
Expand All @@ -79,6 +81,9 @@ Handles require a dedicated physics layer (default name `TransformHandle`). Crea
| events `OnInteractionStartEvent` / `OnInteractionEvent` / `OnInteractionEndEvent` / `OnHandleDestroyedEvent` | `Action<Handle>`. Inspector-friendly `*UnityEvent` mirrors exist. |
| `void ApplySettings(TransformHandleSettings)` | Apply scale/appearance from a settings asset. |

> `Handle.Target` is `[Obsolete]` — it returns the **pivot**, not the selected object. Use `Pivot`
> for the pivot or `Targets` for the manipulated objects.
>
> The camelCase spellings of these members (`target`, `type`, `space`, `axes`, `snappingType`,
> `positionSnap`, `rotationSnap`, `scaleSnap`, `autoScale`, `handleCamera`, `mainCamera`) and the
> `ChangeHandleType`/`ChangeHandleSpace`/`ChangeAxes` methods are `[Obsolete]` shims — they keep
Expand Down
4 changes: 2 additions & 2 deletions Packages/com.orkunmanap.runtime-transform-handles/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ public class SimpleExample : MonoBehaviour
void CreateHandleForObject(Transform target)
{
Handle handle = TransformHandleManager.Instance.CreateHandle(target);
// Note: handle.Target is the internal manipulation pivot (the group's ghost), not your
// object. Capture your own `target` reference for anything object-specific.
// handle.Pivot is the manipulation pivot (the group's ghost); handle.Targets are the
// actual objects being manipulated.
handle.OnInteractionStartEvent += _ => Debug.Log("Started: " + target.name);
handle.OnInteractionEndEvent += _ => Debug.Log("Finished: " + target.name);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,32 @@ public class Handle : MonoBehaviour
/// <summary>UnityEvent fired when handle is destroyed. Configure in Inspector.</summary>
public HandleUnityEvent OnHandleDestroyedUnityEvent => onHandleDestroyed;

/// <summary>The target transform being manipulated. Read-only; set via <see cref="Enable"/>.</summary>
public Transform Target { get; private set; }
/// <summary>
/// The manipulation pivot — the ghost transform the handle moves/rotates/scales around.
/// This is NOT the user's selected object; for those, use <see cref="Targets"/>.
/// Read-only; set via <see cref="Enable"/>.
/// </summary>
public Transform Pivot { get; private set; }

/// <summary>
/// The transforms this handle manipulates (the actual selected objects). Read-only,
/// reflects the current group membership; empty until the handle is enabled with a target
/// or while it is not managed by a <see cref="TransformHandleManager"/>.
/// </summary>
public System.Collections.Generic.IReadOnlyCollection<Transform> Targets =>
Manager != null ? Manager.GetTargets(this) : System.Array.Empty<Transform>();

/// <summary>
/// The manipulation pivot (ghost). Misleadingly named — it returns the pivot, not the
/// selected object(s). Use <see cref="Pivot"/> for the pivot and <see cref="Targets"/> for
/// the manipulated objects.
/// </summary>
[Obsolete("Use Pivot for the manipulation pivot, or Targets for the manipulated objects. Target returns the pivot (ghost), not your selected object.")]
public Transform Target => Pivot;

/// <inheritdoc cref="Target"/>
[Obsolete("Use Target instead.")]
public Transform target => Target;
/// <inheritdoc cref="Pivot"/>
[Obsolete("Use Pivot instead.")]
public Transform target => Pivot;

[SerializeField, FormerlySerializedAs("axes")] private HandleAxes _axes = HandleAxes.XYZ;
/// <summary>Active axes for the handle. Assigning rebuilds the child handles.</summary>
Expand Down Expand Up @@ -248,7 +268,7 @@ protected virtual void LateUpdate()
/// <param name="targetTransform">The transform to manipulate.</param>
public virtual void Enable(Transform targetTransform)
{
Target = targetTransform;
Pivot = targetTransform;
transform.position = targetTransform.position;

CreateHandles();
Expand All @@ -259,7 +279,7 @@ public virtual void Enable(Transform targetTransform)
/// </summary>
public virtual void Disable()
{
Target = null;
Pivot = null;
Clear();
}

Expand Down Expand Up @@ -322,12 +342,12 @@ public virtual void ChangeAxes(HandleAxes handleAxes)

protected virtual void UpdateHandleTransformation()
{
if (!Target) return;
if (!Pivot) return;

transform.position = Target.position;
transform.position = Pivot.position;
if (Space == Space.Self || Type == HandleType.Scale)
{
transform.rotation = Target.rotation;
transform.rotation = Pivot.rotation;
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ public virtual void EndInteraction()
protected Vector3 GetRotatedAxis(Vector3 axis)
{
return ParentHandle.Space == Space.Self
? ParentHandle.Target.rotation * axis
? ParentHandle.Pivot.rotation * axis
: axis;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public override void Interact(Vector3 previousPosition)
if (Mathf.Abs(_axis.z) > 0.5f) position.z = SnapUtils.Snap(position.z, snapping.z);
}

ParentHandle.Target.position = position;
ParentHandle.Pivot.position = position;

base.Interact(previousPosition);
}
Expand All @@ -104,7 +104,7 @@ public override void StartInteraction(Vector3 hitPoint)
{
base.StartInteraction(hitPoint);

_startPosition = ParentHandle.Target.position;
_startPosition = ParentHandle.Pivot.position;

var rAxis = GetRotatedAxis(_axis);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ public override void Interact(Vector3 previousPosition)
if (Mathf.Abs(axis.z) > 0.5f) position.z = SnapUtils.Snap(position.z, snapping.z);
}

ParentHandle.Target.position = position;
ParentHandle.Pivot.position = position;

base.Interact(previousPosition);
}
Expand All @@ -106,7 +106,7 @@ public override void StartInteraction(Vector3 hitPoint)
{
var rPerp = GetRotatedAxis(_perp);

var position = ParentHandle.Target.position;
var position = ParentHandle.Pivot.position;
_plane = new Plane(rPerp, position);

var ray = _handleCamera.ScreenPointToRay(InputWrapper.MousePosition);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public override void Interact(Vector3 previousPosition)
}

var hitPoint = cameraRay.GetPoint(hitT);
var hitDirection = (hitPoint - ParentHandle.Target.position).normalized;
var hitDirection = (hitPoint - ParentHandle.Pivot.position).normalized;
var x = Vector3.Dot(hitDirection, _tangent);
var y = Vector3.Dot(hitDirection, _biTangent);
var angleRadians = Mathf.Atan2(y, x);
Expand All @@ -73,12 +73,12 @@ public override void Interact(Vector3 previousPosition)

if (ParentHandle.Space == Space.Self)
{
ParentHandle.Target.localRotation = _startRotation * Quaternion.AngleAxis(angleDegrees, _axis);
ParentHandle.Pivot.localRotation = _startRotation * Quaternion.AngleAxis(angleDegrees, _axis);
}
else
{
var invertedRotatedAxis = Quaternion.Inverse(_startRotation) * _axis;
ParentHandle.Target.rotation = _startRotation * Quaternion.AngleAxis(angleDegrees, invertedRotatedAxis);
ParentHandle.Pivot.rotation = _startRotation * Quaternion.AngleAxis(angleDegrees, invertedRotatedAxis);
}

// Reuse a single Mesh instead of allocating a new one each frame. CreateArc would
Expand All @@ -98,21 +98,21 @@ public override void StartInteraction(Vector3 hitPoint)
base.StartInteraction(hitPoint);

_startRotation = ParentHandle.Space == Space.Self
? ParentHandle.Target.localRotation
: ParentHandle.Target.rotation;
? ParentHandle.Pivot.localRotation
: ParentHandle.Pivot.rotation;

_rotatedAxis = ParentHandle.Space == Space.Self
? _startRotation * _axis
: _axis;

_axisPlane = new Plane(_rotatedAxis, ParentHandle.Target.position);
_axisPlane = new Plane(_rotatedAxis, ParentHandle.Pivot.position);

var cameraRay = _handleCamera.ScreenPointToRay(InputWrapper.MousePosition);
var startHitPoint = _axisPlane.Raycast(cameraRay, out var hitT)
? cameraRay.GetPoint(hitT)
: _axisPlane.ClosestPointOnPlane(hitPoint);

_tangent = (startHitPoint - ParentHandle.Target.position).normalized;
_tangent = (startHitPoint - ParentHandle.Pivot.position).normalized;
_biTangent = Vector3.Cross(_rotatedAxis, _tangent);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public override void Interact(Vector3 previousPosition)
{
if (_handleCamera == null) return;

var position = ParentHandle.Target.position;
var position = ParentHandle.Pivot.position;
var direction = GetRotatedAxis(_axis);
var handleSize = HandleTransformUtility.GetHandleSize(position, _handleCamera);
var lineTranslation = HandleTransformUtility.CalcLineTranslation(
Expand Down Expand Up @@ -127,7 +127,7 @@ public override void Interact(Vector3 previousPosition)
Delta = dist - 1f;
var scale = Vector3.Scale(_startScale, _axis * Delta + Vector3.one);

ParentHandle.Target.localScale = scale;
ParentHandle.Pivot.localScale = scale;

base.Interact(previousPosition);
}
Expand All @@ -136,7 +136,7 @@ public override void Interact(Vector3 previousPosition)
public override void StartInteraction(Vector3 hitPoint)
{
base.StartInteraction(hitPoint);
_startScale = ParentHandle.Target.localScale;
_startScale = ParentHandle.Pivot.localScale;
_startMousePosition = InputWrapper.MousePosition;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public override void Interact(Vector3 previousPosition)
var camera = ParentHandle.HandleCamera;
if (camera == null) return;

var position = ParentHandle.Target.position;
var position = ParentHandle.Pivot.position;
var handleSize = HandleTransformUtility.GetHandleSize(position, camera);
var lineTranslation = HandleTransformUtility.CalcLineTranslation(
_startMousePosition,
Expand All @@ -60,7 +60,7 @@ public override void Interact(Vector3 previousPosition)
}

Delta = dist;
ParentHandle.Target.localScale = _startScale + Vector3.Scale(_startScale, _axis) * Delta;
ParentHandle.Pivot.localScale = _startScale + Vector3.Scale(_startScale, _axis) * Delta;

base.Interact(previousPosition);
}
Expand All @@ -69,7 +69,7 @@ public override void Interact(Vector3 previousPosition)
public override void StartInteraction(Vector3 hitPoint)
{
base.StartInteraction(hitPoint);
_startScale = ParentHandle.Target.localScale;
_startScale = ParentHandle.Pivot.localScale;
_startMousePosition = InputWrapper.MousePosition;

var camera = ParentHandle.HandleCamera;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ public class TransformGroup
/// <summary>Gets the set of transforms in this group.</summary>
internal HashSet<Transform> Transforms { get; }

/// <summary>
/// The transforms in this group as a read-only, live view (the actual manipulated objects,
/// not the pivot). Do not cache across add/remove.
/// </summary>
public IReadOnlyCollection<Transform> Targets => Transforms;

/// <summary>Gets the mapping of transforms to their mesh renderers (if any).</summary>
internal Dictionary<Transform, MeshRenderer> RenderersMap { get; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,20 @@ public void RemoveTarget(Transform target, Handle handle)
group.GroupGhost.UpdateGhostTransform(averagePosRotScale);
}

/// <summary>
/// Returns the transforms manipulated by <paramref name="handle"/> — the actual selected
/// objects, not the pivot. The result is a read-only, live view of the handle's current
/// group (do not cache it across add/remove); empty if the handle is not managed here.
/// </summary>
/// <param name="handle">The handle to query.</param>
public IReadOnlyCollection<Transform> GetTargets(Handle handle)
{
if (handle == null) throw new ArgumentNullException(nameof(handle));
if (_handleGroupMap != null && _handleGroupMap.TryGetValue(handle, out var group))
return group.Transforms;
return System.Array.Empty<Transform>();
}

protected virtual void Update()
{
if (!_handleActive) return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,8 @@ public void UpdateGroupPosition_moves_every_target_by_the_change()
var a = NewObject("a").transform;
var b = NewObject("b", new Vector3(2f, 0f, 0f)).transform;
var handle = _manager.CreateHandleFromList(new List<Transform> { a, b });
var ghost = handle.Target.GetComponent<Ghost>();
Assert.IsNotNull(ghost, "handle.Target must be the group's ghost pivot");
var ghost = handle.Pivot.GetComponent<Ghost>();
Assert.IsNotNull(ghost, "handle.Pivot must be the group's ghost pivot");

var change = new Vector3(1f, 2f, 3f);
_manager.UpdateGroupPosition(ghost, change);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,33 @@ public void CreateHandle_returns_handle_for_single_target()
var handle = _manager.CreateHandle(target);

Assert.IsNotNull(handle);
Assert.IsNotNull(handle.Target, "handle.Target is the ghost pivot, not the user object");
Assert.AreNotSame(target, handle.Target);
Assert.IsNotNull(handle.Pivot, "handle.Pivot is the manipulation pivot (ghost), not the user object");
Assert.AreNotSame(target, handle.Pivot);
}

[Test]
public void Targets_contains_the_manipulated_object_and_Pivot_is_separate()
{
var target = NewObject("target").transform;
var handle = _manager.CreateHandle(target);

CollectionAssert.Contains(handle.Targets, target, "Targets must expose the selected object");
Assert.AreEqual(1, handle.Targets.Count);
// Pivot is the ghost; the manipulated object is not. (Reference identity, not NUnit
// collection equality, which is unreliable on UnityEngine.Object's fake-null operator.)
Assert.AreNotSame(target, handle.Pivot, "the pivot is not the manipulated object");
Assert.IsNotNull(handle.Pivot.GetComponent<Ghost>(), "Pivot is the ghost pivot");
Assert.IsNull(target.GetComponent<Ghost>(), "the manipulated object is not a ghost");
}

[Test]
public void Targets_reflects_multi_select_membership()
{
var a = NewObject("a").transform;
var b = NewObject("b", new Vector3(2f, 0f, 0f)).transform;
var handle = _manager.CreateHandleFromList(new List<Transform> { a, b });

CollectionAssert.AreEquivalent(new[] { a, b }, handle.Targets);
}

[Test]
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ public class SimpleExample : MonoBehaviour
// Create a handle for a single object
Handle handle = _manager.CreateHandle(target);

// Subscribe to events. Note: handle.Target is the internal manipulation pivot (the
// group's ghost), not your object — capture your own `target` for anything object-specific.
// Subscribe to events. handle.Pivot is the manipulation pivot (the group's ghost);
// handle.Targets are the actual objects being manipulated.
handle.OnInteractionStartEvent += _ => Debug.Log("Started manipulating: " + target.name);
handle.OnInteractionEndEvent += _ => Debug.Log("Finished manipulating: " + target.name);
}
Expand Down
Loading