mirror of
https://github.com/BotChain-Robots/ui.git
synced 2026-07-08 15:07:22 +02:00
Pipeline saving and View popup window added
Made-with: Cursor
This commit is contained in:
50
Assets/DiscoverOverlayController.cs
Normal file
50
Assets/DiscoverOverlayController.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class DiscoverOverlayController : MonoBehaviour
|
||||
{
|
||||
public GameObject overlayPanel;
|
||||
public RectTransform overlayContent;
|
||||
|
||||
public void ShowOverlay()
|
||||
{
|
||||
if (overlayPanel != null)
|
||||
{
|
||||
EnsureScrollRect();
|
||||
overlayPanel.SetActive(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void HideOverlay()
|
||||
{
|
||||
ClearContent();
|
||||
if (overlayPanel != null)
|
||||
overlayPanel.SetActive(false);
|
||||
}
|
||||
|
||||
void EnsureScrollRect()
|
||||
{
|
||||
if (overlayContent == null) return;
|
||||
var scrollArea = overlayContent.parent;
|
||||
if (scrollArea == null) return;
|
||||
var scrollGo = scrollArea.gameObject;
|
||||
if (scrollGo.GetComponent<Mask>() == null)
|
||||
scrollGo.AddComponent<Mask>().showMaskGraphic = false;
|
||||
var sr = scrollGo.GetComponent<ScrollRect>();
|
||||
if (sr == null)
|
||||
{
|
||||
sr = scrollGo.AddComponent<ScrollRect>();
|
||||
sr.horizontal = false;
|
||||
sr.vertical = true;
|
||||
sr.content = overlayContent;
|
||||
sr.movementType = ScrollRect.MovementType.Clamped;
|
||||
}
|
||||
}
|
||||
|
||||
void ClearContent()
|
||||
{
|
||||
if (overlayContent == null) return;
|
||||
for (int i = overlayContent.childCount - 1; i >= 0; i--)
|
||||
Destroy(overlayContent.GetChild(i).gameObject);
|
||||
}
|
||||
}
|
||||
11
Assets/DiscoverOverlayController.cs.meta
Normal file
11
Assets/DiscoverOverlayController.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d1c2b3a4e5f6071829a0b1c2d3e4f5a6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -25,7 +25,7 @@ Transform:
|
||||
m_GameObject: {fileID: 3292419760055380793}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0.010896653, y: -0.010896653, z: 0.7070228, w: 0.70702285}
|
||||
m_LocalPosition: {x: 0.0003, y: -0.0603, z: -0.0017}
|
||||
m_LocalPosition: {x: 0.0003, y: -0.0518, z: -0.0014}
|
||||
m_LocalScale: {x: 0.049999997, y: 0.049999997, z: 0.05}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
|
||||
@@ -112,6 +112,15 @@ public class TopologyBuilder : MonoBehaviour
|
||||
}
|
||||
}
|
||||
_skipControlLibraryCalls = false;
|
||||
|
||||
Debug.Log("=== Control Library Topology ===");
|
||||
Debug.Log($"Modules ({graph.Modules.Count}):");
|
||||
foreach (var m in graph.Modules)
|
||||
Debug.Log($" Id={m.Id}, Type={m.Type}, Degree={m.Degree}");
|
||||
Debug.Log($"Connections ({graph.Connections.Count}):");
|
||||
foreach (var c in graph.Connections)
|
||||
Debug.Log($" {c.FromModuleId} ({c.FromSocket}) -> {c.ToModuleId} ({c.ToSocket}), Orientation={c.Orientation}");
|
||||
Debug.Log("================================");
|
||||
}
|
||||
|
||||
BuildTopologyFromGraph(graph);
|
||||
|
||||
@@ -45,9 +45,11 @@ public abstract class ModuleBase : MonoBehaviour
|
||||
}
|
||||
try
|
||||
{
|
||||
if (0 != ControlLibrary.send_angle_control(Int32.Parse(moduleID), angleRounded))
|
||||
int result = ControlLibrary.send_angle_control(Int32.Parse(moduleID), angleRounded);
|
||||
Debug.Log($"[ControlLibrary] send_angle_control(moduleId={moduleID}, angle={angleRounded}) returned {result}");
|
||||
if (result != 0)
|
||||
{
|
||||
Debug.Log("Control library exited with error");
|
||||
Debug.LogWarning($"[ControlLibrary] send_angle_control returned error code: {result}");
|
||||
}
|
||||
}
|
||||
catch (DllNotFoundException)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.EventSystems;
|
||||
@@ -11,7 +12,7 @@ using UnityEngine.EventSystems;
|
||||
/// </summary>
|
||||
public class ProgrammedMovementsController : MonoBehaviour
|
||||
{
|
||||
public const int TracksCount = 3;
|
||||
public const int TracksCount = 4;
|
||||
public const int TimelineDurationSeconds = 10;
|
||||
public static readonly Color HighlightBlue = new Color(0.2f, 0.5f, 1f, 1f);
|
||||
|
||||
@@ -19,6 +20,10 @@ public class ProgrammedMovementsController : MonoBehaviour
|
||||
public Camera mainCamera;
|
||||
public RectTransform timelineRoot;
|
||||
public Button runButton;
|
||||
public Button saveButton;
|
||||
public Button loadButton;
|
||||
public GameObject filePickerPanel;
|
||||
public RectTransform filePickerContent;
|
||||
|
||||
private RectTransform _bannerRoot;
|
||||
private Transform _timelineView;
|
||||
@@ -45,6 +50,7 @@ public class ProgrammedMovementsController : MonoBehaviour
|
||||
}
|
||||
EnsureTimelineViewShown();
|
||||
WireUpButtons();
|
||||
WireSaveLoadButtons();
|
||||
RefreshRunButton();
|
||||
// Force layout rebuild so block positions align with timeline after view switch
|
||||
Canvas.ForceUpdateCanvases();
|
||||
@@ -96,6 +102,18 @@ public class ProgrammedMovementsController : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
private bool _saveLoadWired;
|
||||
|
||||
void WireSaveLoadButtons()
|
||||
{
|
||||
if (_saveLoadWired) return;
|
||||
_saveLoadWired = true;
|
||||
if (saveButton != null)
|
||||
saveButton.onClick.AddListener(SaveTimelineToFile);
|
||||
if (loadButton != null)
|
||||
loadButton.onClick.AddListener(LoadTimelineFromFile);
|
||||
}
|
||||
|
||||
void WireUpButtons()
|
||||
{
|
||||
WireUpRunButton();
|
||||
@@ -686,4 +704,255 @@ public class ProgrammedMovementsController : MonoBehaviour
|
||||
public int dcDirection = 1; // 1 = forward, -1 = backward
|
||||
public GameObject uiRoot;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
private class SavedMovement
|
||||
{
|
||||
public string ModuleId;
|
||||
public string ModuleType;
|
||||
public float Degree;
|
||||
public int Direction;
|
||||
public int Track;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
private class SavedSecond
|
||||
{
|
||||
public int Second;
|
||||
public List<SavedMovement> Movements = new List<SavedMovement>();
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
private class SavedTimeline
|
||||
{
|
||||
public List<SavedSecond> Timeline = new List<SavedSecond>();
|
||||
}
|
||||
|
||||
public void SaveTimelineToFile()
|
||||
{
|
||||
var bySecond = new Dictionary<int, List<MovementBlock>>();
|
||||
foreach (var b in _blocks)
|
||||
{
|
||||
if (!bySecond.ContainsKey(b.second))
|
||||
bySecond[b.second] = new List<MovementBlock>();
|
||||
bySecond[b.second].Add(b);
|
||||
}
|
||||
|
||||
var saved = new SavedTimeline();
|
||||
var sortedSeconds = new List<int>(bySecond.Keys);
|
||||
sortedSeconds.Sort();
|
||||
foreach (int sec in sortedSeconds)
|
||||
{
|
||||
var entry = new SavedSecond { Second = sec };
|
||||
foreach (var b in bySecond[sec])
|
||||
{
|
||||
var m = new SavedMovement
|
||||
{
|
||||
Degree = b.targetAngle,
|
||||
Direction = b.dcDirection,
|
||||
Track = b.trackIndex
|
||||
};
|
||||
if (b.moduleServo != null)
|
||||
{
|
||||
m.ModuleId = b.moduleServo.moduleID;
|
||||
m.ModuleType = "Servo";
|
||||
}
|
||||
else if (b.moduleDC != null)
|
||||
{
|
||||
m.ModuleId = b.moduleDC.moduleID;
|
||||
m.ModuleType = "DC";
|
||||
}
|
||||
entry.Movements.Add(m);
|
||||
}
|
||||
saved.Timeline.Add(entry);
|
||||
}
|
||||
|
||||
string json = JsonUtility.ToJson(saved, true);
|
||||
string dir = Path.Combine(Application.dataPath, "ProgrammedMovements", "SavedTimelines");
|
||||
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
|
||||
string timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
|
||||
string filename = $"timeline_{timestamp}.json";
|
||||
string path = Path.Combine(dir, filename);
|
||||
File.WriteAllText(path, json);
|
||||
Debug.Log($"[ProgrammedMovements] Saved {_blocks.Count} movements to {path}");
|
||||
}
|
||||
|
||||
public void LoadTimelineFromFile()
|
||||
{
|
||||
string dir = Path.Combine(Application.dataPath, "ProgrammedMovements", "SavedTimelines");
|
||||
if (!Directory.Exists(dir))
|
||||
{
|
||||
Debug.LogWarning($"[ProgrammedMovements] No saved timelines directory: {dir}");
|
||||
return;
|
||||
}
|
||||
var files = Directory.GetFiles(dir, "timeline_*.json");
|
||||
if (files.Length == 0)
|
||||
{
|
||||
Debug.LogWarning($"[ProgrammedMovements] No saved timelines found in {dir}");
|
||||
return;
|
||||
}
|
||||
Array.Sort(files);
|
||||
Array.Reverse(files);
|
||||
ShowFilePicker(files);
|
||||
}
|
||||
|
||||
void ShowFilePicker(string[] filePaths)
|
||||
{
|
||||
if (filePickerPanel == null || filePickerContent == null) return;
|
||||
|
||||
ClearFilePickerContent();
|
||||
EnsureScrollRect();
|
||||
|
||||
float itemHeight = 32f;
|
||||
float gap = 4f;
|
||||
float totalHeight = filePaths.Length * (itemHeight + gap);
|
||||
filePickerContent.sizeDelta = new Vector2(0, totalHeight);
|
||||
|
||||
for (int i = 0; i < filePaths.Length; i++)
|
||||
{
|
||||
string filePath = filePaths[i];
|
||||
string fileName = Path.GetFileNameWithoutExtension(filePath);
|
||||
|
||||
var itemGo = new GameObject("Item_" + i);
|
||||
itemGo.transform.SetParent(filePickerContent, false);
|
||||
var itemRect = itemGo.AddComponent<RectTransform>();
|
||||
itemRect.anchorMin = new Vector2(0, 1);
|
||||
itemRect.anchorMax = new Vector2(1, 1);
|
||||
itemRect.pivot = new Vector2(0.5f, 1);
|
||||
itemRect.anchoredPosition = new Vector2(0, -(i * (itemHeight + gap)));
|
||||
itemRect.sizeDelta = new Vector2(-16, itemHeight);
|
||||
var itemImg = itemGo.AddComponent<Image>();
|
||||
itemImg.color = new Color(0.15f, 0.15f, 0.18f, 1f);
|
||||
var itemBtn = itemGo.AddComponent<Button>();
|
||||
itemBtn.targetGraphic = itemImg;
|
||||
var colors = itemBtn.colors;
|
||||
colors.highlightedColor = new Color(0.35f, 0.45f, 0.7f, 1f);
|
||||
colors.pressedColor = new Color(0.2f, 0.3f, 0.6f, 1f);
|
||||
itemBtn.colors = colors;
|
||||
itemBtn.onClick.AddListener(() => OnFileSelected(filePath));
|
||||
|
||||
var labelGo = new GameObject("Label");
|
||||
labelGo.transform.SetParent(itemGo.transform, false);
|
||||
var labelRect = labelGo.AddComponent<RectTransform>();
|
||||
labelRect.anchorMin = Vector2.zero;
|
||||
labelRect.anchorMax = Vector2.one;
|
||||
labelRect.offsetMin = new Vector2(10, 0);
|
||||
labelRect.offsetMax = new Vector2(-10, 0);
|
||||
var labelTxt = labelGo.AddComponent<Text>();
|
||||
labelTxt.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
|
||||
labelTxt.text = fileName;
|
||||
labelTxt.fontSize = 13;
|
||||
labelTxt.alignment = TextAnchor.MiddleLeft;
|
||||
labelTxt.color = Color.white;
|
||||
labelTxt.horizontalOverflow = HorizontalWrapMode.Overflow;
|
||||
}
|
||||
|
||||
filePickerPanel.SetActive(true);
|
||||
}
|
||||
|
||||
void EnsureScrollRect()
|
||||
{
|
||||
var scrollArea = filePickerContent.parent;
|
||||
if (scrollArea == null) return;
|
||||
var scrollGo = scrollArea.gameObject;
|
||||
if (scrollGo.GetComponent<Mask>() == null)
|
||||
scrollGo.AddComponent<Mask>().showMaskGraphic = false;
|
||||
var sr = scrollGo.GetComponent<ScrollRect>();
|
||||
if (sr == null)
|
||||
{
|
||||
sr = scrollGo.AddComponent<ScrollRect>();
|
||||
sr.horizontal = false;
|
||||
sr.vertical = true;
|
||||
sr.content = filePickerContent;
|
||||
sr.movementType = ScrollRect.MovementType.Clamped;
|
||||
}
|
||||
}
|
||||
|
||||
void ClearFilePickerContent()
|
||||
{
|
||||
if (filePickerContent == null) return;
|
||||
for (int i = filePickerContent.childCount - 1; i >= 0; i--)
|
||||
Destroy(filePickerContent.GetChild(i).gameObject);
|
||||
}
|
||||
|
||||
void OnFileSelected(string filePath)
|
||||
{
|
||||
DismissFilePicker();
|
||||
LoadTimelineFromPath(filePath);
|
||||
}
|
||||
|
||||
public void DismissFilePicker()
|
||||
{
|
||||
ClearFilePickerContent();
|
||||
if (filePickerPanel != null)
|
||||
filePickerPanel.SetActive(false);
|
||||
}
|
||||
|
||||
void LoadTimelineFromPath(string path)
|
||||
{
|
||||
string json = File.ReadAllText(path);
|
||||
Debug.Log($"[ProgrammedMovements] Loading from: {path}");
|
||||
|
||||
var saved = JsonUtility.FromJson<SavedTimeline>(json);
|
||||
if (saved == null || saved.Timeline == null || saved.Timeline.Count == 0)
|
||||
{
|
||||
Debug.LogWarning("[ProgrammedMovements] Save file is empty or invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var b in _blocks)
|
||||
if (b.uiRoot != null) Destroy(b.uiRoot);
|
||||
_blocks.Clear();
|
||||
_blocksBySecond.Clear();
|
||||
|
||||
ResolveReferences();
|
||||
EnsureTimelineViewShown();
|
||||
|
||||
var tracksRect = _timelineView?.Find("TracksContainer");
|
||||
if (tracksRect == null)
|
||||
{
|
||||
Debug.LogError("[ProgrammedMovements] TracksContainer not found, cannot load.");
|
||||
return;
|
||||
}
|
||||
|
||||
int loaded = 0;
|
||||
foreach (var entry in saved.Timeline)
|
||||
{
|
||||
foreach (var m in entry.Movements)
|
||||
{
|
||||
int trackIndex = Mathf.Clamp(m.Track, 0, TracksCount - 1);
|
||||
|
||||
var block = new MovementBlock
|
||||
{
|
||||
trackIndex = trackIndex,
|
||||
second = Mathf.Clamp(entry.Second, 0, TimelineDurationSeconds - 1),
|
||||
targetAngle = m.Degree,
|
||||
dcDirection = m.Direction
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(m.ModuleId) && TopologyBuilder.idToInstance.TryGetValue(m.ModuleId, out var go))
|
||||
{
|
||||
if (m.ModuleType == "DC")
|
||||
block.moduleDC = go.GetComponent<DCMotorModule>();
|
||||
else
|
||||
block.moduleServo = go.GetComponent<ServoMotorModule>();
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(m.ModuleId))
|
||||
{
|
||||
Debug.LogWarning($"[ProgrammedMovements] Module '{m.ModuleId}' not found in topology.");
|
||||
}
|
||||
|
||||
_blocks.Add(block);
|
||||
IndexBlock(block);
|
||||
|
||||
var track = tracksRect.GetChild(trackIndex);
|
||||
var blocksHolder = track.Find("Blocks");
|
||||
if (blocksHolder == null) continue;
|
||||
CreateMovementBlockUI(blocksHolder, block);
|
||||
loaded++;
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"[ProgrammedMovements] Loaded {loaded} movements from {Path.GetFileName(path)}");
|
||||
}
|
||||
}
|
||||
|
||||
8
Assets/ProgrammedMovements/SavedTimelines.meta
Normal file
8
Assets/ProgrammedMovements/SavedTimelines.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 34ea3633f5ba44d789b01b6f3a211dbe
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
39
Assets/Resources/mockDataMMMMHub.json
Normal file
39
Assets/Resources/mockDataMMMMHub.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"Modules": [
|
||||
{ "Id": "hub1", "Type": "Splitter2", "Degree": null },
|
||||
{ "Id": "servo1", "Type": "Servo1", "Degree": 90 },
|
||||
{ "Id": "servo2", "Type": "Servo1", "Degree": 90 },
|
||||
{ "Id": "servo3", "Type": "Servo1", "Degree": 90 },
|
||||
{ "Id": "servo4", "Type": "Servo1", "Degree": 90 }
|
||||
],
|
||||
"Connections": [
|
||||
{
|
||||
"FromModuleId": "hub1",
|
||||
"ToModuleId": "servo1",
|
||||
"FromSocket": "MaleSocket1",
|
||||
"ToSocket": "FemaleSocket",
|
||||
"Orientation": 0
|
||||
},
|
||||
{
|
||||
"FromModuleId": "hub1",
|
||||
"ToModuleId": "servo2",
|
||||
"FromSocket": "MaleSocket2",
|
||||
"ToSocket": "FemaleSocket",
|
||||
"Orientation": 90
|
||||
},
|
||||
{
|
||||
"FromModuleId": "hub1",
|
||||
"ToModuleId": "servo3",
|
||||
"FromSocket": "MaleSocket3",
|
||||
"ToSocket": "FemaleSocket",
|
||||
"Orientation": 180
|
||||
},
|
||||
{
|
||||
"FromModuleId": "hub1",
|
||||
"ToModuleId": "servo4",
|
||||
"FromSocket": "MaleSocket4",
|
||||
"ToSocket": "FemaleSocket",
|
||||
"Orientation": 270
|
||||
}
|
||||
]
|
||||
}
|
||||
7
Assets/Resources/mockDataMMMMHub.json.meta
Normal file
7
Assets/Resources/mockDataMMMMHub.json.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1044ba0a77f7e4e0fba382ec60755349
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user