using sc.modeling.splines.runtime; using Simulator.Data; using Simulator.Data.Transport; using System; using System.Collections.Generic; using System.Linq; using Unity.Collections; using UnityEngine; using UnityEngine.Splines; using UVC.Core; using UVC.Data; using UVC.Data.Core; using UVC.Data.Mqtt; public class ConveyorPathMeta : MonoBehaviour { public string Name; public string From; public string To; public float Speed; public float Length; public int Capacity; } public class ConveyorManager : SingletonScene { [Header("Root & Visual")] [SerializeField] private Transform rootParent; // 생성될 경로의 부모(없으면 빌더 자신) [SerializeField] private float tangentScale = 0.25f; // 곡률(탄젠트 길이 비율) [SerializeField] private ConveyorComponent splineprefab; [SerializeField] private GameObject junction; [Header("Corner Modules")] // ▼ 추가: 코너 프리팹 & 각도 허용 오차 [SerializeField] private GameObject corner45Prefab; [SerializeField] private GameObject corner90Prefab; [SerializeField, Range(1f, 30f)] private float angleTolerance = 10f; public Dictionary Nodes = new Dictionary(); public Dictionary<(string, string), ConveyorComponent> Paths = new Dictionary<(string, string), ConveyorComponent>(); // ▼ 추가: station 연결성 계산용 테이블 // stationName -> (경로 키, station에서 맞닿는 cap index(0=from,1=to), 반대편 노드명) private readonly Dictionary pathKey, int capIndex, string otherNode)>> _stationAdj = new Dictionary, int, string)>>(); public DataMapper conveyorStartDataMapper; public DataMapper conveyorCompleteDataMapper; protected override void Init() { var conveyorStartDataMask = new DataMask(); conveyorStartDataMask.ObjectName = "conveyor"; conveyorStartDataMask.ObjectIdKey = "component_id"; conveyorStartDataMask["component_id"] = ""; conveyorStartDataMask["event_name"] = ""; conveyorStartDataMask["timestamp"] = new DateTime(); conveyorStartDataMask["data"] = new DataMask() { ["entity_id_original"] = "", ["start_node"] = "", ["end_node"] = "", ["travel_time"] = 0.0f }; conveyorStartDataMapper = new DataMapper(conveyorStartDataMask); var conveyorCompleteDataMask = new DataMask(); conveyorCompleteDataMask.ObjectName = "conveyor"; conveyorCompleteDataMask.ObjectIdKey = "component_id"; conveyorCompleteDataMask["component_id"] = ""; conveyorCompleteDataMask["event_name"] = ""; conveyorCompleteDataMask["timestamp"] = new DateTime(); conveyorCompleteDataMask["data"] = new DataMask() { ["entity_id_original"] = "", ["start_node"] = "", ["end_node"] = "", }; conveyorCompleteDataMapper = new DataMapper(conveyorCompleteDataMask); } public void Build(List datas) { if (datas == null || datas.Count <= 0) return; var data = datas[0]; if (rootParent == null) rootParent = transform; Nodes.Clear(); Paths.Clear(); _stationAdj.Clear(); // ▲ 추가: 초기화 if (data.nodes != null) { foreach (var n in data.nodes) Nodes[n.name] = n; } foreach (var path in data.paths) CreatePathGOWithSpline(path, Nodes[path.from_node], Nodes[path.to_node]); SpawnJunction(); // ▲ 추가: station의 연결성(직선/코너) 기반 cap 분리 & 코너 배치 ProcessStations(); SubscribeConveyor(data); } public void SubscribeConveyor(ConveyorDataClass data) { DataRepository.Instance.MqttReceiver.AddTopic($"simulation/{SimulationConfig.SimulationCode}/components/+/{data.name}/+/node_started"); var mqttConfigConveyorStart = new MqttSubscriptionConfig($"simulation/{SimulationConfig.SimulationCode}/components/+/{data.name}/+/node_started"); mqttConfigConveyorStart.SetDataMapper(conveyorStartDataMapper); mqttConfigConveyorStart.SetHandler(OnSetData); DataRepository.Instance.MqttReceiver.Add(mqttConfigConveyorStart); } private void CreatePathGOWithSpline(ConveyorPath path, ConveyorNode fromNode, ConveyorNode toNode) { TryGetNodePos(fromNode.name, out Vector3 fromPos); TryGetNodePos(toNode.name, out Vector3 toPos); var go = Instantiate(splineprefab); Paths.Add((fromNode.name, toNode.name), go); go.transform.SetParent(rootParent, worldPositionStays: false); var container = go.GetComponent(); var spline = new Spline(); Vector3 dir = (toPos - fromPos); float dist = dir.magnitude; Vector3 dirNorm = dist > 1e-5f ? dir / dist : Vector3.forward; Vector3 tan = dirNorm * dist * Mathf.Clamp01(tangentScale); var k0 = new BezierKnot(fromPos) { TangentOut = tan, TangentIn = Vector3.zero }; var k1 = new BezierKnot(toPos) { TangentIn = -tan, TangentOut = Vector3.zero }; spline.Add(k0, TangentMode.Broken); spline.Add(k1, TangentMode.Broken); container.Spline = spline; var mesher = go.GetComponent(); // ▼ 변경: junction만 즉시 cap 분리, station은 후처리에서 판단 if (string.Equals(fromNode.node_type, "junction")) mesher.DetachCap(0); if (string.Equals(toNode.node_type, "junction")) mesher.DetachCap(1); mesher.UpdateCaps(); mesher.Rebuild(); // ▼ 추가: station 인접 정보 기록(후처리에서 cap/코너 결정) if (string.Equals(fromNode.node_type, "station")) AddStationAdj(fromNode.name, (fromNode.name, toNode.name), capIndex: 0, otherNode: toNode.name); if (string.Equals(toNode.node_type, "station")) AddStationAdj(toNode.name, (fromNode.name, toNode.name), capIndex: 1, otherNode: fromNode.name); } // ▼ 추가: station 인접 정보 수집 private void AddStationAdj(string stationName, (string, string) pathKey, int capIndex, string otherNode) { if (!_stationAdj.TryGetValue(stationName, out var list)) { list = new List<(ValueTuple, int, string)>(); _stationAdj[stationName] = list; } list.Add((pathKey, capIndex, otherNode)); } // ▼ 추가: station의 연결성이 직선인지 코너인지 판정하고 cap/코너를 적용 private void ProcessStations() { foreach (var kv in _stationAdj) { string station = kv.Key; var adj = kv.Value; // 연결 경로가 2개일 때만 직선/코너 판정(기타 케이스는 유지) if (adj.Count != 2) continue; if (!TryGetNodePos(station, out var stationPos)) continue; var (pathA, capA, otherA) = adj[0]; var (pathB, capB, otherB) = adj[1]; if (!TryGetNodePos(otherA, out var otherAPos)) continue; if (!TryGetNodePos(otherB, out var otherBPos)) continue; Vector3 vA = (otherAPos - stationPos); vA.y = 0f; Vector3 vB = (otherBPos - stationPos); vB.y = 0f; if (vA.sqrMagnitude < 1e-6f || vB.sqrMagnitude < 1e-6f) continue; float angle = Vector3.SignedAngle(vA.normalized, vB.normalized, Vector3.up); // 0~180 float straightThreshold = 180f - angleTolerance; // 예: tol=10 → 170° Debug.Log(angle); bool isStraight = Mathf.Abs(angle) >= straightThreshold; // 거의 반대 방향이면 직선 if (isStraight) { // 직선: cap 유지 continue; } // 코너: station에 맞닿는 두 cap 분리 if (Paths.TryGetValue(pathA, out var convA)) DetachAndRebuild(convA, capA); if (Paths.TryGetValue(pathB, out var convB)) DetachAndRebuild(convB, capB); var corner = Instantiate(corner90Prefab); corner.transform.position = stationPos; corner.transform.rotation = Quaternion.Euler(new Vector3(-90, -angle, 0)); } } // ▼ 추가: cap 분리 & 리빌드 헬퍼 private void DetachAndRebuild(ConveyorComponent comp, int capIndex) { var mesher = comp.GetComponent(); if (mesher == null) return; mesher.DetachCap(capIndex); mesher.UpdateCaps(); mesher.Rebuild(); } private void SpawnJunction() { foreach (var node in Nodes) { if (string.Equals(node.Value.node_type, "junction")) { var module = Instantiate(junction); module.transform.position = ToVector3(node.Value.physical.position); } } } private void OnSetData(IDataObject idata) { if (idata == null) return; DataObject? obj = idata as DataObject; if (obj == null) return; var data = obj.GetDataObject("data"); var fromNodeName = data.GetString("start_node"); var toNodeName = data.GetString("end_node"); ConveyorComponent? conveyor = Paths[(fromNodeName, toNodeName)]; conveyor.SetModelData(obj); } private bool TryGetNodePos(string nodeName, out Vector3 pos) { if (!string.IsNullOrWhiteSpace(nodeName) && Nodes.TryGetValue(nodeName, out ConveyorNode node)) { pos = ToVector3(node.physical.position); return true; } pos = default; return false; } private Vector3 ToVector3(Position p) { Debug.Log(p); return new Vector3(p.x, p.z, p.y); } }