protected int RegisterTtpAngleAxis() { AxisValueCalculator calculator; calculator = delegate { if (!TwoPoint) { return(0); } float2 p0 = GetPosition(TouchPoints.Touchpoint_0); float2 p1 = GetPosition(TouchPoints.Touchpoint_1); float2 delta = p1 - p0; float angle = (float)System.Math.Atan2(-delta.y, delta.x); // flip y direction (up is positive) so angle values are "maths-conform". return(angle); }; int id = NewAxisID; var calculatedAxisDesc = new AxisDescription { Id = id, Name = "Double-Touch Angle", Direction = AxisDirection.Unknown, Nature = AxisNature.Position, Bounded = AxisBoundedType.Constant, MaxValueOrAxis = (float)System.Math.PI, MinValueOrAxis = -(float)System.Math.PI }; RegisterCalculatedAxis(calculatedAxisDesc, calculator); return(calculatedAxisDesc.Id); }
protected int RegisterTtpDistanceAxis() { AxisValueCalculator calculator; calculator = delegate { if (!TwoPoint) { return(0); } float2 p0 = GetPosition(TouchPoints.Touchpoint_0); float2 p1 = GetPosition(TouchPoints.Touchpoint_1); float distance = (p1 - p0).Length; return(distance); }; int id = NewAxisID; var calculatedAxisDesc = new AxisDescription { Id = id, Name = "Double-Touch Distance", Direction = AxisDirection.Unknown, Nature = AxisNature.Position, Bounded = AxisBoundedType.Unbound, // TODO: Set min and max axes to 0 and window diagonal as bounding axes. }; RegisterCalculatedAxis(calculatedAxisDesc, calculator); return(calculatedAxisDesc.Id); }
protected int RegisterTtpMidpointAxis(int axId0, int axId1, int axIdMin, int axIdMax, AxisDirection dir, string name) { AxisValueCalculator calculator; calculator = delegate { if (!TwoPoint) { return(0); } float v0 = GetAxis(axId0); float v1 = GetAxis(axId1); float midpoint = (v0 + v1) * 0.5f; return(midpoint); }; int id = NewAxisID; var calculatedAxisDesc = new AxisDescription { Id = id, Name = name, Direction = dir, Nature = AxisNature.Position, Bounded = AxisBoundedType.OtherAxis, MinValueOrAxis = axIdMin, MaxValueOrAxis = axIdMax }; RegisterCalculatedAxis(calculatedAxisDesc, calculator); return(calculatedAxisDesc.Id); }
/// <summary> /// Sets the deadzone for the given axis. /// </summary> /// <param name="axisId">The axis' Id as specified in <see cref="AxisDesc"/>.</param> /// <param name="value"></param> public void SetAxisDeadzone(int axisId, float value) { AxisDescription desc = GetAxisDescription(axisId); desc.Deadzone = value; _axes[axisId] = desc; }
// ------------------- public void InitDialog(InputRig rig) { this.rig = rig; // Add axes... UnityInputManagerUtils.InputAxisList inputAxisList = UnityInputManagerUtils.LoadInputManagerAxes(); for (int i = 0; i < inputAxisList.Count; ++i) { if (inputAxisList[i].name.StartsWith("cf")) { continue; } UnityInputManagerUtils.InputAxis inputAxis = inputAxisList[i]; AxisDescription axisDesc = this.FindDescription(inputAxis); if (axisDesc == null) { axisDesc = new AxisDescription(inputAxis); int axisId = 0; axisDesc.enabled = true; axisDesc.wasAlreadyDefined = rig.IsAxisDefined(inputAxis.name, ref axisId); if (inputAxis.name.Equals("Submit", System.StringComparison.OrdinalIgnoreCase) || inputAxis.name.Equals("Cancel", System.StringComparison.OrdinalIgnoreCase)) { axisDesc.enabled = false; } this.axisList.Add(axisDesc); } else { axisDesc.inputAxes.Add(inputAxis); } } }
/// <summary> /// Registers a calculated axis. Calculated axes behave like axes exposed by the underlying /// hardware device but can be calculated from one or more existing axes or buttons. /// </summary> /// <param name="calculatedAxisDescription">The axis description for the new calculated axis.</param> /// <param name="calculator">The calculator method performing the calculation once per frame.</param> /// <param name="initialValue">The initial value for the new axis.</param> /// <remarks> /// To register your own axis you need to provide a working <see cref="AxisValueCalculator"/>. This method /// is called whenever the axis value needs to be present. /// Any state the calculation depends upon should be queried from existing axes presented by the input device /// or "statically" stored in the closure around the calculator. The methodes /// <list type="bullet"></list> /// <item><see cref="RegisterSingleButtonAxis"/></item> /// <item><see cref="RegisterTwoButtonAxis"/></item> /// <item><see cref="RegisterVelocityAxis"/></item> /// </remarks> /// provide pre-defined calculators for certain purposes. public void RegisterCalculatedAxis(AxisDescription calculatedAxisDescription, AxisValueCalculator calculator, float initialValue = 0) { if (calculatedAxisDescription.Id < _nextAxisId) { throw new InvalidOperationException($"Invalid Id for calculated axis '{calculatedAxisDescription.Name}'. Id must be bigger or equal to {_nextAxisId}."); } _nextAxisId = calculatedAxisDescription.Id; var calculatedAxis = new CalculatedAxisDescription { AxisDesc = calculatedAxisDescription, CurrentAxisValue = initialValue, Calculator = calculator }; // Calculated Axes are always to-be-polled axes. _calculatedAxes[calculatedAxisDescription.Id] = calculatedAxis; _axesToPoll[calculatedAxisDescription.Id] = calculatedAxis.CurrentAxisValue; _axes[calculatedAxisDescription.Id] = calculatedAxisDescription; }
// ---------------- private void AddAxesToRig() { // Check for already present axes... int enabledAxisNum = 0; int alreadyPresentAxisNum = 0; string presentAxisNames = ""; for (int i = 0; i < this.axisList.Count; ++i) { if (!this.axisList[i].enabled) { continue; } enabledAxisNum++; int axisId = 0; if (this.rig.IsAxisDefined(this.axisList[i].name, ref axisId)) { ++alreadyPresentAxisNum; if (alreadyPresentAxisNum <= 10) { presentAxisNames += (((alreadyPresentAxisNum == 10) ? "..." : this.axisList[i].name) + "\n"); } } } bool overwriteAll = false; bool igonrePresentAxes = false; if (alreadyPresentAxisNum > 0) { int overwriteMethod = EditorUtility.DisplayDialogComplex(DIALOG_TITLE, "" + alreadyPresentAxisNum + " out of " + enabledAxisNum + " selected axes are already present in selected Input Rig.\n\n" + presentAxisNames + "\n" + "What do you want to do with them?", "Overwrite All", "Ignore All", "Choose one by one"); if (overwriteMethod == 1) { igonrePresentAxes = true; } else if (overwriteMethod == 0) { overwriteAll = true; } } // Apply... CFGUI.CreateUndo("Transfer axes from Input Manager to Input Rig", this.rig); for (int i = 0; i < this.axisList.Count; ++i) { AxisDescription axisDesc = this.axisList[i]; if (!axisDesc.enabled) { continue; } InputRig.AxisConfig axisConfig = this.rig.GetAxisConfig(axisDesc.name); if (axisConfig != null) { if (igonrePresentAxes) { continue; } if (!overwriteAll && !EditorUtility.DisplayDialog(DIALOG_TITLE, "Transfer and overwrite [" + axisDesc.name + "] axis?", "Transfer", "Skip")) { continue; } axisConfig.axisType = axisDesc.targetAxisType; } else { axisConfig = this.rig.axes.Add(axisDesc.name, axisDesc.targetAxisType, false); } axisConfig.keyboardNegative = KeyCode.None; axisConfig.keyboardNegativeAlt0 = KeyCode.None; axisConfig.keyboardPositive = KeyCode.None; axisConfig.keyboardPositiveAlt0 = KeyCode.None; axisConfig.scale = 1; axisConfig.digitalToAnalogDecelTime = 0; axisConfig.digitalToAnalogAccelTime = 0; axisConfig.smoothingTime = 0; axisConfig.rawSmoothingTime = 0; axisConfig.snap = false; for (int ai = 0; ai < axisDesc.inputAxes.Count; ++ai) { UnityInputManagerUtils.InputAxis inputAxis = axisDesc.inputAxes[ai]; switch (inputAxis.type) { case UnityInputManagerUtils.AxisType.KeyOrMouseButton: KeyCode positiveCode = InputRig.NameToKeyCode(!inputAxis.invert ? inputAxis.positiveButton : inputAxis.negativeButton), positiveAltCode = InputRig.NameToKeyCode(!inputAxis.invert ? inputAxis.altPositiveButton : inputAxis.altNegativeButton), negativeCode = InputRig.NameToKeyCode(!inputAxis.invert ? inputAxis.negativeButton : inputAxis.positiveButton), negativeAltCode = InputRig.NameToKeyCode(!inputAxis.invert ? inputAxis.altNegativeButton : inputAxis.altPositiveButton); if ((positiveCode != KeyCode.None) && !UnityInputManagerUtils.IsJoystickKeyCode(positiveCode)) { axisConfig.keyboardPositive = positiveCode; axisConfig.affectedKeyPositive = positiveCode; } if ((positiveAltCode != KeyCode.None) && !UnityInputManagerUtils.IsJoystickKeyCode(positiveAltCode)) { axisConfig.keyboardPositiveAlt0 = positiveAltCode; } if ((negativeCode != KeyCode.None) && !UnityInputManagerUtils.IsJoystickKeyCode(negativeCode)) { axisConfig.keyboardNegative = negativeCode; axisConfig.affectedKeyNegative = negativeCode; } if ((negativeAltCode != KeyCode.None) && !UnityInputManagerUtils.IsJoystickKeyCode(negativeAltCode)) { axisConfig.keyboardNegativeAlt0 = negativeAltCode; } if (inputAxis.snap) { axisConfig.snap = true; } break; case UnityInputManagerUtils.AxisType.JoystickAxis: break; case UnityInputManagerUtils.AxisType.MouseMovement: { // Mouse Delta... if ((inputAxis.axis == UnityInputManagerUtils.MOUSE_X_AXIS_ID) || (inputAxis.axis == UnityInputManagerUtils.MOUSE_Y_AXIS_ID)) { ControlFreak2.Internal.AxisBinding mouseDeltaBinding = (inputAxis.axis == UnityInputManagerUtils.MOUSE_X_AXIS_ID) ? this.rig.mouseConfig.horzDeltaBinding : this.rig.mouseConfig.vertDeltaBinding; mouseDeltaBinding.Clear(); mouseDeltaBinding.Enable(); //.enabled = true; mouseDeltaBinding.AddTarget().SetSingleAxis(axisDesc.name, inputAxis.invert); //, axisDesc.inputAxis.invert); //mouseDeltaBinding.separateAxes = false; //mouseDeltaBinding.singleAxis = axisDesc.name; axisConfig.scale = inputAxis.sensitivity; } // Scroll wheel... else if ((inputAxis.axis == UnityInputManagerUtils.SCROLL_PRIMARY_AXIS_ID) || (inputAxis.axis == UnityInputManagerUtils.SCROLL_SECONDARY_AXIS_ID)) { ControlFreak2.Internal.AxisBinding scrollBinding = (inputAxis.axis == UnityInputManagerUtils.SCROLL_PRIMARY_AXIS_ID) ? this.rig.scrollWheel.vertScrollDeltaBinding.deltaBinding : this.rig.scrollWheel.horzScrollDeltaBinding.deltaBinding; scrollBinding.Clear(); scrollBinding.AddTarget().SetSingleAxis(axisDesc.name, inputAxis.invert); //scrollBinding.enabled = true; //scrollBinding.separateAxes = false; //scrollBinding.singleAxis = axisDesc.name; } } break; } } // Set mouse delta scaling... if (axisDesc.targetAxisType == InputRig.AxisType.Delta) { axisConfig.deltaMode = InputRig.DeltaTransformMode.EmulateMouse; axisConfig.scale = axisDesc.inputAxis.sensitivity; } // Convert gravity to smoothing time... else if ((axisDesc.targetAxisType == InputRig.AxisType.SignedAnalog) || (axisDesc.targetAxisType == InputRig.AxisType.UnsignedAnalog)) { float gravity = 0, sensitivity = 0; // Find biggest gravity and sensitivity... for (int di = 0; di < axisDesc.inputAxes.Count; ++di) { if (axisDesc.inputAxes[di].type != UnityInputManagerUtils.AxisType.KeyOrMouseButton) { continue; } gravity = Mathf.Max(gravity, axisDesc.inputAxes[di].gravity); sensitivity = Mathf.Max(sensitivity, axisDesc.inputAxes[di].sensitivity); } // Convert graivty and sensitivity to digiAccel/Decel times... axisConfig.digitalToAnalogDecelTime = ((gravity < 0.001f) ? 0.2f : (1.0f / gravity)); axisConfig.digitalToAnalogAccelTime = ((sensitivity < 0.001f) ? 0.2f : (1.0f / sensitivity)); axisConfig.smoothingTime = 0; axisConfig.rawSmoothingTime = 0; //if (axisDesc.inputAxis.gravity > 0.1f) // axisConfig.smoothingTime = Mathf.Min(1.0f, (1.0f / axisDesc.inputAxis.gravity)); } //if (axisDesc.inputAxis.invert) // axisConfig.scale = -axisConfig.scale; } CFGUI.EndUndo(this.rig); }
/// <summary> /// Registers a calculated axis from two buttons. The axis' value changes between -1 and 1 as the user hits the button or releases it. /// The time it takes to change the value can be set. /// </summary> /// <param name="origButtonIdNegative">The original button identifier for negative movements.</param> /// <param name="origButtonIdPositive">The original button identifier for positive movements.</param> /// <param name="direction">The direction the new axis is heading towards.</param> /// <param name="rampUpTime">The time it takes to change the value from 0 to 1 (or -1) (in seconds) when one of the buttons is pushed.</param> /// <param name="rampDownTime">The time it takes to change the value from -1 of 1 back to 0 (in seconds) when a pushed button is released.</param> /// <param name="buttonAxisId">The new identifier of the button axis. Note this value must be bigger than all existing axis Ids. Leave this value /// zero to have a new identifier calculated automatically.</param> /// <param name="name">The name of the new axis.</param> /// <returns> /// The axis description of the newly created calculated axis. /// </returns> /// <remarks> /// Button axes are useful to simulate one axis of a joypad or a joystick with the help of two individual buttons. One button acts as pushing the /// joystick into the positve direction along the given axis by animating the axis' value to 1 and the a second button acts as pushing the joystick /// into the negative direction by animating the value to -1. Releasing both buttons will animate the value to 0. Pushing both buttons simultaneously /// will stop the animation and keep the value at its current amount. /// There is a user-defineable acceleration and deceleration period, so a simulation resulting on this input delivers a feeling of inertance. /// </remarks> public AxisDescription RegisterTwoButtonAxis(int origButtonIdNegative, int origButtonIdPositive, AxisDirection direction = AxisDirection.Unknown, float rampUpTime = 0.15f, float rampDownTime = 0.35f, int buttonAxisId = 0, string name = null) { ButtonDescription origButtonDescPos; if (!_buttons.TryGetValue(origButtonIdPositive, out origButtonDescPos)) { throw new InvalidOperationException($"Button Id {origButtonIdPositive} is not known. Cannot register button axis based on unknown button."); } ButtonDescription origButtonDescNeg; if (!_buttons.TryGetValue(origButtonIdNegative, out origButtonDescNeg)) { throw new InvalidOperationException($"Button Id {origButtonIdNegative} is not known. Cannot register button axis based on unknown button."); } // Ramp cannot be 90° as this would require special case handling if (rampUpTime <= float.MinValue) { rampUpTime = 2 * float.MinValue; } if (rampDownTime <= float.MinValue) { rampDownTime = 2 * float.MinValue; } bool closureLastBtnStatePos = GetButton(origButtonIdPositive); bool closureLastBtnStateNeg = GetButton(origButtonIdNegative); float closureLastAxisValue = 0; float closureAnimDirection = 0; AxisValueCalculator calculator = delegate(float deltaTime) { float ret; bool newBtnStatePos = GetButton(origButtonIdPositive); if (newBtnStatePos != closureLastBtnStatePos) { // The state of the button has changed closureAnimDirection = (newBtnStatePos ? 1 : 0) - (closureLastBtnStatePos ? 1 : 0); closureLastBtnStatePos = newBtnStatePos; } bool newBtnStateNeg = GetButton(origButtonIdNegative); if (newBtnStateNeg != closureLastBtnStateNeg) { // The state of the button has changed closureAnimDirection = -((newBtnStateNeg ? 1 : 0) - (closureLastBtnStateNeg ? 1 : 0)); closureLastBtnStateNeg = newBtnStateNeg; } // No button pressed: Goal is 0 (middle) and speed is rampdown var animGoal = 0.0f; var animTime = rampDownTime; if (newBtnStatePos || newBtnStateNeg) { // Some button pressed: Goal is -1 or 1 and speed is rampup animGoal = closureAnimDirection; animTime = rampUpTime; } if (closureAnimDirection > 0) { ret = closureLastAxisValue + deltaTime / animTime; if (ret >= animGoal) { closureAnimDirection = 0; ret = animGoal; } } else if (closureAnimDirection < 0) { ret = closureLastAxisValue - deltaTime / animTime; if (ret <= animGoal) { closureAnimDirection = 0; ret = animGoal; } } else { ret = closureLastAxisValue; } closureLastAxisValue = ret; return(ret); }; int id = _nextAxisId + 1; if (buttonAxisId > id) { id = buttonAxisId; } var calculatedAxisDesc = new AxisDescription { Id = id, Name = name ?? $"{origButtonDescPos.Name} {origButtonDescNeg.Name} Axis", Bounded = AxisBoundedType.Constant, Direction = direction, Nature = AxisNature.Speed, MaxValueOrAxis = 1.0f, MinValueOrAxis = -1.0f }; RegisterCalculatedAxis(calculatedAxisDesc, calculator); return(calculatedAxisDesc); }
/// <summary> /// Registers a calculated axis from a button. The axis' value changes between 0 and 1 as the user hits the button or releases it. /// The time it takes to change the value can be set. /// </summary> /// <param name="origButtonId">The original button identifier.</param> /// <param name="direction">The direction the new axis is heading towards.</param> /// <param name="rampUpTime">The time it takes to change the value from 0 to 1 (in seconds).</param> /// <param name="rampDownTime">The time it takes to change the value from 1 to 0 (in seconds).</param> /// <param name="buttonAxisId">The new identifier of the button axis. Note this value must be bigger than all existing axis Ids. Leave this value /// zero to have a new identifier calculated automatically.</param> /// <param name="name">The name of the new axis.</param> /// <returns>The axis description of the newly created calculated axis.</returns> /// <remarks> /// Button axes are useful to simulate a trigger or thrust panel with the help of individual buttons. There is a user-defineable acceleration and /// deceleration period, so a simulation resulting on this input delivers a feeling of inertance. /// </remarks> public AxisDescription RegisterSingleButtonAxis(int origButtonId, AxisDirection direction = AxisDirection.Unknown, float rampUpTime = 0.2f, float rampDownTime = 0.2f, int buttonAxisId = 0, string name = null) { ButtonDescription origButtonDesc; if (!_buttons.TryGetValue(origButtonId, out origButtonDesc)) { throw new InvalidOperationException($"Button Id {origButtonId} is not known. Cannot register button axis based on unknown button."); } // Ramp cannot be 90° as this would require special case handling if (rampUpTime <= float.MinValue) { rampUpTime = 2 * float.MinValue; } if (rampDownTime <= float.MinValue) { rampDownTime = 2 * float.MinValue; } bool closureLastBtnState = GetButton(origButtonId); float closureLastAxisValue = 0; float closureAnimDirection = 0; AxisValueCalculator calculator = delegate(float deltaTime) { float ret; bool newBtnState = GetButton(origButtonId); if (newBtnState != closureLastBtnState) { // The state of the button has changed closureAnimDirection = (newBtnState ? 0 : 1) - (closureLastBtnState ? 0 : 1); closureLastBtnState = newBtnState; } if (closureAnimDirection > 0) { ret = closureLastAxisValue + deltaTime / rampUpTime; if (ret >= 1) { closureAnimDirection = 0; ret = 1; } } else if (closureAnimDirection < 0) { ret = closureLastAxisValue - deltaTime / rampDownTime; if (ret < 0) { closureAnimDirection = 0; ret = 0; } } else { ret = closureLastAxisValue; } closureLastAxisValue = ret; return(ret); }; int id = _nextAxisId + 1; if (buttonAxisId > id) { id = buttonAxisId; } var calculatedAxisDesc = new AxisDescription { Id = id, Name = name ?? $"{origButtonDesc.Name} Axis", Bounded = AxisBoundedType.Constant, Direction = direction, Nature = AxisNature.Speed, MaxValueOrAxis = 1.0f, MinValueOrAxis = 0.0f }; RegisterCalculatedAxis(calculatedAxisDesc, calculator); return(calculatedAxisDesc); }
/// <summary> /// Registers a calculated axis exhibiting the derivative after the time (Velocity) of the value on the specified original axis. /// </summary> /// <param name="origAxisId">The original axis identifier.</param> /// <param name="triggerButtonId">If a valid id is passed, the derived axis only produces values if the specified button is pressed. The velocity is only /// calculated based on the axis value when the trigger button is pressed. This allows touch velocities to always start with a speed of zero when the touch starts (e.g. the /// button identifying that a touchpoint has contact). Otherwise touch velocites would become huge between two click-like touches on different screen locations. /// If this parameter is 0 (zero), the derived axis will always be calculated based on the original axis only.</param> /// <param name="velocityAxisId">The derived axis identifier. Note this value must be bigger than all existing axis Ids. Leave this value /// zero to have a new identifier calculated automatically.</param> /// <param name="name">The name of the new axis.</param> /// <param name="direction">The direction of the new axis.</param> /// <returns> /// The axis description of the newly created calculated axis. /// </returns> /// <remarks> /// A derived axis is helpful if you have a device delivering absolute positional values but you need the current /// speed of the axis. Imagine a mouse where the speed of the mouse over the screen is important rather than the absolute /// position. /// </remarks> public AxisDescription RegisterVelocityAxis(int origAxisId, int triggerButtonId = 0, int velocityAxisId = 0, string name = null, AxisDirection direction = AxisDirection.Unknown) { AxisDescription origAxisDesc; if (!_axes.TryGetValue(origAxisId, out origAxisDesc)) { throw new InvalidOperationException($"Axis Id {origAxisId} is not known. Cannot register derived axis based on unknown axis."); } //switch (origAxisDesc.Bounded) //{ // case AxisBoundedType.Constant: // scale = 1.0f / (origAxisDesc.MaxValueOrAxis - origAxisDesc.MinValueOrAxis); // break; // case AxisBoundedType.OtherAxis: // scale = 1.0f / (GetAxis((int)origAxisDesc.MaxValueOrAxis) - GetAxis((int)origAxisDesc.MinValueOrAxis)); // break; //} AxisValueCalculator calculator; if (triggerButtonId != 0) { ButtonDescription triggerButtonDesc; if (!_buttons.TryGetValue(triggerButtonId, out triggerButtonDesc)) { throw new InvalidOperationException($"Button Id {triggerButtonId} is not known. Cannot register derived axis based on unknown trigger button id."); } float closureLastValue = GetAxis(origAxisId); bool closureOffLastTime = true; calculator = delegate(float deltaTime) { if (deltaTime <= float.Epsilon) // avoid infinite velocites { return(0); } if (!GetButton(triggerButtonId)) { closureOffLastTime = true; return(0.0f); } if (closureOffLastTime) { closureLastValue = GetAxis(origAxisId); closureOffLastTime = false; } float newVal = GetAxis(origAxisId); float ret = (newVal - closureLastValue) / deltaTime; // v = dr / dt: velocity is position derived after time. closureLastValue = newVal; return(ret); }; } else { float closureLastValue = GetAxis(origAxisId); calculator = delegate(float deltaTime) { if (deltaTime <= float.Epsilon) // avoid infinite velocites { return(0); } float newVal = GetAxis(origAxisId); float ret = (newVal - closureLastValue) / deltaTime; // v = dr / dt: velocity is position derived after time. closureLastValue = newVal; return(ret); }; } int id = _nextAxisId + 1; if (velocityAxisId > id) { id = velocityAxisId; } var calculatedAxisDesc = new AxisDescription { Id = id, Name = name ?? origAxisDesc.Name + " Velocity", Direction = (direction == AxisDirection.Unknown) ? origAxisDesc.Direction : direction, Nature = AxisNature.Speed, Bounded = AxisBoundedType.Unbound, MaxValueOrAxis = float.NaN, MinValueOrAxis = float.NaN }; RegisterCalculatedAxis(calculatedAxisDesc, calculator); return(calculatedAxisDesc); }