## 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>
243 lines
8.3 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|