Files
OCTOPUS_TWIN-Demo/Assets/DownloadAssets/XRLib/Scripts/UVC/Manager/SelectionInputHandler.cs
wsh 6a57b8ea9b feat: PropertyWindow 상호작용 및 Twin Agent 프로세스 관리 개선
## PropertyWindow 상호작용 개선
- 바닥 클릭 시 PropertyWindow 내용 유지 (선택 해제만)
- 같은 엔티티 재선택 시 현재 탭 유지
- 다른 엔티티 선택 시 이벤트 구독 명시적 해제로 메모리 누수 방지
- 중복 탭 방지 로직 추가 (Clear()에서 탭도 함께 제거)

## Twin Agent Auto 프로세스 관리
- 프로세스 상태 관리 (IsRunning, IsCompleted, IsCanceled)
- 프로세스 진행 상태 추적 및 복원 기능 (_stepStates)
- 백그라운드 실행 지원 (다른 엔티티 선택 시에도 계속 실행)
- Cancel 시 즉시 중단 (CancellationToken 전달)
- Cancel 시 현재 항목 "Canceled" 표시 및 주황색 강조
- Cancel 후 Run 시 자동 초기화

## UI 개선
- Run/Cancel 버튼 동적 토글 (ButtonProperty.ButtonText 이벤트)
- 프로세스 상태에 따른 UI 복원 (RestoreUIState)
- 이벤트 핸들러 명시적 관리로 안정성 향상

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
2026-02-09 09:24:55 +09:00

243 lines
8.3 KiB
C#

