Pipeline saving and View popup window added

Made-with: Cursor
This commit is contained in:
Leo Qu
2026-03-10 18:13:32 -04:00
parent 477a86a495
commit 7212f692a6
10 changed files with 5260 additions and 21 deletions

View 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);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d1c2b3a4e5f6071829a0b1c2d3e4f5a6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -25,7 +25,7 @@ Transform:
m_GameObject: {fileID: 3292419760055380793} m_GameObject: {fileID: 3292419760055380793}
serializedVersion: 2 serializedVersion: 2
m_LocalRotation: {x: 0.010896653, y: -0.010896653, z: 0.7070228, w: 0.70702285} 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_LocalScale: {x: 0.049999997, y: 0.049999997, z: 0.05}
m_ConstrainProportionsScale: 0 m_ConstrainProportionsScale: 0
m_Children: [] m_Children: []

View File

@@ -112,6 +112,15 @@ public class TopologyBuilder : MonoBehaviour
} }
} }
_skipControlLibraryCalls = false; _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); BuildTopologyFromGraph(graph);

View File

@@ -45,9 +45,11 @@ public abstract class ModuleBase : MonoBehaviour
} }
try 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) catch (DllNotFoundException)

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using UnityEngine; using UnityEngine;
using UnityEngine.UI; using UnityEngine.UI;
using UnityEngine.EventSystems; using UnityEngine.EventSystems;
@@ -11,7 +12,7 @@ using UnityEngine.EventSystems;
/// </summary> /// </summary>
public class ProgrammedMovementsController : MonoBehaviour public class ProgrammedMovementsController : MonoBehaviour
{ {
public const int TracksCount = 3; public const int TracksCount = 4;
public const int TimelineDurationSeconds = 10; public const int TimelineDurationSeconds = 10;
public static readonly Color HighlightBlue = new Color(0.2f, 0.5f, 1f, 1f); 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 Camera mainCamera;
public RectTransform timelineRoot; public RectTransform timelineRoot;
public Button runButton; public Button runButton;
public Button saveButton;
public Button loadButton;
public GameObject filePickerPanel;
public RectTransform filePickerContent;
private RectTransform _bannerRoot; private RectTransform _bannerRoot;
private Transform _timelineView; private Transform _timelineView;
@@ -45,6 +50,7 @@ public class ProgrammedMovementsController : MonoBehaviour
} }
EnsureTimelineViewShown(); EnsureTimelineViewShown();
WireUpButtons(); WireUpButtons();
WireSaveLoadButtons();
RefreshRunButton(); RefreshRunButton();
// Force layout rebuild so block positions align with timeline after view switch // Force layout rebuild so block positions align with timeline after view switch
Canvas.ForceUpdateCanvases(); 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() void WireUpButtons()
{ {
WireUpRunButton(); WireUpRunButton();
@@ -686,4 +704,255 @@ public class ProgrammedMovementsController : MonoBehaviour
public int dcDirection = 1; // 1 = forward, -1 = backward public int dcDirection = 1; // 1 = forward, -1 = backward
public GameObject uiRoot; 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)}");
}
} }

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 34ea3633f5ba44d789b01b6f3a211dbe
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View 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
}
]
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 1044ba0a77f7e4e0fba382ec60755349
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff