Ejemplo n.º 1
0
  override protected void OnUserBeginInteracting() {
    GrabWidgetData data = WidgetManager.m_Instance.GetCurrentCameraPath();
    m_EatInteractingInput = (data == null) ? false : data.m_WidgetScript != this;
    WidgetManager.m_Instance.SetCurrentCameraPath(this);

    Debug.Assert(m_InteractingController == InputManager.ControllerName.Brush ||
        m_InteractingController == InputManager.ControllerName.Wand);
    m_ActiveKnot = m_LastValidCollisionResults[(int)m_InteractingController];

    // If we just grabbed a knot control, record the Y position of the controller grab point.
    if (m_ActiveKnot.control != CameraPathKnot.kDefaultControl) {
      BaseControllerBehavior b = InputManager.Controllers[(int)m_InteractingController].Behavior;
      if (m_ActiveKnot.knot.KnotType == CameraPathKnot.Type.Fov) {
        CameraPathFovKnot fovKnot = m_ActiveKnot.knot as CameraPathFovKnot;
        m_GrabControlInitialYDiff = b.PointerAttachPoint.transform.position.y -
            fovKnot.GetGrabTransform(
              (int)CameraPathFovKnot.ControlType.FovControl).position.y;
      }
      if (m_ActiveKnot.knot.KnotType == CameraPathKnot.Type.Speed) {
        CameraPathSpeedKnot speedKnot = m_ActiveKnot.knot as CameraPathSpeedKnot;
        m_GrabControlInitialYDiff = b.PointerAttachPoint.transform.position.y -
            speedKnot.GetGrabTransform(
              (int)CameraPathSpeedKnot.ControlType.SpeedControl).position.y;
      }
    }
  }
Ejemplo n.º 2
0
 public void HighlightEntirePath() {
   CameraPathTinter t = WidgetManager.m_Instance.PathTinter;
   for (int i = 0; i < Path.PositionKnots.Count; ++i) {
     CameraPathPositionKnot pk = Path.PositionKnots[i];
     pk.RegisterHighlight(0, true);
     t.TintKnot(pk);
   }
   for (int i = 0; i < Path.RotationKnots.Count; ++i) {
     CameraPathRotationKnot rk = Path.RotationKnots[i];
     rk.RegisterHighlight(0, true);
     t.TintKnot(rk);
   }
   for (int i = 0; i < Path.SpeedKnots.Count; ++i) {
     CameraPathSpeedKnot sk = Path.SpeedKnots[i];
     sk.RegisterHighlight(0, true);
     t.TintKnot(sk);
   }
   for (int i = 0; i < Path.FovKnots.Count; ++i) {
     CameraPathFovKnot fk = Path.FovKnots[i];
     fk.RegisterHighlight(0, true);
     t.TintKnot(fk);
   }
   for (int i = 0; i < Path.Segments.Count; ++i) {
     t.TintSegment(m_Path.Segments[i]);
   }
 }
