optimze unity project for xr_rm
This commit is contained in:
@ -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();
|
||||
}
|
||||
|
||||
|
||||
599
unity/XR_RM_PICO_UDP_Sender/Assets/Scripts/PicoUdpConfigPanel.cs
Executable file → Normal file
599
unity/XR_RM_PICO_UDP_Sender/Assets/Scripts/PicoUdpConfigPanel.cs
Executable file → Normal 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)
|
||||
|
||||
Reference in New Issue
Block a user