Skip to content

Commit c1f6067

Browse files
committed
feat(integration.trackerobjects): library-row Assign/Unbind button
Adds com.basis.integration.trackerobjects, a UI integration package that wires com.basis.trackerobjects into the Library window's Instantiated tab. Each prop or avatar row gains a fourth blue StandardButton with a link/unlink icon — click opens a tracker picker dialog, click again on a bound row removes the binding. Framework hook: adds the static LibraryProvider.OnInstanceRowCreated event so the integration package can append the button mid-row without LibraryProvider needing to know about the trackerobjects package directly. Scoped to user-owned spawns: scene-mode and embedded instances are skipped (no rigidbody to drive, not user-owned).
1 parent 0866767 commit c1f6067

14 files changed

Lines changed: 290 additions & 1 deletion

File tree

Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/en.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,12 @@
344344
{ "key": "library.share", "value": "Share" },
345345
{ "key": "library.teleportTo", "value": "Teleport To" },
346346
{ "key": "library.remove", "value": "Remove" },
347+
{ "key": "library.assignTracker", "value": "Assign" },
348+
{ "key": "library.unbindTracker", "value": "Unbind" },
349+
{ "key": "library.trackerPicker.title", "value": "Choose Tracker" },
350+
{ "key": "library.trackerPicker.empty", "value": "No trackers are currently connected." },
351+
{ "key": "library.trackerPicker.confirm", "value": "Bind" },
352+
{ "key": "library.trackerPicker.cancel", "value": "Cancel" },
347353
{ "key": "library.networkType.local", "value": "Only Me" },
348354
{ "key": "library.networkType.networked", "value": "Everyone (Instant)" },
349355
{ "key": "library.networkType.local.description", "value": "If the item is set to local, it will only be visible and interactive for you." },

Basis/Packages/com.basis.framework/BasisUI/Menus/Library/LibraryProvider.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,13 @@ public override void OnReleaseEvent()
8686
private static protected bool IsProtected = false; // we use this to determine if the user is admin for admin related queries on the library provider
8787
public static BasisMenuPanel panel;
8888

89+
/// <summary>
90+
/// Fires per instantiated-object row right after the Select button is built,
91+
/// before Teleport/Remove. Subscribers can append buttons to the supplied row
92+
/// container — they land between Select and Teleport.
93+
/// </summary>
94+
public static event Action<RectTransform, BasisRuntimeSpawnRegistry.SpawnInstance> OnInstanceRowCreated;
95+
8996
// references to the search query elements
9097
private static PanelTextField searchField; // reference to the search field
9198
private static PanelDropdown dateSorting; // reference to the date sorting dropdown
@@ -1972,9 +1979,11 @@ private static void CreateListEntry(BasisRuntimeSpawnRegistry.SpawnInstance item
19721979
// close the menu
19731980
BasisMainMenu.Close();
19741981
}
1975-
1982+
19761983
};
19771984

1985+
OnInstanceRowCreated?.Invoke(itemListPanel.TabButtonParent, itemKey);
1986+
19781987
PanelButton TeleportToItem = PanelButton.CreateNew(ButtonStyles.StandardButton, itemListPanel.TabButtonParent);
19791988
TeleportToItem.Descriptor.SetTitle(string.Empty);
19801989
TeleportToItem.SetIcon(AddressableAssets.Sprites.TeleportTo);
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 BasisVR
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