Ejemplo n.º 3
0
  override protected void OnUserEndInteracting() {
    base.OnUserEndInteracting();

    if (!m_EatInteractingInput) {
      // Finalize our move commands with the current state of whatever's being actively modified.
      switch (m_ActiveKnot.knot.KnotType) {
      case CameraPathKnot.Type.Position:
        if (m_ActiveKnot.control == 0) {
          SketchMemoryScript.m_Instance.PerformAndRecordCommand(
              new MovePositionKnotCommand(m_Path, m_ActiveKnot,
                TrTransform.FromTransform(m_ActiveKnot.knot.transform), true));
        } else {
          CameraPathPositionKnot pk = m_ActiveKnot.knot as CameraPathPositionKnot;
          Vector3 knotFwd = m_ActiveKnot.knot.transform.forward;
          SketchMemoryScript.m_Instance.PerformAndRecordCommand(
              new ModifyPositionKnotCommand(
                m_Path, m_ActiveKnot, pk.TangentMagnitude, knotFwd, final:true));
        }
        break;
      case CameraPathKnot.Type.Rotation:
      case CameraPathKnot.Type.Speed:
      case CameraPathKnot.Type.Fov:
        // Reset any PreviewWidget overrides.
        m_ActiveKnot.knot.gameObject.SetActive(true);
        InputManager.Wand.Geometry.PreviewKnotHint.Activate(false);
        InputManager.Brush.Geometry.PreviewKnotHint.Activate(false);
        SketchControlsScript.m_Instance.CameraPathCaptureRig.OverridePreviewWidgetPathT(null);

        if (m_ActiveKnot.control == 0) {
          SketchMemoryScript.m_Instance.PerformAndRecordCommand(
              new MoveConstrainedKnotCommand(m_Path, m_ActiveKnot,
                m_ActiveKnot.knot.transform.rotation, final:true));
        } else {
          if (m_ActiveKnot.knot.KnotType == CameraPathKnot.Type.Speed) {
            CameraPathSpeedKnot sk = m_ActiveKnot.knot as CameraPathSpeedKnot;
            SketchMemoryScript.m_Instance.PerformAndRecordCommand(
                new ModifySpeedKnotCommand(sk, sk.SpeedValue));
          } else if (m_ActiveKnot.knot.KnotType == CameraPathKnot.Type.Fov) {
            CameraPathFovKnot fk = m_ActiveKnot.knot as CameraPathFovKnot;
            SketchMemoryScript.m_Instance.PerformAndRecordCommand(
                new ModifyFovKnotCommand(fk, fk.FovValue));
          }
        }
        break;
      }
    }

    m_KnotEditingLastInputXf = null;
    m_ActiveKnot = null;
  }
