From f9fdbd59b46e261ef39569805be1e8e013ad66a5 Mon Sep 17 00:00:00 2001 From: wsh Date: Tue, 10 Feb 2026 09:34:30 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A4=91=EA=B0=84=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 9 + .vscode/extensions.json | 5 + .vscode/launch.json | 10 + .vscode/settings.json | 71 +++++ .../Prefabs/UI/Window/PropertyWindow.prefab | 68 ++--- .../PropertyWindow/EntityPropertyAdapter.cs | 74 +++++- .../Window/PropertyWindow/PropertyWindow.cs | 33 ++- .../PropertyWindow/View/TerminalLogEntry.cs | 37 +++ .../View/TerminalLogEntry.cs.meta | 2 + .../PropertyWindow/View/TerminalView.cs | 244 ++++++++++++++++++ .../PropertyWindow/View/TerminalView.cs.meta | 2 + Assets/Scenes/SystemScene.unity | 12 + Assets/Scripts/Camera/CameraRoute.cs | 36 +++ Assets/Scripts/Camera/OrbitalController.cs | 64 ++++- .../OctopusTwin/ShortcutConfigurator.cs | 2 +- OCTOPUS_TWIN-Demo.slnx | 25 ++ 16 files changed, 632 insertions(+), 62 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 .vscode/extensions.json create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 Assets/DownloadAssets/XRLib/Scripts/UVC/UI/Window/PropertyWindow/View/TerminalLogEntry.cs create mode 100644 Assets/DownloadAssets/XRLib/Scripts/UVC/UI/Window/PropertyWindow/View/TerminalLogEntry.cs.meta create mode 100644 Assets/DownloadAssets/XRLib/Scripts/UVC/UI/Window/PropertyWindow/View/TerminalView.cs create mode 100644 Assets/DownloadAssets/XRLib/Scripts/UVC/UI/Window/PropertyWindow/View/TerminalView.cs.meta create mode 100644 Assets/Scripts/Camera/CameraRoute.cs create mode 100644 OCTOPUS_TWIN-Demo.slnx diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..4aa9d750 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(git log:*)", + "mcp__UnityMCP__refresh_unity", + "mcp__UnityMCP__read_console" + ] + } +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..ddb6ff85 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "visualstudiotoolsforunity.vstuc" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..da60e25a --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,10 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Attach to Unity", + "type": "vstuc", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..b941ac76 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,71 @@ +{ + "files.exclude": { + "**/.DS_Store": true, + "**/.git": true, + "**/.vs": true, + "**/.gitmodules": true, + "**/.vsconfig": true, + "**/*.booproj": true, + "**/*.pidb": true, + "**/*.suo": true, + "**/*.user": true, + "**/*.userprefs": true, + "**/*.unityproj": true, + "**/*.dll": true, + "**/*.exe": true, + "**/*.pdf": true, + "**/*.mid": true, + "**/*.midi": true, + "**/*.wav": true, + "**/*.gif": true, + "**/*.ico": true, + "**/*.jpg": true, + "**/*.jpeg": true, + "**/*.png": true, + "**/*.psd": true, + "**/*.tga": true, + "**/*.tif": true, + "**/*.tiff": true, + "**/*.3ds": true, + "**/*.3DS": true, + "**/*.fbx": true, + "**/*.FBX": true, + "**/*.lxo": true, + "**/*.LXO": true, + "**/*.ma": true, + "**/*.MA": true, + "**/*.obj": true, + "**/*.OBJ": true, + "**/*.asset": true, + "**/*.cubemap": true, + "**/*.flare": true, + "**/*.mat": true, + "**/*.meta": true, + "**/*.prefab": true, + "**/*.unity": true, + "build/": true, + "Build/": true, + "Library/": true, + "library/": true, + "obj/": true, + "Obj/": true, + "Logs/": true, + "logs/": true, + "ProjectSettings/": true, + "UserSettings/": true, + "temp/": true, + "Temp/": true + }, + "files.associations": { + "*.asset": "yaml", + "*.meta": "yaml", + "*.prefab": "yaml", + "*.unity": "yaml", + }, + "explorer.fileNesting.enabled": true, + "explorer.fileNesting.patterns": { + "*.sln": "*.csproj", + "*.slnx": "*.csproj" + }, + "dotnet.defaultSolution": "OCTOPUS_TWIN-Demo.slnx" +} \ No newline at end of file diff --git a/Assets/DownloadAssets/XRLib/Resources/Prefabs/UI/Window/PropertyWindow.prefab b/Assets/DownloadAssets/XRLib/Resources/Prefabs/UI/Window/PropertyWindow.prefab index 283d5a26..82db5b39 100644 --- a/Assets/DownloadAssets/XRLib/Resources/Prefabs/UI/Window/PropertyWindow.prefab +++ b/Assets/DownloadAssets/XRLib/Resources/Prefabs/UI/Window/PropertyWindow.prefab @@ -34,7 +34,7 @@ RectTransform: m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 1} m_AnchorMax: {x: 1, y: 1} - m_AnchoredPosition: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: -16.100006} m_SizeDelta: {x: 0, y: 1} m_Pivot: {x: 0.5, y: 0.5} --- !u!222 &3812911501507659818 @@ -87,14 +87,13 @@ GameObject: - component: {fileID: 6734100349773792131} - component: {fileID: 7955749765911055004} - component: {fileID: 4623412371413303123} - - component: {fileID: 8862115974344976431} m_Layer: 0 m_Name: TerminalView m_TagString: Untagged m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 - m_IsActive: 0 + m_IsActive: 1 --- !u!224 &9100016846584606779 RectTransform: m_ObjectHideFlags: 0 @@ -107,6 +106,7 @@ RectTransform: m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: + - {fileID: 8866481709266374613} - {fileID: 5882288525513767156} - {fileID: 5201271029291240114} m_Father: {fileID: 8065352563668446013} @@ -166,26 +166,9 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: e2bd83bb433a1e043b39643c011b6cc0, type: 3} m_Name: m_EditorClassIdentifier: Assembly-CSharp::UVC.UI.Window.PropertyWindow.TerminalView ---- !u!114 &8862115974344976431 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 720111926108502729} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3} - m_Name: - m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement - m_IgnoreLayout: 0 - m_MinWidth: -1 - m_MinHeight: -1 - m_PreferredWidth: -1 - m_PreferredHeight: -1 - m_FlexibleWidth: -1 - m_FlexibleHeight: -1 - m_LayoutPriority: 1 + _scrollRect: {fileID: 0} + _content: {fileID: 0} + _font: {fileID: 0} --- !u!1 &954952398855517667 GameObject: m_ObjectHideFlags: 0 @@ -727,8 +710,8 @@ MonoBehaviour: m_TargetGraphic: {fileID: 8654063950112981296} m_HandleRect: {fileID: 0} m_Direction: 0 - m_Value: 1 - m_Size: 1 + m_Value: 0 + m_Size: 0.99995804 m_NumberOfSteps: 0 m_OnValueChanged: m_PersistentCalls: @@ -1131,17 +1114,17 @@ RectTransform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 5615275534113093535} - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] - m_Father: {fileID: 1205322734457541541} + m_Father: {fileID: 9100016846584606779} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 1} m_AnchorMax: {x: 0, y: 1} - m_AnchoredPosition: {x: 100, y: -25} - m_SizeDelta: {x: 200, y: 50} + m_AnchoredPosition: {x: 100.2, y: -7.9052124} + m_SizeDelta: {x: 200, y: 15.4105} m_Pivot: {x: 0.5, y: 0.5} --- !u!222 &8512899717493741604 CanvasRenderer: @@ -1171,7 +1154,7 @@ MonoBehaviour: m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_text: Log Texts + m_text: Terminal Log m_isRightToLeft: 0 m_fontAsset: {fileID: 11400000, guid: 2ec62c2b5b163f64e92dc4988250c9c8, type: 2} m_sharedMaterial: {fileID: 1892695685711531568, guid: 2ec62c2b5b163f64e92dc4988250c9c8, type: 2} @@ -1361,14 +1344,13 @@ RectTransform: m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 - m_Children: - - {fileID: 8866481709266374613} + m_Children: [] m_Father: {fileID: 8946140259888033016} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} m_AnchorMax: {x: 1, y: 1} - m_AnchoredPosition: {x: -0.00022888184, y: -0.0010070801} - m_SizeDelta: {x: -12.372238, y: -17.451527} + m_AnchoredPosition: {x: 0, y: 0.0001373291} + m_SizeDelta: {x: 0, y: 0} m_Pivot: {x: 0.5, y: 0.5} --- !u!114 &4790256706883938101 MonoBehaviour: @@ -1389,7 +1371,7 @@ MonoBehaviour: m_Bottom: 0 m_ChildAlignment: 0 m_Spacing: 0 - m_ChildForceExpandWidth: 1 + m_ChildForceExpandWidth: 0 m_ChildForceExpandHeight: 1 m_ChildControlWidth: 0 m_ChildControlHeight: 0 @@ -1403,13 +1385,13 @@ MonoBehaviour: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 6982330689999531319} - m_Enabled: 0 + m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3} m_Name: m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.ContentSizeFitter - m_HorizontalFit: 0 - m_VerticalFit: 0 + m_HorizontalFit: 2 + m_VerticalFit: 2 --- !u!1 &7857615039342438008 GameObject: m_ObjectHideFlags: 0 @@ -1595,9 +1577,9 @@ RectTransform: m_Father: {fileID: 5882288525513767156} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 1, y: 1} + m_AnchorMax: {x: 0, y: 0} m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: -2, y: 0} + m_SizeDelta: {x: 0, y: 0} m_Pivot: {x: 0.5, y: 0.5} --- !u!222 &319972252397042891 CanvasRenderer: @@ -1724,8 +1706,8 @@ RectTransform: m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} m_AnchorMax: {x: 1, y: 1} - m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: -8.299988} + m_SizeDelta: {x: 0, y: -16.6} m_Pivot: {x: 0.5, y: 0.5} --- !u!114 &4112323457797356702 MonoBehaviour: @@ -1740,7 +1722,7 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.ScrollRect m_Content: {fileID: 1205322734457541541} - m_Horizontal: 1 + m_Horizontal: 0 m_Vertical: 1 m_MovementType: 1 m_Elasticity: 0.1 diff --git a/Assets/DownloadAssets/XRLib/Scripts/UVC/UI/Window/PropertyWindow/EntityPropertyAdapter.cs b/Assets/DownloadAssets/XRLib/Scripts/UVC/UI/Window/PropertyWindow/EntityPropertyAdapter.cs index f2252ca1..982a53ce 100644 --- a/Assets/DownloadAssets/XRLib/Scripts/UVC/UI/Window/PropertyWindow/EntityPropertyAdapter.cs +++ b/Assets/DownloadAssets/XRLib/Scripts/UVC/UI/Window/PropertyWindow/EntityPropertyAdapter.cs @@ -1,3 +1,5 @@ +#nullable enable + using System; using System.Collections.Generic; using UnityEngine; @@ -19,7 +21,7 @@ namespace UVC.UI.Window.PropertyWindow /// /// 변환할 Entity /// 탭 데이터 리스트 - public static List GetTabsForEntity(UVC.Entity.Entity entity) + public static List GetTabsForEntity(UVC.Entity.Entity entity, TerminalView? terminalView = null) { if (entity == null) { @@ -30,7 +32,7 @@ namespace UVC.UI.Window.PropertyWindow // Entity 타입별로 분기 if (entity is StageObjectManager.StageObject stageObject) { - return GetTabsForStageObject(stageObject); + return GetTabsForStageObject(stageObject, terminalView); } // 다른 Entity 타입 추가 가능 @@ -45,7 +47,7 @@ namespace UVC.UI.Window.PropertyWindow /// StageObject를 PropertyWindow 탭 데이터로 변환합니다. /// DisplayEquipmentProperties의 로직을 그대로 사용합니다. /// - private static List GetTabsForStageObject(StageObjectManager.StageObject stageObject) + private static List GetTabsForStageObject(StageObjectManager.StageObject stageObject, TerminalView? terminalView = null) { var tabs = new List(); @@ -56,9 +58,9 @@ namespace UVC.UI.Window.PropertyWindow tabs.Add(propertiesTab); // ======================================== - // NETWORK 탭 생성 (나중에 구현) + // NETWORK 탭 생성 // ======================================== - var networkTab = CreateNetworkTab(stageObject); + var networkTab = CreateNetworkTab(stageObject, terminalView); if (networkTab != null) tabs.Add(networkTab); @@ -194,7 +196,7 @@ namespace UVC.UI.Window.PropertyWindow /// StageObject의 NETWORK 탭 데이터를 생성합니다. /// (나중에 구현) /// -private static EntityTabData? CreateNetworkTab(StageObjectManager.StageObject stageObject) +private static EntityTabData? CreateNetworkTab(StageObjectManager.StageObject stageObject, TerminalView? terminalView = null) { var properties = new List(); var propertyDict = new Dictionary(); // Processor 초기화용 @@ -274,6 +276,66 @@ private static EntityTabData? CreateNetworkTab(StageObjectManager.StageObject st processor.onComplete += () => { autoButton.ButtonText = "Run"; }; processor.onCancel += () => { autoButton.ButtonText = "Run"; }; + // TerminalView 바인딩 (터미널 스타일 로그) + if (terminalView != null) + { + // propertyId → 표시명 매핑 + var stepNames = new Dictionary + { + ["get_entity_info_status"] = "Get Entity Info", + ["extract_network_info_status"] = "Extract Network Info", + ["connecting_status"] = "Connecting", + ["server_status"] = "Server", + ["port_status"] = "Port", + ["protocol_status"] = "Protocol", + ["server_status_check"] = "Status Check", + ["speed_status"] = "Speed" + }; + + var tv = terminalView; // 클로저 캡처용 로컬 변수 + + processor.onStart += () => + { + tv.AddLog("> Starting Twin Agent Auto Process...", Color.cyan); + }; + + processor.onMessage += (propId, value) => + { + // ProcessorId 자체의 상태 메시지는 무시 (onStart/onComplete에서 처리) + if (propId == processor.ProcessorId) return; + + string stepName = stepNames.TryGetValue(propId, out var name) ? name : propId; + + if (value == "Done") + { + tv.AddLog($"> [{stepName}] Done", Color.green); + } + else if (value == "Canceled") + { + tv.AddLog($"> [{stepName}] Canceled", new Color(1f, 0.647f, 0f)); + } + else + { + tv.AddLog($"> [{stepName}] {value}"); + } + }; + + processor.onComplete += () => + { + tv.AddLog("> Process completed successfully", Color.green); + }; + + processor.onCancel += () => + { + tv.AddLog("> Process canceled", new Color(1f, 0.647f, 0f)); + }; + + processor.onReset += () => + { + tv.Clear(); + }; + } + // 저장된 상태 복원 if (processor.HasSavedState) { diff --git a/Assets/DownloadAssets/XRLib/Scripts/UVC/UI/Window/PropertyWindow/PropertyWindow.cs b/Assets/DownloadAssets/XRLib/Scripts/UVC/UI/Window/PropertyWindow/PropertyWindow.cs index 3a400b5a..95925ccb 100644 --- a/Assets/DownloadAssets/XRLib/Scripts/UVC/UI/Window/PropertyWindow/PropertyWindow.cs +++ b/Assets/DownloadAssets/XRLib/Scripts/UVC/UI/Window/PropertyWindow/PropertyWindow.cs @@ -33,6 +33,9 @@ private void Awake() { Debug.LogWarning("[PropertyWindow] EntityName GameObject를 찾을 수 없습니다."); } + + // TerminalView 자동 검색 (비활성 자식 포함) + _terminalView = GetComponentInChildren(true); } [SerializeField] @@ -41,6 +44,11 @@ private void Awake() [SerializeField] private PropertyTabView _tabView; + /// + /// NETWORK 탭 선택 시 표시되는 터미널 뷰 + /// + private TerminalView? _terminalView; + /// /// Entity 이름을 표시하는 텍스트 컴포넌트 /// @@ -277,7 +285,7 @@ private void Awake() } /// - /// 그룹을 제거합니다. + /// 그룹을 제거합니다.2 /// /// 제거할 그룹의 ID public void RemoveGroup(string groupId) @@ -547,6 +555,9 @@ private void Awake() // 탭의 속성 로드 LoadProperties(tab.Properties); + // TerminalView 토글 (network 탭에서만 활성화) + UpdateTerminalViewVisibility(tabId); + // 이벤트 발생 TabChanged?.Invoke(this, new TabChangedEventArgs(oldTabId, tabId)); @@ -712,8 +723,9 @@ public void Open(UVC.Entity.Entity entity) // 3. 새 엔티티 설정 _currentEntity = entity; - // 4. Adapter를 통해 Entity → 탭 데이터 변환 - var tabsData = EntityPropertyAdapter.GetTabsForEntity(entity); + // 4. Adapter를 통해 Entity → 탭 데이터 변환 (TerminalView 전달) + if (_terminalView != null) _terminalView.Clear(); + var tabsData = EntityPropertyAdapter.GetTabsForEntity(entity, _terminalView); // 5. 탭 생성 foreach (var tabData in tabsData) @@ -728,9 +740,19 @@ public void Open(UVC.Entity.Entity entity) } /// - /// NETWORK 탭의 Server Type 변경 시 동적 가시성 제어를 설정합니다. + /// 탭 ID에 따라 TerminalView 가시성을 업데이트합니다. + /// network 탭에서는 TerminalView를 표시하고, 다른 탭에서는 숨깁니다. /// + private void UpdateTerminalViewVisibility(string tabId) + { + if (_terminalView == null) return; + bool showTerminal = tabId == "network"; + if (showTerminal) + _terminalView.Show(); + else + _terminalView.Hide(); + } #endregion @@ -803,6 +825,9 @@ public void Clear() // 현재 표시 중인 엔티티 초기화 (윈도우 닫혀도 프로세스는 계속 실행) _currentEntity = null; + // TerminalView 숨김 + _terminalView?.Hide(); + EntriesCleared?.Invoke(this, EventArgs.Empty); // View 갱신하여 UI에서도 항목 제거 diff --git a/Assets/DownloadAssets/XRLib/Scripts/UVC/UI/Window/PropertyWindow/View/TerminalLogEntry.cs b/Assets/DownloadAssets/XRLib/Scripts/UVC/UI/Window/PropertyWindow/View/TerminalLogEntry.cs new file mode 100644 index 00000000..1a0b6969 --- /dev/null +++ b/Assets/DownloadAssets/XRLib/Scripts/UVC/UI/Window/PropertyWindow/View/TerminalLogEntry.cs @@ -0,0 +1,37 @@ +#nullable enable + +using TMPro; +using UnityEngine; +using UnityEngine.UI; + +namespace UVC.UI.Window.PropertyWindow +{ + /// + /// 터미널 뷰의 개별 로그 라인 컴포넌트입니다. + /// 각 로그 메시지를 표시하며, 나중에 아이콘/배경색 등 커스터마이징이 가능합니다. + /// + public class TerminalLogEntry : MonoBehaviour + { + private TMP_Text? _text; + + public string Text => _text != null ? _text.text : string.Empty; + + /// + /// 로그 라인을 초기화합니다. TerminalView에서 동적 생성 시 호출됩니다. + /// + public void Initialize(TMP_Text text) + { + _text = text; + } + + /// + /// 로그 텍스트와 색상을 설정합니다. + /// + public void SetText(string message, Color color) + { + if (_text == null) return; + _text.text = message; + _text.color = color; + } + } +} diff --git a/Assets/DownloadAssets/XRLib/Scripts/UVC/UI/Window/PropertyWindow/View/TerminalLogEntry.cs.meta b/Assets/DownloadAssets/XRLib/Scripts/UVC/UI/Window/PropertyWindow/View/TerminalLogEntry.cs.meta new file mode 100644 index 00000000..98aed720 --- /dev/null +++ b/Assets/DownloadAssets/XRLib/Scripts/UVC/UI/Window/PropertyWindow/View/TerminalLogEntry.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4153316854c2b5b498a614e54dc8ce13 \ No newline at end of file diff --git a/Assets/DownloadAssets/XRLib/Scripts/UVC/UI/Window/PropertyWindow/View/TerminalView.cs b/Assets/DownloadAssets/XRLib/Scripts/UVC/UI/Window/PropertyWindow/View/TerminalView.cs new file mode 100644 index 00000000..926557c8 --- /dev/null +++ b/Assets/DownloadAssets/XRLib/Scripts/UVC/UI/Window/PropertyWindow/View/TerminalView.cs @@ -0,0 +1,244 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using TMPro; +using UnityEngine; +using UnityEngine.UI; + +namespace UVC.UI.Window.PropertyWindow +{ + /// + /// PropertyWindow의 터미널 뷰입니다. + /// NETWORK 탭 선택 시 활성화되며, Processor 진행 상황을 터미널 스타일 로그로 표시합니다. + /// ScrollView + 개별 항목 방식으로, 각 로그 라인이 별도 GameObject입니다. + /// + public class TerminalView : MonoBehaviour + { + [SerializeField] private ScrollRect? _scrollRect; + [SerializeField] private Transform? _content; + [SerializeField] private TMP_FontAsset? _font; + + private const int MaxEntries = 200; + private const float FontSize = 11f; + + private readonly List _entries = new List(); + + private void Awake() + { + EnsureUIHierarchy(); + } + + /// + /// ScrollRect와 Content 영역이 없으면 프로그래밍으로 생성합니다. + /// + private void EnsureUIHierarchy() + { + if (_scrollRect == null) + _scrollRect = GetComponentInChildren(true); + + if (_scrollRect != null && _content == null) + _content = _scrollRect.content; + + // ScrollRect가 없으면 자동 생성 + if (_scrollRect == null) + { + SetupScrollView(); + } + else + { + // 기존 ScrollRect가 있어도 Content 설정 보정 + EnsureContentSettings(); + } + } + + /// + /// 기존 ScrollRect의 Content에 VerticalLayoutGroup, ContentSizeFitter가 없으면 추가하고 + /// 앵커/피봇을 상단 좌측으로 강제 설정합니다. + /// + private void EnsureContentSettings() + { + if (_content == null) return; + + var contentRt = _content as RectTransform; + if (contentRt == null) contentRt = _content.GetComponent(); + if (contentRt == null) return; + + // Content 앵커/피봇을 상단으로 고정 + contentRt.anchorMin = new Vector2(0, 1); + contentRt.anchorMax = new Vector2(1, 1); + contentRt.pivot = new Vector2(0, 1); + + // VerticalLayoutGroup 보정 + var vlg = contentRt.GetComponent(); + if (vlg == null) + vlg = contentRt.gameObject.AddComponent(); + vlg.childAlignment = TextAnchor.UpperLeft; + vlg.childControlHeight = true; + vlg.childControlWidth = true; + vlg.childForceExpandHeight = false; + vlg.childForceExpandWidth = true; + vlg.spacing = 0f; + vlg.padding = new RectOffset(6, 6, 2, 2); + + // ContentSizeFitter 보정 + var csf = contentRt.GetComponent(); + if (csf == null) + csf = contentRt.gameObject.AddComponent(); + csf.verticalFit = ContentSizeFitter.FitMode.PreferredSize; + csf.horizontalFit = ContentSizeFitter.FitMode.Unconstrained; + } + + private void SetupScrollView() + { + var rt = GetComponent(); + if (rt == null) + rt = gameObject.AddComponent(); + + // Viewport + var viewportGo = new GameObject("Viewport", typeof(RectTransform), typeof(Image), typeof(Mask)); + viewportGo.transform.SetParent(transform, false); + var viewportRt = viewportGo.GetComponent(); + viewportRt.anchorMin = Vector2.zero; + viewportRt.anchorMax = Vector2.one; + viewportRt.sizeDelta = Vector2.zero; + viewportRt.offsetMin = Vector2.zero; + viewportRt.offsetMax = Vector2.zero; + var viewportImage = viewportGo.GetComponent(); + viewportImage.color = new Color(0, 0, 0, 0.01f); // 거의 투명하지만 Mask 동작용 + var mask = viewportGo.GetComponent(); + mask.showMaskGraphic = false; + + // Content + var contentGo = new GameObject("Content", typeof(RectTransform), typeof(VerticalLayoutGroup), typeof(ContentSizeFitter)); + contentGo.transform.SetParent(viewportGo.transform, false); + var contentRt = contentGo.GetComponent(); + contentRt.anchorMin = new Vector2(0, 1); + contentRt.anchorMax = new Vector2(1, 1); + contentRt.pivot = new Vector2(0, 1); + contentRt.sizeDelta = new Vector2(0, 0); + + var vlg = contentGo.GetComponent(); + vlg.childAlignment = TextAnchor.UpperLeft; + vlg.childControlHeight = true; + vlg.childControlWidth = true; + vlg.childForceExpandHeight = false; + vlg.childForceExpandWidth = true; + vlg.spacing = 0f; + vlg.padding = new RectOffset(6, 6, 2, 2); + + var csf = contentGo.GetComponent(); + csf.verticalFit = ContentSizeFitter.FitMode.PreferredSize; + csf.horizontalFit = ContentSizeFitter.FitMode.Unconstrained; + + _content = contentRt; + + // ScrollRect + _scrollRect = gameObject.AddComponent(); + _scrollRect.viewport = viewportRt; + _scrollRect.content = contentRt; + _scrollRect.horizontal = false; + _scrollRect.vertical = true; + _scrollRect.movementType = ScrollRect.MovementType.Clamped; + _scrollRect.scrollSensitivity = 20f; + } + + /// + /// 터미널에 로그 라인을 추가합니다. 타임스탬프가 자동으로 붙습니다. + /// + /// 로그 메시지 (타임스탬프 제외) + /// 텍스트 색상 (null이면 White) + public void AddLog(string message, Color? color = null) + { + if (_content == null) return; + + string timestamp = DateTime.Now.ToString("HH:mm:ss"); + string fullMessage = $"[{timestamp}] {message}"; + + // 최대 라인 수 초과 시 가장 오래된 항목 제거 + while (_entries.Count >= MaxEntries) + { + RemoveOldestEntry(); + } + + var entry = CreateLogEntry(fullMessage, color ?? Color.white); + _entries.Add(entry); + + // 다음 프레임에 스크롤 (레이아웃 갱신 후) + ScrollToBottom(); + } + + /// + /// 모든 로그를 제거합니다. + /// + public void Clear() + { + foreach (var entry in _entries) + { + if (entry != null && entry.gameObject != null) + Destroy(entry.gameObject); + } + _entries.Clear(); + } + + public void Show() => gameObject.SetActive(true); + public void Hide() => gameObject.SetActive(false); + public bool IsVisible => gameObject.activeSelf; + + private TerminalLogEntry CreateLogEntry(string message, Color color) + { + var go = new GameObject("LogEntry", typeof(RectTransform), typeof(TerminalLogEntry), typeof(ContentSizeFitter)); + go.transform.SetParent(_content, false); + + // ContentSizeFitter로 텍스트 높이에 맞게 자동 조절 + var csf = go.GetComponent(); + csf.verticalFit = ContentSizeFitter.FitMode.PreferredSize; + csf.horizontalFit = ContentSizeFitter.FitMode.Unconstrained; + + // TextMeshProUGUI + var tmp = go.AddComponent(); + tmp.fontSize = FontSize; + tmp.alignment = TextAlignmentOptions.TopLeft; + tmp.overflowMode = TextOverflowModes.Overflow; + tmp.enableWordWrapping = true; + tmp.richText = true; + tmp.margin = new Vector4(0, 1, 0, 1); // 위아래 1px 여백 + + if (_font != null) + tmp.font = _font; + + // TerminalLogEntry 초기화 + var entry = go.GetComponent(); + entry.Initialize(tmp); + entry.SetText(message, color); + + return entry; + } + + private void RemoveOldestEntry() + { + if (_entries.Count == 0) return; + var oldest = _entries[0]; + _entries.RemoveAt(0); + if (oldest != null && oldest.gameObject != null) + Destroy(oldest.gameObject); + } + + private async void ScrollToBottom() + { + if (_scrollRect == null) return; + + // 레이아웃이 갱신될 때까지 1프레임 대기 + await Cysharp.Threading.Tasks.UniTask.Yield(); + + if (_scrollRect == null || _scrollRect.content == null || _scrollRect.viewport == null) + return; + + // 콘텐츠가 뷰포트보다 클 때만 하단 스크롤 (적을 때는 상단 유지) + float contentHeight = _scrollRect.content.rect.height; + float viewportHeight = _scrollRect.viewport.rect.height; + if (contentHeight > viewportHeight) + _scrollRect.verticalNormalizedPosition = 0f; + } + } +} diff --git a/Assets/DownloadAssets/XRLib/Scripts/UVC/UI/Window/PropertyWindow/View/TerminalView.cs.meta b/Assets/DownloadAssets/XRLib/Scripts/UVC/UI/Window/PropertyWindow/View/TerminalView.cs.meta new file mode 100644 index 00000000..516ef49c --- /dev/null +++ b/Assets/DownloadAssets/XRLib/Scripts/UVC/UI/Window/PropertyWindow/View/TerminalView.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e2bd83bb433a1e043b39643c011b6cc0 \ No newline at end of file diff --git a/Assets/Scenes/SystemScene.unity b/Assets/Scenes/SystemScene.unity index 6e1d252c..a21e74b7 100644 --- a/Assets/Scenes/SystemScene.unity +++ b/Assets/Scenes/SystemScene.unity @@ -346,10 +346,22 @@ PrefabInstance: propertyPath: m_AnchorMax.y value: 0 objectReference: {fileID: 0} + - target: {fileID: 1205322734457541541, guid: 4b98d7ee8b805ff42be384e91f3bf8a4, type: 3} + propertyPath: m_SizeDelta.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1205322734457541541, guid: 4b98d7ee8b805ff42be384e91f3bf8a4, type: 3} + propertyPath: m_SizeDelta.y + value: 0 + objectReference: {fileID: 0} - target: {fileID: 1602519165350808158, guid: 4b98d7ee8b805ff42be384e91f3bf8a4, type: 3} propertyPath: m_Name value: PropertyWindow objectReference: {fileID: 0} + - target: {fileID: 4790256706883938101, guid: 4b98d7ee8b805ff42be384e91f3bf8a4, type: 3} + propertyPath: m_ChildControlWidth + value: 1 + objectReference: {fileID: 0} - target: {fileID: 7125265927081152491, guid: 4b98d7ee8b805ff42be384e91f3bf8a4, type: 3} propertyPath: m_SizeDelta.y value: 0 diff --git a/Assets/Scripts/Camera/CameraRoute.cs b/Assets/Scripts/Camera/CameraRoute.cs new file mode 100644 index 00000000..17e7ea3b --- /dev/null +++ b/Assets/Scripts/Camera/CameraRoute.cs @@ -0,0 +1,36 @@ +using UnityEngine; + +namespace OCTOPUS_TWIN +{ + /// + /// 카메라 경유점. CameraRoute의 자식 오브젝트에 추가하여 궤도 파라미터를 설정. + /// 없으면 자식 Transform의 position만 사용하고 궤도 파라미터는 현재 카메라 상태 유지. + /// + public class CameraRoutePoint : MonoBehaviour + { + public float elevation = 45f; + public float azimuth = 0f; + public float distance = 30f; + [Tooltip("이 지점까지의 이동 시간(초)")] + public float duration = 0.5f; + [Tooltip("이 지점에서의 대기 시간(초)")] + public float waitTime = 0f; + } + + /// + /// 카메라 이동 경로. 자식 Transform 순서대로 카메라가 보간 이동. + /// 각 자식에 CameraRoutePoint를 추가하면 궤도 파라미터(elevation, azimuth, distance)도 제어 가능. + /// + public class CameraRoute : MonoBehaviour + { + public bool loop; + + public int PointCount => transform.childCount; + + public (Vector3 position, CameraRoutePoint point) GetWaypoint(int index) + { + var child = transform.GetChild(index); + return (child.position, child.GetComponent()); + } + } +} diff --git a/Assets/Scripts/Camera/OrbitalController.cs b/Assets/Scripts/Camera/OrbitalController.cs index b8efcda6..8f662ee3 100644 --- a/Assets/Scripts/Camera/OrbitalController.cs +++ b/Assets/Scripts/Camera/OrbitalController.cs @@ -62,6 +62,8 @@ namespace OCTOPUS_TWIN private bool isZoomOperation; public bool Enable; + public bool IsRouteActive => routeSequence != null && routeSequence.IsActive(); + private Sequence routeSequence; public bool IsClickUI { @@ -148,11 +150,11 @@ namespace OCTOPUS_TWIN private void LateUpdate() { - //UI ī޶ + //UI ������ ī�޶� ������ ���� if (IsClickUI || IsOnTheUI) return; - //̺귯 Ʈ ġ ī޶ + //���̺귯������ ������Ʈ ��ġ�� ī�޶� ������ ���� if (!Enable) { return; @@ -298,7 +300,7 @@ namespace OCTOPUS_TWIN { camera.orthographic = false; - currentElevation = perspectiveState.elevation; //90 + currentElevation = perspectiveState.elevation; //90 �� ���� currentDistance = perspectiveState.distance; currentAzimuth = perspectiveState.azimuth; @@ -339,7 +341,7 @@ namespace OCTOPUS_TWIN break; case ViewMode.TopView: - orthoState.elevation = 90f; // 90 or + orthoState.elevation = 90f; // 90 or ������ orthoState.distance = 60f; orthoState.azimuth = 0f; orthoState.pivotPosition = Vector3.zero; @@ -352,18 +354,64 @@ namespace OCTOPUS_TWIN throw new NotImplementedException(); } - public void AnimateToState(Vector3 pivotPosition, Vector3 eulerAngles, float distance, float duration = 0.4f) + public void SetRoute(CameraRoute route) { - // ִϸ̼ ߿ 콺 Է Ȱȭ + StopRoute(); Enable = false; - // DoTween Ͽ Ʈѷ ε巴 + routeSequence = DOTween.Sequence(); + + for (int i = 0; i < route.PointCount; i++) + { + var (position, point) = route.GetWaypoint(i); + + float elev = point != null ? point.elevation : currentElevation; + float azi = point != null ? point.azimuth : currentAzimuth; + float dist = point != null ? point.distance : currentDistance; + float dur = point != null ? point.duration : 0.5f; + float wait = point != null ? point.waitTime : 0f; + + routeSequence.Append( + DOTween.To(() => nextPosition, x => nextPosition = x, position, dur)); + routeSequence.Join( + DOTween.To(() => currentElevation, x => currentElevation = x, elev, dur)); + routeSequence.Join( + DOTween.To(() => currentAzimuth, x => currentAzimuth = x, azi, dur)); + routeSequence.Join( + DOTween.To(() => currentDistance, x => currentDistance = x, dist, dur)); + + if (wait > 0f) + routeSequence.AppendInterval(wait); + } + + if (route.loop) + routeSequence.SetLoops(-1, LoopType.Restart); + + routeSequence.OnUpdate(LastPositioning); + routeSequence.OnKill(() => Enable = true); + } + + public void StopRoute() + { + if (routeSequence != null && routeSequence.IsActive()) + { + routeSequence.Kill(); + routeSequence = null; + } + } + + public void AnimateToState(Vector3 pivotPosition, Vector3 eulerAngles, float distance, float duration = 0.4f) + { + // �ִϸ��̼� �߿��� ���콺 �Է� ��Ȱ��ȭ + Enable = false; + + // DoTween�� ����Ͽ� ��Ʈ�ѷ��� ���� ������ �ε巴�� ���� DOTween.To(() => nextPosition, x => nextPosition = x, pivotPosition, duration); DOTween.To(() => currentElevation, x => currentElevation = x, eulerAngles.x, duration); DOTween.To(() => currentAzimuth, x => currentAzimuth = x, eulerAngles.y, duration); DOTween.To(() => currentDistance, x => currentDistance = x, distance, duration) .OnComplete(() => { - // ִϸ̼ 콺 Է ٽ Ȱȭ + // �ִϸ��̼��� ������ ���콺 �Է��� �ٽ� Ȱ��ȭ Enable = true; }); } diff --git a/Assets/Scripts/OctopusTwin/ShortcutConfigurator.cs b/Assets/Scripts/OctopusTwin/ShortcutConfigurator.cs index 10323387..50317ae0 100644 --- a/Assets/Scripts/OctopusTwin/ShortcutConfigurator.cs +++ b/Assets/Scripts/OctopusTwin/ShortcutConfigurator.cs @@ -82,7 +82,7 @@ public class ShortcutConfigurator : MonoBehaviour // ���� ����Ű - SelectionManager�� ���� ����� ���� shortcutManager.RegisterToolShortcut("select", () => { - //if (selectionManager != null) selectionManager.SetActiveTool(TransformToolType.Select); + if (selectionManager != null) selectionManager.Gizmo.SetActiveTool(TransformToolType.Select); }); shortcutManager.RegisterToolShortcut("move", () => diff --git a/OCTOPUS_TWIN-Demo.slnx b/OCTOPUS_TWIN-Demo.slnx new file mode 100644 index 00000000..bd505b74 --- /dev/null +++ b/OCTOPUS_TWIN-Demo.slnx @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + +