<feat> 로봇 TCP 동기화

This commit is contained in:
SOOBEEN HAN
2025-10-30 09:31:05 +09:00
parent 437f273595
commit 6d62309b68
17 changed files with 1173 additions and 31 deletions

View File

@@ -122,6 +122,40 @@ public class HybridInverseKinematicsNode : MonoBehaviour
return currentRotations;
}
public List<float> GetCurrentJointAxisRotations(char axis = 'z')
{
List<float> jointAngles = new List<float>();
if (nodes == null || nodes.Count == 0) return jointAngles;
foreach (HybridIKJoint node in nodes)
{
if (node.jointTransform != null)
{
// 관절의 부모 기준 로컬 회전값을 Euler 각도(0-360)로 가져옴
Vector3 localEulerAngles = node.jointTransform.localEulerAngles;
// 매개변수로 받은 축에 해당하는 값을 리스트에 추가
switch (axis)
{
case 'x':
case 'X':
jointAngles.Add(localEulerAngles.x);
break;
case 'y':
case 'Y':
jointAngles.Add(localEulerAngles.y);
break;
case 'z':
case 'Z':
default:
jointAngles.Add(localEulerAngles.z);
break;
}
}
}
return jointAngles;
}
public void SetJointTargetPositions(List<Vector3> newPositions)
{
if (nodes == null || nodes.Count != newPositions.Count)
@@ -152,6 +186,46 @@ public class HybridInverseKinematicsNode : MonoBehaviour
}
}
public void SetCurrentJointAxisRotations(List<float> jointAngles, char axis = 'z')
{
// 노드 리스트가 없거나, 받은 각도 리스트의 개수가 일치하지 않으면 오류를 출력하고 중단
if (nodes == null || nodes.Count == 0 || jointAngles == null || nodes.Count != jointAngles.Count)
{
Debug.LogError($"관절 개수가 맞지 않습니다. (모델: {nodes?.Count ?? 0}개, 받은 데이터: {jointAngles?.Count ?? 0}개)");
return;
}
for (int i = 0; i < nodes.Count; i++)
{
if (nodes[i].jointTransform != null)
{
// 현재 로컬 오일러 각도를 Vector3 변수로 가져옴
Vector3 currentLocalEuler = nodes[i].jointTransform.localEulerAngles;
// 매개변수로 받은 축에 해당하는 값을 Vector3 변수에서 수정
switch (axis)
{
case 'x':
case 'X':
currentLocalEuler.x = jointAngles[i];
break;
case 'y':
case 'Y':
currentLocalEuler.y = jointAngles[i];
break;
case 'z':
case 'Z':
default:
currentLocalEuler.z = jointAngles[i];
break;
}
// 수정된 Vector3 전체를 다시 할당
nodes[i].jointTransform.localEulerAngles = currentLocalEuler;
}
}
}
#region DebugDraw
void OnDrawGizmos()
{

View File

@@ -1,9 +1,14 @@
using UnityEngine;
using System.Collections;
using System;
using NUnit.Framework;
using System.Collections.Generic;
public class RobotController : MonoBehaviour
{
[Header("IK")]
[SerializeField] private HybridInverseKinematicsNode kinematicsNode;
[Header("Motor State")]
[SerializeField] private GameObject motorStatusIndicator1;
[SerializeField] private GameObject motorStatusIndicator2;
@@ -11,6 +16,8 @@ public class RobotController : MonoBehaviour
[SerializeField] private Material indicatorMaterial1; // 기본색(회색)
[SerializeField] private Material indicatorMaterial2; // 초록
public event Action OnPoseUpdateRequest;
private bool isMotorOn;
void Start()
@@ -25,6 +32,11 @@ public class RobotController : MonoBehaviour
}
}
private void Update()
{
OnPoseUpdateRequest?.Invoke();// TODO. 로봇을 잡고 움직일 때와 아닐 때 구분하기
}
public void SetMotorState(bool isOn)
{
isMotorOn = isOn;
@@ -47,4 +59,22 @@ public class RobotController : MonoBehaviour
}
}
}
public void SetRobotPosition(RobotData robotData) // 가상 로봇 위치 업데이트
{
// x, y, z, rx, ry, rz => endpoint값
// j1, ..., j6 => 6개 축의 회전값
kinematicsNode.targetTransform.localPosition = new Vector3(robotData.x, robotData.y, robotData.z);
kinematicsNode.targetTransform.localRotation = new Quaternion(robotData.rx, robotData.ry, robotData.rz, 0);
List<float> list_jAngle = new List<float>();
list_jAngle.Add(robotData.j6);
list_jAngle.Add(robotData.j5);
list_jAngle.Add(robotData.j4);
list_jAngle.Add(robotData.j3);
list_jAngle.Add(robotData.j2);
list_jAngle.Add(robotData.j1);
kinematicsNode.SetCurrentJointAxisRotations(list_jAngle, 'x');
}
}

View File