Ejemplo n.º 4
0
        override public void UpdateTool()
        {
            base.UpdateTool();

            // If we're in the recording state, just look for cancel and get out.
            if (CurrentMode == Mode.Recording)
            {
                if (InputManager.m_Instance.GetCommandDown(InputManager.SketchCommands.MenuContextClick))
                {
                    SketchControlsScript.m_Instance.CameraPathCaptureRig.StopRecordingPath(false);
                }
                return;
            }

            var       widgets      = WidgetManager.m_Instance.CameraPathWidgets;
            Transform toolAttachXf = InputManager.Brush.Geometry.ToolAttachPoint;

            bool input     = InputManager.m_Instance.GetCommand(InputManager.SketchCommands.Activate);
            bool inputDown = InputManager.m_Instance.GetCommandDown(InputManager.SketchCommands.Activate);

            // Tint any path we're intersecting with.
            if (!input && m_LastValidPath != null && m_Mode != Mode.RemoveKnot)
            {
                m_LastValidPath.TintSegments(m_LastValidPosition);
            }

            // Initiating input.
            if (inputDown)
            {
                // We clicked, but the path we clicked on isn't the active path.  In that case,
                // switch it to the active path and eat up this input.
                // Don't do this for removing knots.  That input should be explicit.
                if (m_Mode != Mode.RemoveKnot && m_LastValidPath != null)
                {
                    GrabWidgetData data = WidgetManager.m_Instance.GetCurrentCameraPath();
                    bool           lastValidIsCurrent = (data == null) ? false : data.m_WidgetScript == m_LastValidPath;
                    if (!lastValidIsCurrent)
                    {
                        WidgetManager.m_Instance.SetCurrentCameraPath(m_LastValidPath);
                        return;
                    }
                }

                switch (m_Mode)
                {
                case Mode.AddPositionKnot:
                    // Create a new path if none exists or if we're trying to add a position point
                    // in a place where we're not extending an existing path.
                    if (!WidgetManager.m_Instance.AnyCameraPathWidgetsActive ||
                        (m_LastValidPath == null && m_ExtendPath == null))
                    {
                        m_ExtendPath     = WidgetManager.m_Instance.CreatePathWidget();
                        m_ExtendPathType = ExtendPathType.ExtendAtHead;
                        WidgetManager.m_Instance.SetCurrentCameraPath(m_ExtendPath);
                    }

                    if (m_LastValidPath != null)
                    {
                        m_LastValidPath.AddPathConstrainedKnot(
                            CameraPathKnot.Type.Position, m_LastValidPosition, toolAttachXf.rotation);
                        m_LastPlacedKnot     = m_LastValidPath.Path.LastPlacedKnotInfo;
                        m_LastPlacedKnotPath = m_LastValidPath;
                    }
                    else if (m_ExtendPath != null)
                    {
                        // Manipulation of a path we wish to extend.
                        m_ExtendPath.ExtendPath(toolAttachXf.position, m_ExtendPathType);

                        // Remember the index of the path we just added to, so we can manipulate it
                        // while input is held.
                        // Don't record this if we just made our path loop.
                        if (!m_ExtendPath.Path.PathLoops)
                        {
                            m_LastPlacedKnot     = m_ExtendPath.Path.LastPlacedKnotInfo;
                            m_LastPlacedKnotPath = m_ExtendPath;
                        }
                    }
                    break;

                case Mode.AddRotationKnot:
                    if (m_LastValidPath != null)
                    {
                        m_LastValidPath.AddPathConstrainedKnot(
                            CameraPathKnot.Type.Rotation, m_LastValidPosition, toolAttachXf.rotation);
                        m_LastPlacedKnot     = m_LastValidPath.Path.LastPlacedKnotInfo;
                        m_LastPlacedKnotPath = m_LastValidPath;
                    }
                    break;

                case Mode.AddSpeedKnot:
                    if (m_LastValidPath != null)
                    {
                        m_LastValidPath.AddPathConstrainedKnot(
                            CameraPathKnot.Type.Speed, m_LastValidPosition, toolAttachXf.rotation);
                        m_LastPlacedKnot     = m_LastValidPath.Path.LastPlacedKnotInfo;
                        m_LastPlacedKnotPath = m_LastValidPath;
                    }
                    break;

                case Mode.AddFovKnot:
                    if (m_LastValidPath != null)
                    {
                        m_LastValidPath.AddPathConstrainedKnot(
                            CameraPathKnot.Type.Fov, m_LastValidPosition, toolAttachXf.rotation);
                        m_LastPlacedKnot     = m_LastValidPath.Path.LastPlacedKnotInfo;
                        m_LastPlacedKnotPath = m_LastValidPath;
                    }
                    break;

                case Mode.RemoveKnot:
                    CheckToRemoveKnot(toolAttachXf.position);
                    break;
                }

                // Remember what our controller looked like so we can manipulate this knot.
                if (m_LastPlacedKnot != null)
                {
                    Transform   controller  = InputManager.Brush.Transform;
                    Transform   knotXf      = m_LastPlacedKnot.knot.transform;
                    TrTransform newWidgetXf = Coords.AsGlobal[knotXf];
                    m_LastPlacedKnotXf_LS = Coords.AsGlobal[controller].inverse * newWidgetXf;
                    HideAllMeshes();
                }
            }
            else if (input)
            {
                if (m_Mode == Mode.RemoveKnot)
                {
                    CheckToRemoveKnot(toolAttachXf.position);
                }
                else if (m_LastPlacedKnot != null)
                {
                    // Holding input from last frame can allow us to manipulate a just placed position knot.
                    WidgetManager.m_Instance.PathTinter.TintKnot(m_LastPlacedKnot.knot);

                    TrTransform controllerXf = Coords.AsGlobal[InputManager.Brush.Transform];
                    TrTransform inputXf      = controllerXf * m_LastPlacedKnotXf_LS;

                    switch (m_LastPlacedKnot.knot.KnotType)
                    {
                    case CameraPathKnot.Type.Position:
                        if (m_LastPlacedKnot.control != 0)
                        {
                            CameraPathPositionKnot pk = m_LastPlacedKnot.knot as CameraPathPositionKnot;
                            float   tangentMag        = pk.GetTangentMagnitudeFromControlXf(inputXf);
                            Vector3 knotFwd           =
                                (inputXf.translation - m_LastPlacedKnot.knot.transform.position).normalized;
                            if ((CameraPathPositionKnot.ControlType)m_LastPlacedKnot.control ==
                                CameraPathPositionKnot.ControlType.TangentControlBack)
                            {
                                knotFwd *= -1.0f;
                            }

                            SketchMemoryScript.m_Instance.PerformAndRecordCommand(
                                new ModifyPositionKnotCommand(
                                    m_LastPlacedKnotPath.Path, m_LastPlacedKnot, tangentMag, knotFwd,
                                    mergesWithCreateCommand: true));
                        }
                        break;

                    case CameraPathKnot.Type.Rotation:
                        // Rotation knots hide when we grab them, and in their place, we set the preview widget.
                        m_LastPlacedKnot.knot.gameObject.SetActive(false);
                        SketchControlsScript.m_Instance.CameraPathCaptureRig.OverridePreviewWidgetPathT(
                            m_LastPlacedKnot.knot.PathT);
                        SketchMemoryScript.m_Instance.PerformAndRecordCommand(
                            new MoveConstrainedKnotCommand(m_LastPlacedKnotPath.Path, m_LastPlacedKnot,
                                                           inputXf.rotation, mergesWithCreateCommand: true));
                        break;

                    case CameraPathKnot.Type.Speed:
                        CameraPathSpeedKnot sk = m_LastPlacedKnot.knot as CameraPathSpeedKnot;
                        float speed            = sk.GetSpeedValueFromY(
                            InputManager.Brush.Behavior.PointerAttachPoint.transform.position.y);
                        SketchMemoryScript.m_Instance.PerformAndRecordCommand(
                            new ModifySpeedKnotCommand(sk, speed, mergesWithCreateCommand: true));
                        break;

                    case CameraPathKnot.Type.Fov:
                        CameraPathFovKnot fk  = m_LastPlacedKnot.knot as CameraPathFovKnot;
                        float             fov = fk.GetFovValueFromY(
                            InputManager.Brush.Behavior.PointerAttachPoint.transform.position.y);
                        SketchMemoryScript.m_Instance.PerformAndRecordCommand(
                            new ModifyFovKnotCommand(fk, fov, mergesWithCreateCommand: true));
                        break;
                    }
                }
            }
            else
            {
                // No input to work with.  Forget we had anything and make sure our meshes are showing.
                if (m_LastPlacedKnot != null)
                {
                    RefreshMeshVisibility();

                    // Rotation knots hide when we grab them, make sure it's enabled.
                    if (m_LastPlacedKnot.knot.KnotType == CameraPathKnot.Type.Rotation)
                    {
                        m_LastPlacedKnot.knot.gameObject.SetActive(true);
                        SketchControlsScript.m_Instance.CameraPathCaptureRig.OverridePreviewWidgetPathT(null);
                    }
                }
                m_LastPlacedKnot     = null;
                m_LastPlacedKnotPath = null;
            }
        }