Basis/Packages/com.basis.integration.trackerobjects/LICENSE.md.meta

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Basis Tracker Objects Integration
2+
3+
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.
4+
5+
## Why a separate package
6+
7+
`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`.
8+
9+
## What it adds
10+
11+
- 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.
12+
- The button is non-interactable for `SpawnMode.Scene` and `SpawnMethod.Embedded` items — same rule the Select button applies.
13+
- 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:
14+
- `BasisVirtualMidpointInput` instances (the virtual half of an active pair).
15+
- Trackers with `BasisInput.IsLinked == true` (one half of an active pair).
16+
- Trackers whose `UniqueDeviceIdentifier` has a `BasisTrackerRoleOverride.TryGetOverride` hit.
17+
- Devices the input matcher has pinned to a fixed role (HMD, named controllers, etc.).
18+
- Trackers currently driving an avatar bone via calibration. Decalibrate first if you want to reuse a calibrated tracker for a prop.
19+
- Clicking **Unbind** calls `BasisTrackerObjectManager.TryRemoveBinding` directly — no confirmation dialog. Unbind isn't destructive; the binding just lifts.
20+
21+
## Compile guards
22+
23+
The assembly defines two version constraints:
24+
25+
- `com.basis.framework``BASIS_FRAMEWORK_EXISTS`
26+
- `com.basis.trackerobjects``BASIS_TRACKEROBJECTS_EXISTS`
27+
28+
Both must be present for this package to compile. If either is removed from the project, this assembly drops out silently.
29+
30+
## See also
31+
32+
- [`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.
33+
- `LibraryProvider.OnInstanceRowCreated` — the event this package subscribes to. Lives in `com.basis.framework`.
34+
- `com.basis.integration.audiolink` — sibling integration package that follows the same bridge pattern.

Basis/Packages/com.basis.integration.trackerobjects/README.md.meta

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

Basis/Packages/com.basis.integration.trackerobjects/Runtime.meta

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"name": "Basis.Integration.TrackerObjects",
3+
"rootNamespace": "Basis.Integration.TrackerObjects",
4+
"references": [
5+
"Basis Framework",
6+
"BasisSDK",
7+
"BasisDebug",
8+
"BasisTrackerObjects",
9+
"Unity.TextMeshPro"
10+
],
11+
"includePlatforms": [],
12+
"excludePlatforms": [],
13+
"allowUnsafeCode": false,
14+
"overrideReferences": false,
15+
"precompiledReferences": [],
16+
"autoReferenced": true,
17+
"defineConstraints": [
18+
"BASIS_FRAMEWORK_EXISTS",
19+
"BASIS_TRACKEROBJECTS_EXISTS"
20+
],
21+
"versionDefines": [
22+
{
23+
"name": "com.basis.framework",
24+
"expression": "",
25+
"define": "BASIS_FRAMEWORK_EXISTS"
26+
},
27+
{
28+
"name": "com.basis.trackerobjects",
29+
"expression": "",
30+
"define": "BASIS_TRACKEROBJECTS_EXISTS"
31+
}
32+
],
33+
"noEngineReferences": false
34+
}

Basis/Packages/com.basis.integration.trackerobjects/Runtime/Basis.Integration.TrackerObjects.asmdef.meta

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
using System.Collections.Generic;
2+
using System.Threading.Tasks;
3+
using Basis.BasisUI;
4+
using Basis.Scripts.Avatar;
5+
using Basis.Scripts.Device_Management;
6+
using Basis.Scripts.Device_Management.Devices;
7+
using Basis.Scripts.Device_Management.Devices.Pairing;
8+
using Basis.Scripts.TransformBinders.BoneControl;
9+
using Basis.TrackerObjects;
10+
using UnityEngine;
11+
12+
namespace Basis.Integration.TrackerObjects
13+
{
14+
internal static class BasisTrackerObjectsLibraryHook
15+
{
16+
private static readonly Vector2 PickerSize = new Vector2(900, 720);
17+
private static readonly Vector2 RowSize = new Vector2(80, 80);
18+
private static readonly Vector2 PickerRowSize = new Vector2(700, 60);
19+
20+
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
21+
private static void Subscribe()
22+
{
23+
LibraryProvider.OnInstanceRowCreated -= OnRowCreated;
24+
LibraryProvider.OnInstanceRowCreated += OnRowCreated;
25+
}
26+
27+
private static void OnRowCreated(RectTransform parent, BasisRuntimeSpawnRegistry.SpawnInstance instance)
28+
{
29+
if (instance == null) return;
30+
string netID = instance.LoadedNetID;
31+
if (string.IsNullOrEmpty(netID)) return;
32+
33+
// Scene-mode and embedded instances can't host a tracker binding (no
34+
// pickup/rigid surface to drive, and they're not user-owned spawns), so
35+
// skip adding the button at all — a disabled fourth button just pushes
36+
// the Select/Teleport/Remove row over.
37+
if (instance.SpawnMode == BasisRuntimeSpawnRegistry.SpawnMode.Scene) return;
38+
if (instance.SpawnMethod == BasisRuntimeSpawnRegistry.SpawnMethod.Embedded) return;
39+
40+
bool hasBinding = BasisTrackerObjectManager.TryGetBindingByLoadedNetID(netID, out _);
41+
PanelButton button = PanelButton.CreateNew(PanelButton.ButtonStyles.StandardButton, parent);
42+
button.Descriptor.SetTitle(string.Empty);
43+
button.SetIcon(hasBinding ? AddressableAssets.Sprites.Unlink : AddressableAssets.Sprites.Link);
44+
button.SetSize(RowSize);
45+
46+
button.OnClicked += async () =>
47+
{
48+
if (BasisTrackerObjectManager.TryGetBindingByLoadedNetID(netID, out BasisTrackerBinding existing))
49+
{
50+
BasisTrackerObjectManager.TryRemoveBinding(existing.Id);
51+
button.SetIcon(AddressableAssets.Sprites.Link);
52+
return;
53+
}
54+
55+
if (!BasisRuntimeSpawnRegistry.SpawnedGameobjects.TryGetValue(netID, out GameObject go) || go == null)
56+
{
57+
BasisDebug.LogWarning($"AssignTracker: spawn instance {netID} has no resolved GameObject", BasisDebug.LogTag.TrackerObjects);
58+
return;
59+
}
60+
61+
BasisInput chosen = await OpenPickerAsync();
62+
if (chosen == null) return;
63+
64+
if (BasisTrackerObjectManager.TryCreateBinding(chosen, go.transform, netID, out _))
65+
{
66+
button.SetIcon(AddressableAssets.Sprites.Unlink);
67+
}
68+
};
69+
}
70+
71+
private static async Task<BasisInput> OpenPickerAsync()
72+
{
73+
DialogBox<BasisInput> picker = DialogBox<BasisInput>.Create(
74+
LibraryProvider.panel,
75+
PickerSize,
76+
BasisLocalization.Get("library.trackerPicker.title"),
77+
description: null,
78+
icon: AddressableAssets.Sprites.Information);
79+
80+
PanelButton cancel = PanelButton.CreateNew(PanelButton.ButtonStyles.ExitButton, picker.Descriptor.Header);
81+
cancel.Descriptor.SetTitle(BasisLocalization.Get("library.trackerPicker.cancel"));
82+
cancel.rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, 125);
83+
cancel.rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, 50);
84+
cancel.OnClicked += () => picker.Cancel(null);
85+
86+
List<BasisInput> candidates = CollectBindableTrackers();
87+
if (candidates.Count == 0)
88+
{
89+
PanelTextField empty = PanelTextField.CreateNew(PanelTextField.TextFieldStyles.Entry, picker.Descriptor.ContentParent);
90+
empty._inputField.gameObject.SetActive(false);
91+
empty.Descriptor.SetTitle(BasisLocalization.Get("library.trackerPicker.empty"));
92+
}
93+
else
94+
{
95+
for (int index = 0; index < candidates.Count; index++)
96+
{
97+
BasisInput tracker = candidates[index];
98+
string roleLabel = tracker.TryGetRole(out BasisBoneTrackedRole role)
99+
? role.ToString()
100+
: "Tracker";
101+
PanelButton row = PanelButton.CreateNew(PanelButton.ButtonStyles.StandardButton, picker.Descriptor.ContentParent);
102+
row.Descriptor.SetTitle($"{roleLabel}{tracker.UniqueDeviceIdentifier}");
103+
row.SetSize(PickerRowSize);
104+
row.OnClicked += () => picker.CloseWithResult(tracker);
105+
}
106+
}
107+
108+
return await picker.WaitAsync();
109+
}
110+
111+
private static List<BasisInput> CollectBindableTrackers()
112+
{
113+
List<BasisInput> result = new List<BasisInput>();
114+
BasisObservableList<BasisInput> devices = BasisDeviceManagement.Instance?.AllInputDevices;
115+
if (devices == null) return result;
116+
117+
for (int i = 0; i < devices.Count; i++)
118+
{
119+
BasisInput input = devices[i];
120+
if (input == null) continue;
121+
if (string.IsNullOrEmpty(input.UniqueDeviceIdentifier)) continue;
122+
if (input is BasisVirtualMidpointInput) continue;
123+
if (input.IsLinked) continue;
124+
if (BasisTrackerRoleOverride.TryGetOverride(input.UniqueDeviceIdentifier, out _)) continue;
125+
if (input.DeviceMatchSettings != null && input.DeviceMatchSettings.HasTrackedRole) continue;
126+
// A tracker already driving a body bone (post-calibration) is excluded so
127+
// calibration and prop binding can't fight over the same device. To reuse
128+
// a calibrated tracker, decalibrate first.
129+
if (input.TryGetRole(out _)) continue;
130+
131+
result.Add(input);
132+
}
133+
return result;
134+
}
135+
}
136+
}

0 commit comments

Comments
 (0)