@@ -0,0 +1,77 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &280273810869499429
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 23272902704394604}
- component: {fileID: 8410384716740639099}
- component: {fileID: 7782236473570627370}
m_Layer: 5
m_Name: Point_Prefab
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &23272902704394604
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 280273810869499429}
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: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0.2, y: 0.3}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &8410384716740639099
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 280273810869499429}
m_CullTransparentMesh: 1
--- !u!114 &7782236473570627370
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 280273810869499429}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 21300000, guid: a4e764ee05645514cab2cf0636654f5f, type: 3}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 3e14dc8ccaa006641934bf740bd1e88d
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -12581,7 +12581,7 @@ Transform:
m_GameObject: {fileID: 445093996}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0.174, y: 1.394, z: -1.015}
m_LocalPosition: {x: -1.27, y: 1.406, z: -0.054}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
@@ -16515,15 +16515,15 @@ PrefabInstance:
objectReference: {fileID: 0}
- target: {fileID: -8679921383154817045, guid: 6c52fe1416d967a409332c0065a41575, type: 3}
propertyPath: m_LocalPosition.x
value: 1.521
value: -0.14
objectReference: {fileID: 0}
- target: {fileID: -8679921383154817045, guid: 6c52fe1416d967a409332c0065a41575, type: 3}
propertyPath: m_LocalPosition.y
value: 0.00000011920929
value: 0
objectReference: {fileID: 0}
- target: {fileID: -8679921383154817045, guid: 6c52fe1416d967a409332c0065a41575, type: 3}
propertyPath: m_LocalPosition.z
value: 0.52
value: 1.66
objectReference: {fileID: 0}
- target: {fileID: -8679921383154817045, guid: 6c52fe1416d967a409332c0065a41575, type: 3}
propertyPath: m_LocalRotation.w
@@ -20564,7 +20564,7 @@ Transform:
m_GameObject: {fileID: 722849471}
serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 1.443, y: 0, z: -0.961}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -25875,7 +25875,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 1}
m_AnchorMax: {x: 0.5, y: 1}
m_AnchoredPosition: {x: 23.400135, y: -65}
m_AnchoredPosition: {x: 23.400135, y: -64.999985}
m_SizeDelta: {x: 400, y: 50}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &906004066
@@ -36822,6 +36822,10 @@ MonoBehaviour:
view: {fileID: 1243479632}
tcpView: {fileID: 1456747455}
robotController: {fileID: 806304512143720359}
interactionView: {fileID: 1568384462}
pointManagerView: {fileID: 1568384461}
pathLineView: {fileID: 1568384459}
popupView: {fileID: 1313589743}
motorStatePollInterval: 1
--- !u!4 &1299890571
Transform:
@@ -37154,6 +37158,7 @@ GameObject:
- component: {fileID: 1313589740}
- component: {fileID: 1313589739}
- component: {fileID: 1313589738}
- component: {fileID: 1313589743}
m_Layer: 5
m_Name: Canvas
m_TagString: Untagged
@@ -37266,6 +37271,25 @@ RectTransform:
m_AnchoredPosition: {x: -0.2, y: 1.221}
m_SizeDelta: {x: 0.8, y: 0.8}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1313589743
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1313589737}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 9abda78e6c8fdb34a925ac2483efc48b, type: 3}
m_Name:
m_EditorClassIdentifier:
popupPanel: {fileID: 1313589737}
titleText: {fileID: 331501807}
messageText: {fileID: 827370641}
confirmButton: {fileID: 618711572}
cancelButton: {fileID: 1352558441}
option1Button: {fileID: 1755737344}
option2Button: {fileID: 196844229}
--- !u!1001 &1315555405
PrefabInstance:
m_ObjectHideFlags: 0
@@ -46151,6 +46175,256 @@ Transform:
m_CorrespondingSourceObject: {fileID: 2525146012768536108, guid: af5398c451de74544b8b16ae846f351a, type: 3}
m_PrefabInstance: {fileID: 1567594223}
m_PrefabAsset: {fileID: 0}
--- !u!1 &1568384454
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1568384458}
- component: {fileID: 1568384457}
- component: {fileID: 1568384456}
- component: {fileID: 1568384455}
- component: {fileID: 1568384460}
- component: {fileID: 1568384459}
- component: {fileID: 1568384462}
- component: {fileID: 1568384461}
m_Layer: 5
m_Name: Canvas
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &1568384455
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1568384454}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: dc42784cf147c0c48a680349fa168899, type: 3}
m_Name:
m_EditorClassIdentifier:
m_IgnoreReversedGraphics: 1
m_BlockingObjects: 0
m_BlockingMask:
serializedVersion: 2
m_Bits: 4294967295
--- !u!114 &1568384456
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1568384454}
m_Enabled: 0
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3}
m_Name:
m_EditorClassIdentifier:
m_UiScaleMode: 0
m_ReferencePixelsPerUnit: 100
m_ScaleFactor: 1
m_ReferenceResolution: {x: 800, y: 600}
m_ScreenMatchMode: 0
m_MatchWidthOrHeight: 0
m_PhysicalUnit: 3
m_FallbackScreenDPI: 96
m_DefaultSpriteDPI: 96
m_DynamicPixelsPerUnit: 1
m_PresetInfoIsWorld: 1
--- !u!223 &1568384457
Canvas:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1568384454}
m_Enabled: 1
serializedVersion: 3
m_RenderMode: 2
m_Camera: {fileID: 1928531731}
m_PlaneDistance: 100
m_PixelPerfect: 0
m_ReceivesEvents: 1
m_OverrideSorting: 0
m_OverridePixelPerfect: 0
m_SortingBucketNormalizedSize: 0
m_VertexColorAlwaysGammaSpace: 0
m_AdditionalShaderChannelsFlag: 0
m_UpdateRectTransformForStandalone: 0
m_SortingLayerID: 0
m_SortingOrder: 0
m_TargetDisplay: 0
--- !u!224 &1568384458
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1568384454}
m_LocalRotation: {x: 0, y: 0.7071068, z: 0, w: 0.7071068}
m_LocalPosition: {x: 0, y: 0, z: -0.94}
m_LocalScale: {x: 0.5, y: 0.5, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 4070703782762572036}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: -0.2, y: 1.221}
m_SizeDelta: {x: 0.8, y: 0.8}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1568384459
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1568384454}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: c98a419633709d44ca6c0652dcf63a07, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!120 &1568384460
LineRenderer:
serializedVersion: 2
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1568384454}
m_Enabled: 1
m_CastShadows: 1
m_ReceiveShadows: 1
m_DynamicOccludee: 1
m_StaticShadowCaster: 0
m_MotionVectors: 0
m_LightProbeUsage: 0
m_ReflectionProbeUsage: 0
m_RayTracingMode: 0
m_RayTraceProcedural: 0
m_RayTracingAccelStructBuildFlagsOverride: 0
m_RayTracingAccelStructBuildFlags: 1
m_SmallMeshCulling: 1
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- {fileID: 0}
m_StaticBatchInfo:
firstSubMesh: 0
subMeshCount: 0
m_StaticBatchRoot: {fileID: 0}
m_ProbeAnchor: {fileID: 0}
m_LightProbeVolumeOverride: {fileID: 0}
m_ScaleInLightmap: 1
m_ReceiveGI: 1
m_PreserveUVs: 0
m_IgnoreNormalsForChartDetection: 0
m_ImportantGI: 0
m_StitchLightmapSeams: 1
m_SelectedEditorRenderState: 3
m_MinimumChartSize: 4
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_Positions:
- {x: 0, y: 0, z: 0}
- {x: 0, y: 0, z: 1}
m_Parameters:
serializedVersion: 3
widthMultiplier: 1
widthCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
colorGradient:
serializedVersion: 2
key0: {r: 1, g: 1, b: 1, a: 1}
key1: {r: 1, g: 1, b: 1, a: 1}
key2: {r: 0, g: 0, b: 0, a: 0}
key3: {r: 0, g: 0, b: 0, a: 0}
key4: {r: 0, g: 0, b: 0, a: 0}
key5: {r: 0, g: 0, b: 0, a: 0}
key6: {r: 0, g: 0, b: 0, a: 0}
key7: {r: 0, g: 0, b: 0, a: 0}
ctime0: 0
ctime1: 65535
ctime2: 0
ctime3: 0
ctime4: 0
ctime5: 0
ctime6: 0
ctime7: 0
atime0: 0
atime1: 65535
atime2: 0
atime3: 0
atime4: 0
atime5: 0
atime6: 0
atime7: 0
m_Mode: 0
m_ColorSpace: -1
m_NumColorKeys: 2
m_NumAlphaKeys: 2
numCornerVertices: 0
numCapVertices: 0
alignment: 0
textureMode: 0
textureScale: {x: 1, y: 1}
shadowBias: 0.5
generateLightingData: 0
m_MaskInteraction: 0
m_UseWorldSpace: 1
m_Loop: 0
m_ApplyActiveColorSpace: 1
--- !u!114 &1568384461
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1568384454}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 06c8786b6262dad4a86217e213be1b96, type: 3}
m_Name:
m_EditorClassIdentifier:
pointPrefab: {fileID: 280273810869499429, guid: 3e14dc8ccaa006641934bf740bd1e88d, type: 3}
--- !u!114 &1568384462
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1568384454}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: c1b136fe9693203418aa8d9bacb7cfcf, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!1 &1570547337
GameObject:
m_ObjectHideFlags: 0
@@ -57054,15 +57328,15 @@ PrefabInstance:
objectReference: {fileID: 0}
- target: {fileID: -8679921383154817045, guid: 9d5ec35fa0043fd488588e61261d23de, type: 3}
propertyPath: m_LocalPosition.x
value: 1.451
value: -0.21000004
objectReference: {fileID: 0}
- target: {fileID: -8679921383154817045, guid: 9d5ec35fa0043fd488588e61261d23de, type: 3}
propertyPath: m_LocalPosition.y
value: 0.483
value: 0.4829999
objectReference: {fileID: 0}
- target: {fileID: -8679921383154817045, guid: 9d5ec35fa0043fd488588e61261d23de, type: 3}
propertyPath: m_LocalPosition.z
value: 0.562
value: 1.7019999
objectReference: {fileID: 0}
- target: {fileID: -8679921383154817045, guid: 9d5ec35fa0043fd488588e61261d23de, type: 3}
propertyPath: m_LocalRotation.w
@@ -60885,15 +61159,15 @@ PrefabInstance:
objectReference: {fileID: 0}
- target: {fileID: -8679921383154817045, guid: 6c52fe1416d967a409332c0065a41575, type: 3}
propertyPath: m_LocalPosition.x
value: 1.521
value: -0.13999999
objectReference: {fileID: 0}
- target: {fileID: -8679921383154817045, guid: 6c52fe1416d967a409332c0065a41575, type: 3}
propertyPath: m_LocalPosition.y
value: 0.00000011920929
value: 0
objectReference: {fileID: 0}
- target: {fileID: -8679921383154817045, guid: 6c52fe1416d967a409332c0065a41575, type: 3}
propertyPath: m_LocalPosition.z
value: -2.71
value: -1.57
objectReference: {fileID: 0}
- target: {fileID: -8679921383154817045, guid: 6c52fe1416d967a409332c0065a41575, type: 3}
propertyPath: m_LocalRotation.w
@@ -61191,7 +61465,7 @@ RectTransform:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2084353331}
m_LocalRotation: {x: -0, y: -0.72839886, z: -0, w: 0.68515337}
m_LocalPosition: {x: 0, y: 0, z: -0.368}
m_LocalPosition: {x: 0, y: 0, z: 0.55}
m_LocalScale: {x: 0.673158, y: 0.904753, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
@@ -61200,7 +61474,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: -93.505, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 2.676, y: 1.548}
m_AnchoredPosition: {x: 1.67, y: 1.548}
m_SizeDelta: {x: 1, y: 1}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!1001 &2085318412
@@ -62731,7 +63005,7 @@ Transform:
m_GameObject: {fileID: 34692899277808924}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 1.443, y: 0, z: -0.961}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
@@ -62750,6 +63024,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: c12f4ab377ddfdc46820089b240eaf27, type: 3}
m_Name:
m_EditorClassIdentifier:
kinematicsNode: {fileID: 722849473}
motorStatusIndicator1: {fileID: 1475297475771640280}
motorStatusIndicator2: {fileID: 2476781507827223150}
indicatorMaterial1: {fileID: 2100000, guid: 8429ea8a04d5dd844875dc07c5f6c06b, type: 2}
@@ -63190,6 +63465,108 @@ Transform:
m_Children: []
m_Father: {fileID: 6476108356885335244}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1001 &4070703782762572035
PrefabInstance:
m_ObjectHideFlags: 0
serializedVersion: 2
m_Modification:
serializedVersion: 3
m_TransformParent: {fileID: 1568384458}
m_Modifications:
- target: {fileID: 23272902704394604, guid: 3e14dc8ccaa006641934bf740bd1e88d, type: 3}
propertyPath: m_Pivot.x
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 23272902704394604, guid: 3e14dc8ccaa006641934bf740bd1e88d, type: 3}
propertyPath: m_Pivot.y
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 23272902704394604, guid: 3e14dc8ccaa006641934bf740bd1e88d, type: 3}
propertyPath: m_AnchorMax.x
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 23272902704394604, guid: 3e14dc8ccaa006641934bf740bd1e88d, type: 3}
propertyPath: m_AnchorMax.y
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 23272902704394604, guid: 3e14dc8ccaa006641934bf740bd1e88d, type: 3}
propertyPath: m_AnchorMin.x
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 23272902704394604, guid: 3e14dc8ccaa006641934bf740bd1e88d, type: 3}
propertyPath: m_AnchorMin.y
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 23272902704394604, guid: 3e14dc8ccaa006641934bf740bd1e88d, type: 3}
propertyPath: m_SizeDelta.x
value: 0.2
objectReference: {fileID: 0}
- target: {fileID: 23272902704394604, guid: 3e14dc8ccaa006641934bf740bd1e88d, type: 3}
propertyPath: m_SizeDelta.y
value: 0.3
objectReference: {fileID: 0}
- target: {fileID: 23272902704394604, guid: 3e14dc8ccaa006641934bf740bd1e88d, type: 3}
propertyPath: m_LocalPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 23272902704394604, guid: 3e14dc8ccaa006641934bf740bd1e88d, type: 3}
propertyPath: m_LocalPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 23272902704394604, guid: 3e14dc8ccaa006641934bf740bd1e88d, type: 3}
propertyPath: m_LocalPosition.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 23272902704394604, guid: 3e14dc8ccaa006641934bf740bd1e88d, type: 3}
propertyPath: m_LocalRotation.w
value: 1
objectReference: {fileID: 0}
- target: {fileID: 23272902704394604, guid: 3e14dc8ccaa006641934bf740bd1e88d, type: 3}
propertyPath: m_LocalRotation.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 23272902704394604, guid: 3e14dc8ccaa006641934bf740bd1e88d, type: 3}
propertyPath: m_LocalRotation.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 23272902704394604, guid: 3e14dc8ccaa006641934bf740bd1e88d, type: 3}
propertyPath: m_LocalRotation.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 23272902704394604, guid: 3e14dc8ccaa006641934bf740bd1e88d, type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 23272902704394604, guid: 3e14dc8ccaa006641934bf740bd1e88d, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 23272902704394604, guid: 3e14dc8ccaa006641934bf740bd1e88d, type: 3}
propertyPath: m_LocalEulerAnglesHint.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 23272902704394604, guid: 3e14dc8ccaa006641934bf740bd1e88d, type: 3}
propertyPath: m_LocalEulerAnglesHint.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 23272902704394604, guid: 3e14dc8ccaa006641934bf740bd1e88d, type: 3}
propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 280273810869499429, guid: 3e14dc8ccaa006641934bf740bd1e88d, type: 3}
propertyPath: m_Name
value: Point_Prefab
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
m_AddedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: 3e14dc8ccaa006641934bf740bd1e88d, type: 3}
--- !u!224 &4070703782762572036 stripped
RectTransform:
m_CorrespondingSourceObject: {fileID: 23272902704394604, guid: 3e14dc8ccaa006641934bf740bd1e88d, type: 3}
m_PrefabInstance: {fileID: 4070703782762572035}
m_PrefabAsset: {fileID: 0}
--- !u!23 &4421136600761446311
MeshRenderer:
m_ObjectHideFlags: 0
@@ -63960,6 +64337,7 @@ SceneRoots:
- {fileID: 1814192178}
- {fileID: 2084353335}
- {fileID: 1313589742}
- {fileID: 1568384458}
- {fileID: 1299890571}
- {fileID: 580519499}
- {fileID: 2077727516}

