using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using InfraredManager; using ZIM; using System.Linq; using Serenegiant.UVC; using System; public class InfraredDemo : MonoBehaviour { public static bool DebugInEditor = true; public static InfraredDemo _ins; [SerializeField] RectTransform canvasRectTransform; public static void Create() { if (_ins) return; GameObject o = Instantiate(Resources.Load("InfraredDemo")); DontDestroyOnLoad(o); _ins = o.GetComponent(); //直接初始化一次 _ins.InitInfraredCamera(); } void Start() { if (Application.platform != RuntimePlatform.WindowsEditor) DebugInEditor = false; SetVisiable(false); InitDebugScreenPoint(); gameObject.AddComponent(); //直接初始化一次 InitInfraredCamera(); //如果本地有记录,初始化一次 initScreenLocateManual(); } void Update() { UpdateInfraredCamera(); UpdateDebugScreenPoint(); } public void OnClick_See() { SetVisiable(!_visiable); } bool _visiable; void SetVisiable(bool value) { _visiable = value; transform.Find("Background").gameObject.SetActive(value); transform.Find("InfraredCamera").gameObject.SetActive(value); transform.Find("BtnSee").GetComponentInChildren().text = value ? "隐藏界面" : "调试红外"; //if (value) InitInfraredCamera(); } Text _spText; Vector2 _screenPoint; void InitDebugScreenPoint() { _spText = transform.Find("SPText").GetComponent(); //_spText.gameObject.SetActive(DebugInEditor); _spText.gameObject.SetActive(false); } void UpdateDebugScreenPoint() { if (!DebugInEditor) return; if (infraredCameraHelper == null) return; _screenPoint.x = Mathf.Clamp( _screenPoint.x + Input.GetAxis("Horizontal") * Screen.width / 400f, 0, Screen.width); _screenPoint.y = Mathf.Clamp( _screenPoint.y + Input.GetAxis("Vertical") * Screen.height / 400f, 0, Screen.height); infraredCameraHelper.InvokeOnPositionUpdate(_screenPoint); _spText.text = _screenPoint.ToString(); } #region 红外摄像 [SerializeField] RawImage _cameraRender; [SerializeField] MaintainAspectRatio _MaintainAspectRatio; [SerializeField] List _crosshairsInCamera; //[SerializeField] Slider _sliderBrightness; //[SerializeField] Slider _sliderSaturation; //[SerializeField] Slider _sliderContrast; [SerializeField] Slider _sliderShakeFilter; [SerializeField] Button _btnReset; [SerializeField] Button _btnScreenLocateManual; [SerializeField] Button _btnScreenLocateManualAuto; [SerializeField] Slider _sliderCapture; [SerializeField] Slider _sliderDelay; [SerializeField] Dropdown _dropdownResolution; [SerializeField] Dropdown _dropdownResolution2; [SerializeField] Slider _sliderLineWidth; //红外线阈值 [SerializeField] Slider _infraredFilter; //调试UVC参数 [SerializeField] GameObject _cameraParameterPanel; //Dictionary dUVCParameters = new Dictionary(); //public ParamFloatValue brightness = new ParamFloatValue("ic_brightness", 1.0f); //public ParamFloatValue saturation = new ParamFloatValue("ic_saturation", 1.0f); //public ParamFloatValue contrast = new ParamFloatValue("ic_contrast", 1.0f); public ParamFloatValue shakeFilterValue = new ParamFloatValue("ic_shakeFilterValue2", 6.0f); public ParamFloatValue resoution = new ParamFloatValue("ic_resoution2", 2); public ParamFloatValue resoutionNew = new ParamFloatValue("ic_resoutionNew", 0); //线段宽度阈值 public ParamFloatValue lineWidth = new ParamFloatValue("ic_lineWidth", 20.0f); public ParamFloatValue captureValue = new ParamFloatValue("ic_captureValue", 30.0f); public ParamFloatValue delayValue = new ParamFloatValue("ic_delayValue", 30.0f); //亮度过滤阈值 public ParamFloatValue infraredFileterValue = new ParamFloatValue("ic_infraredFileterValue", 0.8f); //十字准心 public ParamFloatValue crosshairValue = new ParamFloatValue("ic_crosshairValue",0); public static InfraredCameraHelper infraredCameraHelper; public static bool running { get => infraredCameraHelper != null; } private bool _inited; #region 参数控制 public GameObject togglePrefab; // 拖入一个Toggle预设体 public GameObject sliderPrefab; // 拖入一个Slider预设体 public GameObject spawnPoint; //自动曝光 bool bAutoAE = false; Toggle CTRLAEToggle; Slider CTRLAEABSSlider; string[] sliderNameArray = new string[]{ "自动曝光模式", "曝光时间(绝对)", "亮度", // "对比度", "色调", "饱和度", "锐度", "伽玛", //"白平衡温度", //"白平衡分量", "背光补偿", "增益" }; string[] sliderStrArray = new string[]{ "CTRL_AE", "CTRL_AE_ABS", "PU_BRIGHTNESS", "PU_CONTRAST", "PU_HUE", "PU_SATURATION", "PU_SHARPNESS", "PU_GAMMA", //"PU_WB_TEMP", //"PU_WB_COMPO", "PU_BACKLIGHT", "PU_GAIN" }; UVCManager.CameraInfo currentCameraInfo; //初始化相机参数 public void initSlider(UVCManager.CameraInfo cameraInfo) { if (currentCameraInfo != null) return; currentCameraInfo = cameraInfo; for (int i = 0; i < sliderStrArray.Length; i++) { string typeStr = sliderStrArray[i]; UInt64 _VALUE = cameraInfo.GetTypeByName(typeStr); //不支持的,跳过 bool bContains = cameraInfo.ContainsKey(_VALUE); if (!bContains) continue; if (typeStr == "CTRL_AE") { //曝光Toggle GameObject toggleObject = Instantiate(togglePrefab, spawnPoint.transform); toggleObject.SetActive(true); toggleObject.name = typeStr; Text labelObj = toggleObject.transform.Find("Label").GetComponent(); labelObj.text = sliderNameArray[i]; Toggle toggle = toggleObject.GetComponent(); CTRLAEToggle = toggle; UVCCtrlInfo _AEInfo = cameraInfo.GetInfo(_VALUE); Debug.Log("UVCCtrlInfo:" + _AEInfo.ToString()); int _currentValue = cameraInfo.GetValue(_VALUE); bAutoAE = _currentValue == 8 ? true : false; //如果当前值是 8,则当前摄像机是自动曝光模式 toggle.isOn = bAutoAE; //cameraInfo.SetValue(_VALUE, _currentValue!=8? 8:1); toggle.onValueChanged.AddListener((bool bValue) => { //Debug.Log("Toggle value changed to: " + bValue + " from " + _VALUE); if (typeStr == "CTRL_AE") { //开关控制是否自动曝光 bAutoAE = bValue; if (CTRLAEABSSlider) CTRLAEABSSlider.interactable = !bAutoAE; int _value = bValue ? 8 : 1; //Debug.Log("_value " + _value); cameraInfo.SetValue(_VALUE, _value); } }); } else { //其余使用slider GameObject sliderObject = Instantiate(sliderPrefab, spawnPoint.transform); sliderObject.SetActive(true); sliderObject.name = typeStr; Slider slider = sliderObject.GetComponent(); Text textObj = sliderObject.transform.Find("text").GetComponent(); Text titleTextObj = sliderObject.transform.Find("title").GetComponent(); titleTextObj.text = sliderNameArray[i];//类型名字 if (slider != null) { UVCCtrlInfo _UVCCtrlInfo = cameraInfo.GetInfo(_VALUE); slider.minValue = _UVCCtrlInfo.min; slider.maxValue = _UVCCtrlInfo.max; slider.wholeNumbers = true; //记录一个typeStr类型的数据存储操作对象 ParamFloatValue paramFloatValue = new ParamFloatValue("ic_uvc_" + typeStr, _UVCCtrlInfo.def); //dUVCParameters.Add(typeStr, paramFloatValue); //获取当前值 int _currentValue = cameraInfo.GetValue(_VALUE); //int _saveValue = (int)dUVCParameters.GetValueOrDefault(typeStr).Get(); slider.value = _currentValue; textObj.text = _currentValue + ""; //存储初始值,设置一次到UVC参数 Debug.Log(_UVCCtrlInfo.ToString()); Debug.Log("dUVCParameters2:" + _currentValue + " == " + _UVCCtrlInfo.def); //如果是曝光slider if (typeStr == "CTRL_AE_ABS") { CTRLAEABSSlider = slider; slider.interactable = !bAutoAE; //if (!bAutoAE) { // cameraInfo.SetValue(_VALUE, (int)paramFloatValue.Get()); //} } //else{ // if (_currentValue != _saveValue) cameraInfo.SetValue(_VALUE, (int)paramFloatValue.Get()); //} slider.onValueChanged.AddListener((newValue) => { var _value = Mathf.FloorToInt(newValue); textObj.text = _value + ""; //Debug.Log("Slider value changed to: " + newValue + " from " + _VALUE); cameraInfo.SetValue(_VALUE, _value); //存储值 //dUVCParameters.GetValueOrDefault(typeStr).Set(_value); }); } } } } public void OpenUVCPanel() { _cameraParameterPanel.SetActive(true); } public void CloseUVCPanel() { _cameraParameterPanel.SetActive(false); } public void onResetUVCData() { if (currentCameraInfo == null) return; for (int i = 0; i < sliderStrArray.Length; i++) { string typeStr = sliderStrArray[i]; UInt64 _VALUE = currentCameraInfo.GetTypeByName(typeStr); bool bContains = currentCameraInfo.ContainsKey(_VALUE); if (!bContains) continue; if (typeStr == "CTRL_AE") { //toggle值不进行重置 Debug.Log("CTRL_AE 不需要重置"); } else if (typeStr == "CTRL_AE_ABS") { if (!bAutoAE) { //如果是手动曝光,重置值 Transform trans = _cameraParameterPanel.transform.Find(typeStr); Slider slider = trans.GetComponent(); Text textObj = trans.Find("text").GetComponent(); UVCCtrlInfo _AEInfo = currentCameraInfo.GetInfo(_VALUE); //获取当前值 int _currentValue = currentCameraInfo.GetValue(_VALUE); Debug.Log("CTRL_AE_ABS:" + _currentValue + " = " + _AEInfo.def + " = " + ",bAutoAE:" + bAutoAE); textObj.text = _AEInfo.def + ""; slider.value = _AEInfo.def; if (_AEInfo.def != _currentValue) currentCameraInfo.SetValue(_VALUE, _AEInfo.def); } } else { Transform trans = _cameraParameterPanel.transform.Find(typeStr); Slider slider = trans.GetComponent(); Text textObj = trans.Find("text").GetComponent(); UVCCtrlInfo _UVCCtrlInfo = currentCameraInfo.GetInfo(_VALUE); textObj.text = _UVCCtrlInfo.def + ""; slider.value = _UVCCtrlInfo.def; //获取当前值 int _currentValue = currentCameraInfo.GetValue(_VALUE); //重置存储值 //dUVCParameters.GetValueOrDefault(typeStr).Set(_UVCCtrlInfo.def); //存储初始值,设置一次到UVC参数 if (_UVCCtrlInfo.def != _currentValue) currentCameraInfo.SetValue(_VALUE, _UVCCtrlInfo.def); } } } #endregion void InitInfraredCamera() { if (_inited) return; _inited = true; //SDK创建 if (infraredCameraHelper == null) { infraredCameraHelper = InfraredCameraHelper.GetInstance(); infraredCameraHelper.Create(); infraredCameraHelper.OnPositionUpdate += (Vector2 point) => { //跑九轴时候,不处理这里位置 if (AimHandler.ins && AimHandler.ins.bRuning9Axis()) return; if (Camera.main == null) return; Ray ray = Camera.main.ScreenPointToRay(point); Vector3 rayEndPoint = ray.GetPoint(200); Quaternion quat = Quaternion.LookRotation(rayEndPoint - Camera.main.transform.position); // 挑战场景中其相机的父级有旋转,需要换算 if (UnityEngine.SceneManagement.SceneManager.GetActiveScene().name == "GameChallenge") { quat = Quaternion.AngleAxis(-180, Vector3.up) * quat; } if (CameraToLook.ins != null) CameraToLook.ins.localRotation = quat; if (SB_EventSystem.ins) SB_EventSystem.ins.MoveSimulateMouseByInfrared(point); }; infraredCameraHelper.OnUVCIsReady += (UVCManager.CameraInfo camera) => { //生成控制摄像机的参数滑条 Debug.Log("初始化摄像机!"); initSlider(camera); //_cameraRender.texture = ScreenLocate.Main.getUVCTexture; //infraredCameraHelper.GetCameraTexture(); //_cameraRender.SetNativeSize(); }; //InfraredCameraHelper.InfraredCameraHelperRawImageList.Add(_cameraRender); } //参数面板 SetShakeFilterValue(shakeFilterValue.Get()); _sliderShakeFilter.onValueChanged.AddListener(SetShakeFilterValue); //功能按钮 _btnReset.onClick.AddListener(OnClick_Reset); _btnScreenLocateManual.onClick.AddListener(OnClick_ScreenLocateManual); _btnScreenLocateManualAuto.onClick.AddListener(OnClick_ScreenLocateManualAuto); SetCaptureValue(captureValue.Get()); _sliderCapture.onValueChanged.AddListener(SetCaptureValue); SetDelayValue(delayValue.Get()); _sliderDelay.onValueChanged.AddListener(SetDelayValue); //分辨率 SetResolution((int)resoution.Get()); _dropdownResolution.onValueChanged.AddListener(v => { SetResolution(v); StartCoroutine(RestartOrKillApp()); }); SetResolutionNew((int)resoutionNew.Get()); _dropdownResolution2.onValueChanged.AddListener(v => { SetResolutionNew(v); }); //绘制线段 SetLineWidth(lineWidth.Get()); _sliderLineWidth.onValueChanged.AddListener(SetLineWidth); //检测红外亮度阈值 SetInfraredFilterValue(infraredFileterValue.Get()); _infraredFilter.onValueChanged.AddListener(SetInfraredFilterValue); } public int getCrosshairValue() { return (int)crosshairValue.Get(); } public void setCrosshairValue(bool bshow) { crosshairValue.Set(bshow?1:0); } public void onStartPreview() { infraredCameraHelper.onStartPreview(); } public void onStopPreview() { infraredCameraHelper.onStopPreview(); } //获取并且初始化一次数据 void initScreenLocateManual() { ScreenLocate.GetScreenLocateVectorList(); infraredCameraHelper.QuitScreenLocateManual(ScreenLocate.quadUnityVectorList); ScreenLocate.Main.SyncInfraredDemo(); } IEnumerator RestartOrKillApp() { yield return new WaitForSeconds(0.3f); if (Application.isEditor) yield break; if (Application.platform == RuntimePlatform.Android) { using (var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) { const int kIntent_FLAG_ACTIVITY_CLEAR_TASK = 0x00008000; const int kIntent_FLAG_ACTIVITY_NEW_TASK = 0x10000000; var currentActivity = unityPlayer.GetStatic("currentActivity"); var pm = currentActivity.Call("getPackageManager"); var intent = pm.Call("getLaunchIntentForPackage", Application.identifier); intent.Call("setFlags", kIntent_FLAG_ACTIVITY_NEW_TASK | kIntent_FLAG_ACTIVITY_CLEAR_TASK); currentActivity.Call("startActivity", intent); currentActivity.Call("finish"); var process = new AndroidJavaClass("android.os.Process"); int pid = process.CallStatic("myPid"); process.CallStatic("killProcess", pid); } } else Application.Quit(); } void UpdateInfraredCamera() { if (!_visiable) return; if (!_inited) return; if (infraredCameraHelper == null) return; //渲染相机画面 //_cameraRender.texture = infraredCameraHelper.GetCameraTexture(); //_cameraRender.material = infraredCameraHelper.GetCameraMaterial(); if (ScreenLocate.Main.getUVCTexture) { _cameraRender.texture = ScreenLocate.Main.getUVCTexture; _cameraRender.SetNativeSize(); _MaintainAspectRatio.AdjustSize(); } //在相机画面显示准心 if (ScreenLocate.Main) { var _sl = ScreenLocate.Main; var buffer = _sl.infraredSpotBuffer; if (buffer != null) { for (int i = 0; i < buffer.Length; i++) { if (buffer[i].CameraLocation != null) { // 检测到光点 var pos = buffer[i].CameraLocation.Value.pixelToLocalPosition_AnchorCenter(_sl.mUVCCameraInfo.Size, _cameraRender.rectTransform.rect); _crosshairsInCamera[i].gameObject.SetActive(true); _crosshairsInCamera[i].anchoredPosition = pos; } else _crosshairsInCamera[i].gameObject.SetActive(false); } } } } public void SetLocatePointsToCameraRender(List points, float w, float h) { Transform pointsTF2 = _cameraRender.transform.Find("Points"); if (pointsTF2.childCount == points.Count) { Vector2 texSize = new Vector2(w, h); for (int i = 0; i < pointsTF2.childCount; i++) { Transform pointTF = pointsTF2.GetChild(i); Vector2 pos = points[i]; pointTF.localPosition = pos.pixelToLocalPosition_AnchorCenter(texSize, _cameraRender.rectTransform.rect); pointTF.gameObject.SetActive(true); } } } void SetShakeFilterValue(float v) { shakeFilterValue.Set(v); _sliderShakeFilter.SetValueWithoutNotify(shakeFilterValue.Get()); _sliderShakeFilter.transform.Find("Value").GetComponent().text = shakeFilterValue.Get().ToString("f1"); infraredCameraHelper.SetShakeFilterValue(shakeFilterValue.Get()); } public void resolutionRestartApp(int index) { SetResolution(index); StartCoroutine(RestartOrKillApp()); } //SetResolution((int)resoution.Get()); public void SetResolutionGuider(int optionIndex) { resoution.Set(optionIndex); switch (optionIndex) { case 0: infraredCameraHelper.SetCameraResolution(1280, 720); break; case 1: infraredCameraHelper.SetCameraResolution(640, 360); break; case 2: infraredCameraHelper.SetCameraResolution(320, 240); break; } } public void SetResolution(int optionIndex) { resoution.Set(optionIndex); _dropdownResolution.SetValueWithoutNotify(optionIndex); switch (optionIndex) { case 0: infraredCameraHelper.SetCameraResolution(1280, 720); break; case 1: infraredCameraHelper.SetCameraResolution(640, 360); break; case 2: infraredCameraHelper.SetCameraResolution(320, 240); break; } } public int ResolutionIndex => _dropdownResolution2.value; public void SetResolutionNew(int optionIndex) { resoutionNew.Set(optionIndex); _dropdownResolution2.SetValueWithoutNotify(optionIndex); switch (optionIndex) { case 0: infraredCameraHelper.SetCameraResolutionNew(1920, 1080); break; case 1: infraredCameraHelper.SetCameraResolutionNew(1280, 800); break; case 2: infraredCameraHelper.SetCameraResolutionNew(1280, 720); break; case 3: infraredCameraHelper.SetCameraResolutionNew(1024, 768); break; case 4: infraredCameraHelper.SetCameraResolutionNew(960, 540); break; case 5: infraredCameraHelper.SetCameraResolutionNew(800, 600); break; case 6: infraredCameraHelper.SetCameraResolutionNew(848, 480); break; case 7: infraredCameraHelper.SetCameraResolutionNew(640, 480); break; case 8: infraredCameraHelper.SetCameraResolutionNew(640, 360); break; case 9: infraredCameraHelper.SetCameraResolutionNew(352, 288); break; case 10: infraredCameraHelper.SetCameraResolutionNew(320, 240); break; case 11: //不支持这种分辨率设置 infraredCameraHelper.SetCameraResolutionNew(1280, 960); break; } } void OnClick_Reset() { //SetBrightness(1); //SetSaturation(1); //SetContrast(1); SetShakeFilterValue(6); SetLineWidth(10); } void OnClick_ScreenLocateManual() { ViewManager2.ShowView(ViewManager2.Path_InfraredScreenPositioningView); FindObjectOfType().enterFromInfraredDemo = true; } void OnClick_ScreenLocateManualAuto() { infraredCameraHelper.EnterScreenLocateManualAuto(); } void SetCaptureValue(float v) { captureValue.Set(v); _sliderCapture.SetValueWithoutNotify(captureValue.Get()); _sliderCapture.transform.Find("Value").GetComponent().text = captureValue.Get().ToString("f1"); infraredCameraHelper.SetCapture((int)captureValue.Get()); } void SetDelayValue(float v) { delayValue.Set(v); _sliderDelay.SetValueWithoutNotify(delayValue.Get()); _sliderDelay.transform.Find("Value").GetComponent().text = delayValue.Get().ToString("f1"); infraredCameraHelper.SetDelay((int)delayValue.Get()); } #endregion #region 相机感光度 public void onSetSliderValue(Slider _slider) { if (running) { string typeStr = "PU_BRIGHTNESS"; UInt64 _VALUE = currentCameraInfo.GetTypeByName(typeStr); UVCCtrlInfo _UVCCtrlInfo = currentCameraInfo.GetInfo(_VALUE); float v = currentCameraInfo.GetValue(_VALUE); // infraredCameraHelper.GetBrightness(); //float v2 = (v / _UVCCtrlInfo.max) * 10; // 目标区间 [0, 10] 的边界值 double targetMin = 0.0; double targetMax = 10.0; double originalMin = _UVCCtrlInfo.min; double originalMax = _UVCCtrlInfo.max; // 计算转换后的值 double v2 = MapValue(v, originalMin, originalMax, targetMin, targetMax); Debug.Log("获取相机的感光度:" + _VALUE + " = " + v + " = " + v2); _slider.SetValueWithoutNotify((float)v2); } else _slider.SetValueWithoutNotify(5); } public void onSliderEvent(float value) { Debug.Log(value); //修改亮度时,调试界面的亮度也应该一起修改 if (running) { //Slider slider = transform.Find("InfraredCamera/Layout/SliderBrightness") // .GetComponent(); string typeStr = "PU_BRIGHTNESS"; UInt64 _VALUE = currentCameraInfo.GetTypeByName(typeStr); Transform trans = _cameraParameterPanel.transform.Find(typeStr); Slider slider = trans.GetComponent(); Text textObj = trans.Find("text").GetComponent(); UVCCtrlInfo _UVCCtrlInfo = currentCameraInfo.GetInfo(_VALUE); int _currentUVCValue = currentCameraInfo.GetValue(_VALUE); //value 0 ~ 10 // 原始区间和目标区间的边界值 double originalMin = 0.0; double originalMax = 10.0; double targetMin = _UVCCtrlInfo.min; double targetMax = _UVCCtrlInfo.max; // 计算转换后的值 double result = MapValue(value, originalMin, originalMax, targetMin, targetMax); int _current = (int)(result); Debug.Log("_current:" + value + " , result:" + result); textObj.text = _current + ""; slider.value = _current; //dUVCParameters.GetValueOrDefault(typeStr).Set(_current); //存储初始值,设置一次到UVC参数 if (_currentUVCValue != _current) currentCameraInfo.SetValue(_VALUE, _current); } } double MapValue(double value, double originalMin, double originalMax, double targetMin, double targetMax) { // 线性插值公式 return targetMin + (value - originalMin) * (targetMax - targetMin) / (originalMax - originalMin); } #endregion #region 绘制线段部分 public void SetLineWidth(float v) { lineWidth.Set(v); _sliderLineWidth.SetValueWithoutNotify(lineWidth.Get()); _sliderLineWidth.transform.Find("Value").GetComponent().text = lineWidth.Get().ToString("f1"); } #endregion #region 亮度检测部分 public void SetInfraredFilterValue(float v) { infraredFileterValue.Set(v); _infraredFilter.SetValueWithoutNotify(infraredFileterValue.Get()); _infraredFilter.transform.Find("Value").GetComponent().text = infraredFileterValue.Get().ToString("f1"); infraredCameraHelper.SetInfraredLocateBrightnessThreshold(infraredFileterValue.Get()); } #endregion public void resetInfraredPlayerPrefs() { //测试用 PlayerPrefs.DeleteKey("entry-guider-infrared-" + LoginMgr.myUserInfo.id); PlayerPrefs.DeleteKey("hideInfraredBowAndArrow"); } } public class ParamFloatValue { private string _saveKey; private float _valueDefault; private bool _valueLoaded; private float _value; public ParamFloatValue(string saveKey, float valueDefault) { _saveKey = saveKey; _valueDefault = valueDefault; } public float Get() { if (!_valueLoaded) _value = PlayerPrefs.GetFloat(_saveKey, _valueDefault); return _value; } public void Set(float value) { _value = value; PlayerPrefs.SetFloat(_saveKey, _value); PlayerPrefs.Save(); } }