Ejemplo n.º 5
0
  static public void CreateFromSaveData(CameraPathMetadata cameraPath) {
    // Create a new widget.
    CameraPathWidget widget = Instantiate<CameraPathWidget>(
        WidgetManager.m_Instance.CameraPathWidgetPrefab);
    widget.transform.parent = App.Scene.MainCanvas.transform;

    // The scale of path widgets is arbitrary.  However, the scale should be one at creation
    // time so the knots added below have appropriate mesh scales.
    widget.transform.localScale = Vector3.one;
    widget.transform.localPosition = Vector3.zero;
    widget.transform.localRotation = Quaternion.identity;

    // Add the path knots and set their tangent speed.
    for (int i = 0; i < cameraPath.PathKnots.Length; ++i) {
      GameObject go = Instantiate<GameObject>(
          WidgetManager.m_Instance.CameraPathPositionKnotPrefab);
      go.transform.position = cameraPath.PathKnots[i].Xf.translation;
      go.transform.rotation = cameraPath.PathKnots[i].Xf.rotation;
      go.transform.parent = widget.transform;

      CameraPathPositionKnot knot = go.GetComponent<CameraPathPositionKnot>();
      knot.TangentMagnitude = cameraPath.PathKnots[i].TangentMagnitude;

      widget.m_Path.PositionKnots.Add(knot);
      widget.m_Path.AllKnots.Add(knot);

      if (i > 0) {
        widget.m_Path.Segments.Add(CameraPath.CreateSegment(widget.transform));
      }
    }

    // Refresh the path so the segment curves are correct.
    for (int i = 0; i < cameraPath.PathKnots.Length - 1; ++i) {
      widget.m_Path.RefreshSegment(i);
    }

    // Add the rotation knots.  Note this list is ordered, and they're serialized in order,
    // so we need to make sure they're created in order.
    for (int i = 0; i < cameraPath.RotationKnots.Length; ++i) {
      GameObject go = Instantiate<GameObject>(
          WidgetManager.m_Instance.CameraPathRotationKnotPrefab);
      go.transform.position = cameraPath.RotationKnots[i].Xf.translation;
      go.transform.rotation = cameraPath.RotationKnots[i].Xf.rotation;
      go.transform.parent = widget.transform;

      CameraPathRotationKnot knot = go.GetComponent<CameraPathRotationKnot>();
      knot.PathT = new PathT(cameraPath.RotationKnots[i].PathTValue);
      knot.DistanceAlongSegment = widget.m_Path.GetSegmentDistanceToT(knot.PathT);

      widget.m_Path.RotationKnots.Add(knot);
      widget.m_Path.AllKnots.Add(knot);
    }
    // Align quaternions on all rotation knots so we don't have unexpected camera flips
    // when calculating rotation as we walk the path.
    widget.m_Path.RefreshRotationKnotPolarities();

    // Add the speed knots.  Note this list is ordered, and they're serialized in order,
    // so we need to make sure they're created in order.
    for (int i = 0; i < cameraPath.SpeedKnots.Length; ++i) {
      GameObject go = Instantiate<GameObject>(
          WidgetManager.m_Instance.CameraPathSpeedKnotPrefab);
      go.transform.position = cameraPath.SpeedKnots[i].Xf.translation;
      go.transform.rotation = cameraPath.SpeedKnots[i].Xf.rotation;
      go.transform.parent = widget.transform;

      CameraPathSpeedKnot knot = go.GetComponent<CameraPathSpeedKnot>();

      knot.PathT = new PathT(cameraPath.SpeedKnots[i].PathTValue);
      knot.DistanceAlongSegment = widget.m_Path.GetSegmentDistanceToT(knot.PathT);
      knot.SpeedValue = cameraPath.SpeedKnots[i].Speed;

      widget.m_Path.SpeedKnots.Add(knot);
      widget.m_Path.AllKnots.Add(knot);
    }

    // Add the fov knots.  Note this list is ordered, and they're serialized in order,
    // so we need to make sure they're created in order.
    for (int i = 0; i < cameraPath.FovKnots.Length; ++i) {
      GameObject go = Instantiate<GameObject>(
          WidgetManager.m_Instance.CameraPathFovKnotPrefab);
      go.transform.position = cameraPath.FovKnots[i].Xf.translation;
      go.transform.rotation = cameraPath.FovKnots[i].Xf.rotation;
      go.transform.parent = widget.transform;

      CameraPathFovKnot knot = go.GetComponent<CameraPathFovKnot>();

      knot.PathT = new PathT(cameraPath.FovKnots[i].PathTValue);
      knot.DistanceAlongSegment = widget.m_Path.GetSegmentDistanceToT(knot.PathT);
      knot.FovValue = cameraPath.FovKnots[i].Fov;

      widget.m_Path.FovKnots.Add(knot);
      widget.m_Path.AllKnots.Add(knot);
    }

    // Refresh visuals on the whole path.
    for (int i = 0; i < widget.m_Path.AllKnots.Count; ++i) {
      widget.m_Path.AllKnots[i].RefreshVisuals();
      widget.m_Path.AllKnots[i].ActivateTint(false);
      widget.m_Path.AllKnots[i].SetActivePathVisuals(false);
    }

    // And turn them off.
    widget.m_Path.ValidatePathLooping();
    widget.m_Path.SetKnotsActive(false);
    App.Switchboard.TriggerCameraPathCreated();
  }