View File

@@ -1,6 +1,7 @@
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using UnityEngine;
@@ -9,12 +10,19 @@ public class AppManager : MonoBehaviour
[SerializeField] private ProgramView view;
[SerializeField] private TCPView tcpView;
[SerializeField] private RobotController robotController;
[SerializeField] private InteractionView interactionView;
[SerializeField] private PointManagerView pointManagerView;
[SerializeField] private PathLineView pathLineView;
[SerializeField] private PopupView popupView;
[SerializeField] private float motorStatePollInterval = 1.0f;
ProgramPresenter presenter;
private ProgramPresenter presenter;
private string hostip;
private int tcpPort;
private int udpPort;
private string configFileName = "config.cfg";
private CancellationToken cancellationToken;
async void Start()
{
@@ -22,15 +30,13 @@ public class AppManager : MonoBehaviour
ProgramModel model = new ProgramModel(hostip, tcpPort, udpPort);
await model.InitializeAsync();
_ = model.GetTCPAsync();
_ = model.GetTCPAsync(cancellationToken);
presenter = new ProgramPresenter(model, view, tcpView);
presenter = new ProgramPresenter(model, view, tcpView, interactionView, pointManagerView, popupView, pathLineView);
presenter.RegisterControlledRobot(robotController);
await presenter.UpdateMotorStateAsync();
view.DisplayProgram(null);
StartCoroutine(PollMotorStateCoroutine());
}