#nullable enable
using OCTOPUS_TWIN.Command;
using RTGLite;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using UVC.Command;
using UVC.Core;
using UVC.Object3d.Manager;
using UVC.UI.Window.PropertyWindow;
namespace UVC.Studio.Manager
{
/// <summary>
/// 화면에서 마우스 클릭을 감지하여 객체 선택을 처리하는 컴포넌트
/// RTScene.Raycast를 사용하여 Collider 없이도 MeshRenderer 기반으로 객체 감지
/// </summary>
public class SelectionInputHandler : MonoBehaviour
{
private SelectionManager? _selectionManager;
private StageObjectManager? _stageObjectManager;
/// <summary>
/// 더블클릭 감지 시간 간격 (초)
/// </summary>
private const float DoubleClickThreshold = 0.5f;
/// <summary>
/// 마지막 클릭 시간
/// </summary>
private float _lastClickTime = 0f;
/// <summary>
/// 마지막 클릭된 StageObject (더블클릭 확인용)
/// </summary>
private StageObjectManager.StageObject? _lastClickedStageObject;
protected async void Start()
{
// InjectorSceneContext 초기화 대기
//await InjectorSceneContext.Instance.WaitForInitializationAsync();
_selectionManager = InjectorAppContext.Instance.Get<SelectionManager>();
_selectionManager.InitializeGizmos();
_stageObjectManager = InjectorAppContext.Instance.Get<StageObjectManager>();
if (_selectionManager == null)
{
Debug.LogWarning("[SelectionInputHandler] SelectionManager not found.");
}
if (_stageObjectManager == null)
{
Debug.LogWarning("[SelectionInputHandler] StageObjectManager not found.");
}
}
protected void Update()
{
// 마우스 왼쪽 버튼 클릭 감지
// 기즈모 UI 위에서는 선택하지 않음
if (Input.GetMouseButtonUp(0) && (RTGizmos.get == null || !RTGizmos.get.IsGizmoGUIHovered()))
{
HandleClick();
}
if (IsDraggingGizmo())
{
_selectionManager?.TickPropertyWindow();
}
}
bool IsDraggingGizmo()
{
return RTGizmos.get != null && RTGizmos.get.draggedGizmo != null;
}
/// <summary>
/// 마우스 클릭 처리
/// </summary>
private void HandleClick()
{
// UI 위에서 클릭한 경우 무시
if (IsPointerOverInteractableUI())
{
return;
}
if (_selectionManager == null || _stageObjectManager == null)
{
return;
}
// RTScene을 사용한 레이캠스트 (Collider 없이 MeshRenderer로 감지)
//ToDo -> pickobject 레이어 설정 영민 프로 작업하션
var pickedObject = PickObject();
Debug.Log($"[SelectionInputHandler] Picked Object: {(pickedObject != null ? pickedObject.name : "None")}");
// Shift 또는 Ctrl 키로 다중 선택
bool addToSelection = Input.GetKey(KeyCode.LeftShift) ||
Input.GetKey(KeyCode.RightShift) ||
Input.GetKey(KeyCode.LeftControl) ||
Input.GetKey(KeyCode.RightControl);
if (pickedObject == null)
{
// 빈 공간 클릭 시 선택 해제
// Shift/Ctrl 키를 누르고 있지 않으면 선택 해제
// 단, 기즈모 드래그 직후에는 선택 해제하지 않음
// PropertyWindow는 유지 (clearPropertyWindow: false)
if (!addToSelection)
{
var gizmoUndoBridge = GizmoUndoBridge.Instance;
if (gizmoUndoBridge == null || !gizmoUndoBridge.IsDraggingOrJustEnded)
{
_selectionManager.DeselectAll(clearPropertyWindow: false);
}
}
return;
}
// 클릭된 객체 또는 부모에서 StageObject 찾기
var stageObject = FindStageObject(pickedObject);
if (stageObject != null)
{
// 더블클릭 감지
float currentTime = Time.unscaledTime;
bool isDoubleClick = (currentTime - _lastClickTime <= DoubleClickThreshold) &&
(_lastClickedStageObject == stageObject);
if (isDoubleClick)
{
// 더블클릭: 카메라 포커스 이동
_stageObjectManager.Focus(stageObject.GameObject);
_lastClickTime = 0f; // 연속 더블클릭 방지
_lastClickedStageObject = null;
return;
}
// 클릭 정보 저장
_lastClickTime = currentTime;
_lastClickedStageObject = stageObject;
if (addToSelection)
{
// 다중 선택: 토글
_selectionManager.ToggleSelection(stageObject, true);
}
else
{
// 단일 선택
_selectionManager.Select(stageObject, false);
}
}
else
{
// 빈 공간 클릭 시 더블클릭 상태 초기화
_lastClickTime = 0f;
_lastClickedStageObject = null;
}
}
/// <summary>
/// Physics.Raycast를 사용하여 마우스 위치에서 객체를 픽킹합니다.
/// </summary>
/// <returns>픽킹된 GameObject, 없으면 null</returns>
private GameObject? PickObject()
{
if (Camera.main == null) return null;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out RaycastHit hit))
{
return hit.collider.gameObject;
}
return null;
}
/// <summary>
/// GameObject 또는 부모에서 StageObject 찾기
/// </summary>
private StageObjectManager.StageObject? FindStageObject(GameObject clickedObject)
{
if (_stageObjectManager == null) return null;
// 클릭된 객체에서 시작하여 부모까지 탐색
Transform? current = clickedObject.transform;
while (current != null)
{
var stageObject = _stageObjectManager.GetByGameObject(current.gameObject);
if (stageObject != null)
{
return stageObject;
}
current = current.parent;
}
return null;
}
/// <summary>
/// 실제 상호작용 가능한 UI 위에 포인터가 있는지 확인
/// </summary>
private bool IsPointerOverInteractableUI()
{
if (EventSystem.current == null) return false;
var pointerEventData = new PointerEventData(EventSystem.current)
{
position = Input.mousePosition
};
var raycastResults = new List<RaycastResult>();
EventSystem.current.RaycastAll(pointerEventData, raycastResults);
foreach (var result in raycastResults)
{
// UI 레이어인지 확인
if (result.gameObject.layer != LayerMask.NameToLayer("UI")) continue;
// Graphic 컴포넌트 확인 (Image, Text 등)
var graphic = result.gameObject.GetComponent<Graphic>();
if (graphic != null && graphic.raycastTarget)
{
var selectable = result.gameObject.GetComponentInParent<Selectable>();
var hasInteraction = selectable != null && selectable.interactable;
if (graphic.color.a > 0.1f || hasInteraction)
{
return true;
}
}
}
return false;
}
}
}