<feat> 위치 기록/수정/삭제

This commit is contained in:
SOOBEEN HAN
2025-11-06 20:14:27 +09:00
parent 033c6aeb8e
commit 74759b0ab5
24 changed files with 1385 additions and 380 deletions

View File

@@ -24,6 +24,13 @@ public class InteractionView : MonoBehaviour, IInteractionView
private XRBaseInteractor baseInteractor;
private IXRRayProvider rayProvider;
[SerializeField]
[Tooltip("드래그 시 위치를 표시할 반투명 로봇 모델")]
private GameObject ghostRobot;
[SerializeField]
[Tooltip("드래그용 마우스 이미지")]
private GameObject dragArrow;
private bool isInitialized = false;
private bool isGrabbingPoint = false;
private int currentGrabbedPointIndex = -1;
@@ -92,8 +99,6 @@ public class InteractionView : MonoBehaviour, IInteractionView
// XRI 이벤트 구독
baseInteractor.selectEntered.AddListener(HandleGrabStart);
baseInteractor.selectExited.AddListener(HandleGrabEnd);
//interactor.activated.AddListener(OnActivated);
//interactor.deactivated.AddListener(OnDeactivated);
isInitialized = true;
AppManager.Instance.RegisterView(this);
@@ -112,57 +117,39 @@ public class InteractionView : MonoBehaviour, IInteractionView
// --- XRI 이벤트 핸들러 ---
// "잡기" 버튼을 눌렀을 때
private void HandleGrabStart(SelectEnterEventArgs args)
{
GameObject grabbedGO = args.interactableObject.transform.gameObject;
// 잡은 것이 "포인트"인지 확인
//RobotPoint point = grabbedGO.GetComponent<RobotPoint>();
//if (point != null)
//{
// isGrabbingPoint = true;
// isGrabbingRobot = false;
// currentGrabbedPointIndex = point.pointIndex;
RobotPoint point = grabbedGO.GetComponent<RobotPoint>();
if (point != null)
{
isGrabbingPoint = true;
isGrabbingRobot = false;
currentGrabbedPointIndex = point.pointIndex;
// // (Presenter의 HandlePointDragStart 호출)
// OnPointDragStart?.Invoke(currentGrabbedPointIndex);
//}
// 2. 잡은 것이 "로봇 핸들"인지 확인
if (grabbedGO.CompareTag("RobotArm"))
OnPointDragStart?.Invoke(currentGrabbedPointIndex);
}
else if (grabbedGO.CompareTag("RobotArm"))
{
isGrabbingPoint = false;
isGrabbingRobot = true;
// (로봇 자체를 잡는 것은 -1 인덱스로 Presenter에게 알림)
currentGrabbedPointIndex = -1;
// (로봇 잡기도 "PointDragStart" 이벤트로 통합하여 Presenter에 알림)
OnPointDragStart?.Invoke(currentGrabbedPointIndex);
}
//if (!enableSelectToDrag) return;
//BeginDrag(args.interactorObject);
}
// "잡기" 버튼을 뗐을 때
private void HandleGrabEnd(SelectExitEventArgs args)
{
//if (!enableSelectToDrag) return;
//EndDrag();
//// "포인트"를 놓고 있는 중이었다면
//if (isGrabbingPoint)
//{
// // Presenter에게 "드래그 끝" 이벤트 전송 (팝업 트리거용)
// OnPointDragEnd?.Invoke(currentGrabbedPointIndex);
//}
//// "로봇"을 놓고 있는 중이었다면
//else if (isGrabbingRobot)
//{
// // Presenter에게 "로봇 놓기" 이벤트 전송 (팝업 트리거용)
// RobotData currentPose = GetCurrentRobotPoseFromController();
// OnRobotReleased?.Invoke(currentPose);
//}
if (isGrabbingPoint)
{
OnPointDragEnd?.Invoke(currentGrabbedPointIndex);
}
else if (isGrabbingRobot)
{
OnRobotReleased?.Invoke(new RobotData());
}
// 상태 초기화
isGrabbingPoint = false;
@@ -170,128 +157,77 @@ public class InteractionView : MonoBehaviour, IInteractionView
currentGrabbedPointIndex = -1;
}
// ---- Drag lifecycle ----
//private void BeginDrag(IXRInteractor interactor)
//{
// currentInteractor = interactor;
// isDragging = true;
// startPosition = transform.position;
// //pathLine.enabled = true;
// uiController?.UpdateStatus("드래그 중...");
// if (communicationScript != null) communicationScript.isMoving = false;
// // ← 드래그 루프 시작
// StartDragMoveLoop();
//}
//private void EndDrag()
//{
// if (!isDragging) return;
// isDragging = false;
// uiController?.UpdateStatus("완료!");
// communicationScript.isMoving = false;
// // ← 루프 중지
// StopDragMoveLoop();
// currentInteractor = null;
//}
//private void StopDragMoveLoop()
//{
// if (dragLoopCts != null)
// {
// dragLoopCts.Cancel();
// dragLoopCts.Dispose();
// dragLoopCts = null;
// }
//}
//private void StartDragMoveLoop()
//{
// StopDragMoveLoop(); // 중복 방지
// dragLoopCts = new CancellationTokenSource();
// _ = DragMoveLoopAsync(dragLoopCts.Token);
//}
//private async Task DragMoveLoopAsync(CancellationToken token)
//{
// // 드래그 동안 100ms 주기로 전송
// while (!token.IsCancellationRequested && isDragging)
// {
// try
// {
// // 로봇이 아직 이동 중이 아니라면(대기 상태) 최신 dragPosition으로 지령 전송
// if (communicationScript != null && !communicationScript.isMoving)
// {
// await MovingTask(); // 내부에서 StratMovement(dragPosition) 호출 및 isMoving 갱신 :contentReference[oaicite:4]{index=4}
// }
// await Task.Delay(10, token); // 100ms 주기
// }
// catch (TaskCanceledException) { /* 정상 취소 */ }
// }
//}
//// ---- Per-frame ----
//void Update()
//{
// if (isDragging && currentInteractor != null)
// {
// communicationScript.isMoving = false;
// // 1) Ray Interactor: 현재 3D Raycast 히트 포인트가 있으면 그대로 사용
// if (currentInteractor is XRRayInteractor ray &&
// ray.TryGetCurrent3DRaycastHit(out RaycastHit hit))
// {
// dragPosition = hit.point;
// }
// else
// {
// // 2) Direct Interactor 또는 Ray 히트가 없을 때: 컨트롤러(또는 attachTransform) 위치 사용
// var t = currentInteractor.transform;
// var attach = currentInteractor.GetAttachTransform(null);
// dragPosition = attach != null ? attach.position : t.position;
// }
// uiController?.UpdateCoordinates(dragPosition); // 기존 UI 갱신 유지 :contentReference[oaicite:2]{index=2}
// }
//}
//private async Task MovingTask()
//{
// if (communicationScript == null) return;
// bool ok = await communicationScript.StratMovement(dragPosition); // 기존 호출 그대로 사용 :contentReference[oaicite:3]{index=3}
// if (ok) communicationScript.isMoving = true;
//}
private RobotData GetCurrentRobotPoseFromController()
// 로봇 좌표계(mm)를 Unity 월드 좌표계(m)로 변환
private Vector3 ConvertRobotDataToVector3(RobotData pose)
{
// 이 View는 Presenter의 변환 로직(ConvertVrToRobotPose)을 알 수 없으므로,
// 현재 컨트롤러 위치/회전을 기반으로 '가짜' RobotData를 만듦
// Presenter가 이 데이터를 받아 어차피 다시 변환(또는 무시)해야 함
return new RobotData(); // 임시 데이터
float x = Convert.ToSingle(pose.x / -1000.0);
float y = Convert.ToSingle(pose.z / 1000.0);
float z = Convert.ToSingle(pose.y / -1000.0);
return new Vector3(x, y, z);
}
// --- Presenter가 호출할 함수들 ---
public void ShowGhostRobot(RobotData pose)
{
/* 반투명 로봇2 활성화 및 위치 설정 */
// 로봇 오일러 각(rx,ry,rz)을 Unity 쿼터니언으로 변환
private Quaternion ConvertRobotDataToQuaternion(RobotData pose)
{
return Quaternion.Euler(pose.rx, pose.ry, pose.rz);
}
public void HideGhostRobot()
{
/* 반투명 로봇2 비활성화 */
// 고스트 로봇의 위치/회전 설정
public void ShowGhostRobot()
{
if (ghostRobot == null)
{
Debug.LogWarning("Ghost Robot이 Inspector에 할당되지 않았습니다.");
return;
}
ghostRobot.SetActive(true);
}
public void ShowDragArrow(Vector3 position)
{
/* 드래그용 화살표 UI 활성화 및 위치 설정 */
// 고스트 로봇 비활성화
public void HideGhostRobot()
{
if (ghostRobot != null)
{
ghostRobot.SetActive(false);
}
}
public void HideDragArrow()
{
/* 드래그용 화살표 UI 비활성화 */
// 드래그 화살표 활성화 및 위치 설정
public void ShowDragArrow(Vector3 position)
{
if (dragArrow == null)
{
Debug.LogWarning("Drag Arrow가 Inspector에 할당되지 않았습니다.");
return;
}
dragArrow.SetActive(true);
dragArrow.transform.position = position;
}
// 드래그 화살표 비활성화
public void HideDragArrow()
{
if (dragArrow != null)
{
dragArrow.SetActive(false);
}
}
// 광선 끝 위치 반환
public Vector3 GetCurrentRayPosition()
{
if (rayProvider != null)
{
return rayProvider.rayEndPoint;
}
if (baseInteractor != null)
{
return baseInteractor.attachTransform.position;
}
return transform.position;
}
}

View File

@@ -1,10 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
// 인터페이스
public interface IPathLineView
{
void DrawPath(List<RobotData> poses); // (RobotProgram.Steps¿¡¼­ º¯È¯)
}
[RequireComponent(typeof(LineRenderer))]
@@ -15,21 +16,54 @@ public class PathLineView : MonoBehaviour, IPathLineView
void Awake()
{
lineRenderer = GetComponent<LineRenderer>();
lineRenderer.positionCount = 0;
// Line Renderer 기본 설정
lineRenderer.startWidth = 0.01f;
lineRenderer.endWidth = 0.01f;
lineRenderer.material = new Material(Shader.Find("Legacy Shaders/Particles/Alpha Blended Premultiply"));
lineRenderer.startColor = Color.cyan;
lineRenderer.endColor = Color.cyan;
}
private Vector3 ConvertRobotDataToVector3(RobotData pose)
{
float x = Convert.ToSingle(pose.x / -1000.0); // mm -> m
float y = Convert.ToSingle(pose.z / 1000.0); // Robot Z(mm) -> Unity Y(m)
float z = Convert.ToSingle(pose.y / -1000.0); // Robot Y(mm) -> Unity Z(m)
return new Vector3(x, y, z);
}
/// <summary>
/// 프로그램의 모든 포인트를 순서대로 잇는 선을 그림
/// </summary>
public void DrawPath(List<RobotData> poses)
{
if (poses == null || poses.Count < 2)
{
lineRenderer.positionCount = 0;
lineRenderer.positionCount = 0; // 포인트가 2개 미만이면 선을 지움
return;
}
lineRenderer.positionCount = poses.Count;
for (int i = 0; i < poses.Count; i++)
Vector3[] positions = poses.Select(ConvertRobotDataToVector3).ToArray();
lineRenderer.positionCount = positions.Length;
lineRenderer.SetPositions(positions);
}
// 실시간 경로 그리기
public void DrawPath(List<RobotData> poses, int modifiedIndex, RobotData tempPose)
{
if (poses == null || poses.Count < 2 ||
modifiedIndex < 0 || modifiedIndex >= poses.Count)
{
lineRenderer.SetPosition(i, new Vector3(poses[i].x, poses[i].y, poses[i].z));
DrawPath(poses);
return;
}
Vector3[] positions = poses.Select(ConvertRobotDataToVector3).ToArray();
positions[modifiedIndex] = ConvertRobotDataToVector3(tempPose);
lineRenderer.positionCount = positions.Length;
lineRenderer.SetPositions(positions);
}
}

View File

@@ -1,32 +1,51 @@
using System;
using System.Collections.Generic;
using UnityEditor.Experimental.GraphView;
using UnityEngine;
// 인터페이스
public interface IPointManagerView
{
void CreatePoint(RobotData pose);
void UpdatePointPosition(int index, RobotData pose);
void DeletePoint(int index);
void RedrawPoints(List<RobotData> poses); // (RobotProgram.Steps에서 변환)
}
public class PointManagerView : MonoBehaviour, IPointManagerView
{
[SerializeField] private GameObject pointPrefab; // 인스펙터에서 포인트 프리팹 연결
[SerializeField] private GameObject pointPrefab;
private List<GameObject> activePoints = new List<GameObject>();
public void CreatePoint(RobotData pose)
[SerializeField] public GameObject movingAlert;
[SerializeField]
[Tooltip("반투명 로봇 모델의 IK")]
public HybridInverseKinematicsNode kinematicsNode;
private Vector3 ConvertRobotDataToVector3(RobotData pose)
{
Vector3 position = new Vector3(pose.x, pose.y, pose.z);
float x = Convert.ToSingle(pose.x / -1000.0); // mm -> m
float y = Convert.ToSingle(pose.z / 1000.0); // Robot Z(mm) -> Unity Y(m)
float z = Convert.ToSingle(pose.y / -1000.0); // Robot Y(mm) -> Unity Z(m)
return new Vector3(x, y, z);
}
public void CreatePoint(RobotData pose, int index)
{
Vector3 position = ConvertRobotDataToVector3(pose);
GameObject pointObj = Instantiate(pointPrefab, position, Quaternion.identity, this.transform);
activePoints.Add(pointObj);
// (참고: 이 pointObj에 'InteractionView'가 감지할 수 있는 콜라이더와 스크립트가 있어야 함)
RobotPoint pointComponent = pointObj.GetComponent<RobotPoint>();
if (pointComponent != null)
{
pointComponent.pointIndex = activePoints.Count - 1;
}
else
{
Debug.LogError("point프리팹에 RobotPoint스크립트가 없음");
}
}
public void UpdatePointPosition(int index, RobotData pose)
{
if (index < 0 || index >= activePoints.Count) return;
activePoints[index].transform.position = new Vector3(pose.x, pose.y, pose.z);
activePoints[index].transform.position = ConvertRobotDataToVector3(pose);
}
public void DeletePoint(int index)
@@ -38,10 +57,9 @@ public class PointManagerView : MonoBehaviour, IPointManagerView
public void RedrawPoints(List<RobotData> poses)
{
// 기존 포인트 모두 삭제
foreach (var point in activePoints) Destroy(point);
activePoints.Clear();
// 모든 포인트 새로 생성
foreach (var pose in poses) CreatePoint(pose);
if (poses == null) return;
for (int i= 0; i < poses.Count; i++) CreatePoint(poses[i], i);
}
}

View File

@@ -2,75 +2,83 @@ using System;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using Unity.XR.CoreUtils;
// 팝업 응답 타입
public enum PopupResponse
{
Confirm, // 확정
Cancel, // 취소
Option1, // (예: 여기로 이동)
Option2 // (예: 삭제)
InsConfirm, // 기록 확정
ModConfirm, // 수정 확정
Move, // 여기로 이동
Delete, // 삭제
DelConfirm, // 삭제 확정
Cancel // 취소
}
public interface IPopupView
{
event Action<PopupResponse> OnPopupResponse;
void ShowConfirmPopup(string title, string message); // 2, 5단계용 (확정/취소)
void ShowOptionPopup(string title, string message, string opt1Text, string opt2Text); // 8단계용 (이동/삭제)
void HidePopup();
}
public class PopupView : MonoBehaviour, IPopupView
{
public event Action<PopupResponse> OnPopupResponse;
[SerializeField] private GameObject popupPanel;
[SerializeField] private TextMeshProUGUI titleText;
[SerializeField] private TextMeshProUGUI messageText;
[SerializeField] private GameObject confirmPopupPanel;
[SerializeField] private GameObject modifyPopupPanel;
[SerializeField] private GameObject deletePopupPanel;
[SerializeField] private GameObject optionPopupPanel;
[SerializeField] private Button confirmButton; // '확정' 버튼
[SerializeField] private Button cancelButton; // '취소' 버튼
[SerializeField] private Button option1Button; // '옵션1(이동)' 버튼
[SerializeField] private Button option2Button; // '옵션2(삭제)' 버튼
[SerializeField] private Button ins_confirmButton; // '기록확정' 버튼
[SerializeField] private Button mod_confirmButton; // '수정확정' 버튼
[SerializeField] private Button moveButton; // '이동' 버튼
[SerializeField] private Button deleteButton; // '삭제' 버튼
[SerializeField] private Button del_confirmButton; // '삭제확정' 버튼
[SerializeField] private Button[] cancelButton; // '취소' 버튼
void Start()
{
// 각 버튼이 클릭되면 Presenter에게 응답 이벤트를 보냄
confirmButton.onClick.AddListener(() => OnPopupResponse?.Invoke(PopupResponse.Confirm));
cancelButton.onClick.AddListener(() => OnPopupResponse?.Invoke(PopupResponse.Cancel));
option1Button.onClick.AddListener(() => OnPopupResponse?.Invoke(PopupResponse.Option1));
option2Button.onClick.AddListener(() => OnPopupResponse?.Invoke(PopupResponse.Option2));
ins_confirmButton.onClick.AddListener(() => OnPopupResponse?.Invoke(PopupResponse.InsConfirm));
mod_confirmButton.onClick.AddListener(() => OnPopupResponse?.Invoke(PopupResponse.ModConfirm));
moveButton.onClick.AddListener(() => OnPopupResponse?.Invoke(PopupResponse.Move));
deleteButton.onClick.AddListener(() => OnPopupResponse?.Invoke(PopupResponse.Delete));
del_confirmButton.onClick.AddListener(() => OnPopupResponse?.Invoke(PopupResponse.DelConfirm));
for (int i = 0; i < cancelButton.Length; i++)
cancelButton[i].onClick.AddListener(() => OnPopupResponse?.Invoke(PopupResponse.Cancel));
popupPanel.SetActive(false);
confirmPopupPanel.SetActive(false);
modifyPopupPanel.SetActive(false);
deletePopupPanel.SetActive(false);
optionPopupPanel.SetActive(false);
}
public void ShowConfirmPopup(string title, string message)
public void ShowConfirmPopup(Transform popPose)
{
titleText.text = title;
messageText.text = message;
confirmButton.gameObject.SetActive(true);
cancelButton.gameObject.SetActive(true);
option1Button.gameObject.SetActive(false);
option2Button.gameObject.SetActive(false);
popupPanel.SetActive(true);
confirmPopupPanel.transform.SetPositionAndRotation(popPose.position, Quaternion.identity);
confirmPopupPanel.SetActive(true);
}
public void ShowOptionPopup(string title, string message, string opt1Text, string opt2Text)
public void ShowModifyPopup(Transform popPose)
{
titleText.text = title;
messageText.text = message;
option1Button.GetComponentInChildren<Text>().text = opt1Text;
option2Button.GetComponentInChildren<Text>().text = opt2Text;
modifyPopupPanel.transform.SetPositionAndRotation(popPose.position, Quaternion.identity);
modifyPopupPanel.SetActive(true);
}
confirmButton.gameObject.SetActive(false);
cancelButton.gameObject.SetActive(true); // 취소 버튼은 공용으로 사용
option1Button.gameObject.SetActive(true);
option2Button.gameObject.SetActive(true);
popupPanel.SetActive(true);
public void ShowDeletePopup(Transform popPose)
{
deletePopupPanel.transform.SetPositionAndRotation(popPose.position, Quaternion.identity);
deletePopupPanel.SetActive(true);
}
public void ShowOptionPopup(Transform popPose)
{
optionPopupPanel.transform.SetPositionAndRotation(popPose.position, Quaternion.identity);
optionPopupPanel.SetActive(true);
}
public void HidePopup()
{
popupPanel.SetActive(false);
confirmPopupPanel.SetActive(false);
modifyPopupPanel.SetActive(false);
deletePopupPanel.SetActive(false);
optionPopupPanel.SetActive(false);
}
}

View File

@@ -109,7 +109,7 @@ public class ProgramView : MonoBehaviour, IProgramView
Debug.Log("No Program Loaded");
return;
}
Debug.Log($"연결된 프로그램: {programId}.job");
Debug.Log($"연결된 프로그램: {programId}");
}
public void ShowProgramList(List<string> programIds)