Ejemplo n.º 6
0
  override public float GetActivationScore(Vector3 point, InputManager.ControllerName name) {
    float nearestKnotScore = -1.0f;
    KnotDescriptor nearestResult = new KnotDescriptor();

    if (VideoRecorderUtils.ActiveVideoRecording == null &&
        !WidgetManager.m_Instance.WidgetsDormant &&
        !InputManager.m_Instance.GetCommand(InputManager.SketchCommands.Activate)) {
      // Check against all knots and put our results in storage bins.
      // Note that we're walking along the sorted lists instead of using m_Path.m_AllKnots.
      // This ensures the knotIndex stored in KnotCollisionResult is correct.
      for (int i = 0; i < m_Path.PositionKnots.Count; ++i) {
        int control = -1;
        CameraPathPositionKnot pk = m_Path.PositionKnots[i];
        float knotScore = pk.gameObject.activeSelf ?
            pk.CollisionWithPoint(point, out control) : -1.0f;
        if (knotScore > nearestKnotScore) {
          nearestResult.Set(pk, control, i, null);
          nearestKnotScore = knotScore;
        }
      }
      for (int i = 0; i < m_Path.RotationKnots.Count; ++i) {
        int control = -1;
        CameraPathRotationKnot rk = m_Path.RotationKnots[i];
        float knotScore = rk.CollisionWithPoint(point, out control);
        if (knotScore > nearestKnotScore) {
          nearestResult.Set(rk, control, null, rk.PathT);
          nearestKnotScore = knotScore;
        }
      }
      for (int i = 0; i < m_Path.SpeedKnots.Count; ++i) {
        int control = -1;
        CameraPathSpeedKnot sk = m_Path.SpeedKnots[i];
        float knotScore = sk.CollisionWithPoint(point, out control);
        if (knotScore > nearestKnotScore) {
          nearestResult.Set(sk, control, null, sk.PathT);
          nearestKnotScore = knotScore;
        }
      }
      for (int i = 0; i < m_Path.FovKnots.Count; ++i) {
        int control = -1;
        CameraPathFovKnot fk = m_Path.FovKnots[i];
        float knotScore = fk.CollisionWithPoint(point, out control);
        if (knotScore > nearestKnotScore) {
          nearestResult.Set(fk, control, null, fk.PathT);
          nearestKnotScore = knotScore;
        }
      }
    }

    bool brushOrWand = (name == InputManager.ControllerName.Brush) ||
        (name == InputManager.ControllerName.Wand);
    if (!m_UserInteracting) {
      if (brushOrWand) {
        m_LastCollisionResults[(int)name].Set(nearestResult);
        if ((nearestResult.knot != null) && (nearestResult.control != -1)) {
          m_LastValidCollisionResults[(int)name].Set(nearestResult);
        }
      }
    } else if (m_InteractingController != name && brushOrWand) {
      m_LastCollisionResults[(int)name].Set(null, CameraPathKnot.kDefaultControl, null, null);
    }
    return nearestKnotScore;
  }
