Implemented new discover flow and implemented unit test for it

Made-with: Cursor
This commit is contained in:
Leo Qu
2026-03-10 19:37:00 -04:00
parent 7212f692a6
commit 71088de101
4 changed files with 318 additions and 13 deletions

View File

@@ -5,14 +5,43 @@ public class DiscoverOverlayController : MonoBehaviour
{ {
public GameObject overlayPanel; public GameObject overlayPanel;
public RectTransform overlayContent; public RectTransform overlayContent;
public TopologyBuilder topologyBuilder;
public void ShowOverlay() public void ShowOverlay()
{ {
if (overlayPanel != null) if (overlayPanel == null) return;
{
EnsureScrollRect(); EnsureScrollRect();
overlayPanel.SetActive(true); ClearContent();
if (topologyBuilder == null)
topologyBuilder = FindObjectOfType<TopologyBuilder>();
bool useMock = topologyBuilder != null && topologyBuilder.mockControlLibrary;
if (useMock)
{
PopulateMockLeaders();
} }
else
{
int[] leaders = null;
try
{
leaders = ControlLibrary.getRobotLeaders();
}
catch (System.Exception ex)
{
Debug.LogError($"[Discover] getRobotLeaders failed: {ex.Message}");
}
if (leaders != null && leaders.Length > 0)
PopulateLeaders(leaders);
else
CreateLabel("No leaders found. Is the hardware connected?");
}
overlayPanel.SetActive(true);
} }
public void HideOverlay() public void HideOverlay()
@@ -22,6 +51,195 @@ public class DiscoverOverlayController : MonoBehaviour
overlayPanel.SetActive(false); overlayPanel.SetActive(false);
} }
void PopulateLeaders(int[] leaderIds)
{
float buttonHeight = 60f;
float spacing = 10f;
Font font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
for (int i = 0; i < leaderIds.Length; i++)
{
int leaderId = leaderIds[i];
GameObject btnGo = new GameObject($"LeaderBtn_{leaderId}", typeof(RectTransform));
btnGo.transform.SetParent(overlayContent, false);
RectTransform rt = btnGo.GetComponent<RectTransform>();
rt.anchorMin = new Vector2(0.05f, 1f);
rt.anchorMax = new Vector2(0.95f, 1f);
rt.pivot = new Vector2(0.5f, 1f);
rt.anchoredPosition = new Vector2(0f, -(spacing + i * (buttonHeight + spacing)));
rt.sizeDelta = new Vector2(0f, buttonHeight);
Image bg = btnGo.AddComponent<Image>();
bg.color = new Color(0.2f, 0.2f, 0.2f, 1f);
Button btn = btnGo.AddComponent<Button>();
var colors = btn.colors;
colors.highlightedColor = new Color(0.35f, 0.35f, 0.35f, 1f);
colors.pressedColor = new Color(0.5f, 0.5f, 0.5f, 1f);
btn.colors = colors;
GameObject txtGo = new GameObject("Text", typeof(RectTransform));
txtGo.transform.SetParent(btnGo.transform, false);
RectTransform txtRt = txtGo.GetComponent<RectTransform>();
txtRt.anchorMin = Vector2.zero;
txtRt.anchorMax = Vector2.one;
txtRt.offsetMin = Vector2.zero;
txtRt.offsetMax = Vector2.zero;
Text txt = txtGo.AddComponent<Text>();
txt.text = $"Leader {leaderId}";
txt.font = font;
txt.fontSize = 22;
txt.alignment = TextAnchor.MiddleCenter;
txt.color = Color.white;
btn.onClick.AddListener(() => OnLeaderSelected(leaderId));
}
float totalHeight = leaderIds.Length * (buttonHeight + spacing) + spacing;
overlayContent.sizeDelta = new Vector2(overlayContent.sizeDelta.x, totalHeight);
}
void PopulateMockLeaders()
{
float buttonHeight = 60f;
float spacing = 10f;
Font font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
int i = 0;
foreach (var kvp in TopologyBuilder.MockLeaders)
{
int id = kvp.Key;
string jsonName = kvp.Value;
int idx = i;
GameObject btnGo = new GameObject($"MockLeaderBtn_{id}", typeof(RectTransform));
btnGo.transform.SetParent(overlayContent, false);
RectTransform rt = btnGo.GetComponent<RectTransform>();
rt.anchorMin = new Vector2(0.05f, 1f);
rt.anchorMax = new Vector2(0.95f, 1f);
rt.pivot = new Vector2(0.5f, 1f);
rt.anchoredPosition = new Vector2(0f, -(spacing + idx * (buttonHeight + spacing)));
rt.sizeDelta = new Vector2(0f, buttonHeight);
Image bg = btnGo.AddComponent<Image>();
bg.color = new Color(0.2f, 0.2f, 0.2f, 1f);
Button btn = btnGo.AddComponent<Button>();
var colors = btn.colors;
colors.highlightedColor = new Color(0.35f, 0.35f, 0.35f, 1f);
colors.pressedColor = new Color(0.5f, 0.5f, 0.5f, 1f);
btn.colors = colors;
GameObject txtGo = new GameObject("Text", typeof(RectTransform));
txtGo.transform.SetParent(btnGo.transform, false);
RectTransform txtRt = txtGo.GetComponent<RectTransform>();
txtRt.anchorMin = Vector2.zero;
txtRt.anchorMax = Vector2.one;
txtRt.offsetMin = Vector2.zero;
txtRt.offsetMax = Vector2.zero;
Text txt = txtGo.AddComponent<Text>();
txt.text = $"Mock Leader {id} ({jsonName})";
txt.font = font;
txt.fontSize = 22;
txt.alignment = TextAnchor.MiddleCenter;
txt.color = Color.white;
string capturedJsonName = jsonName;
btn.onClick.AddListener(() => OnMockLeaderSelected(capturedJsonName));
i++;
}
float totalHeight = TopologyBuilder.MockLeaders.Count * (buttonHeight + spacing) + spacing;
overlayContent.sizeDelta = new Vector2(overlayContent.sizeDelta.x, totalHeight);
}
void OnMockLeaderSelected(string jsonFileName)
{
Debug.Log($"[Discover] Mock leader selected, loading {jsonFileName}...");
if (topologyBuilder == null)
topologyBuilder = FindObjectOfType<TopologyBuilder>();
if (topologyBuilder == null)
{
Debug.LogError("[Discover] No TopologyBuilder found in scene!");
HideOverlay();
return;
}
try
{
topologyBuilder.BuildTopologyFromJsonFile(jsonFileName);
Debug.Log($"[Discover] Mock topology built from {jsonFileName}");
var cam = FindObjectOfType<UserCameraControl>();
if (cam != null) cam.FindModuleStructure();
}
catch (System.Exception ex)
{
Debug.LogError($"[Discover] Failed to build mock topology: {ex.Message}");
}
HideOverlay();
}
void OnLeaderSelected(int leaderId)
{
Debug.Log($"[Discover] Selected leader {leaderId}, building topology...");
if (topologyBuilder == null)
{
topologyBuilder = FindObjectOfType<TopologyBuilder>();
}
if (topologyBuilder == null)
{
Debug.LogError("[Discover] No TopologyBuilder found in scene!");
HideOverlay();
return;
}
try
{
topologyBuilder.BuildTopologyFromLeader(leaderId);
Debug.Log($"[Discover] Topology built successfully for leader {leaderId}");
var cam = FindObjectOfType<UserCameraControl>();
if (cam != null) cam.FindModuleStructure();
}
catch (System.Exception ex)
{
Debug.LogError($"[Discover] Failed to build topology for leader {leaderId}: {ex.Message}");
}
HideOverlay();
}
void CreateLabel(string message)
{
Font font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
GameObject go = new GameObject("InfoLabel", typeof(RectTransform));
go.transform.SetParent(overlayContent, false);
RectTransform rt = go.GetComponent<RectTransform>();
rt.anchorMin = new Vector2(0.05f, 1f);
rt.anchorMax = new Vector2(0.95f, 1f);
rt.pivot = new Vector2(0.5f, 1f);
rt.anchoredPosition = new Vector2(0f, -20f);
rt.sizeDelta = new Vector2(0f, 50f);
Text txt = go.AddComponent<Text>();
txt.text = message;
txt.font = font;
txt.fontSize = 20;
txt.alignment = TextAnchor.MiddleCenter;
txt.color = new Color(1f, 0.6f, 0.6f, 1f);
}
void EnsureScrollRect() void EnsureScrollRect()
{ {
if (overlayContent == null) return; if (overlayContent == null) return;

View File

@@ -19,16 +19,7 @@ public class DiscoveryButton : MonoBehaviour
public void OnDiscoveryPressed() public void OnDiscoveryPressed()
{ {
if (topologyBuilder != null) Debug.Log("Discovery button pressed topology building deferred to overlay selection.");
{
Debug.Log("Discovery button pressed.");
topologyBuilder.BuildTopologyFromJson();
FocusCameraOnTopology();
}
else
{
Debug.LogError("TopologyBuilder is not assigned!");
}
} }
void FocusCameraOnTopology() void FocusCameraOnTopology()

View File

@@ -23,6 +23,10 @@ public class TopologyBuilder : MonoBehaviour
[Tooltip("JSON file name (without .json) in Resources folder. e.g. 'mockData' or 'mockDataSimple'")] [Tooltip("JSON file name (without .json) in Resources folder. e.g. 'mockData' or 'mockDataSimple'")]
public string jsonFileName = "mockDataNewConfig"; public string jsonFileName = "mockDataNewConfig";
[Header("Mock Control Library")]
[Tooltip("When true, Discover overlay shows mock leaders backed by JSON files instead of calling the real native library.")]
public bool mockControlLibrary = false;
public static Dictionary<string, GameObject> idToInstance = new(); public static Dictionary<string, GameObject> idToInstance = new();
public static bool _skipControlLibraryCalls = true; public static bool _skipControlLibraryCalls = true;
@@ -126,6 +130,96 @@ public class TopologyBuilder : MonoBehaviour
BuildTopologyFromGraph(graph); BuildTopologyFromGraph(graph);
} }
public static readonly Dictionary<int, string> MockLeaders = new()
{
{ 1, "mockData" },
{ 2, "mockDataWithDC" }
};
public void BuildTopologyFromJsonFile(string fileName)
{
TopologyGraph graph = new TopologyGraph();
Dictionary<int, ModuleType> idToType = new();
TextAsset jsonAsset = Resources.Load<TextAsset>(fileName);
if (jsonAsset == null) { Debug.LogError($"JSON not found: Resources/{fileName}.json"); return; }
JsonTopologyGraph jsonGraph = JsonUtility.FromJson<JsonTopologyGraph>(jsonAsset.text);
if (jsonGraph?.Modules == null || jsonGraph.Modules.Count == 0) { Debug.LogError("JSON has no modules"); return; }
Dictionary<string, int> stringToIntId = new();
int nextIntId = 1;
foreach (var m in jsonGraph.Modules)
{
if (!stringToIntId.ContainsKey(m.Id)) stringToIntId[m.Id] = nextIntId++;
int intId = stringToIntId[m.Id];
ModuleType mt = ParseModuleType(m.Type);
idToType[intId] = mt;
graph.Modules.Add(new ModuleData { Id = intId, Type = mt.ToString(), Degree = m.Degree });
}
if (jsonGraph.Connections != null)
foreach (var c in jsonGraph.Connections)
{
if (!stringToIntId.TryGetValue(c.FromModuleId, out int fromId) || !stringToIntId.TryGetValue(c.ToModuleId, out int toId)) continue;
Orientation o = c.Orientation == 90 ? Orientation.Deg90 : c.Orientation == 180 ? Orientation.Deg180 : c.Orientation == 270 ? Orientation.Deg270 : Orientation.Deg0;
graph.Connections.Add(new Connection { FromModuleId = fromId, ToModuleId = toId, FromSocket = c.FromSocket ?? "MaleSocket", ToSocket = c.ToSocket ?? "FemaleSocket", Orientation = o });
}
Debug.Log($"[TopologyBuilder] Built from JSON file: {fileName}");
_skipControlLibraryCalls = true;
BuildTopologyFromGraph(graph);
}
public void BuildTopologyFromLeader(int leaderId)
{
TopologyGraph graph = new TopologyGraph();
Dictionary<int, ModuleType> idToType = new();
RobotConfiguration config = ControlLibrary.getRobotConfiguration(leaderId);
int moduleCount = config.ModulesLength;
for (int i = 0; i < moduleCount; i++)
{
var n_module = config.Modules(i);
if (n_module != null)
{
var module = n_module.Value;
ModuleType moduleType = Enum.Parse<ModuleType>(module.ModuleType.ToString());
float degree = module.ConfigurationAsMotorState().Angle;
if (moduleType == ModuleType.SERVO_1 || moduleType == ModuleType.SERVO_2)
degree = 90;
graph.Modules.Add(new ModuleData { Id = module.Id, Type = module.ModuleType.ToString(), Degree = degree });
idToType.Add(module.Id, module.ModuleType);
}
}
int connectionCount = config.ConnectionsLength;
for (int i = 0; i < connectionCount; i++)
{
var n_connection = config.Connections(i);
if (n_connection != null)
{
var connection = n_connection.Value;
if (connection.FromSocket == 0) continue;
graph.Connections.Add(new Connection
{
FromModuleId = connection.FromModuleId,
FromSocket =
idToType[connection.FromModuleId] == ModuleType.SPLITTER ||
idToType[connection.FromModuleId] == ModuleType.SPLITTER_2 ||
idToType[connection.FromModuleId] == ModuleType.SPLITTER_3 ||
idToType[connection.FromModuleId] == ModuleType.SPLITTER_4
? "MaleSocket" + (connection.FromSocket == 0 ? "" : connection.FromSocket)
: "MaleSocket",
ToModuleId = connection.ToModuleId,
ToSocket = "FemaleSocket",
Orientation = connection.Orientation
});
}
}
_skipControlLibraryCalls = false;
Debug.Log($"[TopologyBuilder] Building from leader {leaderId}: {graph.Modules.Count} modules, {graph.Connections.Count} connections");
BuildTopologyFromGraph(graph);
}
private static ModuleType ParseModuleType(string typeString) private static ModuleType ParseModuleType(string typeString)
{ {
if (string.IsNullOrEmpty(typeString)) return ModuleType.SPLITTER; if (string.IsNullOrEmpty(typeString)) return ModuleType.SPLITTER;

View File

@@ -2667,6 +2667,7 @@ MonoBehaviour:
useJsonFile: 0 useJsonFile: 0
skipControlLibraryCalls: 0 skipControlLibraryCalls: 0
jsonFileName: mockData jsonFileName: mockData
mockControlLibrary: 0
--- !u!4 &387671720 --- !u!4 &387671720
Transform: Transform:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -21345,6 +21346,7 @@ MonoBehaviour:
m_EditorClassIdentifier: m_EditorClassIdentifier:
overlayPanel: {fileID: 8840000001} overlayPanel: {fileID: 8840000001}
overlayContent: {fileID: 8840000042} overlayContent: {fileID: 8840000042}
topologyBuilder: {fileID: 387671719}
--- !u!1 &168756400964845240 --- !u!1 &168756400964845240
GameObject: GameObject:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0