View File

@@ -3,6 +3,7 @@ using Newtonsoft.Json.Linq;
using Palmmedia.ReportGenerator.Core;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Sockets;
@@ -22,6 +23,7 @@ public class ProgramModel : IProgramModel
private string tcpBaseUrl;
private string udpBaseUrl;
HttpClient httpClient = new HttpClient();
private SingleTcpClient tcpClient;
private SingleUdpClient udpClientForHttp;
public UdpClientManager manager = new UdpClientManager();
@@ -32,8 +34,10 @@ public class ProgramModel : IProgramModel
private readonly object lockObject = new object();
private bool hasNewData;
public bool isUdpLoopRunning = false;
public bool IsUdpLoopRunning = false;
public bool IsMoving;
public CancellationTokenSource cancellationTokenSource;
private Vector3 startMovementPosition;
public ProgramModel(string hostip, int tcpPort, int udpPort)
{
@@ -46,7 +50,8 @@ public class ProgramModel : IProgramModel
{
await LoadAllPrograms();
hasNewData = false;
isUdpLoopRunning = true;
IsUdpLoopRunning = true;
IsMoving = false;
return;
}
@@ -63,10 +68,14 @@ public class ProgramModel : IProgramModel
lock (lockObject)
{
hasNewData = false; // 데이터를 읽었으므로 플래그를 내림
return robotData; // (데이터 복사본을 반환하는 것이 더 안전할 수 있음)
return robotData;
}
}
/// <summary>
/// 프로그램 생성/불러오기 시스템
/// </summary>
public async Task<bool> CheckProgramExists(string jobProgramName)
{
string requestUri = $"{tcpBaseUrl}/file_manager/file_exist?pathname=project/jobs/{jobProgramName}";
@@ -171,9 +180,9 @@ public class ProgramModel : IProgramModel
}
}
public async Task GetTCPAsync()
public async Task GetTCPAsync(CancellationToken token)
{
while (isUdpLoopRunning)
while (IsUdpLoopRunning)
{
try
{
@@ -260,8 +269,148 @@ public class ProgramModel : IProgramModel
return ids;
}
/// <summary>
/// 로봇 위치 기록 시스템
/// </summary>
public async Task<bool> SavePointToProgramAsync(RobotData pointData, int index = -1)
{
if (CurrentProgram == null)
{
Debug.LogError("저장할 프로그램이 로드되지 않았습니다.");
return false;
}
// DTO(전송 객체) 생성
// (서버가 RobotData와 index, 프로그램 ID를 어떻게 받는지에 따라 수정 필요)
var payload = new
{
programId = CurrentProgram.ProgramId,
indexToUpdate = index, // -1이면 새 포인트, 0 이상이면 해당 인덱스 수정
pose = pointData
};
string jsonPayload = JsonConvert.SerializeObject(payload);
HttpContent content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
string requestUri = (index == -1)
? $"{tcpBaseUrl}/project/jobs/{CurrentProgram.ProgramId}/ins_cmd_line"
: $"{tcpBaseUrl}/project/jobs/{CurrentProgram.ProgramId}/pose_modify";
try
{
HttpResponseMessage result = await httpClient.PostAsync(requestUri, content);
if (result.IsSuccessStatusCode)
{
// 서버 저장이 성공하면, 메모리(CurrentProgram)에도 반영
if (index == -1)
CurrentProgram.AddStep(pointData); // RobotProgram에 새 RobotMoveStep 추가
else
CurrentProgram.UpdateStep(index, pointData); // RobotProgram의 해당 스텝 갱신
return true;
}
return false;
}
catch (Exception e)
{
Debug.LogError($"포인트 저장 실패: {e.Message}");
return false;
}
}
// 서버에 포인트 삭제 요청
public async Task<bool> DeletePointFromProgramAsync(int index)
{
if (CurrentProgram == null) return false;
var payload = new { programId = CurrentProgram.ProgramId, indexToDelete = index };
string jsonPayload = JsonConvert.SerializeObject(payload);
HttpContent content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
string requestUri = $"{tcpBaseUrl}/project/jobs/{CurrentProgram.ProgramId}/del_cmd_line";
try
{
HttpResponseMessage result = await httpClient.PostAsync(requestUri, content);
if (result.IsSuccessStatusCode)
{
// 서버 삭제 성공 시, 메모리(CurrentProgram)에서도 삭제
CurrentProgram.DeleteStep(index);
return true;
}
return false;
}
catch (Exception e)
{
Debug.LogError($"포인트 삭제 실패: {e.Message}");
return false;
}
}
// 실시간 로봇 TCP 이동
public async Task StreamPoseToRobotUdpAsync(RobotData pose)
{
try
{
byte[] udpPacket = ConvertPoseToPacket(pose);
await udpClientForHttp.SendBytesAsync(udpPacket);
}
catch (Exception e)
{
Debug.LogWarning($"UDP 스트리밍 실패: {e.Message}");
}
}
private byte[] ConvertPoseToPacket(RobotData pose)
{
using (MemoryStream stream = new MemoryStream())
{
using (BinaryWriter writer = new BinaryWriter(stream))
{
writer.Write(pose.x);
writer.Write(pose.y);
writer.Write(pose.z);
writer.Write(pose.rx);
writer.Write(pose.ry);
writer.Write(pose.rz);
}
return stream.ToArray();
}
}
//사용자 tcp 드래깅 후 제어기로 이동 명령 하달
public async Task<bool> StartMovement(Vector3 position)
{
startMovementPosition.x = Convert.ToSingle(Math.Round(-1 * position.x * 1000, 2));
startMovementPosition.y = Convert.ToSingle(Math.Round(-1 * position.z * 1000, 2));
startMovementPosition.z = Convert.ToSingle(Math.Round(position.y * 1000, 2));
var jsonResponse = await tcpClient.SendPostRequestAsync("/project/robot/move_to_pose_manual", $"{{\"pose_tg\":{{\"crd\":\"robot\",\"_type\":\"Pose\",\"mechinfo\":1,\"x\":{startMovementPosition.x},\"y\":{startMovementPosition.y},\"z\":{startMovementPosition.z}, \"rx\":{robotData.rx}, \"ry\":{robotData.ry}, \"rz\":{robotData.rz}}}}}");
return jsonResponse.Contains("200");
}
//타겟 포지션 도달까지 이동 명령
private async Task MovementLoopAsync()
{
while (!cancellationTokenSource.Token.IsCancellationRequested)
{
if (IsMoving)
{
await udpClientForHttp.SendFilledBytesAsync(new Dictionary<int, byte> { { 2, 0x20 } });
await Task.Delay(100);
bool isApproximatelyX = Mathf.Approximately(startMovementPosition.x, Convert.ToSingle(Math.Round(robotData.x, 2)));
bool isApproximatelyY = Mathf.Approximately(startMovementPosition.y, Convert.ToSingle(Math.Round(robotData.y, 2)));
bool isApproximatelyZ = Mathf.Approximately(startMovementPosition.z, Convert.ToSingle(Math.Round(robotData.z, 2)));
if (isApproximatelyX && isApproximatelyY && isApproximatelyZ)
{
IsMoving = false;
}
}
}
}
void OnDestroy()
{
isUdpLoopRunning = false;
IsUdpLoopRunning = false;
}
}

View File

@@ -17,10 +17,10 @@ public class RobotProgram
private void ParseJobContent(string rawText)
{
// 1. 헤더 파싱 (예: "Robot Job File; { version: 2.0, ... }")
// 헤더 파싱 (예: "Robot Job File; { version: 2.0, ... }")
// 정규식이나 Substring을 사용해 version, mech_type 등 추출
// 2. 스텝(S1, S2...) 파싱
// 스텝(S1, S2...) 파싱
string[] lines = rawText.Split('\n');
foreach (string line in lines)
{
@@ -45,4 +45,30 @@ public class RobotProgram
}
return string.Empty;
}
public void AddStep(RobotData data)
{
}
public void UpdateStep(int index, RobotData data)
{
}
public void DeleteStep(int index)
{
}
public RobotData GetStepPose(int index)
{
RobotData data = null;
return data;
}
public void GetAllStepPoses()
{
}
}