Ejemplo n.º 7
0
  override public void RecordAndSetPosRot(TrTransform inputXf) {
    // Don't manipulate anything if we're eating input.
    if (m_EatInteractingInput) {
      return;
    }

    SnapEnabled = (m_ActiveKnot.knot.KnotType == CameraPathKnot.Type.Rotation ||
                   m_ActiveKnot.knot.KnotType == CameraPathKnot.Type.Position) &&
                  InputManager.Controllers[(int)m_InteractingController].GetCommand(
                      InputManager.SketchCommands.MenuContextClick);
    inputXf = GetDesiredTransform(inputXf);

    if (m_ActiveKnot.knot.KnotType == CameraPathKnot.Type.Position) {
      // Move the base knot.
      if (m_ActiveKnot.control == 0) {
        // If this knot is the tail or the head and we're within snapping distance of the other
        // end, snap to the transform.
        TrTransform snappedXf = inputXf;
        int positionKnot = m_ActiveKnot.positionKnotIndex.Value;
        if (positionKnot == 0 || positionKnot == Path.NumPositionKnots - 1) {
          int otherIndex = positionKnot == 0 ? Path.NumPositionKnots - 1 : 0;
          float distToOther = Vector3.Distance(inputXf.translation,
              Path.PositionKnots[otherIndex].KnotXf.position);
          if (distToOther < m_KnotSnapDistanceToEnd) {
            snappedXf.translation = Path.PositionKnots[otherIndex].KnotXf.position;
            snappedXf.rotation = Path.PositionKnots[otherIndex].KnotXf.rotation;
          }
        }

        SketchMemoryScript.m_Instance.PerformAndRecordCommand(
            new MovePositionKnotCommand(m_Path, m_ActiveKnot, snappedXf));
      } else {
        // Modify the knot tangents.
        CameraPathPositionKnot pk = m_ActiveKnot.knot as CameraPathPositionKnot;
        if (SnapEnabled) {
          Vector3 snappedTranslation = inputXf.translation;
          snappedTranslation.y = pk.transform.position.y;
          inputXf.translation = snappedTranslation;
        }
        float tangentMag = pk.GetTangentMagnitudeFromControlXf(inputXf);
        Vector3 knotFwd = (inputXf.translation - m_ActiveKnot.knot.transform.position).normalized;
        if ((CameraPathPositionKnot.ControlType)m_ActiveKnot.control ==
            CameraPathPositionKnot.ControlType.TangentControlBack) {
          knotFwd *= -1.0f;
        }
        SketchMemoryScript.m_Instance.PerformAndRecordCommand(
            new ModifyPositionKnotCommand(m_Path, m_ActiveKnot, tangentMag, knotFwd));
      }
      return;
    }

    // Constrain rotation and speed knots to the path.
    // Instead of testing the raw value that comes in from the controller position, test our
    // last valid path position plus any translation that's happened the past frame.  This
    // method keeps the test positions near the path, allowing continuous movement when the
    // user has moved beyond the intersection distance to the path.
    Vector3 positionToProject = inputXf.translation;
    if (m_KnotEditingLastInputXf.HasValue) {
      Vector3 translationDiff = inputXf.translation - m_KnotEditingLastInputXf.Value;
      positionToProject = m_ActiveKnot.knot.KnotXf.position + translationDiff;
    }
    m_KnotEditingLastInputXf = inputXf.translation;

    // Project transform on to the path to get t.
    Vector3 error = Vector3.zero;
    if (m_Path.ProjectPositionOnToPath(positionToProject, out PathT pathT, out error)) {
      // Move the base knot.
      if (m_ActiveKnot.control == 0) {
        // Path constrained knots are a little sticky on the ends of the path.  Knots very
        // near the ends *probably* want to be on the ends, and when there are small deltas
        // near the ends, it causes unwanted erratic curves.
        m_ActiveKnot.pathT = m_Path.MaybeSnapPathTToEnd(pathT, m_KnotSnapDistanceToEnd);

        // Rotation knots allow the user to place the preview widget at their position
        // for live preview.
        if (m_ActiveKnot.knot.KnotType == CameraPathKnot.Type.Rotation) {
          CheckForPreviewWidgetOverride(pathT);
        }

        SketchMemoryScript.m_Instance.PerformAndRecordCommand(
            new MoveConstrainedKnotCommand(m_Path, m_ActiveKnot, inputXf.rotation));
      } else {
        // Alternate controls.
        BaseControllerBehavior b = InputManager.Controllers[(int)m_InteractingController].Behavior;
        float controllerY = b.PointerAttachPoint.transform.position.y;

        if (m_ActiveKnot.knot.KnotType == CameraPathKnot.Type.Speed) {
          CameraPathSpeedKnot sk = m_ActiveKnot.knot as CameraPathSpeedKnot;
          float speed = sk.GetSpeedValueFromY(controllerY - m_GrabControlInitialYDiff);
          SketchMemoryScript.m_Instance.PerformAndRecordCommand(
              new ModifySpeedKnotCommand(sk, speed));
        } else if (m_ActiveKnot.knot.KnotType == CameraPathKnot.Type.Fov) {
          CameraPathFovKnot fk = m_ActiveKnot.knot as CameraPathFovKnot;
          float fov = fk.GetFovValueFromY(controllerY - m_GrabControlInitialYDiff);
          CheckForPreviewWidgetOverride(fk.PathT);
          SketchMemoryScript.m_Instance.PerformAndRecordCommand(
              new ModifyFovKnotCommand(fk, fov));
        }
      }
    }
    m_KnotEditingLastInputXf -= error;
  }