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:
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:
|
||||
Reference in New Issue
Block a user