mirror of
https://github.com/BotChain-Robots/ui.git
synced 2026-07-08 15:07:22 +02:00
BTS-150: Programmed Movements UI - timeline, config panel, Run button fixes
- Timeline blocks aligned with second markers (Blocks rect full width) - Config panel: Angle/Direction sections, Save/Back/Delete - DC module support: Fwd/Bwd, degrees input - Run button: re-wire on completion, periodic re-wire when idle - Fix AngleInput path (ConfigRow2/AngleSection/AngleInput) for correct angle save - Default module None for new blocks, Run text 'Running' Made-with: Cursor
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
8
Assets/ProgrammedMovements.meta
Normal file
8
Assets/ProgrammedMovements.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 482781b67c03b448ba71ae05ddf4b82b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/ProgrammedMovements/Editor.meta
Normal file
8
Assets/ProgrammedMovements/Editor.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b37c4fea096a34f9dac835964cbdaec9
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
579
Assets/ProgrammedMovements/Editor/ProgrammedMovementsSetup.cs
Normal file
579
Assets/ProgrammedMovements/Editor/ProgrammedMovementsSetup.cs
Normal file
@@ -0,0 +1,579 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEditor;
|
||||
using TMPro;
|
||||
using UnityEditor.SceneManagement;
|
||||
|
||||
/// <summary>
|
||||
/// Editor menu to build ProgrammedMovements UI hierarchy in the scene.
|
||||
/// Run once via Tools > ProgrammedMovements > Setup UI Hierarchy.
|
||||
/// The hierarchy is created in the scene (not at runtime) so you can modify it.
|
||||
/// </summary>
|
||||
public static class ProgrammedMovementsSetup
|
||||
{
|
||||
[InitializeOnLoadMethod]
|
||||
static void OnLoad()
|
||||
{
|
||||
EditorSceneManager.sceneOpened += (scene, mode) =>
|
||||
{
|
||||
if (Application.isPlaying) return;
|
||||
var pmView = GameObject.Find("Views")?.transform?.Find("ProgrammedMovementsView");
|
||||
if (pmView == null) return;
|
||||
var canvas = pmView.Find("ProgrammedMovementsCanvas");
|
||||
if (canvas == null)
|
||||
{
|
||||
SetupHierarchy();
|
||||
EditorSceneManager.MarkSceneDirty(scene);
|
||||
return;
|
||||
}
|
||||
var banner = canvas.Find("TimelineBanner");
|
||||
if (banner != null && banner.Find("ConfigView") == null && banner.Find("TracksContainer") != null)
|
||||
{
|
||||
MigrateToNewLayout();
|
||||
EditorSceneManager.MarkSceneDirty(scene);
|
||||
}
|
||||
};
|
||||
}
|
||||
const int TracksCount = 3;
|
||||
const int TimelineDurationSeconds = 10;
|
||||
|
||||
[MenuItem("Tools/ProgrammedMovements/Setup UI Hierarchy")]
|
||||
public static void SetupHierarchy()
|
||||
{
|
||||
var pmView = GameObject.Find("Views")?.transform?.Find("ProgrammedMovementsView");
|
||||
if (pmView == null)
|
||||
{
|
||||
Debug.LogError("[ProgrammedMovementsSetup] Views/ProgrammedMovementsView not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
var existing = pmView.Find("ProgrammedMovementsCanvas");
|
||||
if (existing != null)
|
||||
{
|
||||
Debug.Log("[ProgrammedMovementsSetup] Hierarchy already exists. Use Tools > ProgrammedMovements > Force Rebuild UI to replace.");
|
||||
return;
|
||||
}
|
||||
DoSetup(pmView);
|
||||
}
|
||||
|
||||
[MenuItem("Tools/ProgrammedMovements/Force Rebuild UI")]
|
||||
public static void ForceRebuildHierarchy()
|
||||
{
|
||||
var pmView = GameObject.Find("Views")?.transform?.Find("ProgrammedMovementsView");
|
||||
if (pmView == null)
|
||||
{
|
||||
Debug.LogError("[ProgrammedMovementsSetup] Views/ProgrammedMovementsView not found.");
|
||||
return;
|
||||
}
|
||||
var existing = pmView.Find("ProgrammedMovementsCanvas");
|
||||
if (existing != null)
|
||||
{
|
||||
Object.DestroyImmediate(existing.gameObject);
|
||||
}
|
||||
DoSetup(pmView);
|
||||
}
|
||||
|
||||
[MenuItem("Tools/ProgrammedMovements/Migrate to New Layout (add Config panel)")]
|
||||
public static void MigrateToNewLayout()
|
||||
{
|
||||
var pmView = GameObject.Find("Views")?.transform?.Find("ProgrammedMovementsView");
|
||||
if (pmView == null)
|
||||
{
|
||||
Debug.LogError("[ProgrammedMovementsSetup] Views/ProgrammedMovementsView not found.");
|
||||
return;
|
||||
}
|
||||
var canvas = pmView.Find("ProgrammedMovementsCanvas");
|
||||
if (canvas == null)
|
||||
{
|
||||
Debug.Log("[ProgrammedMovementsSetup] No canvas found. Run Setup UI Hierarchy first.");
|
||||
return;
|
||||
}
|
||||
var banner = canvas.Find("TimelineBanner");
|
||||
if (banner == null)
|
||||
{
|
||||
Debug.LogError("[ProgrammedMovementsSetup] TimelineBanner not found.");
|
||||
return;
|
||||
}
|
||||
if (banner.Find("ConfigView") != null)
|
||||
{
|
||||
Debug.Log("[ProgrammedMovementsSetup] Already has ConfigView. No migration needed.");
|
||||
return;
|
||||
}
|
||||
var bannerRect = banner.GetComponent<RectTransform>();
|
||||
if (bannerRect == null)
|
||||
{
|
||||
Debug.LogError("[ProgrammedMovementsSetup] TimelineBanner has no RectTransform.");
|
||||
return;
|
||||
}
|
||||
|
||||
Transform timeLabels = banner.Find("TimeLabels");
|
||||
Transform verticalLines = banner.Find("VerticalLines");
|
||||
Transform tracksContainer = banner.Find("TracksContainer");
|
||||
var timelineViewExisting = banner.Find("TimelineView");
|
||||
if (timeLabels == null && timelineViewExisting != null)
|
||||
{
|
||||
timeLabels = timelineViewExisting.Find("TimeLabels");
|
||||
verticalLines = timelineViewExisting.Find("VerticalLines");
|
||||
tracksContainer = timelineViewExisting.Find("TracksContainer");
|
||||
}
|
||||
if (timeLabels == null || verticalLines == null || tracksContainer == null)
|
||||
{
|
||||
Debug.LogError("[ProgrammedMovementsSetup] Old structure not found. Use Force Rebuild UI.");
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject timelineViewGO;
|
||||
if (timelineViewExisting != null)
|
||||
{
|
||||
timelineViewGO = timelineViewExisting.gameObject;
|
||||
}
|
||||
else
|
||||
{
|
||||
timelineViewGO = new GameObject("TimelineView");
|
||||
Undo.RegisterCreatedObjectUndo(timelineViewGO, "Migrate PM");
|
||||
timelineViewGO.transform.SetParent(banner, false);
|
||||
var tr = timelineViewGO.AddComponent<RectTransform>();
|
||||
tr.anchorMin = Vector2.zero;
|
||||
tr.anchorMax = Vector2.one;
|
||||
tr.offsetMin = Vector2.zero;
|
||||
tr.offsetMax = Vector2.zero;
|
||||
Undo.SetTransformParent(timeLabels, timelineViewGO.transform, "Migrate PM");
|
||||
Undo.SetTransformParent(verticalLines, timelineViewGO.transform, "Migrate PM");
|
||||
Undo.SetTransformParent(tracksContainer, timelineViewGO.transform, "Migrate PM");
|
||||
}
|
||||
|
||||
var configViewGO = new GameObject("ConfigView");
|
||||
Undo.RegisterCreatedObjectUndo(configViewGO, "Migrate PM");
|
||||
configViewGO.transform.SetParent(banner, false);
|
||||
configViewGO.SetActive(false);
|
||||
var configViewRect = configViewGO.AddComponent<RectTransform>();
|
||||
configViewRect.anchorMin = Vector2.zero;
|
||||
configViewRect.anchorMax = Vector2.one;
|
||||
configViewRect.offsetMin = Vector2.zero;
|
||||
configViewRect.offsetMax = Vector2.zero;
|
||||
|
||||
var configContent = new GameObject("ConfigContent");
|
||||
configContent.transform.SetParent(configViewRect, false);
|
||||
var configContentRect = configContent.AddComponent<RectTransform>();
|
||||
configContentRect.anchorMin = Vector2.zero;
|
||||
configContentRect.anchorMax = Vector2.one;
|
||||
configContentRect.offsetMin = new Vector2(12, 8);
|
||||
configContentRect.offsetMax = new Vector2(-12, -8);
|
||||
|
||||
var configVertical = configContent.AddComponent<VerticalLayoutGroup>();
|
||||
configVertical.spacing = 8;
|
||||
configVertical.padding = new RectOffset(8, 8, 8, 8);
|
||||
configVertical.childAlignment = TextAnchor.MiddleLeft;
|
||||
configVertical.childControlWidth = true;
|
||||
configVertical.childControlHeight = true;
|
||||
configVertical.childForceExpandWidth = true;
|
||||
configVertical.childForceExpandHeight = false;
|
||||
|
||||
var row1 = CreateConfigRow1(configContent.transform);
|
||||
var row2 = CreateConfigRow2(configContent.transform);
|
||||
|
||||
var tracksRect = tracksContainer.GetComponent<RectTransform>();
|
||||
if (tracksRect == null)
|
||||
{
|
||||
Debug.LogError("[ProgrammedMovementsSetup] TracksContainer has no RectTransform.");
|
||||
return;
|
||||
}
|
||||
for (int t = 0; t < TracksCount; t++)
|
||||
{
|
||||
if (t >= tracksRect.childCount) break;
|
||||
var track = tracksRect.GetChild(t);
|
||||
if (track == null) continue;
|
||||
var blocks = track.Find("Blocks");
|
||||
if (blocks != null)
|
||||
{
|
||||
if (blocks.GetComponent<Image>() == null)
|
||||
{
|
||||
var img = blocks.gameObject.AddComponent<Image>();
|
||||
img.color = new Color(0, 0, 0, 0);
|
||||
}
|
||||
if (blocks.GetComponent<TrackClickHandler>() == null)
|
||||
blocks.gameObject.AddComponent<TrackClickHandler>();
|
||||
}
|
||||
}
|
||||
|
||||
EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
|
||||
Debug.Log("[ProgrammedMovementsSetup] Migrated to new layout. Save the scene. Enter Play mode to see Config panel when adding/editing blocks.");
|
||||
}
|
||||
|
||||
static void DoSetup(Transform pmView)
|
||||
{
|
||||
var canvasGO = new GameObject("ProgrammedMovementsCanvas");
|
||||
Undo.RegisterCreatedObjectUndo(canvasGO, "Create PM Canvas");
|
||||
canvasGO.transform.SetParent(pmView, false);
|
||||
|
||||
var canvasRect = canvasGO.AddComponent<RectTransform>();
|
||||
canvasRect.anchorMin = Vector2.zero;
|
||||
canvasRect.anchorMax = Vector2.one;
|
||||
canvasRect.offsetMin = Vector2.zero;
|
||||
canvasRect.offsetMax = Vector2.zero;
|
||||
|
||||
canvasGO.AddComponent<Canvas>().renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
canvasGO.GetComponent<Canvas>().sortingOrder = 2;
|
||||
var scaler = canvasGO.AddComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(800, 600);
|
||||
canvasGO.AddComponent<GraphicRaycaster>();
|
||||
|
||||
float bannerHeight = 100f;
|
||||
float runButtonHeight = 36f;
|
||||
|
||||
var runGO = CreateButton(canvasRect, "Run", 16);
|
||||
runGO.name = "RunButton";
|
||||
var runRect = runGO.GetComponent<RectTransform>();
|
||||
runRect.anchorMin = new Vector2(0, 0);
|
||||
runRect.anchorMax = new Vector2(0, 0);
|
||||
runRect.pivot = new Vector2(0, 0);
|
||||
runRect.anchoredPosition = new Vector2(12, bannerHeight + 8);
|
||||
runRect.sizeDelta = new Vector2(80, runButtonHeight);
|
||||
|
||||
var bannerGO = new GameObject("TimelineBanner");
|
||||
Undo.RegisterCreatedObjectUndo(bannerGO, "Create PM Banner");
|
||||
bannerGO.transform.SetParent(canvasRect, false);
|
||||
var bannerRect = bannerGO.AddComponent<RectTransform>();
|
||||
bannerRect.anchorMin = new Vector2(0, 0);
|
||||
bannerRect.anchorMax = new Vector2(1, 0);
|
||||
bannerRect.pivot = new Vector2(0.5f, 0);
|
||||
bannerRect.anchoredPosition = Vector2.zero;
|
||||
bannerRect.sizeDelta = new Vector2(0, bannerHeight);
|
||||
bannerGO.AddComponent<Image>().color = new Color(0.12f, 0.12f, 0.14f, 0.95f);
|
||||
|
||||
var timelineViewGO = new GameObject("TimelineView");
|
||||
timelineViewGO.transform.SetParent(bannerRect, false);
|
||||
var timelineViewRect = timelineViewGO.AddComponent<RectTransform>();
|
||||
timelineViewRect.anchorMin = Vector2.zero;
|
||||
timelineViewRect.anchorMax = Vector2.one;
|
||||
timelineViewRect.offsetMin = Vector2.zero;
|
||||
timelineViewRect.offsetMax = Vector2.zero;
|
||||
|
||||
var configViewGO = new GameObject("ConfigView");
|
||||
configViewGO.transform.SetParent(bannerRect, false);
|
||||
configViewGO.SetActive(false);
|
||||
var configViewRect = configViewGO.AddComponent<RectTransform>();
|
||||
configViewRect.anchorMin = Vector2.zero;
|
||||
configViewRect.anchorMax = Vector2.one;
|
||||
configViewRect.offsetMin = Vector2.zero;
|
||||
configViewRect.offsetMax = Vector2.zero;
|
||||
|
||||
var configContent = new GameObject("ConfigContent");
|
||||
configContent.transform.SetParent(configViewRect, false);
|
||||
var configContentRect = configContent.AddComponent<RectTransform>();
|
||||
configContentRect.anchorMin = Vector2.zero;
|
||||
configContentRect.anchorMax = Vector2.one;
|
||||
configContentRect.offsetMin = new Vector2(12, 8);
|
||||
configContentRect.offsetMax = new Vector2(-12, -8);
|
||||
|
||||
var configVertical = configContent.AddComponent<VerticalLayoutGroup>();
|
||||
configVertical.spacing = 8;
|
||||
configVertical.padding = new RectOffset(8, 8, 8, 8);
|
||||
configVertical.childAlignment = TextAnchor.MiddleLeft;
|
||||
configVertical.childControlWidth = true;
|
||||
configVertical.childControlHeight = true;
|
||||
configVertical.childForceExpandWidth = true;
|
||||
configVertical.childForceExpandHeight = false;
|
||||
|
||||
CreateConfigRow1(configContent.transform);
|
||||
CreateConfigRow2(configContent.transform);
|
||||
|
||||
var timeLabelsGO = new GameObject("TimeLabels");
|
||||
timeLabelsGO.transform.SetParent(timelineViewRect, false);
|
||||
var timeLabelsRect = timeLabelsGO.AddComponent<RectTransform>();
|
||||
timeLabelsRect.anchorMin = new Vector2(0, 1);
|
||||
timeLabelsRect.anchorMax = new Vector2(1, 1);
|
||||
timeLabelsRect.pivot = new Vector2(0.5f, 1);
|
||||
timeLabelsRect.anchoredPosition = new Vector2(0, -8);
|
||||
timeLabelsRect.sizeDelta = new Vector2(0, 20);
|
||||
|
||||
for (int s = 0; s <= TimelineDurationSeconds; s++)
|
||||
{
|
||||
float x = (float)s / TimelineDurationSeconds;
|
||||
var label = CreateText(timeLabelsRect.transform, $"{s}s", 12);
|
||||
label.name = $"Label{s}s";
|
||||
var lr = label.GetComponent<RectTransform>();
|
||||
lr.anchorMin = new Vector2(x, 0);
|
||||
lr.anchorMax = new Vector2(x, 1);
|
||||
lr.pivot = new Vector2(0.5f, 0.5f);
|
||||
lr.anchoredPosition = Vector2.zero;
|
||||
lr.sizeDelta = new Vector2(28, 0);
|
||||
}
|
||||
var linesGO = new GameObject("VerticalLines");
|
||||
linesGO.transform.SetParent(timelineViewRect, false);
|
||||
var linesRect = linesGO.AddComponent<RectTransform>();
|
||||
linesRect.anchorMin = new Vector2(0, 0);
|
||||
linesRect.anchorMax = new Vector2(1, 1);
|
||||
linesRect.offsetMin = new Vector2(4, 4);
|
||||
linesRect.offsetMax = new Vector2(-4, -32);
|
||||
for (int s = 0; s <= TimelineDurationSeconds; s++)
|
||||
{
|
||||
float x = (float)s / TimelineDurationSeconds;
|
||||
var line = new GameObject($"Line{s}");
|
||||
line.transform.SetParent(linesRect, false);
|
||||
var lineImg = line.AddComponent<Image>();
|
||||
lineImg.color = new Color(0.35f, 0.35f, 0.4f, 0.7f);
|
||||
var lineRect = line.GetComponent<RectTransform>();
|
||||
lineRect.anchorMin = new Vector2(x, 0);
|
||||
lineRect.anchorMax = new Vector2(x, 1);
|
||||
lineRect.pivot = new Vector2(0.5f, 0);
|
||||
lineRect.anchoredPosition = Vector2.zero;
|
||||
lineRect.sizeDelta = new Vector2(2, 0);
|
||||
}
|
||||
|
||||
var tracksGO = new GameObject("TracksContainer");
|
||||
tracksGO.transform.SetParent(timelineViewRect, false);
|
||||
var tracksRect = tracksGO.AddComponent<RectTransform>();
|
||||
tracksRect.anchorMin = new Vector2(0, 0);
|
||||
tracksRect.anchorMax = new Vector2(1, 1);
|
||||
tracksRect.offsetMin = new Vector2(4, 4);
|
||||
tracksRect.offsetMax = new Vector2(-4, -32);
|
||||
|
||||
for (int t = 0; t < TracksCount; t++)
|
||||
{
|
||||
var trackGO = new GameObject($"Track{t}");
|
||||
trackGO.transform.SetParent(tracksRect, false);
|
||||
var tr = trackGO.AddComponent<RectTransform>();
|
||||
tr.anchorMin = new Vector2(0, (float)(TracksCount - 1 - t) / TracksCount);
|
||||
tr.anchorMax = new Vector2(1, (float)(TracksCount - t) / TracksCount);
|
||||
tr.offsetMin = new Vector2(2, 2);
|
||||
tr.offsetMax = new Vector2(-2, -2);
|
||||
|
||||
var trackBg = trackGO.AddComponent<Image>();
|
||||
trackBg.color = new Color(0.18f, 0.18f, 0.2f, 0.9f);
|
||||
|
||||
var blocksGO = new GameObject("Blocks");
|
||||
blocksGO.transform.SetParent(tr, false);
|
||||
var blocksRect = blocksGO.AddComponent<RectTransform>();
|
||||
blocksRect.anchorMin = Vector2.zero;
|
||||
blocksRect.anchorMax = Vector2.one;
|
||||
blocksRect.offsetMin = Vector2.zero;
|
||||
blocksRect.offsetMax = Vector2.zero; // Full width so block coords (0-1) align with timeline seconds
|
||||
blocksGO.AddComponent<Image>().color = new Color(0, 0, 0, 0);
|
||||
blocksGO.AddComponent<TrackClickHandler>();
|
||||
|
||||
var addBtn = CreateButton(tr, "+");
|
||||
addBtn.name = "AddButton";
|
||||
var addRect = addBtn.GetComponent<RectTransform>();
|
||||
addRect.anchorMin = new Vector2(1, 0);
|
||||
addRect.anchorMax = new Vector2(1, 1);
|
||||
addRect.pivot = new Vector2(1, 0.5f);
|
||||
addRect.anchoredPosition = new Vector2(-4, 0);
|
||||
addRect.sizeDelta = new Vector2(28, 0);
|
||||
}
|
||||
|
||||
var controller = pmView.GetComponentInChildren<ProgrammedMovementsController>(true);
|
||||
if (controller != null)
|
||||
{
|
||||
controller.timelineRoot = bannerRect;
|
||||
controller.runButton = runGO.GetComponent<Button>();
|
||||
EditorUtility.SetDirty(controller);
|
||||
}
|
||||
|
||||
Debug.Log("[ProgrammedMovementsSetup] ProgrammedMovements UI hierarchy created. Save the scene to persist.");
|
||||
}
|
||||
|
||||
static GameObject CreateText(Transform parent, string text, int fontSize)
|
||||
{
|
||||
var go = new GameObject();
|
||||
go.transform.SetParent(parent, false);
|
||||
var tmp = go.AddComponent<TextMeshProUGUI>();
|
||||
tmp.text = text;
|
||||
tmp.fontSize = fontSize;
|
||||
tmp.color = Color.white;
|
||||
tmp.alignment = TextAlignmentOptions.Center;
|
||||
go.AddComponent<RectTransform>();
|
||||
return go;
|
||||
}
|
||||
|
||||
static GameObject CreateButton(Transform parent, string label, int fontSize = 14)
|
||||
{
|
||||
var go = new GameObject();
|
||||
go.transform.SetParent(parent, false);
|
||||
var img = go.AddComponent<Image>();
|
||||
img.color = new Color(0.3f, 0.5f, 0.8f, 1f);
|
||||
go.AddComponent<Button>();
|
||||
var textGO = CreateText(go.transform, label, fontSize);
|
||||
var textRect = textGO.GetComponent<RectTransform>();
|
||||
textRect.anchorMin = Vector2.zero;
|
||||
textRect.anchorMax = Vector2.one;
|
||||
textRect.offsetMin = Vector2.zero;
|
||||
textRect.offsetMax = Vector2.zero;
|
||||
return go;
|
||||
}
|
||||
|
||||
static void CreateConfigLabel(Transform parent, string text)
|
||||
{
|
||||
var go = CreateText(parent, text, 12);
|
||||
if (go == null) return;
|
||||
var rect = go.GetComponent<RectTransform>();
|
||||
if (rect != null) rect.sizeDelta = new Vector2(60, 24);
|
||||
var le = go.AddComponent<LayoutElement>();
|
||||
le.preferredWidth = 60;
|
||||
le.preferredHeight = 24;
|
||||
}
|
||||
|
||||
static GameObject CreateConfigRow1(Transform parent)
|
||||
{
|
||||
var row = new GameObject("ConfigRow1");
|
||||
row.transform.SetParent(parent, false);
|
||||
row.AddComponent<RectTransform>();
|
||||
var h = row.AddComponent<HorizontalLayoutGroup>();
|
||||
h.spacing = 10;
|
||||
h.padding = new RectOffset(0, 0, 0, 0);
|
||||
h.childAlignment = TextAnchor.MiddleLeft;
|
||||
h.childControlWidth = true;
|
||||
h.childControlHeight = true;
|
||||
h.childForceExpandWidth = false;
|
||||
h.childForceExpandHeight = false;
|
||||
var rowLe = row.AddComponent<LayoutElement>();
|
||||
rowLe.preferredHeight = 32;
|
||||
|
||||
CreateConfigLabel(row.transform, "Module:");
|
||||
var moduleLabelGO = new GameObject("ModuleLabel");
|
||||
moduleLabelGO.transform.SetParent(row.transform, false);
|
||||
var moduleLabelTmp = moduleLabelGO.AddComponent<TextMeshProUGUI>();
|
||||
moduleLabelTmp.text = "None";
|
||||
moduleLabelTmp.fontSize = 12;
|
||||
moduleLabelTmp.color = Color.white;
|
||||
var mlRect = moduleLabelGO.GetComponent<RectTransform>() ?? moduleLabelGO.AddComponent<RectTransform>();
|
||||
mlRect.sizeDelta = new Vector2(120, 24);
|
||||
var mlLe = moduleLabelGO.AddComponent<LayoutElement>();
|
||||
mlLe.preferredWidth = 120;
|
||||
mlLe.preferredHeight = 24;
|
||||
|
||||
var selectBtn = CreateButton(row.transform, "Select");
|
||||
selectBtn.name = "SelectButton";
|
||||
SetLayout(selectBtn, 70, 28);
|
||||
var setBtn = CreateButton(row.transform, "Set");
|
||||
setBtn.name = "SetButton";
|
||||
setBtn.GetComponent<Image>().color = new Color(0.2f, 0.6f, 0.3f, 1f);
|
||||
SetLayout(setBtn, 60, 28);
|
||||
return row;
|
||||
}
|
||||
|
||||
static GameObject CreateConfigRow2(Transform parent)
|
||||
{
|
||||
var row = new GameObject("ConfigRow2");
|
||||
row.transform.SetParent(parent, false);
|
||||
row.AddComponent<RectTransform>();
|
||||
var h = row.AddComponent<HorizontalLayoutGroup>();
|
||||
h.spacing = 10;
|
||||
h.padding = new RectOffset(0, 0, 0, 0);
|
||||
h.childAlignment = TextAnchor.MiddleLeft;
|
||||
h.childControlWidth = true;
|
||||
h.childControlHeight = true;
|
||||
h.childForceExpandWidth = false;
|
||||
h.childForceExpandHeight = false;
|
||||
var rowLe = row.AddComponent<LayoutElement>();
|
||||
rowLe.preferredHeight = 32;
|
||||
|
||||
var angleSection = new GameObject("AngleSection");
|
||||
angleSection.transform.SetParent(row.transform, false);
|
||||
angleSection.AddComponent<RectTransform>();
|
||||
var angleSectionH = angleSection.AddComponent<HorizontalLayoutGroup>();
|
||||
angleSectionH.spacing = 6;
|
||||
angleSectionH.childForceExpandWidth = false;
|
||||
var angleSectionLe = angleSection.AddComponent<LayoutElement>();
|
||||
angleSectionLe.preferredWidth = 90;
|
||||
angleSectionLe.preferredHeight = 28;
|
||||
CreateConfigLabel(angleSection.transform, "Angle:");
|
||||
var angleInputGO = CreateSimpleInputField(angleSection.transform, "90");
|
||||
angleInputGO.name = "AngleInput";
|
||||
SetLayout(angleInputGO, 55, 26);
|
||||
|
||||
var directionSection = new GameObject("DirectionSection");
|
||||
directionSection.transform.SetParent(row.transform, false);
|
||||
directionSection.SetActive(false);
|
||||
directionSection.AddComponent<RectTransform>();
|
||||
var dirSectionH = directionSection.AddComponent<HorizontalLayoutGroup>();
|
||||
dirSectionH.spacing = 6;
|
||||
dirSectionH.childForceExpandWidth = false;
|
||||
var dirSectionLe = directionSection.AddComponent<LayoutElement>();
|
||||
dirSectionLe.preferredWidth = 220;
|
||||
dirSectionLe.preferredHeight = 28;
|
||||
CreateConfigLabel(directionSection.transform, "Direction:");
|
||||
var dirButtons = new GameObject("DirectionButtons");
|
||||
dirButtons.transform.SetParent(directionSection.transform, false);
|
||||
var dirH = dirButtons.AddComponent<HorizontalLayoutGroup>();
|
||||
dirH.spacing = 4;
|
||||
dirH.childForceExpandWidth = false;
|
||||
var dirLe = dirButtons.AddComponent<LayoutElement>();
|
||||
dirLe.preferredWidth = 100;
|
||||
dirLe.preferredHeight = 26;
|
||||
var fwdBtn = CreateButton(dirButtons.transform, "Fwd", 11);
|
||||
fwdBtn.name = "ForwardButton";
|
||||
SetLayout(fwdBtn, 45, 24);
|
||||
var bwdBtn = CreateButton(dirButtons.transform, "Bwd", 11);
|
||||
bwdBtn.name = "BackwardButton";
|
||||
SetLayout(bwdBtn, 45, 24);
|
||||
CreateConfigLabel(directionSection.transform, "Degrees:");
|
||||
var dcDegreesInput = CreateSimpleInputField(directionSection.transform, "90");
|
||||
dcDegreesInput.name = "DCDegreesInput";
|
||||
SetLayout(dcDegreesInput, 45, 26);
|
||||
|
||||
var movementLabel = CreateText(row.transform, "Movement set at", 12);
|
||||
movementLabel.name = "MovementSetAtLabel";
|
||||
movementLabel.GetComponent<TextMeshProUGUI>().alignment = TextAlignmentOptions.MidlineLeft;
|
||||
var mslLe = movementLabel.AddComponent<LayoutElement>();
|
||||
mslLe.preferredWidth = 95;
|
||||
mslLe.preferredHeight = 24;
|
||||
var secondInputGO = CreateSimpleInputField(row.transform, "0");
|
||||
secondInputGO.name = "SecondInput";
|
||||
SetLayout(secondInputGO, 45, 26);
|
||||
var secondSuffix = CreateText(row.transform, " second", 12);
|
||||
secondSuffix.name = "SecondSuffix";
|
||||
var ssLe = secondSuffix.AddComponent<LayoutElement>();
|
||||
ssLe.preferredWidth = 50;
|
||||
ssLe.preferredHeight = 24;
|
||||
|
||||
var saveBtn = CreateButton(row.transform, "Save");
|
||||
saveBtn.name = "SaveButton";
|
||||
SetLayout(saveBtn, 65, 28);
|
||||
|
||||
var backBtn = CreateButton(row.transform, "Back");
|
||||
backBtn.name = "BackButton";
|
||||
SetLayout(backBtn, 65, 28);
|
||||
|
||||
var deleteBtn = CreateButton(row.transform, "Delete");
|
||||
deleteBtn.name = "DeleteButton";
|
||||
deleteBtn.GetComponent<Image>().color = new Color(0.7f, 0.3f, 0.3f, 1f);
|
||||
SetLayout(deleteBtn, 65, 28);
|
||||
return row;
|
||||
}
|
||||
|
||||
static void SetLayout(GameObject go, float w, float h)
|
||||
{
|
||||
var rect = go.GetComponent<RectTransform>();
|
||||
if (rect != null) rect.sizeDelta = new Vector2(w, h);
|
||||
var le = go.GetComponent<LayoutElement>();
|
||||
if (le == null) le = go.AddComponent<LayoutElement>();
|
||||
le.preferredWidth = w;
|
||||
le.preferredHeight = h;
|
||||
}
|
||||
|
||||
static GameObject CreateSimpleInputField(Transform parent, string initial)
|
||||
{
|
||||
var go = new GameObject("Input");
|
||||
go.transform.SetParent(parent, false);
|
||||
var img = go.AddComponent<Image>();
|
||||
img.color = new Color(0.2f, 0.2f, 0.25f, 1f);
|
||||
var input = go.AddComponent<TMP_InputField>();
|
||||
var textGO = new GameObject("Text");
|
||||
textGO.transform.SetParent(go.transform, false);
|
||||
var text = textGO.AddComponent<TextMeshProUGUI>();
|
||||
text.text = initial;
|
||||
text.fontSize = 11;
|
||||
text.color = Color.white;
|
||||
text.alignment = TextAlignmentOptions.Center;
|
||||
var textRect = textGO.GetComponent<RectTransform>();
|
||||
textRect.anchorMin = Vector2.zero;
|
||||
textRect.anchorMax = Vector2.one;
|
||||
textRect.offsetMin = new Vector2(4, 2);
|
||||
textRect.offsetMax = new Vector2(-4, -2);
|
||||
input.textComponent = text;
|
||||
input.text = initial;
|
||||
return go;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
686
Assets/ProgrammedMovements/ProgrammedMovementsController.cs
Normal file
686
Assets/ProgrammedMovements/ProgrammedMovementsController.cs
Normal file
@@ -0,0 +1,686 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.EventSystems;
|
||||
using TMPro;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for ProgrammedMovements view: timeline banner at bottom.
|
||||
/// 3 tracks, 0-10 seconds, movement blocks, Run button.
|
||||
/// </summary>
|
||||
public class ProgrammedMovementsController : MonoBehaviour
|
||||
{
|
||||
public const int TracksCount = 3;
|
||||
public const int TimelineDurationSeconds = 10;
|
||||
public static readonly Color HighlightBlue = new Color(0.2f, 0.5f, 1f, 1f);
|
||||
|
||||
[Header("References")]
|
||||
public Camera mainCamera;
|
||||
public RectTransform timelineRoot;
|
||||
public Button runButton;
|
||||
|
||||
private RectTransform _bannerRoot;
|
||||
private Transform _timelineView;
|
||||
private Transform _configView;
|
||||
private readonly List<MovementBlock> _blocks = new List<MovementBlock>();
|
||||
private readonly Dictionary<int, List<MovementBlock>> _blocksBySecond = new Dictionary<int, List<MovementBlock>>();
|
||||
private bool _isRunning;
|
||||
private float _runStartTime;
|
||||
private int _lastExecutedSecond = -1;
|
||||
private float _lastRewireTime = -1f;
|
||||
private ModuleBase _highlightedModule;
|
||||
private readonly Dictionary<Renderer, Material[]> _originalMaterialsByRenderer = new Dictionary<Renderer, Material[]>();
|
||||
private MovementBlock _pickModeBlock;
|
||||
private MovementBlock _editingBlock;
|
||||
private ModuleBase _pendingModule;
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
ResolveReferences();
|
||||
if (_bannerRoot == null)
|
||||
{
|
||||
Debug.LogError("[ProgrammedMovements] timelineRoot not set. Run Tools > ProgrammedMovements > Setup UI Hierarchy in the Editor.");
|
||||
return;
|
||||
}
|
||||
EnsureTimelineViewShown();
|
||||
WireUpButtons();
|
||||
RefreshRunButton();
|
||||
// Force layout rebuild so block positions align with timeline after view switch
|
||||
Canvas.ForceUpdateCanvases();
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(_bannerRoot);
|
||||
}
|
||||
|
||||
void EnsureTimelineViewShown()
|
||||
{
|
||||
if (_timelineView != null) _timelineView.gameObject.SetActive(true);
|
||||
if (_configView != null) _configView.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
void ResolveReferences()
|
||||
{
|
||||
if (timelineRoot != null)
|
||||
{
|
||||
_bannerRoot = timelineRoot;
|
||||
}
|
||||
else
|
||||
{
|
||||
var pmView = transform.parent;
|
||||
if (pmView == null) return;
|
||||
var canvas = pmView.Find("ProgrammedMovementsCanvas");
|
||||
if (canvas == null) return;
|
||||
var banner = canvas.Find("TimelineBanner");
|
||||
if (banner != null)
|
||||
{
|
||||
_bannerRoot = banner.GetComponent<RectTransform>();
|
||||
if (timelineRoot == null) timelineRoot = _bannerRoot;
|
||||
}
|
||||
}
|
||||
if (_bannerRoot != null)
|
||||
{
|
||||
_timelineView = _bannerRoot.Find("TimelineView");
|
||||
_configView = _bannerRoot.Find("ConfigView");
|
||||
if (_timelineView == null)
|
||||
{
|
||||
_timelineView = _bannerRoot;
|
||||
}
|
||||
}
|
||||
if (runButton == null && _bannerRoot != null)
|
||||
{
|
||||
var canvas = _bannerRoot.parent;
|
||||
if (canvas != null)
|
||||
{
|
||||
var runGO = canvas.Find("RunButton");
|
||||
if (runGO != null) runButton = runGO.GetComponent<Button>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WireUpButtons()
|
||||
{
|
||||
WireUpRunButton();
|
||||
var tracks = _timelineView != null ? _timelineView.Find("TracksContainer") : null;
|
||||
if (tracks != null)
|
||||
{
|
||||
for (int t = 0; t < TracksCount; t++)
|
||||
{
|
||||
var track = tracks.GetChild(t);
|
||||
var addBtn = track.Find("AddButton")?.GetComponent<Button>();
|
||||
if (addBtn != null)
|
||||
{
|
||||
int trackIndex = t;
|
||||
addBtn.onClick.RemoveAllListeners();
|
||||
addBtn.onClick.AddListener(() => AddBlock(trackIndex, 0));
|
||||
}
|
||||
var blocks = track.Find("Blocks");
|
||||
var clickHandler = blocks?.GetComponent<TrackClickHandler>();
|
||||
if (clickHandler != null)
|
||||
{
|
||||
int trackIndex = t;
|
||||
clickHandler.OnTrackClicked = (tr, sec) => AddBlock(tr, sec);
|
||||
}
|
||||
}
|
||||
}
|
||||
var config = _configView?.Find("ConfigContent");
|
||||
if (config != null)
|
||||
{
|
||||
var row1 = config.Find("ConfigRow1");
|
||||
var row2 = config.Find("ConfigRow2");
|
||||
var selectBtn = (row1 != null ? row1.Find("SelectButton") : config.Find("SelectButton"))?.GetComponent<Button>();
|
||||
if (selectBtn != null)
|
||||
{
|
||||
selectBtn.onClick.RemoveAllListeners();
|
||||
selectBtn.onClick.AddListener(OnConfigSelectClicked);
|
||||
}
|
||||
var setBtn = (row1 != null ? row1.Find("SetButton") : null)?.GetComponent<Button>();
|
||||
if (setBtn != null)
|
||||
{
|
||||
setBtn.onClick.RemoveAllListeners();
|
||||
setBtn.onClick.AddListener(OnConfigSetClicked);
|
||||
}
|
||||
var saveBtn = (row2 != null ? row2.Find("SaveButton") : config.Find("SaveButton"))?.GetComponent<Button>();
|
||||
if (saveBtn != null)
|
||||
{
|
||||
saveBtn.onClick.RemoveAllListeners();
|
||||
saveBtn.onClick.AddListener(OnConfigSaveClicked);
|
||||
}
|
||||
var backBtn = (row2 != null ? row2.Find("BackButton") : null)?.GetComponent<Button>();
|
||||
if (backBtn != null)
|
||||
{
|
||||
backBtn.onClick.RemoveAllListeners();
|
||||
backBtn.onClick.AddListener(OnConfigBackClicked);
|
||||
}
|
||||
var deleteBtn = (row2 != null ? row2.Find("DeleteButton") : config.Find("DeleteButton"))?.GetComponent<Button>();
|
||||
if (deleteBtn != null)
|
||||
{
|
||||
deleteBtn.onClick.RemoveAllListeners();
|
||||
deleteBtn.onClick.AddListener(OnConfigDeleteClicked);
|
||||
}
|
||||
var dirButtons = (row2 != null ? row2.Find("DirectionSection/DirectionButtons") : config.Find("DirectionSection/DirectionButtons"));
|
||||
var fwdBtn = dirButtons?.Find("ForwardButton")?.GetComponent<Button>();
|
||||
var bwdBtn = dirButtons?.Find("BackwardButton")?.GetComponent<Button>();
|
||||
if (fwdBtn != null)
|
||||
{
|
||||
fwdBtn.onClick.RemoveAllListeners();
|
||||
fwdBtn.onClick.AddListener(OnDirectionForwardClicked);
|
||||
}
|
||||
if (bwdBtn != null)
|
||||
{
|
||||
bwdBtn.onClick.RemoveAllListeners();
|
||||
bwdBtn.onClick.AddListener(OnDirectionBackwardClicked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OnDirectionForwardClicked()
|
||||
{
|
||||
if (_editingBlock == null) return;
|
||||
_editingBlock.dcDirection = 1;
|
||||
var config = _configView?.Find("ConfigContent");
|
||||
var dirButtons = config?.Find("ConfigRow2/DirectionSection/DirectionButtons") ?? config?.Find("DirectionSection/DirectionButtons");
|
||||
if (dirButtons != null) UpdateDirectionButtonsVisual(dirButtons, 1);
|
||||
}
|
||||
|
||||
void OnDirectionBackwardClicked()
|
||||
{
|
||||
if (_editingBlock == null) return;
|
||||
_editingBlock.dcDirection = -1;
|
||||
var config = _configView?.Find("ConfigContent");
|
||||
var dirButtons = config?.Find("ConfigRow2/DirectionSection/DirectionButtons") ?? config?.Find("DirectionSection/DirectionButtons");
|
||||
if (dirButtons != null) UpdateDirectionButtonsVisual(dirButtons, -1);
|
||||
}
|
||||
|
||||
void UpdateDirectionButtonsVisual(Transform dirButtons, int dcDirection)
|
||||
{
|
||||
var fwdImg = dirButtons.Find("ForwardButton")?.GetComponent<Image>();
|
||||
var bwdImg = dirButtons.Find("BackwardButton")?.GetComponent<Image>();
|
||||
var selected = new Color(0.2f, 0.6f, 0.4f, 1f);
|
||||
var normal = new Color(0.3f, 0.5f, 0.8f, 1f);
|
||||
if (fwdImg != null) fwdImg.color = dcDirection >= 0 ? selected : normal;
|
||||
if (bwdImg != null) bwdImg.color = dcDirection < 0 ? selected : normal;
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
_isRunning = false;
|
||||
ClearModuleHighlight();
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (_isRunning)
|
||||
{
|
||||
float elapsed = Time.time - _runStartTime;
|
||||
if (elapsed >= TimelineDurationSeconds)
|
||||
{
|
||||
_isRunning = false;
|
||||
_lastExecutedSecond = -1;
|
||||
RefreshRunButton();
|
||||
Invoke(nameof(WireUpRunButton), 0.05f); // Delayed re-wire so Run works again
|
||||
return;
|
||||
}
|
||||
int currentSecond = Mathf.FloorToInt(elapsed);
|
||||
if (currentSecond != _lastExecutedSecond && _blocksBySecond.TryGetValue(currentSecond, out var list))
|
||||
{
|
||||
_lastExecutedSecond = currentSecond;
|
||||
foreach (var b in list)
|
||||
{
|
||||
if (b.moduleServo != null)
|
||||
b.moduleServo.SetAngleAndSendControlLibrary(b.targetAngle, 0f);
|
||||
else if (b.moduleDC != null)
|
||||
b.moduleDC.Rotate(b.targetAngle, b.dcDirection);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Periodically re-wire Run button when idle (every 2s) so it stays clickable
|
||||
if (Time.time - _lastRewireTime > 2f)
|
||||
{
|
||||
_lastRewireTime = Time.time;
|
||||
WireUpRunButton();
|
||||
}
|
||||
}
|
||||
|
||||
if (_pickModeBlock != null && Input.GetMouseButtonDown(0) && !EventSystem.current.IsPointerOverGameObject())
|
||||
{
|
||||
var cam = mainCamera != null ? mainCamera : Camera.main;
|
||||
if (cam != null && Physics.Raycast(cam.ScreenPointToRay(Input.mousePosition), out var hit))
|
||||
{
|
||||
var servo = hit.collider.GetComponentInParent<ServoMotorModule>();
|
||||
var dc = hit.collider.GetComponentInParent<DCMotorModule>();
|
||||
// Prefer DC when both exist (e.g. DC attached to servo arm) - user clicked the DC
|
||||
var module = (ModuleBase)dc ?? servo;
|
||||
if (module != null && IsInGeneratedTopology(module.transform))
|
||||
{
|
||||
_pendingModule = module;
|
||||
SetModuleHighlight(module);
|
||||
UpdateConfigModuleLabel(GetModuleTypeName(module));
|
||||
}
|
||||
else
|
||||
{
|
||||
_pendingModule = null;
|
||||
ClearModuleHighlight();
|
||||
UpdateConfigModuleLabel("None");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GameObject CreateText(Transform parent, string text, int fontSize)
|
||||
{
|
||||
var go = new GameObject();
|
||||
go.transform.SetParent(parent, false);
|
||||
var tmp = go.AddComponent<TextMeshProUGUI>();
|
||||
tmp.text = text;
|
||||
tmp.fontSize = fontSize;
|
||||
tmp.color = Color.white;
|
||||
tmp.alignment = TextAlignmentOptions.Center;
|
||||
go.AddComponent<RectTransform>();
|
||||
return go;
|
||||
}
|
||||
|
||||
GameObject CreateButton(Transform parent, string label, Action onClick)
|
||||
{
|
||||
var go = new GameObject();
|
||||
go.transform.SetParent(parent, false);
|
||||
var img = go.AddComponent<Image>();
|
||||
img.color = new Color(0.3f, 0.5f, 0.8f, 1f);
|
||||
var btn = go.AddComponent<Button>();
|
||||
btn.onClick.AddListener(() => onClick?.Invoke());
|
||||
var textGO = CreateText(go.transform, label, 14);
|
||||
var textRect = textGO.GetComponent<RectTransform>();
|
||||
textRect.anchorMin = Vector2.zero;
|
||||
textRect.anchorMax = Vector2.one;
|
||||
textRect.offsetMin = Vector2.zero;
|
||||
textRect.offsetMax = Vector2.zero;
|
||||
return go;
|
||||
}
|
||||
|
||||
void AddBlock(int trackIndex, float second)
|
||||
{
|
||||
if (_bannerRoot == null) return;
|
||||
|
||||
int sec = Mathf.Clamp(Mathf.FloorToInt(second), 0, TimelineDurationSeconds - 1);
|
||||
var block = new MovementBlock { trackIndex = trackIndex, second = sec, targetAngle = 90f };
|
||||
_blocks.Add(block);
|
||||
IndexBlock(block);
|
||||
|
||||
var tracksRect = _timelineView?.Find("TracksContainer");
|
||||
if (tracksRect == null) return;
|
||||
var track = tracksRect.GetChild(trackIndex);
|
||||
var blocksHolder = track.Find("Blocks");
|
||||
if (blocksHolder == null) return;
|
||||
|
||||
CreateMovementBlockUI(blocksHolder, block);
|
||||
OpenConfigPanel(block);
|
||||
}
|
||||
|
||||
GameObject CreateMovementBlockUI(Transform parent, MovementBlock block)
|
||||
{
|
||||
var go = new GameObject($"Block_{block.second}s_T{block.trackIndex}");
|
||||
go.transform.SetParent(parent, false);
|
||||
|
||||
var rect = go.AddComponent<RectTransform>();
|
||||
float x = (float)block.second / TimelineDurationSeconds;
|
||||
float w = 1f / TimelineDurationSeconds; // 1 second width to align with timeline
|
||||
rect.anchorMin = new Vector2(x, 0);
|
||||
rect.anchorMax = new Vector2(x + w, 1);
|
||||
rect.offsetMin = new Vector2(2, 2);
|
||||
rect.offsetMax = new Vector2(-2, -2);
|
||||
|
||||
var bg = go.AddComponent<Image>();
|
||||
bg.color = new Color(0.35f, 0.5f, 0.7f, 0.95f);
|
||||
|
||||
var btn = go.AddComponent<Button>();
|
||||
btn.onClick.AddListener(() => OpenConfigPanel(block));
|
||||
|
||||
block.uiRoot = go;
|
||||
|
||||
return go;
|
||||
}
|
||||
|
||||
void OpenConfigPanel(MovementBlock block)
|
||||
{
|
||||
_editingBlock = block;
|
||||
_pendingModule = (ModuleBase)block.moduleServo ?? block.moduleDC;
|
||||
_pickModeBlock = null;
|
||||
if (_configView != null)
|
||||
{
|
||||
if (_timelineView != null) _timelineView.gameObject.SetActive(false);
|
||||
_configView.gameObject.SetActive(true);
|
||||
PopulateConfigPanel(block);
|
||||
}
|
||||
var mod = (ModuleBase)block.moduleServo ?? block.moduleDC;
|
||||
if (mod != null) SetModuleHighlight(mod);
|
||||
else ClearModuleHighlight();
|
||||
}
|
||||
|
||||
void CloseConfigPanel()
|
||||
{
|
||||
_editingBlock = null;
|
||||
_pickModeBlock = null;
|
||||
_pendingModule = null;
|
||||
ClearModuleHighlight();
|
||||
if (_timelineView != null) _timelineView.gameObject.SetActive(true);
|
||||
if (_configView != null) _configView.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
void PopulateConfigPanel(MovementBlock block)
|
||||
{
|
||||
var config = _configView?.Find("ConfigContent");
|
||||
if (config == null) return;
|
||||
|
||||
var mod = (ModuleBase)block.moduleServo ?? block.moduleDC;
|
||||
var moduleLabel = config.Find("ConfigRow1/ModuleLabel") ?? config.Find("ModuleLabel");
|
||||
var ml = moduleLabel?.GetComponent<TextMeshProUGUI>();
|
||||
if (ml != null) ml.text = mod != null ? GetModuleTypeName(mod) : "None";
|
||||
|
||||
var row2 = config.Find("ConfigRow2");
|
||||
var angleSection = row2?.Find("AngleSection") ?? config.Find("AngleSection");
|
||||
var angleInput = angleSection?.Find("AngleInput") ?? row2?.Find("AngleInput") ?? config.Find("AngleInput");
|
||||
var directionSection = row2?.Find("DirectionSection") ?? config.Find("DirectionSection");
|
||||
var dirButtons = directionSection?.Find("DirectionButtons");
|
||||
var dcDegreesInput = directionSection?.Find("DCDegreesInput");
|
||||
|
||||
bool isDC = block.moduleDC != null;
|
||||
if (angleSection != null) angleSection.gameObject.SetActive(!isDC);
|
||||
if (directionSection != null) directionSection.gameObject.SetActive(isDC);
|
||||
|
||||
if (!isDC)
|
||||
{
|
||||
var ai = angleInput?.GetComponent<TMP_InputField>();
|
||||
if (ai != null) ai.text = block.targetAngle.ToString("F0");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (dirButtons != null) UpdateDirectionButtonsVisual(dirButtons, block.dcDirection);
|
||||
var dcd = dcDegreesInput?.GetComponent<TMP_InputField>();
|
||||
if (dcd != null) dcd.text = block.targetAngle.ToString("F0");
|
||||
}
|
||||
|
||||
var secondInput = config.Find("ConfigRow2/SecondInput") ?? config.Find("SecondInput");
|
||||
var si = secondInput?.GetComponent<TMP_InputField>();
|
||||
if (si != null) si.text = block.second.ToString();
|
||||
}
|
||||
|
||||
void UpdateConfigModuleLabel(string text)
|
||||
{
|
||||
var config = _configView?.Find("ConfigContent");
|
||||
var moduleLabel = config?.Find("ConfigRow1/ModuleLabel") ?? config?.Find("ModuleLabel");
|
||||
var ml = moduleLabel?.GetComponent<TextMeshProUGUI>();
|
||||
if (ml != null) ml.text = text;
|
||||
}
|
||||
|
||||
static string GetModuleTypeName(ModuleBase m)
|
||||
{
|
||||
if (m == null) return "None";
|
||||
var t = m.GetType();
|
||||
if (t.Name == "ServoBendModule") return "Servo Bend";
|
||||
if (t.Name == "ServoStraightModule") return "Servo Straight";
|
||||
if (t.Name == "DCMotorModule") return "DC Motor";
|
||||
return t.Name.Replace("Module", "");
|
||||
}
|
||||
|
||||
void OnConfigSelectClicked()
|
||||
{
|
||||
if (_editingBlock == null) return;
|
||||
_pickModeBlock = _editingBlock;
|
||||
_pendingModule = (ModuleBase)_editingBlock.moduleServo ?? _editingBlock.moduleDC;
|
||||
if (_pendingModule != null)
|
||||
{
|
||||
SetModuleHighlight(_pendingModule);
|
||||
UpdateConfigModuleLabel(GetModuleTypeName(_pendingModule));
|
||||
}
|
||||
else
|
||||
{
|
||||
ClearModuleHighlight();
|
||||
UpdateConfigModuleLabel("Click a module, then Set");
|
||||
}
|
||||
}
|
||||
|
||||
void OnConfigSetClicked()
|
||||
{
|
||||
if (_editingBlock == null) return;
|
||||
var block = _editingBlock;
|
||||
block.moduleServo = null;
|
||||
block.moduleDC = null;
|
||||
if (_pendingModule is ServoMotorModule sm)
|
||||
block.moduleServo = sm;
|
||||
else if (_pendingModule is DCMotorModule dm)
|
||||
block.moduleDC = dm;
|
||||
_pickModeBlock = null;
|
||||
if (_pendingModule != null)
|
||||
{
|
||||
SetModuleHighlight(_pendingModule);
|
||||
UpdateConfigModuleLabel(GetModuleTypeName(_pendingModule));
|
||||
}
|
||||
else
|
||||
{
|
||||
ClearModuleHighlight();
|
||||
UpdateConfigModuleLabel("None");
|
||||
}
|
||||
PopulateConfigPanel(block);
|
||||
}
|
||||
|
||||
void OnConfigBackClicked()
|
||||
{
|
||||
CloseConfigPanel();
|
||||
}
|
||||
|
||||
void OnConfigSaveClicked()
|
||||
{
|
||||
if (_editingBlock == null) return;
|
||||
ApplyConfigToBlock();
|
||||
CloseConfigPanel();
|
||||
}
|
||||
|
||||
void OnConfigDeleteClicked()
|
||||
{
|
||||
if (_editingBlock == null) return;
|
||||
var block = _editingBlock;
|
||||
UnindexBlock(block);
|
||||
_blocks.Remove(block);
|
||||
if (block.uiRoot != null) Destroy(block.uiRoot);
|
||||
CloseConfigPanel();
|
||||
}
|
||||
|
||||
void ApplyConfigToBlock()
|
||||
{
|
||||
if (_editingBlock == null) return;
|
||||
var config = _configView?.Find("ConfigContent");
|
||||
if (config == null) return;
|
||||
|
||||
var block = _editingBlock;
|
||||
var directionSection = config.Find("ConfigRow2/DirectionSection") ?? config.Find("DirectionSection");
|
||||
if (block.moduleDC == null)
|
||||
{
|
||||
var angleInput = config.Find("ConfigRow2/AngleSection/AngleInput") ?? config.Find("ConfigRow2/AngleInput") ?? config.Find("AngleInput");
|
||||
if (angleInput != null && float.TryParse(angleInput.GetComponent<TMP_InputField>()?.text, out float angle))
|
||||
block.targetAngle = Mathf.Clamp(angle, 0f, 180f);
|
||||
}
|
||||
else
|
||||
{
|
||||
var dcDegreesInput = directionSection?.Find("DCDegreesInput");
|
||||
if (dcDegreesInput != null && float.TryParse(dcDegreesInput.GetComponent<TMP_InputField>()?.text, out float degrees))
|
||||
block.targetAngle = Mathf.Clamp(degrees, 1f, 3600f);
|
||||
}
|
||||
|
||||
var secondInput = config.Find("ConfigRow2/SecondInput") ?? config.Find("SecondInput");
|
||||
if (secondInput != null && int.TryParse(secondInput.GetComponent<TMP_InputField>()?.text, out int sec))
|
||||
{
|
||||
UnindexBlock(_editingBlock);
|
||||
_editingBlock.second = Mathf.Clamp(sec, 0, TimelineDurationSeconds - 1);
|
||||
IndexBlock(_editingBlock);
|
||||
UpdateBlockPosition(_editingBlock);
|
||||
}
|
||||
}
|
||||
|
||||
GameObject CreateSimpleInput(Transform parent, string initial, Action<string> onEndEdit)
|
||||
{
|
||||
var go = new GameObject("Input");
|
||||
go.transform.SetParent(parent, false);
|
||||
var img = go.AddComponent<Image>();
|
||||
img.color = new Color(0.2f, 0.2f, 0.25f, 1f);
|
||||
var input = go.AddComponent<TMP_InputField>();
|
||||
var textGO = new GameObject("Text");
|
||||
textGO.transform.SetParent(go.transform, false);
|
||||
var text = textGO.AddComponent<TextMeshProUGUI>();
|
||||
text.text = initial;
|
||||
text.fontSize = 11;
|
||||
text.color = Color.white;
|
||||
text.alignment = TextAlignmentOptions.Center;
|
||||
var textRect = textGO.GetComponent<RectTransform>();
|
||||
textRect.anchorMin = Vector2.zero;
|
||||
textRect.anchorMax = Vector2.one;
|
||||
textRect.offsetMin = new Vector2(4, 2);
|
||||
textRect.offsetMax = new Vector2(-4, -2);
|
||||
input.textComponent = text;
|
||||
input.text = initial;
|
||||
input.onEndEdit.AddListener((s) => onEndEdit?.Invoke(s));
|
||||
return go;
|
||||
}
|
||||
|
||||
bool IsInGeneratedTopology(Transform t)
|
||||
{
|
||||
while (t != null)
|
||||
{
|
||||
if (t.name == "GeneratedTopology") return true;
|
||||
t = t.parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
List<ModuleBase> GetModulesInTopology()
|
||||
{
|
||||
var list = new List<ModuleBase>();
|
||||
var root = GameObject.Find("GeneratedTopology")?.transform;
|
||||
if (root == null) return list;
|
||||
|
||||
foreach (var s in root.GetComponentsInChildren<ServoMotorModule>(true))
|
||||
list.Add(s);
|
||||
foreach (var d in root.GetComponentsInChildren<DCMotorModule>(true))
|
||||
list.Add(d);
|
||||
return list;
|
||||
}
|
||||
|
||||
void IndexBlock(MovementBlock b)
|
||||
{
|
||||
if (!_blocksBySecond.ContainsKey(b.second))
|
||||
_blocksBySecond[b.second] = new List<MovementBlock>();
|
||||
_blocksBySecond[b.second].Add(b);
|
||||
}
|
||||
|
||||
void UnindexBlock(MovementBlock b)
|
||||
{
|
||||
if (_blocksBySecond.TryGetValue(b.second, out var list))
|
||||
{
|
||||
list.Remove(b);
|
||||
if (list.Count == 0) _blocksBySecond.Remove(b.second);
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateBlockPosition(MovementBlock b)
|
||||
{
|
||||
if (b.uiRoot == null) return;
|
||||
var rect = b.uiRoot.GetComponent<RectTransform>();
|
||||
if (rect == null) return;
|
||||
float x = (float)b.second / TimelineDurationSeconds;
|
||||
float w = 1f / TimelineDurationSeconds;
|
||||
rect.anchorMin = new Vector2(x, 0);
|
||||
rect.anchorMax = new Vector2(x + w, 1);
|
||||
b.uiRoot.name = $"Block_{b.second}s_T{b.trackIndex}";
|
||||
}
|
||||
|
||||
void WireUpRunButton()
|
||||
{
|
||||
if (runButton != null)
|
||||
{
|
||||
runButton.onClick.RemoveAllListeners();
|
||||
runButton.onClick.AddListener(OnRunClicked);
|
||||
}
|
||||
}
|
||||
|
||||
void OnRunClicked()
|
||||
{
|
||||
CancelInvoke(nameof(WireUpRunButton)); // Cancel any pending delayed re-wire
|
||||
_isRunning = true;
|
||||
_runStartTime = Time.time;
|
||||
_lastExecutedSecond = -1;
|
||||
RefreshRunButton();
|
||||
}
|
||||
|
||||
void RefreshRunButton()
|
||||
{
|
||||
if (runButton != null)
|
||||
{
|
||||
runButton.interactable = true;
|
||||
var tmp = runButton.GetComponentInChildren<TextMeshProUGUI>();
|
||||
if (tmp != null) tmp.text = _isRunning ? "Running" : "Run";
|
||||
}
|
||||
}
|
||||
|
||||
void SetModuleHighlight(ModuleBase module)
|
||||
{
|
||||
ClearModuleHighlight();
|
||||
_highlightedModule = module;
|
||||
if (module == null) return;
|
||||
|
||||
foreach (var r in GetRenderersOwnedByModule(module))
|
||||
{
|
||||
if (!_originalMaterialsByRenderer.ContainsKey(r))
|
||||
_originalMaterialsByRenderer[r] = r.sharedMaterials;
|
||||
var mats = r.materials;
|
||||
for (int i = 0; i < mats.Length; i++)
|
||||
{
|
||||
if (mats[i].HasProperty("_BaseColor")) mats[i].SetColor("_BaseColor", HighlightBlue);
|
||||
if (mats[i].HasProperty("_Color")) mats[i].SetColor("_Color", HighlightBlue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ClearModuleHighlight()
|
||||
{
|
||||
if (_highlightedModule == null) return;
|
||||
foreach (var kv in _originalMaterialsByRenderer)
|
||||
{
|
||||
if (kv.Key != null && kv.Value != null)
|
||||
kv.Key.sharedMaterials = kv.Value;
|
||||
}
|
||||
_originalMaterialsByRenderer.Clear();
|
||||
_highlightedModule = null;
|
||||
}
|
||||
|
||||
static IEnumerable<Renderer> GetRenderersOwnedByModule(ModuleBase rootModule)
|
||||
{
|
||||
if (rootModule == null) yield break;
|
||||
var root = rootModule.transform;
|
||||
var stack = new Stack<Transform>();
|
||||
stack.Push(root);
|
||||
while (stack.Count > 0)
|
||||
{
|
||||
var t = stack.Pop();
|
||||
if (t != root)
|
||||
{
|
||||
var other = t.GetComponent<ModuleBase>();
|
||||
if (other != null && other != rootModule) continue;
|
||||
}
|
||||
foreach (var r in t.GetComponents<Renderer>())
|
||||
if (r != null) yield return r;
|
||||
for (int i = 0; i < t.childCount; i++)
|
||||
stack.Push(t.GetChild(i));
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class MovementBlock
|
||||
{
|
||||
public int trackIndex;
|
||||
public int second;
|
||||
public float targetAngle;
|
||||
public ServoMotorModule moduleServo;
|
||||
public DCMotorModule moduleDC;
|
||||
public int dcDirection = 1; // 1 = forward, -1 = backward
|
||||
public GameObject uiRoot;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/ProgrammedMovements/Shaders.meta
Normal file
8
Assets/ProgrammedMovements/Shaders.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d315dbe0db54646ad8f59ae6fec4719e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
23
Assets/ProgrammedMovements/TrackClickHandler.cs
Normal file
23
Assets/ProgrammedMovements/TrackClickHandler.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
|
||||
/// <summary>
|
||||
/// Attach to a track's Blocks rect. On click, invokes callback with the clicked time (0-10).
|
||||
/// </summary>
|
||||
public class TrackClickHandler : MonoBehaviour, IPointerClickHandler
|
||||
{
|
||||
public System.Action<int, float> OnTrackClicked;
|
||||
|
||||
public void OnPointerClick(PointerEventData eventData)
|
||||
{
|
||||
if (OnTrackClicked == null) return;
|
||||
var rect = GetComponent<RectTransform>();
|
||||
if (rect == null) return;
|
||||
|
||||
RectTransformUtility.ScreenPointToLocalPointInRectangle(rect, eventData.position, eventData.pressEventCamera, out var localPoint);
|
||||
float normalizedX = Mathf.InverseLerp(rect.rect.xMin, rect.rect.xMax, localPoint.x);
|
||||
float second = Mathf.Clamp(normalizedX * ProgrammedMovementsController.TimelineDurationSeconds, 0f, ProgrammedMovementsController.TimelineDurationSeconds - 0.1f);
|
||||
int trackIndex = transform.parent.GetSiblingIndex();
|
||||
OnTrackClicked.Invoke(trackIndex, second);
|
||||
}
|
||||
}
|
||||
11
Assets/ProgrammedMovements/TrackClickHandler.cs.meta
Normal file
11
Assets/ProgrammedMovements/TrackClickHandler.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user