optimze unity project for xr_rm

This commit is contained in:
2026-06-03 14:16:52 +08:00
parent 9260e46b3c
commit 3fc8024648
98 changed files with 20352 additions and 1568 deletions

View File

@ -5,6 +5,9 @@ using System.Net.Sockets;
using System.Text;
using UnityEngine;
using UnityEngine.XR;
#if !PICO_OPENXR_SDK
using Unity.XR.PXR;
#endif
public class PicoControllerUdpSender : MonoBehaviour
{
@ -12,7 +15,12 @@ public class PicoControllerUdpSender : MonoBehaviour
private const string PortPrefsKey = "xr_rm_udp_sender.port";
private const string SendHzPrefsKey = "xr_rm_udp_sender.send_hz";
private const string ConvertPrefsKey = "xr_rm_udp_sender.convert_coordinates";
private const int MaxPacketsPerFrame = 4;
private const string PoseSourceNone = "none";
private const string PoseSourceUnityXr = "unity_xr";
private const string PoseSourcePxrPredict = "pxr_predict";
private const int InvalidTrackingState = -1;
private const int UnknownControllerStatus = -1;
private const double SendScheduleEpsilonSeconds = 0.0001;
[Header("ROS UDP Target")]
public string host = "192.168.9.99";
@ -25,15 +33,24 @@ public class PicoControllerUdpSender : MonoBehaviour
private UdpClient client;
private IPEndPoint endPoint;
private float nextSendTime;
private double nextSendTime;
private bool sendEnabled;
private readonly Packet packet = new Packet();
private readonly List<InputDevice> deviceBuffer = new List<InputDevice>();
private int nextSeq = 1;
public bool SendEnabled => sendEnabled;
public bool HasEndpoint => endPoint != null;
public bool LeftDeviceValid { get; private set; }
public bool RightDeviceValid { get; private set; }
public bool LeftPoseValid { get; private set; }
public bool RightPoseValid { get; private set; }
public string LeftPoseSource { get; private set; } = PoseSourceNone;
public string RightPoseSource { get; private set; } = PoseSourceNone;
public int LeftTrackingState { get; private set; } = InvalidTrackingState;
public int RightTrackingState { get; private set; } = InvalidTrackingState;
public int LeftControllerStatus { get; private set; } = UnknownControllerStatus;
public int RightControllerStatus { get; private set; } = UnknownControllerStatus;
public bool LeftGripPressed { get; private set; }
public bool RightGripPressed { get; private set; }
public string LastError { get; private set; } = string.Empty;
@ -44,6 +61,8 @@ public class PicoControllerUdpSender : MonoBehaviour
private class Packet
{
public double t;
public double source_time;
public int seq;
public string frame_id = "xr_world";
public Controllers controllers = new Controllers();
}
@ -62,6 +81,23 @@ public class PicoControllerUdpSender : MonoBehaviour
public float trigger;
public float[] pos = new float[] { 0.0f, 1.0f, 0.0f };
public float[] quat = new float[] { 0.0f, 0.0f, 0.0f, 1.0f };
public bool pose_valid;
public int tracking_state = -1;
public int controller_status = -1;
public float grip_value;
public float[] axis = new float[] { 0.0f, 0.0f };
public ButtonsPayload buttons = new ButtonsPayload();
public string pose_source = PoseSourceNone;
}
[Serializable]
private class ButtonsPayload
{
public bool grip;
public bool primary;
public bool secondary;
public bool menu;
public bool axis_click;
}
private void OnEnable()
@ -69,7 +105,7 @@ public class PicoControllerUdpSender : MonoBehaviour
LoadConfig();
ReopenClient();
sendEnabled = sendOnStart;
nextSendTime = 0.0f;
nextSendTime = 0.0;
}
private void OnDisable()
@ -86,7 +122,7 @@ public class PicoControllerUdpSender : MonoBehaviour
return;
}
nextSendTime = 0.0f;
nextSendTime = 0.0;
}
private void OnApplicationFocus(bool hasFocus)
@ -97,7 +133,7 @@ public class PicoControllerUdpSender : MonoBehaviour
return;
}
nextSendTime = 0.0f;
nextSendTime = 0.0;
}
private void Update()
@ -110,21 +146,21 @@ public class PicoControllerUdpSender : MonoBehaviour
FillController(XRNode.LeftHand, packet.controllers.left);
FillController(XRNode.RightHand, packet.controllers.right);
float now = Time.unscaledTime;
float interval = 1.0f / Mathf.Max(sendHz, 1.0f);
if (nextSendTime <= 0.0f || now - nextSendTime > interval * MaxPacketsPerFrame)
double now = Time.realtimeSinceStartupAsDouble;
double interval = 1.0 / Math.Max((double)sendHz, 1.0);
if (nextSendTime <= 0.0 || now - nextSendTime > interval)
{
nextSendTime = now;
}
int sentThisFrame = 0;
while (now >= nextSendTime && sentThisFrame < MaxPacketsPerFrame)
if (now + SendScheduleEpsilonSeconds < nextSendTime)
{
packet.t = Time.realtimeSinceStartupAsDouble;
SendPacket();
nextSendTime += interval;
sentThisFrame++;
return;
}
StampPacket();
SendPacket();
nextSendTime += interval;
}
public bool ApplyConfig(string newHost, int newPort, float newSendHz, bool newConvertUnityToProjectCoordinates, bool save)
@ -169,7 +205,7 @@ public class PicoControllerUdpSender : MonoBehaviour
}
sendEnabled = enabled;
nextSendTime = 0.0f;
nextSendTime = 0.0;
}
private void LoadConfig()
@ -228,6 +264,8 @@ public class PicoControllerUdpSender : MonoBehaviour
{
InputDevice device = GetControllerDevice(node);
bool isLeft = node == XRNode.LeftHand;
ResetControllerPayload(payload);
if (isLeft)
{
LeftDeviceValid = device.isValid;
@ -239,52 +277,174 @@ public class PicoControllerUdpSender : MonoBehaviour
if (!device.isValid)
{
payload.grip = false;
payload.trigger = 0.0f;
SetGripState(isLeft, false);
SetPoseState(isLeft, false, PoseSourceNone, InvalidTrackingState, UnknownControllerStatus);
return;
}
Vector3 position = Vector3.zero;
Quaternion rotation = Quaternion.identity;
Vector3 unityPosition = Vector3.zero;
Quaternion unityRotation = Quaternion.identity;
float trigger = 0.0f;
float gripValue = 0.0f;
Vector2 axis = Vector2.zero;
bool gripButton = false;
bool primaryButton = false;
bool secondaryButton = false;
bool menuButton = false;
bool axisClick = false;
bool isTracked = true;
InputTrackingState trackingState = 0;
device.TryGetFeatureValue(CommonUsages.devicePosition, out position);
device.TryGetFeatureValue(CommonUsages.deviceRotation, out rotation);
bool hasUnityPosition = device.TryGetFeatureValue(CommonUsages.devicePosition, out unityPosition);
bool hasUnityRotation = device.TryGetFeatureValue(CommonUsages.deviceRotation, out unityRotation);
device.TryGetFeatureValue(CommonUsages.trigger, out trigger);
device.TryGetFeatureValue(CommonUsages.grip, out gripValue);
device.TryGetFeatureValue(CommonUsages.gripButton, out gripButton);
device.TryGetFeatureValue(CommonUsages.primary2DAxis, out axis);
device.TryGetFeatureValue(CommonUsages.primary2DAxisClick, out axisClick);
device.TryGetFeatureValue(CommonUsages.primaryButton, out primaryButton);
device.TryGetFeatureValue(CommonUsages.secondaryButton, out secondaryButton);
device.TryGetFeatureValue(CommonUsages.menuButton, out menuButton);
payload.grip = gripButton || gripValue > 0.5f;
SetGripState(isLeft, payload.grip);
payload.trigger = Mathf.Clamp01(trigger);
bool hasIsTracked = device.TryGetFeatureValue(CommonUsages.isTracked, out isTracked);
bool hasTrackingState = device.TryGetFeatureValue(CommonUsages.trackingState, out trackingState);
int trackingStateValue = hasTrackingState ? (int)trackingState : InvalidTrackingState;
bool trackedOk = !hasIsTracked || isTracked;
bool trackingStateOk = !hasTrackingState
|| ((trackingState & InputTrackingState.Position) != 0
&& (trackingState & InputTrackingState.Rotation) != 0);
bool unityPoseValid = hasUnityPosition
&& hasUnityRotation
&& trackedOk
&& trackingStateOk
&& IsFinite(unityPosition)
&& IsFinite(unityRotation);
if (convertUnityToProjectCoordinates)
Vector3 posePosition = Vector3.zero;
Quaternion poseRotation = Quaternion.identity;
string poseSource = PoseSourceNone;
int controllerStatus = UnknownControllerStatus;
bool poseValid = TryGetPicoPredictedPose(isLeft, out posePosition, out poseRotation, out controllerStatus);
if (poseValid)
{
payload.pos[0] = position.x;
payload.pos[1] = position.y;
payload.pos[2] = -position.z;
poseSource = PoseSourcePxrPredict;
}
else if (unityPoseValid)
{
posePosition = unityPosition;
poseRotation = unityRotation;
poseSource = PoseSourceUnityXr;
poseValid = true;
}
payload.quat[0] = -rotation.x;
payload.quat[1] = -rotation.y;
payload.quat[2] = rotation.z;
payload.quat[3] = rotation.w;
payload.trigger = Mathf.Clamp01(trigger);
payload.grip_value = Mathf.Clamp01(gripValue);
payload.axis[0] = axis.x;
payload.axis[1] = axis.y;
payload.buttons.grip = gripButton;
payload.buttons.primary = primaryButton;
payload.buttons.secondary = secondaryButton;
payload.buttons.menu = menuButton;
payload.buttons.axis_click = axisClick;
payload.pose_valid = poseValid;
payload.pose_source = poseSource;
payload.tracking_state = trackingStateValue;
payload.controller_status = controllerStatus;
payload.grip = poseValid && (gripButton || gripValue > 0.5f);
SetGripState(isLeft, payload.grip);
SetPoseState(isLeft, poseValid, poseSource, trackingStateValue, controllerStatus);
WritePose(payload, posePosition, poseRotation, poseSource);
}
private void WritePose(ControllerPayload payload, Vector3 position, Quaternion rotation, string poseSource)
{
if (convertUnityToProjectCoordinates && poseSource == PoseSourcePxrPredict)
{
WritePoseValues(
payload,
ConvertPicoNativePositionToProject(position),
ConvertPicoNativeRotationToProject(rotation));
}
else
{
payload.pos[0] = position.x;
payload.pos[1] = position.y;
payload.pos[2] = position.z;
payload.quat[0] = rotation.x;
payload.quat[1] = rotation.y;
payload.quat[2] = rotation.z;
payload.quat[3] = rotation.w;
WritePoseValues(payload, position, rotation);
}
}
private static Vector3 ConvertPicoNativePositionToProject(Vector3 position)
{
return new Vector3(position.z, position.y, -position.x);
}
private static Quaternion ConvertPicoNativeRotationToProject(Quaternion rotation)
{
return Quaternion.Euler(0.0f, 90.0f, 0.0f) * rotation;
}
private static void WritePoseValues(ControllerPayload payload, Vector3 position, Quaternion rotation)
{
payload.pos[0] = position.x;
payload.pos[1] = position.y;
payload.pos[2] = position.z;
payload.quat[0] = rotation.x;
payload.quat[1] = rotation.y;
payload.quat[2] = rotation.z;
payload.quat[3] = rotation.w;
}
private bool TryGetPicoPredictedPose(
bool isLeft,
out Vector3 position,
out Quaternion rotation,
out int controllerStatus)
{
position = Vector3.zero;
rotation = Quaternion.identity;
controllerStatus = UnknownControllerStatus;
#if !PICO_OPENXR_SDK && !UNITY_EDITOR
try
{
PXR_Input.Controller controller = isLeft
? PXR_Input.Controller.LeftController
: PXR_Input.Controller.RightController;
if (!PXR_Input.IsControllerConnected(controller))
{
return false;
}
PXR_Input.ControllerStatus status = PXR_Input.GetControllerStatus(controller);
controllerStatus = (int)status;
if (!IsPositionCapablePicoStatus(status))
{
return false;
}
double predictTime = PXR_System.GetPredictedDisplayTime();
position = PXR_Input.GetControllerPredictPosition(controller, predictTime);
rotation = PXR_Input.GetControllerPredictRotation(controller, predictTime);
return IsFinite(position) && IsFinite(rotation);
}
catch (Exception exception)
{
LastError = $"PXR pose failed: {exception.Message}";
return false;
}
#else
return false;
#endif
}
#if !PICO_OPENXR_SDK && !UNITY_EDITOR
private static bool IsPositionCapablePicoStatus(PXR_Input.ControllerStatus status)
{
return status == PXR_Input.ControllerStatus.Static
|| status == PXR_Input.ControllerStatus.SixDof
|| status == PXR_Input.ControllerStatus.CollidedIn6Dof;
}
#endif
private InputDevice GetControllerDevice(XRNode node)
{
InputDevice device = InputDevices.GetDeviceAtXRNode(node);
@ -340,15 +500,106 @@ public class PicoControllerUdpSender : MonoBehaviour
}
}
private void SendSafeStopPacket()
private void SetPoseState(
bool isLeft,
bool poseValid,
string poseSource,
int trackingState,
int controllerStatus)
{
if (isLeft)
{
LeftPoseValid = poseValid;
LeftPoseSource = poseSource;
LeftTrackingState = trackingState;
LeftControllerStatus = controllerStatus;
}
else
{
RightPoseValid = poseValid;
RightPoseSource = poseSource;
RightTrackingState = trackingState;
RightControllerStatus = controllerStatus;
}
}
private static void ResetControllerPayload(ControllerPayload payload)
{
if (payload.pos == null || payload.pos.Length != 3)
{
payload.pos = new float[] { 0.0f, 0.0f, 0.0f };
}
if (payload.quat == null || payload.quat.Length != 4)
{
payload.quat = new float[] { 0.0f, 0.0f, 0.0f, 1.0f };
}
if (payload.axis == null || payload.axis.Length != 2)
{
payload.axis = new float[] { 0.0f, 0.0f };
}
if (payload.buttons == null)
{
payload.buttons = new ButtonsPayload();
}
payload.grip = false;
payload.trigger = 0.0f;
payload.pos[0] = 0.0f;
payload.pos[1] = 0.0f;
payload.pos[2] = 0.0f;
payload.quat[0] = 0.0f;
payload.quat[1] = 0.0f;
payload.quat[2] = 0.0f;
payload.quat[3] = 1.0f;
payload.pose_valid = false;
payload.tracking_state = InvalidTrackingState;
payload.controller_status = UnknownControllerStatus;
payload.grip_value = 0.0f;
payload.axis[0] = 0.0f;
payload.axis[1] = 0.0f;
payload.buttons.grip = false;
payload.buttons.primary = false;
payload.buttons.secondary = false;
payload.buttons.menu = false;
payload.buttons.axis_click = false;
payload.pose_source = PoseSourceNone;
}
private void StampPacket()
{
packet.t = Time.realtimeSinceStartupAsDouble;
packet.controllers.left.grip = false;
packet.controllers.left.trigger = 0.0f;
packet.controllers.right.grip = false;
packet.controllers.right.trigger = 0.0f;
packet.source_time = packet.t;
packet.seq = nextSeq;
nextSeq = nextSeq == int.MaxValue ? 1 : nextSeq + 1;
}
private static bool IsFinite(Vector3 value)
{
return IsFinite(value.x) && IsFinite(value.y) && IsFinite(value.z);
}
private static bool IsFinite(Quaternion value)
{
return IsFinite(value.x) && IsFinite(value.y) && IsFinite(value.z) && IsFinite(value.w);
}
private static bool IsFinite(float value)
{
return !float.IsNaN(value) && !float.IsInfinity(value);
}
private void SendSafeStopPacket()
{
StampPacket();
ResetControllerPayload(packet.controllers.left);
ResetControllerPayload(packet.controllers.right);
LeftGripPressed = false;
RightGripPressed = false;
SetPoseState(true, false, PoseSourceNone, InvalidTrackingState, UnknownControllerStatus);
SetPoseState(false, false, PoseSourceNone, InvalidTrackingState, UnknownControllerStatus);
SendPacket();
}

View File

@ -1,5 +1,9 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Net;
using System.Net.Sockets;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.XR;
@ -11,29 +15,55 @@ using Unity.XR.PXR;
public class PicoUdpConfigPanel : MonoBehaviour
{
private const int RowCount = 6;
private const int RowCount = 8;
private const int MaxSavedIps = 5;
private const float AxisRepeatDelay = 0.22f;
private const string RecentIpPrefsKey = "xr_rm_udp_sender.ip_recent";
private const string FavoriteIpPrefsKey = "xr_rm_udp_sender.ip_favorites";
private const string IpListSeparator = "|";
private const string RegularFontResourcePath = "Fonts/Roboto-Regular SDF";
private const string EmphasisFontResourcePath = "Fonts/Roboto-Bold SDF";
private const int RowTargetIp = 0;
private const int RowSavedIp = 1;
private const int RowFavorite = 2;
private const int RowTargetPort = 3;
private const int RowSendHz = 4;
private const int RowCoordinates = 5;
private const int RowSending = 6;
private const int RowSaveApply = 7;
public int targetFrameRate = 90;
public bool showOnStart = true;
public bool togglePanelWithMenuButton = true;
private readonly Text[] rowTexts = new Text[RowCount];
private readonly Color normalColor = new Color(0.86f, 0.91f, 0.96f, 1.0f);
private readonly Color selectedColor = new Color(0.35f, 0.82f, 1.0f, 1.0f);
private readonly Color mutedColor = new Color(0.58f, 0.66f, 0.74f, 1.0f);
private readonly ConfigRow[] rows = new ConfigRow[RowCount];
private readonly List<string> recentIps = new List<string>();
private readonly List<string> favoriteIps = new List<string>();
private readonly List<string> savedIpChoices = new List<string>();
private readonly Color normalColor = new Color(0.92f, 0.96f, 0.98f, 1.0f);
private readonly Color selectedColor = new Color(0.32f, 0.86f, 1.0f, 1.0f);
private readonly Color mutedColor = new Color(0.64f, 0.72f, 0.78f, 1.0f);
private readonly Color cardColor = new Color(0.018f, 0.026f, 0.032f, 0.86f);
private readonly Color rowColor = new Color(0.08f, 0.105f, 0.125f, 0.66f);
private readonly Color selectedRowColor = new Color(0.05f, 0.20f, 0.28f, 0.92f);
private PicoControllerUdpSender sender;
private Canvas canvas;
private GameObject configPanelObject;
private GameObject runHudObject;
private Text titleText;
private Text statusText;
private Text footerText;
private Text hudTitleText;
private Text hudStatusText;
private Text hudDetailText;
private Text hudHintText;
private Image hudStatusStripe;
private TextMeshProUGUI titleText;
private TextMeshProUGUI statusText;
private TextMeshProUGUI connectionHeaderText;
private TextMeshProUGUI runtimeHeaderText;
private TextMeshProUGUI footerText;
private TextMeshProUGUI hudTitleText;
private TextMeshProUGUI hudStatusText;
private TextMeshProUGUI hudDetailText;
private TextMeshProUGUI hudHintText;
private TMP_FontAsset regularFontAsset;
private TMP_FontAsset emphasisFontAsset;
private TouchScreenKeyboard keyboard;
private KeyboardTarget keyboardTarget;
private string hostDraft;
@ -42,6 +72,7 @@ public class PicoUdpConfigPanel : MonoBehaviour
private bool convertDraft;
private PicoKeepAwake keepAwake;
private int selectedRow;
private int selectedSavedIpIndex;
private float nextAxisTime;
private bool wasActivatePressed;
private bool wasApplyPressed;
@ -56,6 +87,15 @@ public class PicoUdpConfigPanel : MonoBehaviour
SendHz
}
private class ConfigRow
{
public GameObject root;
public Image background;
public Image accent;
public TextMeshProUGUI labelText;
public TextMeshProUGUI valueText;
}
private void Awake()
{
sender = GetComponent<PicoControllerUdpSender>();
@ -69,6 +109,8 @@ public class PicoUdpConfigPanel : MonoBehaviour
{
keepAwake = gameObject.AddComponent<PicoKeepAwake>();
}
LoadFonts();
}
private void Start()
@ -76,6 +118,7 @@ public class PicoUdpConfigPanel : MonoBehaviour
ApplyRuntimeDefaults();
EnsurePanel();
LoadDraftsFromSender();
LoadIpLists();
canvas.gameObject.SetActive(true);
SetConfigPanelVisible(showOnStart, false);
RefreshPanel();
@ -96,6 +139,7 @@ public class PicoUdpConfigPanel : MonoBehaviour
Screen.sleepTimeout = SleepTimeout.NeverSleep;
XRSettings.eyeTextureResolutionScale = 1.0f;
RequestPicoDisplayRefreshRate();
RequestPicoPassthrough();
}
private void RequestPicoDisplayRefreshRate()
@ -120,6 +164,24 @@ public class PicoUdpConfigPanel : MonoBehaviour
#endif
}
private void RequestPicoPassthrough()
{
#if UNITY_ANDROID && !UNITY_EDITOR
try
{
#if PICO_OPENXR_SDK
PassthroughFeature.EnableVideoSeeThrough = true;
#else
PXR_Manager.EnableVideoSeeThrough = true;
#endif
}
catch (Exception exception)
{
Debug.LogWarning($"XR-RM failed to enable PICO video see-through: {exception.Message}");
}
#endif
}
private void EnsurePanel()
{
Camera panelCamera = Camera.main;
@ -130,8 +192,6 @@ public class PicoUdpConfigPanel : MonoBehaviour
panelCamera = cameraObject.AddComponent<Camera>();
panelCamera.nearClipPlane = 0.05f;
panelCamera.farClipPlane = 100.0f;
panelCamera.clearFlags = CameraClearFlags.SolidColor;
panelCamera.backgroundColor = new Color(0.02f, 0.025f, 0.03f, 1.0f);
if (FindObjectOfType<AudioListener>() == null)
{
@ -141,6 +201,9 @@ public class PicoUdpConfigPanel : MonoBehaviour
AddTrackedPoseDriverIfAvailable(cameraObject);
}
panelCamera.clearFlags = CameraClearFlags.SolidColor;
panelCamera.backgroundColor = new Color(0.0f, 0.0f, 0.0f, 0.0f);
GameObject canvasObject = new GameObject("XR-RM UDP Config Canvas", typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler));
canvasObject.transform.SetParent(panelCamera.transform, false);
canvasObject.transform.localPosition = new Vector3(0.0f, 0.0f, 2.0f);
@ -156,75 +219,102 @@ public class PicoUdpConfigPanel : MonoBehaviour
canvasRect.sizeDelta = new Vector2(980.0f, 640.0f);
CanvasScaler scaler = canvasObject.GetComponent<CanvasScaler>();
scaler.dynamicPixelsPerUnit = 12.0f;
scaler.dynamicPixelsPerUnit = 18.0f;
scaler.referencePixelsPerUnit = 100.0f;
configPanelObject = new GameObject("Config Panel", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
configPanelObject.transform.SetParent(canvasObject.transform, false);
RectTransform backgroundRect = configPanelObject.GetComponent<RectTransform>();
backgroundRect.anchorMin = Vector2.zero;
backgroundRect.anchorMax = Vector2.one;
backgroundRect.offsetMin = Vector2.zero;
backgroundRect.offsetMax = Vector2.zero;
SetRect(backgroundRect, new Vector2(0.08f, 0.08f), new Vector2(0.92f, 0.92f), Vector2.zero, Vector2.zero);
Image background = configPanelObject.GetComponent<Image>();
background.color = new Color(0.045f, 0.06f, 0.075f, 0.94f);
background.color = cardColor;
titleText = CreateText("Title", configPanelObject.transform, 38, FontStyle.Bold, TextAnchor.MiddleLeft);
SetRect(titleText.rectTransform, new Vector2(0.06f, 0.82f), new Vector2(0.94f, 0.96f), Vector2.zero, Vector2.zero);
titleText = CreateText("Title", configPanelObject.transform, 40, FontStyles.Bold, TextAlignmentOptions.Left);
SetRect(titleText.rectTransform, new Vector2(0.06f, 0.875f), new Vector2(0.94f, 0.965f), Vector2.zero, Vector2.zero);
statusText = CreateText("Status", configPanelObject.transform, 22, FontStyle.Normal, TextAnchor.MiddleLeft);
statusText = CreateText("Status", configPanelObject.transform, 21, FontStyles.Normal, TextAlignmentOptions.Left);
statusText.color = mutedColor;
SetRect(statusText.rectTransform, new Vector2(0.06f, 0.72f), new Vector2(0.94f, 0.82f), Vector2.zero, Vector2.zero);
SetRect(statusText.rectTransform, new Vector2(0.06f, 0.795f), new Vector2(0.94f, 0.872f), Vector2.zero, Vector2.zero);
for (int i = 0; i < RowCount; i++)
{
Text rowText = CreateText("Row " + i, configPanelObject.transform, 27, FontStyle.Normal, TextAnchor.MiddleLeft);
float top = 0.68f - i * 0.09f;
SetRect(rowText.rectTransform, new Vector2(0.08f, top - 0.075f), new Vector2(0.92f, top), Vector2.zero, Vector2.zero);
rowTexts[i] = rowText;
}
connectionHeaderText = CreateText("Connection Header", configPanelObject.transform, 18, FontStyles.Bold, TextAlignmentOptions.Left);
connectionHeaderText.color = selectedColor;
SetRect(connectionHeaderText.rectTransform, new Vector2(0.06f, 0.725f), new Vector2(0.94f, 0.775f), Vector2.zero, Vector2.zero);
footerText = CreateText("Footer", configPanelObject.transform, 23, FontStyle.Normal, TextAnchor.MiddleCenter);
footerText.color = normalColor;
SetRect(footerText.rectTransform, new Vector2(0.05f, 0.025f), new Vector2(0.95f, 0.15f), Vector2.zero, Vector2.zero);
CreateConfigRow(RowTargetIp, 0.655f, 0.715f);
CreateConfigRow(RowSavedIp, 0.585f, 0.645f);
CreateConfigRow(RowFavorite, 0.515f, 0.575f);
runtimeHeaderText = CreateText("Runtime Header", configPanelObject.transform, 18, FontStyles.Bold, TextAlignmentOptions.Left);
runtimeHeaderText.color = selectedColor;
SetRect(runtimeHeaderText.rectTransform, new Vector2(0.06f, 0.445f), new Vector2(0.94f, 0.495f), Vector2.zero, Vector2.zero);
CreateConfigRow(RowTargetPort, 0.375f, 0.435f);
CreateConfigRow(RowSendHz, 0.305f, 0.365f);
CreateConfigRow(RowCoordinates, 0.235f, 0.295f);
CreateConfigRow(RowSending, 0.165f, 0.225f);
CreateConfigRow(RowSaveApply, 0.095f, 0.155f);
footerText = CreateText("Footer", configPanelObject.transform, 19, FontStyles.Normal, TextAlignmentOptions.Center);
footerText.color = mutedColor;
SetRect(footerText.rectTransform, new Vector2(0.05f, 0.020f), new Vector2(0.95f, 0.075f), Vector2.zero, Vector2.zero);
CreateRunHud(canvasObject.transform);
}
private void CreateConfigRow(int index, float bottom, float top)
{
GameObject rowObject = new GameObject("Row " + index, typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
rowObject.transform.SetParent(configPanelObject.transform, false);
RectTransform rowRect = rowObject.GetComponent<RectTransform>();
SetRect(rowRect, new Vector2(0.06f, bottom), new Vector2(0.94f, top), Vector2.zero, Vector2.zero);
Image background = rowObject.GetComponent<Image>();
background.color = rowColor;
GameObject accentObject = CreateImage("Accent", rowObject.transform, new Color(0.0f, 0.0f, 0.0f, 0.0f));
SetRect(accentObject.GetComponent<RectTransform>(), new Vector2(0.0f, 0.0f), new Vector2(0.018f, 1.0f), Vector2.zero, Vector2.zero);
TextMeshProUGUI labelText = CreateText("Label", rowObject.transform, 20, FontStyles.Bold, TextAlignmentOptions.Left);
SetRect(labelText.rectTransform, new Vector2(0.045f, 0.10f), new Vector2(0.38f, 0.90f), Vector2.zero, Vector2.zero);
TextMeshProUGUI valueText = CreateText("Value", rowObject.transform, 21, FontStyles.Normal, TextAlignmentOptions.Left);
SetRect(valueText.rectTransform, new Vector2(0.39f, 0.10f), new Vector2(0.965f, 0.90f), Vector2.zero, Vector2.zero);
rows[index] = new ConfigRow
{
root = rowObject,
background = background,
accent = accentObject.GetComponent<Image>(),
labelText = labelText,
valueText = valueText,
};
}
private void CreateRunHud(Transform canvasParent)
{
runHudObject = new GameObject("Run HUD", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
runHudObject.transform.SetParent(canvasParent, false);
RectTransform hudRect = runHudObject.GetComponent<RectTransform>();
hudRect.anchorMin = Vector2.zero;
hudRect.anchorMax = Vector2.one;
hudRect.offsetMin = Vector2.zero;
hudRect.offsetMax = Vector2.zero;
SetRect(hudRect, new Vector2(0.035f, 0.055f), new Vector2(0.54f, 0.285f), Vector2.zero, Vector2.zero);
Image hudBackground = runHudObject.GetComponent<Image>();
hudBackground.color = new Color(0.018f, 0.026f, 0.032f, 0.88f);
hudBackground.color = new Color(0.012f, 0.020f, 0.026f, 0.82f);
GameObject headerObject = CreateImage("HUD Header", runHudObject.transform, new Color(0.035f, 0.05f, 0.06f, 0.88f));
SetRect(headerObject.GetComponent<RectTransform>(), new Vector2(0.05f, 0.70f), new Vector2(0.95f, 0.92f), Vector2.zero, Vector2.zero);
GameObject stripeObject = CreateImage("HUD Status Stripe", runHudObject.transform, selectedColor);
SetRect(stripeObject.GetComponent<RectTransform>(), new Vector2(0.0f, 0.0f), new Vector2(0.018f, 1.0f), Vector2.zero, Vector2.zero);
hudStatusStripe = stripeObject.GetComponent<Image>();
hudTitleText = CreateText("HUD Title", headerObject.transform, 34, FontStyle.Bold, TextAnchor.MiddleLeft);
SetRect(hudTitleText.rectTransform, new Vector2(0.04f, 0.54f), new Vector2(0.96f, 0.95f), Vector2.zero, Vector2.zero);
hudTitleText = CreateText("HUD Title", runHudObject.transform, 24, FontStyles.Bold, TextAlignmentOptions.Left);
SetRect(hudTitleText.rectTransform, new Vector2(0.055f, 0.74f), new Vector2(0.95f, 0.96f), Vector2.zero, Vector2.zero);
hudStatusText = CreateText("HUD Status", headerObject.transform, 28, FontStyle.Bold, TextAnchor.MiddleLeft);
hudStatusText = CreateText("HUD Status", runHudObject.transform, 22, FontStyles.Bold, TextAlignmentOptions.Left);
hudStatusText.color = selectedColor;
SetRect(hudStatusText.rectTransform, new Vector2(0.04f, 0.15f), new Vector2(0.96f, 0.56f), Vector2.zero, Vector2.zero);
SetRect(hudStatusText.rectTransform, new Vector2(0.055f, 0.49f), new Vector2(0.95f, 0.75f), Vector2.zero, Vector2.zero);
hudDetailText = CreateText("HUD Detail", runHudObject.transform, 25, FontStyle.Normal, TextAnchor.MiddleCenter);
SetRect(hudDetailText.rectTransform, new Vector2(0.07f, 0.36f), new Vector2(0.93f, 0.64f), Vector2.zero, Vector2.zero);
hudDetailText = CreateText("HUD Detail", runHudObject.transform, 19, FontStyles.Normal, TextAlignmentOptions.Left);
SetRect(hudDetailText.rectTransform, new Vector2(0.055f, 0.16f), new Vector2(0.95f, 0.50f), Vector2.zero, Vector2.zero);
hudHintText = CreateText("HUD Hint", runHudObject.transform, 23, FontStyle.Normal, TextAnchor.MiddleCenter);
hudHintText = CreateText("HUD Hint", runHudObject.transform, 17, FontStyles.Normal, TextAlignmentOptions.Left);
hudHintText.color = mutedColor;
SetRect(hudHintText.rectTransform, new Vector2(0.07f, 0.08f), new Vector2(0.93f, 0.20f), Vector2.zero, Vector2.zero);
GameObject verticalLine = CreateImage("HUD Vertical Reticle", runHudObject.transform, new Color(0.35f, 0.82f, 1.0f, 0.38f));
SetRect(verticalLine.GetComponent<RectTransform>(), new Vector2(0.498f, 0.27f), new Vector2(0.502f, 0.36f), Vector2.zero, Vector2.zero);
GameObject horizontalLine = CreateImage("HUD Horizontal Reticle", runHudObject.transform, new Color(0.35f, 0.82f, 1.0f, 0.38f));
SetRect(horizontalLine.GetComponent<RectTransform>(), new Vector2(0.47f, 0.313f), new Vector2(0.53f, 0.319f), Vector2.zero, Vector2.zero);
SetRect(hudHintText.rectTransform, new Vector2(0.055f, 0.02f), new Vector2(0.95f, 0.16f), Vector2.zero, Vector2.zero);
}
private static void AddTrackedPoseDriverIfAvailable(GameObject cameraObject)
@ -246,25 +336,62 @@ public class PicoUdpConfigPanel : MonoBehaviour
}
}
private Text CreateText(string name, Transform parent, int fontSize, FontStyle fontStyle, TextAnchor alignment)
private void LoadFonts()
{
GameObject textObject = new GameObject(name, typeof(RectTransform), typeof(CanvasRenderer), typeof(Text));
regularFontAsset = Resources.Load<TMP_FontAsset>(RegularFontResourcePath);
emphasisFontAsset = Resources.Load<TMP_FontAsset>(EmphasisFontResourcePath);
if (regularFontAsset == null)
{
regularFontAsset = TMP_Settings.GetFontAsset();
}
if (emphasisFontAsset == null)
{
emphasisFontAsset = regularFontAsset;
}
if (regularFontAsset == null)
{
Debug.LogWarning("XR-RM failed to load Roboto TMP font assets. Rebuild with XR-RM/Rebuild Roboto TMP Font Assets.");
}
}
private TextMeshProUGUI CreateText(string name, Transform parent, int fontSize, FontStyles fontStyle, TextAlignmentOptions alignment)
{
GameObject textObject = new GameObject(name, typeof(RectTransform), typeof(CanvasRenderer), typeof(TextMeshProUGUI));
textObject.transform.SetParent(parent, false);
Text text = textObject.GetComponent<Text>();
text.font = Resources.GetBuiltinResource<Font>("Arial.ttf");
TextMeshProUGUI text = textObject.GetComponent<TextMeshProUGUI>();
TMP_FontAsset fontAsset = GetFontAssetForStyle(fontStyle);
if (fontAsset != null)
{
text.font = fontAsset;
}
text.fontSize = fontSize;
text.fontStyle = fontStyle;
text.alignment = alignment;
text.horizontalOverflow = HorizontalWrapMode.Wrap;
text.verticalOverflow = VerticalWrapMode.Truncate;
text.resizeTextForBestFit = true;
text.resizeTextMinSize = Mathf.Max(12, fontSize - 8);
text.resizeTextMaxSize = fontSize;
text.enableAutoSizing = true;
text.fontSizeMin = Mathf.Max(13, fontSize - 6);
text.fontSizeMax = fontSize;
text.enableWordWrapping = false;
text.overflowMode = TextOverflowModes.Ellipsis;
text.raycastTarget = false;
text.color = normalColor;
Shadow shadow = textObject.AddComponent<Shadow>();
shadow.effectColor = new Color(0.0f, 0.0f, 0.0f, 0.78f);
shadow.effectDistance = new Vector2(1.4f, -1.4f);
shadow.useGraphicAlpha = true;
return text;
}
private TMP_FontAsset GetFontAssetForStyle(FontStyles fontStyle)
{
return fontStyle == FontStyles.Bold && emphasisFontAsset != null ? emphasisFontAsset : regularFontAsset;
}
private static void SetRect(RectTransform rectTransform, Vector2 anchorMin, Vector2 anchorMax, Vector2 offsetMin, Vector2 offsetMax)
{
rectTransform.anchorMin = anchorMin;
@ -290,6 +417,61 @@ public class PicoUdpConfigPanel : MonoBehaviour
convertDraft = sender.convertUnityToProjectCoordinates;
}
private void LoadIpLists()
{
LoadIpList(RecentIpPrefsKey, recentIps);
LoadIpList(FavoriteIpPrefsKey, favoriteIps);
RefreshSavedIpChoices(hostDraft);
}
private void LoadIpList(string prefsKey, List<string> target)
{
target.Clear();
string raw = PlayerPrefs.GetString(prefsKey, string.Empty);
if (string.IsNullOrEmpty(raw))
{
return;
}
string[] values = raw.Split(new[] { IpListSeparator }, StringSplitOptions.RemoveEmptyEntries);
for (int i = 0; i < values.Length && target.Count < MaxSavedIps; i++)
{
AddUniqueIp(target, values[i]);
}
}
private void SaveIpLists()
{
PlayerPrefs.SetString(RecentIpPrefsKey, string.Join(IpListSeparator, recentIps.ToArray()));
PlayerPrefs.SetString(FavoriteIpPrefsKey, string.Join(IpListSeparator, favoriteIps.ToArray()));
PlayerPrefs.Save();
}
private void RefreshSavedIpChoices(string preferredIp)
{
savedIpChoices.Clear();
for (int i = 0; i < favoriteIps.Count; i++)
{
AddUniqueIp(savedIpChoices, favoriteIps[i]);
}
for (int i = 0; i < recentIps.Count; i++)
{
AddUniqueIp(savedIpChoices, recentIps[i]);
}
if (savedIpChoices.Count == 0 && IsValidIpv4(hostDraft))
{
savedIpChoices.Add(hostDraft);
}
selectedSavedIpIndex = Mathf.Clamp(FindIpIndex(savedIpChoices, preferredIp), 0, Mathf.Max(0, savedIpChoices.Count - 1));
if (selectedSavedIpIndex < 0)
{
selectedSavedIpIndex = 0;
}
}
private void HandleKeyboard()
{
if (keyboard == null)
@ -317,6 +499,7 @@ public class PicoUdpConfigPanel : MonoBehaviour
if (keyboardTarget == KeyboardTarget.Host)
{
hostDraft = trimmed;
RefreshSavedIpChoices(hostDraft);
panelMessage = "Target IP edited";
}
else if (keyboardTarget == KeyboardTarget.Port)
@ -403,22 +586,30 @@ public class PicoUdpConfigPanel : MonoBehaviour
private void AdjustSelectedValue(int direction)
{
if (selectedRow == 1)
if (selectedRow == RowSavedIp)
{
SelectSavedIp(direction);
}
else if (selectedRow == RowFavorite)
{
ToggleFavoriteIp();
}
else if (selectedRow == RowTargetPort)
{
portDraft = Mathf.Clamp(portDraft + direction, 1, 65535);
panelMessage = "Target port adjusted";
}
else if (selectedRow == 2)
else if (selectedRow == RowSendHz)
{
sendHzDraft = Mathf.Clamp(sendHzDraft + direction * 5.0f, 1.0f, 120.0f);
panelMessage = "Send rate adjusted";
}
else if (selectedRow == 3)
else if (selectedRow == RowCoordinates)
{
convertDraft = !convertDraft;
panelMessage = "Coordinate mode toggled";
}
else if (selectedRow == 4)
else if (selectedRow == RowSending)
{
ToggleSending();
}
@ -426,33 +617,91 @@ public class PicoUdpConfigPanel : MonoBehaviour
private void ActivateSelectedRow()
{
if (selectedRow == 0)
if (selectedRow == RowTargetIp)
{
OpenKeyboard(KeyboardTarget.Host, hostDraft, TouchScreenKeyboardType.URL);
}
else if (selectedRow == 1)
else if (selectedRow == RowSavedIp)
{
OpenKeyboard(KeyboardTarget.Port, portDraft.ToString(), TouchScreenKeyboardType.NumberPad);
ApplySelectedSavedIp();
}
else if (selectedRow == 2)
else if (selectedRow == RowFavorite)
{
OpenKeyboard(KeyboardTarget.SendHz, Mathf.RoundToInt(sendHzDraft).ToString(), TouchScreenKeyboardType.NumberPad);
ToggleFavoriteIp();
}
else if (selectedRow == 3)
else if (selectedRow == RowTargetPort)
{
OpenKeyboard(KeyboardTarget.Port, portDraft.ToString(CultureInfo.InvariantCulture), TouchScreenKeyboardType.NumberPad);
}
else if (selectedRow == RowSendHz)
{
OpenKeyboard(KeyboardTarget.SendHz, Mathf.RoundToInt(sendHzDraft).ToString(CultureInfo.InvariantCulture), TouchScreenKeyboardType.NumberPad);
}
else if (selectedRow == RowCoordinates)
{
convertDraft = !convertDraft;
panelMessage = "Coordinate mode toggled";
}
else if (selectedRow == 4)
else if (selectedRow == RowSending)
{
ToggleSending();
}
else if (selectedRow == 5)
else if (selectedRow == RowSaveApply)
{
ApplyDrafts();
}
}
private void SelectSavedIp(int direction)
{
if (savedIpChoices.Count == 0)
{
panelMessage = "No saved IPs";
return;
}
selectedSavedIpIndex = (selectedSavedIpIndex + direction + savedIpChoices.Count) % savedIpChoices.Count;
panelMessage = "Saved IP selected";
}
private void ApplySelectedSavedIp()
{
if (savedIpChoices.Count == 0)
{
panelMessage = "No saved IPs";
return;
}
selectedSavedIpIndex = Mathf.Clamp(selectedSavedIpIndex, 0, savedIpChoices.Count - 1);
hostDraft = savedIpChoices[selectedSavedIpIndex];
panelMessage = "Saved IP applied";
}
private void ToggleFavoriteIp()
{
if (!IsValidIpv4(hostDraft))
{
panelMessage = "Invalid IP for favorite";
return;
}
int index = FindIpIndex(favoriteIps, hostDraft);
if (index >= 0)
{
favoriteIps.RemoveAt(index);
panelMessage = "Favorite removed";
}
else
{
InsertIpAtFront(favoriteIps, hostDraft);
TrimIpList(favoriteIps);
panelMessage = "Favorite saved";
}
SaveIpLists();
RefreshSavedIpChoices(hostDraft);
}
private void OpenKeyboard(KeyboardTarget target, string text, TouchScreenKeyboardType keyboardType)
{
keyboardTarget = target;
@ -469,7 +718,83 @@ public class PicoUdpConfigPanel : MonoBehaviour
private void ApplyDrafts()
{
bool applied = sender.ApplyConfig(hostDraft, portDraft, sendHzDraft, convertDraft, true);
panelMessage = applied ? "Saved and applied" : sender.LastError;
if (!applied)
{
panelMessage = sender.LastError;
return;
}
RecordRecentIp(hostDraft);
SaveIpLists();
RefreshSavedIpChoices(hostDraft);
panelMessage = "Saved and applied";
}
private void RecordRecentIp(string ip)
{
if (!IsValidIpv4(ip))
{
return;
}
InsertIpAtFront(recentIps, ip);
TrimIpList(recentIps);
}
private static void InsertIpAtFront(List<string> values, string ip)
{
int existingIndex = FindIpIndex(values, ip);
if (existingIndex >= 0)
{
values.RemoveAt(existingIndex);
}
values.Insert(0, ip.Trim());
}
private static bool AddUniqueIp(List<string> values, string ip)
{
string trimmed = ip == null ? string.Empty : ip.Trim();
if (!IsValidIpv4(trimmed) || FindIpIndex(values, trimmed) >= 0)
{
return false;
}
values.Add(trimmed);
return true;
}
private static void TrimIpList(List<string> values)
{
while (values.Count > MaxSavedIps)
{
values.RemoveAt(values.Count - 1);
}
}
private static int FindIpIndex(List<string> values, string ip)
{
string trimmed = ip == null ? string.Empty : ip.Trim();
for (int i = 0; i < values.Count; i++)
{
if (string.Equals(values[i], trimmed, StringComparison.OrdinalIgnoreCase))
{
return i;
}
}
return -1;
}
private static bool IsValidIpv4(string value)
{
string trimmed = value == null ? string.Empty : value.Trim();
return IPAddress.TryParse(trimmed, out IPAddress address) && address.AddressFamily == AddressFamily.InterNetwork;
}
private bool IsCurrentHostFavorite()
{
return FindIpIndex(favoriteIps, hostDraft) >= 0;
}
private bool IsActivatePressed()
@ -479,12 +804,19 @@ public class PicoUdpConfigPanel : MonoBehaviour
private static Vector2 ReadPrimaryAxis()
{
if (TryReadPrimaryAxis(XRNode.RightHand, out Vector2 rightAxis))
bool hasRightAxis = TryReadPrimaryAxis(XRNode.RightHand, out Vector2 rightAxis);
bool hasLeftAxis = TryReadPrimaryAxis(XRNode.LeftHand, out Vector2 leftAxis);
if (hasRightAxis && hasLeftAxis)
{
return rightAxis.sqrMagnitude >= leftAxis.sqrMagnitude ? rightAxis : leftAxis;
}
if (hasRightAxis)
{
return rightAxis;
}
if (TryReadPrimaryAxis(XRNode.LeftHand, out Vector2 leftAxis))
if (hasLeftAxis)
{
return leftAxis;
}
@ -552,15 +884,19 @@ public class PicoUdpConfigPanel : MonoBehaviour
{
titleText.text = "XR-RM PICO UDP Sender";
statusText.text = BuildConfigStatusText();
connectionHeaderText.text = "Connection";
runtimeHeaderText.text = "Runtime";
SetRow(0, "Target IP", hostDraft);
SetRow(1, "Target Port", portDraft.ToString());
SetRow(2, "Send Rate", Mathf.RoundToInt(sendHzDraft) + " Hz");
SetRow(3, "Coordinates", convertDraft ? "Project (+Z forward)" : "Unity raw");
SetRow(4, "UDP Sending", sender.SendEnabled ? "ON" : "OFF");
SetRow(5, "Save & Apply", "press trigger");
SetRow(RowTargetIp, "Target IP", hostDraft);
SetRow(RowSavedIp, "Saved IP", BuildSavedIpText());
SetRow(RowFavorite, "Favorite", IsCurrentHostFavorite() ? "ON" : "OFF");
SetRow(RowTargetPort, "Target Port", portDraft.ToString(CultureInfo.InvariantCulture));
SetRow(RowSendHz, "Send Rate", Mathf.RoundToInt(sendHzDraft).ToString(CultureInfo.InvariantCulture) + " Hz");
SetRow(RowCoordinates, "Coordinates", convertDraft ? "Project (+Z back)" : "Source raw");
SetRow(RowSending, "UDP Sending", sender.SendEnabled ? "ON" : "OFF");
SetRow(RowSaveApply, "Save & Apply", "press trigger");
footerText.text = "Stick: select / adjust Trigger, A or X: edit\nB or Y: save Menu: run view";
footerText.text = "Stick: select / adjust Trigger, A or X: edit/apply B or Y: save Menu: run view";
}
private void RefreshRunHud()
@ -570,11 +906,13 @@ public class PicoUdpConfigPanel : MonoBehaviour
return;
}
hudTitleText.text = "XR-RM UDP Sender";
Color stateColor = sender.SendEnabled ? new Color(0.28f, 0.92f, 0.62f, 1.0f) : new Color(1.0f, 0.68f, 0.30f, 1.0f);
hudStatusStripe.color = stateColor;
hudTitleText.text = "XR-RM UDP";
hudStatusText.color = stateColor;
hudStatusText.text = (sender.SendEnabled ? "SENDING" : "PAUSED") + " " + BuildEndpointText();
hudDetailText.text = BuildTrackingStateText() + " " + BuildGripStateText() + "\n"
+ BuildPacketStateText(false) + " " + BuildKeepAwakeStateText();
hudHintText.text = "Menu: config panel";
hudDetailText.text = BuildCompactTrackingStateText() + "\n" + BuildPacketStateText(false);
hudHintText.text = "Menu: config";
}
private string BuildConfigStatusText()
@ -593,14 +931,70 @@ public class PicoUdpConfigPanel : MonoBehaviour
return sender.HasEndpoint ? sender.host + ":" + sender.port : "not configured";
}
private string BuildTrackingStateText()
private string BuildSavedIpText()
{
return "L " + (sender.LeftDeviceValid ? "ok" : "--") + " / R " + (sender.RightDeviceValid ? "ok" : "--");
if (savedIpChoices.Count == 0)
{
return "none";
}
selectedSavedIpIndex = Mathf.Clamp(selectedSavedIpIndex, 0, savedIpChoices.Count - 1);
string ip = savedIpChoices[selectedSavedIpIndex];
string marker = FindIpIndex(favoriteIps, ip) >= 0 ? "favorite" : "recent";
return ip + " " + marker + " " + (selectedSavedIpIndex + 1).ToString(CultureInfo.InvariantCulture) + "/" + savedIpChoices.Count.ToString(CultureInfo.InvariantCulture);
}
private string BuildGripStateText()
private string BuildTrackingStateText()
{
return "Grip L " + (sender.LeftGripPressed ? "ON" : "--") + " / R " + (sender.RightGripPressed ? "ON" : "--");
return "L " + BuildPoseSummary(true) + " / R " + BuildPoseSummary(false);
}
private string BuildCompactTrackingStateText()
{
return "L " + BuildCompactPoseSummary(true) + " / R " + BuildCompactPoseSummary(false);
}
private string BuildCompactPoseSummary(bool left)
{
bool deviceValid = left ? sender.LeftDeviceValid : sender.RightDeviceValid;
if (!deviceValid)
{
return "--";
}
bool poseValid = left ? sender.LeftPoseValid : sender.RightPoseValid;
string source = ShortPoseSource(left ? sender.LeftPoseSource : sender.RightPoseSource);
return (poseValid ? "valid" : "invalid") + " " + source;
}
private string BuildPoseSummary(bool left)
{
bool deviceValid = left ? sender.LeftDeviceValid : sender.RightDeviceValid;
if (!deviceValid)
{
return "--";
}
bool poseValid = left ? sender.LeftPoseValid : sender.RightPoseValid;
string source = ShortPoseSource(left ? sender.LeftPoseSource : sender.RightPoseSource);
int status = left ? sender.LeftControllerStatus : sender.RightControllerStatus;
int tracking = left ? sender.LeftTrackingState : sender.RightTrackingState;
return (poseValid ? "valid" : "invalid") + " " + source + " s" + status + " t" + tracking;
}
private static string ShortPoseSource(string source)
{
if (source == "pxr_predict")
{
return "pxr";
}
if (source == "unity_xr")
{
return "unity";
}
return "none";
}
private string BuildPacketStateText(bool compact)
@ -628,11 +1022,20 @@ public class PicoUdpConfigPanel : MonoBehaviour
private void SetRow(int index, string label, string value)
{
Text rowText = rowTexts[index];
ConfigRow row = rows[index];
bool selected = index == selectedRow;
rowText.color = selected ? selectedColor : normalColor;
rowText.fontStyle = selected ? FontStyle.Bold : FontStyle.Normal;
rowText.text = (selected ? "> " : " ") + label.PadRight(14) + value;
row.background.color = selected ? selectedRowColor : rowColor;
row.accent.color = selected ? selectedColor : new Color(0.18f, 0.25f, 0.30f, 0.70f);
row.labelText.text = label;
row.valueText.text = value;
row.labelText.color = selected ? selectedColor : mutedColor;
row.valueText.color = normalColor;
row.valueText.fontStyle = selected ? FontStyles.Bold : FontStyles.Normal;
TMP_FontAsset valueFontAsset = GetFontAssetForStyle(row.valueText.fontStyle);
if (valueFontAsset != null)
{
row.valueText.font = valueFontAsset;
}
}
private static bool TryParseFloat(string value, out float parsedValue)