View File

@@ -1,8 +1,18 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.XR.ARSubsystems;
public enum PopupState
{
None,
ConfirmAddPoint,
ConfirmModifyPoint,
MoveOrDelete,
ConfirmDelete
}
public class ProgramPresenter
{
private ProgramModel model;
@@ -12,7 +22,19 @@ public class ProgramPresenter
private string _programId;
private bool lastKnownMotorState = false;
public ProgramPresenter(ProgramModel model, IProgramView view, TCPView tcpView)
private IInteractionView interactionView;
private IPointManagerView pointManagerView;
private IPathLineView pathLineView;
private IPopupView popupView;
private PopupState currentPopupState = PopupState.None;
private RobotData pendingPointData; // 팝업창에 대한 응답을 기다리는 임시 데이터
private int activePointIndex = -1; // 현재 수정/삭제 중인 포인트의 인덱스
private bool IsDragging = false;
public ProgramPresenter(ProgramModel model, IProgramView view, TCPView tcpView,
IInteractionView interactionView, IPointManagerView pmView, IPopupView popView, IPathLineView pathLineView)
{
this.model = model;
this.view = view;
@@ -25,11 +47,20 @@ public class ProgramPresenter
this.view.OnSaveClicked += HandleSaveProgram;
this.view.OnAddPointClicked += HandleAddPoint;
this.tcpView.OnTCPupdateRequested += HandleTCPViewUpdate;
//this.interactionView.OnRobotReleased += HandleRobotReleased;
//this.interactionView.OnPointClicked += HandlePointClicked;
//this.interactionView.OnPointDragStart += HandlePointDragStart;
//this.interactionView.OnPointDragUpdate += HandlePointDragUpdate;
//this.interactionView.OnPointDragEnd += HandlePointDragEnd;
//this.popupView.OnPopupResponse += HandlePopupResponse;
}
public void RegisterControlledRobot(RobotController robot)
{
this.controlledRobot = robot;
this.controlledRobot.OnPoseUpdateRequest += HandlePoseViewUpdate;
}
public async Task UpdateMotorStateAsync()
@@ -62,7 +93,6 @@ public class ProgramPresenter
}
}
private async Task HandleCreateProgram(string programId)
{
if (await model.CreateNewProgram(programId))
@@ -126,6 +156,143 @@ public class ProgramPresenter
tcpView.SetCoordinates(data);
}
}
// --- 실시간 동기화 ---
private void HandlePoseViewUpdate()
{
RobotData data = model.GetLatestRobotData();
controlledRobot.SetRobotPosition(data); // 3D 가상 로봇 모델 위치 업데이트
}
// --- 새 포인트 추가 ---
private void HandleRobotReleased(RobotData pose)
{
pendingPointData = pose; // 1. 임시 저장
currentPopupState = PopupState.ConfirmAddPoint; // 2. 상태 설정
popupView.ShowConfirmPopup("위치 확정", "이 위치를 새 포인트로 저장하시겠습니까?"); // 3. 팝업 요청
}
// --- 포인트 클릭 ---
private void HandlePointClicked(int index)
{
activePointIndex = index; // 인덱스 저장
currentPopupState = PopupState.MoveOrDelete; // 상태 설정
popupView.ShowOptionPopup("포인트 작업", "무엇을 하시겠습니까?", "여기로 이동", "삭제"); // 팝업 요청
}
// --- 포인트 드래그 ---
private RobotData originalDragPose; // 취소 시 돌아갈 원본 위치
private void HandlePointDragStart(int index)
{
IsDragging = true;
activePointIndex = index;
originalDragPose = model.CurrentProgram.GetStepPose(index);
//interactionView.ShowDragArrow(GetPositionFromPose(originalDragPose));
interactionView.ShowGhostRobot(originalDragPose);
}
private async void HandlePointDragUpdate(int index, Vector3 newWorldPos)
{
if (!IsDragging) return;
//RobotData newPose = ConvertVectorToRobotData(newWorldPos);
// 고스트 로봇, 포인트, 경로 실시간 이동
//interactionView.ShowGhostRobot(newPose);
//pointManagerView.UpdatePointPosition(index, newPose);
//pathLineView.DrawPath(GetFullPathOfProgramWithTempChange(index, newPose)); // 임시 경로 그리기
//await model.StreamPoseToRobotUdpAsync(newPose);
}
private void HandlePointDragEnd(int index)
{
//IsDragging = false;
//interactionView.HideDragArrow();
//interactionView.HideGhostRobot();
//// (가상 로봇 위치 이동 로직 - 5단계)
//// robotController.SetRobotPosition(newPose);
//pendingPointData = ConvertVectorToRobotData(GetLastDragPosition()); // 임시 저장
//currentPopupState = PopupState.ConfirmModifyPoint; // 상태 설정
//popupView.ShowConfirmPopup("위치 수정", "이 위치로 포인트를 수정하시겠습니까?"); // 팝업 요청
}
// --- 팝업 응답 통합 핸들러 ---
private async void HandlePopupResponse(PopupResponse response)
{
popupView.HidePopup();
switch (currentPopupState)
{
case PopupState.ConfirmAddPoint:
if (response == PopupResponse.Confirm) // 확정
{
await model.SavePointToProgramAsync(pendingPointData);
RedrawSceneFromModel(); // 뷰 갱신
}
break;
// 드래그 확정/취소
case PopupState.ConfirmModifyPoint:
if (response == PopupResponse.Confirm) // 확정
{
// 최종 위치를 서버에 저장
// 드래그가 끝났으니 이 위치를 프로그램에 저장
await model.SavePointToProgramAsync(pendingPointData, activePointIndex);
RedrawSceneFromModel();
}
else // 취소
{
pointManagerView.UpdatePointPosition(activePointIndex, originalDragPose); // 원위치
RedrawSceneFromModel(); // 경로 원위치
}
break;
// 이동/삭제
case PopupState.MoveOrDelete:
if (response == PopupResponse.Option1) // 여기로 이동
{
RobotData targetPose = model.CurrentProgram.GetStepPose(activePointIndex);
//await model.StartMovement(targetPose);
}
else if (response == PopupResponse.Option2) // 삭제
{
currentPopupState = PopupState.ConfirmDelete;
popupView.ShowConfirmPopup("삭제 확인", "정말로 이 포인트를 삭제하시겠습니까?");
}
break;
// 최종 삭제
case PopupState.ConfirmDelete:
if (response == PopupResponse.Confirm)
{
await model.DeletePointFromProgramAsync(activePointIndex);
RedrawSceneFromModel();
}
break;
}
// 모든 작업 후 상태 초기화
currentPopupState = PopupState.None;
activePointIndex = -1;
}
// Model의 현재 상태를 읽어 모든 View(포인트, 경로)를 새로 고침
private void RedrawSceneFromModel()
{
//if (model.CurrentProgram == null) return;
//// (RobotProgram.Steps (List<RobotMoveStep>)를 List<RobotData>로 변환하는 로직)
//List<RobotData> poses = model.CurrentProgram.GetAllStepPoses();
//pointManagerView.RedrawPoints(poses); // 포인트 다시 그림
//pathLineView.DrawPath(poses); // 경로 다시 그림
}
private void Destroy()
{
this.view.OnCreateProgramClicked -= async (id) => await HandleCreateProgram(id);

View File

@@ -0,0 +1,62 @@
using System;
using UnityEngine;
// Presenter가 InteractionView를 제어하기 위한 인터페이스
public interface IInteractionView
{
// VR 컨트롤러가 로봇을 잡았다 놨을 때 발생
event Action<RobotData> OnRobotReleased;
// VR 컨트롤러가 특정 포인트를 클릭했을 때 발생
event Action<int> OnPointClicked;
// VR 컨트롤러가 포인트를 잡고 드래그 시작/중/끝 했을 때 발생
event Action<int> OnPointDragStart;
event Action<int, Vector3> OnPointDragUpdate; // (포인트 인덱스, 새 월드 좌표)
event Action<int> OnPointDragEnd;
// Presenter가 호출할 함수들
void ShowGhostRobot(RobotData pose);
void HideGhostRobot();
void ShowDragArrow(Vector3 position);
void HideDragArrow();
}
// (이 스크립트는 VR 컨트롤러 로직이 있는 곳에 붙어야 합니다)
public class InteractionView : MonoBehaviour, IInteractionView
{
public event Action<RobotData> OnRobotReleased;
public event Action<int> OnPointClicked;
public event Action<int> OnPointDragStart;
public event Action<int, Vector3> OnPointDragUpdate;
public event Action<int> OnPointDragEnd;
void Update()
{
// 컨트롤러로 로봇을 잡고있는지 감지
// if (IsGrabbingRobot()) { ... }
// 컨트롤러 그랩 버튼을 뗐을 때
// if (OnGrabRelease())
// {
// RobotData currentPose = GetCurrentRobotPose();
// OnRobotReleased?.Invoke(currentPose); // 2. "위치 확정?" 팝업 요청
// }
// 컨트롤러로 포인트를 클릭했을 때
// if (OnPointClick(out int clickedPointIndex))
// {
// OnPointClicked?.Invoke(clickedPointIndex); // 8. "이동/삭제?" 팝업 요청
// }
// 컨트롤러로 포인트를 꾹 누르기 시작(드래그 시작)
// if (OnPointHold(out int draggedPointIndex))
// {
// OnPointDragStart?.Invoke(draggedPointIndex); // 5. 드래그 시작
// }
}
// --- Presenter가 호출할 함수들 ---
public void ShowGhostRobot(RobotData pose) { /* 반투명 로봇2 활성화 및 위치 설정 */ }
public void HideGhostRobot() { /* 반투명 로봇2 비활성화 */ }
public void ShowDragArrow(Vector3 position) { /* 드래그용 화살표 UI 활성화 및 위치 설정 */ }
public void HideDragArrow() { /* 드래그용 화살표 UI 비활성화 */ }
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c1b136fe9693203418aa8d9bacb7cfcf

View File

@@ -0,0 +1,35 @@
using System.Collections.Generic;
using UnityEngine;
// ÀÎÅÍÆäÀ̽º
public interface IPathLineView
{
void DrawPath(List<RobotData> poses); // (RobotProgram.Steps¿¡¼­ º¯È¯)
}
[RequireComponent(typeof(LineRenderer))]
public class PathLineView : MonoBehaviour, IPathLineView
{
private LineRenderer lineRenderer;
void Awake()
{
lineRenderer = GetComponent<LineRenderer>();
lineRenderer.positionCount = 0;
}
public void DrawPath(List<RobotData> poses)
{
if (poses == null || poses.Count < 2)
{
lineRenderer.positionCount = 0;
return;
}
lineRenderer.positionCount = poses.Count;
for (int i = 0; i < poses.Count; i++)
{
lineRenderer.SetPosition(i, new Vector3(poses[i].x, poses[i].y, poses[i].z));
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c98a419633709d44ca6c0652dcf63a07

View File

@@ -0,0 +1,47 @@
using System.Collections.Generic;
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; // 인스펙터에서 포인트 프리팹 연결
private List<GameObject> activePoints = new List<GameObject>();
public void CreatePoint(RobotData pose)
{
Vector3 position = new Vector3(pose.x, pose.y, pose.z);
GameObject pointObj = Instantiate(pointPrefab, position, Quaternion.identity, this.transform);
activePoints.Add(pointObj);
// (참고: 이 pointObj에 'InteractionView'가 감지할 수 있는 콜라이더와 스크립트가 있어야 함)
}
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);
}
public void DeletePoint(int index)
{
if (index < 0 || index >= activePoints.Count) return;
Destroy(activePoints[index]);
activePoints.RemoveAt(index);
}
public void RedrawPoints(List<RobotData> poses)
{
// 기존 포인트 모두 삭제
foreach (var point in activePoints) Destroy(point);
activePoints.Clear();
// 모든 포인트 새로 생성
foreach (var pose in poses) CreatePoint(pose);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 06c8786b6262dad4a86217e213be1b96

View File

@@ -0,0 +1,76 @@
using System;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
// 팝업 응답 타입
public enum PopupResponse
{
Confirm, // 확정
Cancel, // 취소
Option1, // (예: 여기로 이동)
Option2 // (예: 삭제)
}
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 Button confirmButton; // '확정' 버튼
[SerializeField] private Button cancelButton; // '취소' 버튼
[SerializeField] private Button option1Button; // '옵션1(이동)' 버튼
[SerializeField] private Button option2Button; // '옵션2(삭제)' 버튼
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));
popupPanel.SetActive(false);
}
public void ShowConfirmPopup(string title, string message)
{
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);
}
public void ShowOptionPopup(string title, string message, string opt1Text, string opt2Text)
{
titleText.text = title;
messageText.text = message;
option1Button.GetComponentInChildren<Text>().text = opt1Text;
option2Button.GetComponentInChildren<Text>().text = opt2Text;
confirmButton.gameObject.SetActive(false);
cancelButton.gameObject.SetActive(true); // 취소 버튼은 공용으로 사용
option1Button.gameObject.SetActive(true);
option2Button.gameObject.SetActive(true);
popupPanel.SetActive(true);
}
public void HidePopup()
{
popupPanel.SetActive(false);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9abda78e6c8fdb34a925ac2483efc48b