Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,12 @@
{ "key": "library.share", "value": "Share" },
{ "key": "library.teleportTo", "value": "Teleport To" },
{ "key": "library.remove", "value": "Remove" },
{ "key": "library.assignTracker", "value": "Assign" },
{ "key": "library.unbindTracker", "value": "Unbind" },
{ "key": "library.trackerPicker.title", "value": "Choose Tracker" },
{ "key": "library.trackerPicker.empty", "value": "No trackers are currently connected." },
{ "key": "library.trackerPicker.confirm", "value": "Bind" },
{ "key": "library.trackerPicker.cancel", "value": "Cancel" },
{ "key": "library.networkType.local", "value": "Only Me" },
{ "key": "library.networkType.networked", "value": "Everyone (Instant)" },
{ "key": "library.networkType.local.description", "value": "If the item is set to local, it will only be visible and interactive for you." },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,13 @@ public override void OnReleaseEvent()
private static protected bool IsProtected = false; // we use this to determine if the user is admin for admin related queries on the library provider
public static BasisMenuPanel panel;

/// <summary>
/// Fires per instantiated-object row right after the Select button is built,
/// before Teleport/Remove. Subscribers can append buttons to the supplied row
/// container — they land between Select and Teleport.
/// </summary>
public static event Action<RectTransform, BasisRuntimeSpawnRegistry.SpawnInstance> OnInstanceRowCreated;

// references to the search query elements
private static PanelTextField searchField; // reference to the search field
private static PanelDropdown dateSorting; // reference to the date sorting dropdown
Expand Down Expand Up @@ -1974,9 +1981,11 @@ private static void CreateListEntry(BasisRuntimeSpawnRegistry.SpawnInstance item
// close the menu
BasisMainMenu.Close();
}

};

OnInstanceRowCreated?.Invoke(itemListPanel.TabButtonParent, itemKey);

PanelButton TeleportToItem = PanelButton.CreateNew(ButtonStyles.StandardButton, itemListPanel.TabButtonParent);
TeleportToItem.Descriptor.SetTitle(string.Empty);
TeleportToItem.SetIcon(AddressableAssets.Sprites.TeleportTo);
Expand Down
21 changes: 21 additions & 0 deletions Basis/Packages/com.basis.integration.trackerobjects/LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 BasisVR

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions Basis/Packages/com.basis.integration.trackerobjects/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Basis Tracker Objects Integration

Bridges [`com.basis.trackerobjects`](../com.basis.trackerobjects/REQUIREMENTS.md) into the Basis library menu. When a prop has been instantiated and shows up in the library's instantiated-items tab, this package adds an **Assign** button to the row. Clicking it opens a tracker picker; confirming a tracker binds the prop's GameObject to that tracker via `BasisTrackerObjectManager`. Clicking **Unbind** removes the binding.

## Why a separate package

`com.basis.trackerobjects` references `Basis Framework` for the types it needs to drive a transform (`BasisInput`, `BasisLocalPlayer`, `BasisRuntimeSpawnRegistry`). That means `Basis Framework` can't reference `com.basis.trackerobjects` back — the asmdef graph would cycle. This integration package references both and is the only place that can wire a library-menu button into a `BasisTrackerObjectManager.TryCreateBinding` call. Same pattern as `com.basis.integration.audiolink`.

## What it adds

- A `[RuntimeInitializeOnLoadMethod(SubsystemRegistration)]` subscriber on `LibraryProvider.OnInstanceRowCreated`. For every instantiated-object row that `LibraryProvider` builds, this appends a `StandardButton` between the existing Select and Teleport buttons.
- The button is non-interactable for `SpawnMode.Scene` and `SpawnMethod.Embedded` items — same rule the Select button applies.
- Clicking **Assign** opens a `DialogBox<BasisInput>` modal listing the currently-connected trackers eligible for prop binding. Confirming a row calls `BasisTrackerObjectManager.TryCreateBinding` with the spawn instance's `LoadedNetID` and `GameObject` transform. The picker excludes:
- `BasisVirtualMidpointInput` instances (the virtual half of an active pair).
- Trackers with `BasisInput.IsLinked == true` (one half of an active pair).
- Trackers whose `UniqueDeviceIdentifier` has a `BasisTrackerRoleOverride.TryGetOverride` hit.
- Devices the input matcher has pinned to a fixed role (HMD, named controllers, etc.).
- Trackers currently driving an avatar bone via calibration. Decalibrate first if you want to reuse a calibrated tracker for a prop.
- Clicking **Unbind** calls `BasisTrackerObjectManager.TryRemoveBinding` directly — no confirmation dialog. Unbind isn't destructive; the binding just lifts.

## Compile guards

The assembly defines two version constraints:

- `com.basis.framework` → `BASIS_FRAMEWORK_EXISTS`
- `com.basis.trackerobjects` → `BASIS_TRACKEROBJECTS_EXISTS`

Both must be present for this package to compile. If either is removed from the project, this assembly drops out silently.

## See also

- [`com.basis.trackerobjects/REQUIREMENTS.md`](../com.basis.trackerobjects/REQUIREMENTS.md) — full v1 spec for the binding manager, pose drive, pickup veto, and registry-cleanup contract.
- `LibraryProvider.OnInstanceRowCreated` — the event this package subscribes to. Lives in `com.basis.framework`.
- `com.basis.integration.audiolink` — sibling integration package that follows the same bridge pattern.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "Basis.Integration.TrackerObjects",
"rootNamespace": "Basis.Integration.TrackerObjects",
"references": [
"Basis Framework",
"BasisSDK",
"BasisDebug",
"BasisTrackerObjects",
"Unity.TextMeshPro"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [
"BASIS_FRAMEWORK_EXISTS",
"BASIS_TRACKEROBJECTS_EXISTS"
],
"versionDefines": [
{
"name": "com.basis.framework",
"expression": "",
"define": "BASIS_FRAMEWORK_EXISTS"
},
{
"name": "com.basis.trackerobjects",
"expression": "",
"define": "BASIS_TRACKEROBJECTS_EXISTS"
}
],
"noEngineReferences": false
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Basis.BasisUI;
using Basis.Scripts.Avatar;
using Basis.Scripts.Device_Management;
using Basis.Scripts.Device_Management.Devices;
using Basis.Scripts.Device_Management.Devices.Pairing;
using Basis.Scripts.TransformBinders.BoneControl;
using Basis.TrackerObjects;
using UnityEngine;

namespace Basis.Integration.TrackerObjects
{
internal static class BasisTrackerObjectsLibraryHook
{
private static readonly Vector2 PickerSize = new Vector2(900, 720);
private static readonly Vector2 RowSize = new Vector2(80, 80);
private static readonly Vector2 PickerRowSize = new Vector2(700, 60);

[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
private static void Subscribe()
{
LibraryProvider.OnInstanceRowCreated -= OnRowCreated;
LibraryProvider.OnInstanceRowCreated += OnRowCreated;
}

private static void OnRowCreated(RectTransform parent, BasisRuntimeSpawnRegistry.SpawnInstance instance)
{
if (instance == null) return;
string netID = instance.LoadedNetID;
if (string.IsNullOrEmpty(netID)) return;

// Scene-mode and embedded instances can't host a tracker binding (no
// pickup/rigid surface to drive, and they're not user-owned spawns), so
// skip adding the button at all — a disabled fourth button just pushes
// the Select/Teleport/Remove row over.
if (instance.SpawnMode == BasisRuntimeSpawnRegistry.SpawnMode.Scene) return;
if (instance.SpawnMethod == BasisRuntimeSpawnRegistry.SpawnMethod.Embedded) return;

bool hasBinding = BasisTrackerObjectManager.TryGetBindingByLoadedNetID(netID, out _);
PanelButton button = PanelButton.CreateNew(PanelButton.ButtonStyles.StandardButton, parent);
button.Descriptor.SetTitle(string.Empty);
button.SetIcon(hasBinding ? AddressableAssets.Sprites.Unlink : AddressableAssets.Sprites.Link);
button.SetSize(RowSize);
// Match the row's left-side status-icon padding (PE Image Simple Square inset).
button.Descriptor.IconImage.rectTransform.sizeDelta = new Vector2(-30, -30);

button.OnClicked += async () =>
{
if (BasisTrackerObjectManager.TryGetBindingByLoadedNetID(netID, out BasisTrackerBinding existing))
{
BasisTrackerObjectManager.TryRemoveBinding(existing.Id);
button.SetIcon(AddressableAssets.Sprites.Link);
return;
}

if (!BasisRuntimeSpawnRegistry.SpawnedGameobjects.TryGetValue(netID, out GameObject go) || go == null)
{
BasisDebug.LogWarning($"AssignTracker: spawn instance {netID} has no resolved GameObject", BasisDebug.LogTag.TrackerObjects);
return;
}

BasisInput chosen = await OpenPickerAsync();
if (chosen == null) return;

if (BasisTrackerObjectManager.TryCreateBinding(chosen, go.transform, netID, out _))
{
button.SetIcon(AddressableAssets.Sprites.Unlink);
}
};
}

private static async Task<BasisInput> OpenPickerAsync()
{
DialogBox<BasisInput> picker = DialogBox<BasisInput>.Create(
LibraryProvider.panel,
PickerSize,
BasisLocalization.Get("library.trackerPicker.title"),
description: null,
icon: AddressableAssets.Sprites.Information);

PanelButton cancel = PanelButton.CreateNew(PanelButton.ButtonStyles.ExitButton, picker.Descriptor.Header);
cancel.Descriptor.SetTitle(BasisLocalization.Get("library.trackerPicker.cancel"));
cancel.rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, 125);
cancel.rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, 50);
cancel.OnClicked += () => picker.Cancel(null);

List<BasisInput> candidates = CollectBindableTrackers();
if (candidates.Count == 0)
{
PanelTextField empty = PanelTextField.CreateNew(PanelTextField.TextFieldStyles.Entry, picker.Descriptor.ContentParent);
empty._inputField.gameObject.SetActive(false);
empty.Descriptor.SetTitle(BasisLocalization.Get("library.trackerPicker.empty"));
}
else
{
for (int index = 0; index < candidates.Count; index++)
{
BasisInput tracker = candidates[index];
string roleLabel = tracker.TryGetRole(out BasisBoneTrackedRole role)
? role.ToString()
: "Tracker";
PanelButton row = PanelButton.CreateNew(PanelButton.ButtonStyles.StandardButton, picker.Descriptor.ContentParent);
row.Descriptor.SetTitle($"{roleLabel} — {tracker.UniqueDeviceIdentifier}");
row.SetSize(PickerRowSize);
row.OnClicked += () => picker.CloseWithResult(tracker);
}
}

return await picker.WaitAsync();
}

private static List<BasisInput> CollectBindableTrackers()
{
List<BasisInput> result = new List<BasisInput>();
BasisObservableList<BasisInput> devices = BasisDeviceManagement.Instance?.AllInputDevices;
if (devices == null) return result;

for (int i = 0; i < devices.Count; i++)
{
BasisInput input = devices[i];
if (input == null) continue;
if (string.IsNullOrEmpty(input.UniqueDeviceIdentifier)) continue;
if (input is BasisVirtualMidpointInput) continue;
if (input.IsLinked) continue;
if (BasisTrackerRoleOverride.TryGetOverride(input.UniqueDeviceIdentifier, out _)) continue;
if (input.DeviceMatchSettings != null && input.DeviceMatchSettings.HasTrackedRole) continue;
// A tracker already driving a body bone (post-calibration) is excluded so
// calibration and prop binding can't fight over the same device. To reuse
// a calibrated tracker, decalibrate first.
if (input.TryGetRole(out _)) continue;

result.Add(input);
}
return result;
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions Basis/Packages/com.basis.integration.trackerobjects/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "com.basis.integration.trackerobjects",
"displayName": "Basis Tracker Objects Integration",
"version": "0.0.1",
"description": "Wires com.basis.trackerobjects into the Basis library menu. Adds the Assign/Unbind Tracker button and the tracker-picker dialog. Compiles only when both com.basis.framework and com.basis.trackerobjects are present.",
"author": {
"name": "BasisVR",
"url": "https://github.com/BasisVR"
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ public enum LogTag
Shims,
Props,
LocalNetwork,
TrackerObjects,
}

public enum MessageType
Expand Down
21 changes: 21 additions & 0 deletions Basis/Packages/com.basis.trackerobjects/LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 BasisVR

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
7 changes: 7 additions & 0 deletions Basis/Packages/com.basis.trackerobjects/LICENSE.md.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions Basis/Packages/com.basis.trackerobjects/Runtime.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Basis.Scripts.BasisSdk.Interactions;
using Basis.Scripts.Device_Management.Devices;
using UnityEngine;

namespace Basis.TrackerObjects
{
public class BasisTrackerBinding
{
public int Id;
public BasisInput Tracker;
public Transform Target;
public string UniqueDeviceIdentifier;
public string LoadedNetID;
public Vector3 LocalPositionOffset;
public Quaternion LocalRotationOffset;

public BasisPickupInteractable PickupRef;
public Rigidbody RigidRef;
public bool PreBindKinematic;
public bool HasKinematicCaptured;
}
}
Loading
Loading