18

I am creating bezier curves with the following code. The curves can be extended to join several bezier curves by shift clicking in the scene view. My code has functionality for making the whole curve continuous or non-continuous. I realised that I need to make individual points (specifically anchor points) have this functionality.

I believe the most ideal way to go about this is creating a new class for the points with this functionality (making points continuous or non-continuous) since this can be used to add other properties that might be specific to the points. How can do this?

Path

[System.Serializable]
public class Path {

[SerializeField, HideInInspector]
List<Vector2> points;

[SerializeField, HideInInspector]
public bool isContinuous;

public Path(Vector2 centre)
{
    points = new List<Vector2>
    {
        centre+Vector2.left,
        centre+(Vector2.left+Vector2.up)*.5f,
        centre + (Vector2.right+Vector2.down)*.5f,
        centre + Vector2.right
    };
}

public Vector2 this[int i]
{
    get
    {
        return points[i];
    }
}

public int NumPoints
{
    get
    {
        return points.Count;
    }
}

public int NumSegments
{
    get
    {
        return (points.Count - 4) / 3 + 1;
    }
}

public void AddSegment(Vector2 anchorPos)
{
    points.Add(points[points.Count - 1] * 2 - points[points.Count - 2]);
    points.Add((points[points.Count - 1] + anchorPos) * .5f);
    points.Add(anchorPos);
}

public Vector2[] GetPointsInSegment(int i)
{
    return new Vector2[] { points[i * 3], points[i * 3 + 1], points[i * 3 + 2], points[i * 3 + 3] };
}

public void MovePoint(int i, Vector2 pos)
{

    if (isContinuous)
    { 

        Vector2 deltaMove = pos - points[i];
        points[i] = pos;

        if (i % 3 == 0)
        {
            if (i + 1 < points.Count)
            {
                points[i + 1] += deltaMove;
            }
            if (i - 1 >= 0)
            {
                points[i - 1] += deltaMove;
            }
        }
        else
        {
            bool nextPointIsAnchor = (i + 1) % 3 == 0;
            int correspondingControlIndex = (nextPointIsAnchor) ? i + 2 : i - 2;
            int anchorIndex = (nextPointIsAnchor) ? i + 1 : i - 1;

            if (correspondingControlIndex >= 0 && correspondingControlIndex < points.Count)
            {
                float dst = (points[anchorIndex] - points[correspondingControlIndex]).magnitude;
                Vector2 dir = (points[anchorIndex] - pos).normalized;
            points[correspondingControlIndex] = points[anchorIndex] + dir * dst;
                }
            }
        }
    }

    else {
         points[i] = pos;
    }
}

PathCreator

public class PathCreator : MonoBehaviour {

[HideInInspector]
public Path path;


public void CreatePath()
{
    path = new Path(transform.position);
}
}   

PathEditor

[CustomEditor(typeof(PathCreator))]
public class PathEditor : Editor {

PathCreator creator;
Path path;

public override void OnInspectorGUI()
{
    base.OnInspectorGUI();
    EditorGUI.BeginChangeCheck();

    bool continuousControlPoints = GUILayout.Toggle(path.isContinuous, "Set Continuous Control Points");
    if (continuousControlPoints != path.isContinuous)
    {
        Undo.RecordObject(creator, "Toggle set continuous controls");
        path.isContinuous = continuousControlPoints;
    }

    if (EditorGUI.EndChangeCheck())
    {
        SceneView.RepaintAll();
    }
}

void OnSceneGUI()
{
    Input();
    Draw();
}

void Input()
 {
    Event guiEvent = Event.current;
    Vector2 mousePos = HandleUtility.GUIPointToWorldRay(guiEvent.mousePosition).origin;

    if (guiEvent.type == EventType.MouseDown && guiEvent.button == 0 && guiEvent.shift)
    {
        Undo.RecordObject(creator, "Add segment");
        path.AddSegment(mousePos);
    }
}

void Draw()
{

    for (int i = 0; i < path.NumSegments; i++)
    {
        Vector2[] points = path.GetPointsInSegment(i);
        Handles.color = Color.black;
        Handles.DrawLine(points[1], points[0]);
        Handles.DrawLine(points[2], points[3]);
        Handles.DrawBezier(points[0], points[3], points[1], points[2], Color.green, null, 2);
    }

    Handles.color = Color.red;
    for (int i = 0; i < path.NumPoints; i++)
    {
        Vector2 newPos = Handles.FreeMoveHandle(path[i], Quaternion.identity, .1f, Vector2.zero, Handles.CylinderHandleCap);
        if (path[i] != newPos)
        {
            Undo.RecordObject(creator, "Move point");
            path.MovePoint(i, newPos);
        }
    }
}

void OnEnable()
{
    creator = (PathCreator)target;
    if (creator.path == null)
    {
        creator.CreatePath();
    }
    path = creator.path;
}
}
Programmer
  • 121,791
  • 22
  • 236
  • 328
  • I think that the curve is continuous if the point and both of its control points are on the same line. So if any of the three move off of that line one __or__ both of the others must be adjusted.. – TaW Jun 10 '18 at 21:34
  • @TaW That's true. I've been able to provide this functionality for the whole curve (regardless of the number of control points and anchors) as shown in the code above. My issue is I didn't factor doing this for only individual anchor points. I need this to be done in a smart way, most preferably a new `ControlPoints` or `Points` class that can be extended with other functionality which I may need in the future. – SuperHyperMegaSomething Jun 10 '18 at 21:56
  • 2
    I see. I think I would use a AnchorPoint class that includes the two ControlPoints, links to the neighboring AnchorPoints on the path, probably the path itself, current state and restarints.. - As usual, I suggest making a list of use cases, as large as you can think of.. Maybe all points ought to inherit from a 'moveable point' class. – TaW Jun 11 '18 at 01:12
  • 2
    @TaW that should work out really well. I would really appreciate it if you could provide an answer with sample code based on your recommendation and the code I provided, for continuity and non-continuity. – SuperHyperMegaSomething Jun 11 '18 at 03:47
  • I don't do unity, so I'm afraid I can't quite do that. If you like I can write up a few thoughts, though. My own exposure to beziers comes form GDI+ GraphicsPath; [see for example here](https://stackoverflow.com/questions/50714327/edit-points-of-freeshape/50718793#50718793) – TaW Jun 11 '18 at 09:05
  • @TaW the Unity code I provided are C# scripts so I guess you should be able to find your way around it, just that you won't be able to test what you provide. I would really appreciate your thoughts on this, possibly with some example code on how the `AnchorPoint` would look. – SuperHyperMegaSomething Jun 11 '18 at 20:59
  • 2
    OK, I'll try to write up an answer to add to the pool. In fact the topic has intrgued me so much, I am in the process of writing an small path editor to work with GDI+ GraphicsPath. For this I guess one or two point classes are called for. The result could be relevant for your framework as well.. - I'll keep you posted.. – TaW Jun 13 '18 at 15:44

2 Answers2

9

I think your idea is fine: you can write two classes, named ControlPoint and HandlePoint (make them serializable).

ControlPoint may represent p0 and p3 of each curve - the points the path indeed pass through. For continuity, you must assert that p3 of one segment equals to p0 of the next segment.

HandlePoint may represent p1 and p2 of each curve - the points that are tangents of the curve and provide direction and inclination. For smoothness, you must assert that (p3 - p2).normalized of one segment equals to (p1 - p0).normalized of the next segment. (if you want symetric smoothness, p3 - p2 of one must equals p1 - p0 of the other.)

Tip #1: Always consider matrix transformations when assigning or comparing points of each segment. I suggest you to convert any point to global space before performing the operations.

Tip #2: consider applying a constraint between points inside a segment, so when you move arround p0 or p3 of a curve, p1 or p2 move accordingly by the same amount, respectively (just like any graphics editor software do on bezier curves).


Edit -> Code provided

I did a sample implementation of the idea. Actually, after start coding I realized that just one class ControlPoint (instead of two) will do the job. A ControlPoint have 2 tangents. The desired behaviour is controled by the field smooth, that can be set for each point.

ControlPoint.cs

using System;
using UnityEngine;

[Serializable]
public class ControlPoint
{
  [SerializeField] Vector2 _position;
  [SerializeField] bool _smooth;
  [SerializeField] Vector2 _tangentBack;
  [SerializeField] Vector2 _tangentFront;

  public Vector2 position
  {
    get { return _position; }
    set { _position = value; }
  }

  public bool smooth
  {
    get { return _smooth; }
    set { if (_smooth = value) _tangentBack = -_tangentFront; }
  }

  public Vector2 tangentBack
  {
    get { return _tangentBack; }
    set
    {
      _tangentBack = value;
      if (_smooth) _tangentFront = _tangentFront.magnitude * -value.normalized;
    }
  }

  public Vector2 tangentFront
  {
    get { return _tangentFront; }
    set
    {
      _tangentFront = value;
      if (_smooth) _tangentBack = _tangentBack.magnitude * -value.normalized;
    }
  }

  public ControlPoint(Vector2 position, bool smooth = true)
  {
    this._position = position;
    this._smooth = smooth;
    this._tangentBack = -Vector2.one;
    this._tangentFront = Vector2.one;
  }
}

I also coded a custom PropertyDrawer for the ControlPoint class, so it can be shown better on the inspector. It is just a naive implementation. You could improve it very much.

ControlPointDrawer.cs

using UnityEngine;
using UnityEditor;

[CustomPropertyDrawer(typeof(ControlPoint))]
public class ControlPointDrawer : PropertyDrawer
{
  public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
  {

    EditorGUI.BeginProperty(position, label, property);
    int indent = EditorGUI.indentLevel;
    EditorGUI.indentLevel = 0; //-= 1;
    var propPos = new Rect(position.x, position.y, position.x + 18, position.height);
    var prop = property.FindPropertyRelative("_smooth");
    EditorGUI.PropertyField(propPos, prop, GUIContent.none);
    propPos = new Rect(position.x + 20, position.y, position.width - 20, position.height);
    prop = property.FindPropertyRelative("_position");
    EditorGUI.PropertyField(propPos, prop, GUIContent.none);
    EditorGUI.indentLevel = indent;
    EditorGUI.EndProperty();
  }

  public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
  {
    return EditorGUIUtility.singleLineHeight;
  }
}

I followed the same architecture of your solution, but with the needed adjustments to fit the ControlPoint class, and other fixes/changes. For example, I stored all the point values in local coordinates, so the transformations on the component or parents reflect in the curve.

Path.cs

using System;
using UnityEngine;
using System.Collections.Generic;

[Serializable]
public class Path
{
  [SerializeField] List<ControlPoint> _points;

  [SerializeField] bool _loop = false;

  public Path(Vector2 position)
  {
    _points = new List<ControlPoint>
    {
      new ControlPoint(position),
      new ControlPoint(position + Vector2.right)
    };
  }

  public bool loop { get { return _loop; } set { _loop = value; } }

  public ControlPoint this[int i] { get { return _points[(_loop && i == _points.Count) ? 0 : i]; } }

  public int NumPoints { get { return _points.Count; } }

  public int NumSegments { get { return _points.Count - (_loop ? 0 : 1); } }

  public ControlPoint InsertPoint(int i, Vector2 position, bool smooth)
  {
    _points.Insert(i, new ControlPoint(position, smooth));
    return this[i];
  }
  public ControlPoint RemovePoint(int i)
  {
    var item = this[i];
    _points.RemoveAt(i);
    return item;
  }
  public Vector2[] GetBezierPointsInSegment(int i)
  {
    var pointBack = this[i];
    var pointFront = this[i + 1];
    return new Vector2[4]
    {
      pointBack.position,
      pointBack.position + pointBack.tangentFront,
      pointFront.position + pointFront.tangentBack,
      pointFront.position
    };
  }

  public ControlPoint MovePoint(int i, Vector2 position)
  {
    this[i].position = position;
    return this[i];
  }

  public ControlPoint MoveTangentBack(int i, Vector2 position)
  {
    this[i].tangentBack = position;
    return this[i];
  }

  public ControlPoint MoveTangentFront(int i, Vector2 position)
  {
    this[i].tangentFront = position;
    return this[i];
  }
}

PathEditor is pretty much the same thing.

PathCreator.cs

using UnityEngine;

public class PathCreator : MonoBehaviour
{

  public Path path;

  public Path CreatePath()
  {
    return path = new Path(Vector2.zero);
  }

  void Reset()
  {
    CreatePath();
  }
}

Finally, all the magic happens in the PathCreatorEditor. Two comments here:

1) I moved the drawing of the lines to a custom DrawGizmo static function, so you can have the lines even when the object is not Active (i.e. shown in the Inspector) You could even make it pickable if you want to. I don't know if you want this behaviour, but you could easily revert;

2) Notice the Handles.matrix = creator.transform.localToWorldMatrix lines over the class. It automatically transforms the scale and rotation of the points to the world coordinates. There is a detail with PivotRotation over there too.

PathCreatorEditor.cs

using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(PathCreator))]
public class PathCreatorEditor : Editor
{
  PathCreator creator;
  Path path;
  SerializedProperty property;

  public override void OnInspectorGUI()
  {
    serializedObject.Update();
    EditorGUI.BeginChangeCheck();
    EditorGUILayout.PropertyField(property, true);
    if (EditorGUI.EndChangeCheck()) serializedObject.ApplyModifiedProperties();
  }

  void OnSceneGUI()
  {
    Input();
    Draw();
  }

  void Input()
  {
    Event guiEvent = Event.current;
    Vector2 mousePos = HandleUtility.GUIPointToWorldRay(guiEvent.mousePosition).origin;
    mousePos = creator.transform.InverseTransformPoint(mousePos);
    if (guiEvent.type == EventType.MouseDown && guiEvent.button == 0 && guiEvent.shift)
    {
      Undo.RecordObject(creator, "Insert point");
      path.InsertPoint(path.NumPoints, mousePos, false);
    }
    else if (guiEvent.type == EventType.MouseDown && guiEvent.button == 0 && guiEvent.control)
    {
      for (int i = 0; i < path.NumPoints; i++)
      {
        if (Vector2.Distance(mousePos, path[i].position) <= .25f)
        {
          Undo.RecordObject(creator, "Remove point");
          path.RemovePoint(i);
          break;
        }
      }
    }
  }

  void Draw()
  {
    Handles.matrix = creator.transform.localToWorldMatrix;
    var rot = Tools.pivotRotation == PivotRotation.Local ? creator.transform.rotation : Quaternion.identity;
    var snap = Vector2.zero;
    Handles.CapFunction cap = Handles.CylinderHandleCap;
    for (int i = 0; i < path.NumPoints; i++)
    {
      var pos = path[i].position;
      var size = .1f;
      Handles.color = Color.red;
      Vector2 newPos = Handles.FreeMoveHandle(pos, rot, size, snap, cap);
      if (pos != newPos)
      {
        Undo.RecordObject(creator, "Move point position");
        path.MovePoint(i, newPos);
      }
      pos = newPos;
      if (path.loop || i != 0)
      {
        var tanBack = pos + path[i].tangentBack;
        Handles.color = Color.black;
        Handles.DrawLine(pos, tanBack);
        Handles.color = Color.red;
        Vector2 newTanBack = Handles.FreeMoveHandle(tanBack, rot, size, snap, cap);
        if (tanBack != newTanBack)
        {
          Undo.RecordObject(creator, "Move point tangent");
          path.MoveTangentBack(i, newTanBack - pos);
        }
      }
      if (path.loop || i != path.NumPoints - 1)
      {
        var tanFront = pos + path[i].tangentFront;
        Handles.color = Color.black;
        Handles.DrawLine(pos, tanFront);
        Handles.color = Color.red;
        Vector2 newTanFront = Handles.FreeMoveHandle(tanFront, rot, size, snap, cap);
        if (tanFront != newTanFront)
        {
          Undo.RecordObject(creator, "Move point tangent");
          path.MoveTangentFront(i, newTanFront - pos);
        }
      }
    }
  }

  [DrawGizmo(GizmoType.Selected | GizmoType.NonSelected)]
  static void DrawGizmo(PathCreator creator, GizmoType gizmoType)
  {
    Handles.matrix = creator.transform.localToWorldMatrix;
    var path = creator.path;
    for (int i = 0; i < path.NumSegments; i++)
    {
      Vector2[] points = path.GetBezierPointsInSegment(i);
      Handles.DrawBezier(points[0], points[3], points[1], points[2], Color.green, null, 2);
    }
  }

  void OnEnable()
  {
    creator = (PathCreator)target;
    path = creator.path ?? creator.CreatePath();
    property = serializedObject.FindProperty("path");
  }
}

Moreover, I added a loop field in case you want the curve to be closed, and I added a naive funcionality to remove points by Ctrl+click on the Scene. Summing up, this is just basic stuff, but you could do it as advanced as you want. Also, you can reuse your ControlPoint class with other Components, like a Catmull-Rom spline, geometric shapes, other parametric functions...

Rodrigo Rodrigues
  • 7,545
  • 1
  • 24
  • 36
  • My code handles continuity and non-continuity already for the whole curve just need to break it down for Anchors and control points. My code has already implemented Tip #2. An example of how the `ControlPoint` and `HandlePoint` classes would turn out based on the code I provided would be most welcome. – SuperHyperMegaSomething Jun 12 '18 at 13:11
  • 1
    In my opinion you seem to be on track, just that the user wants code. If you have plans of updating this, you might want to consider doing so before the bounty expires :) – Hilarious404 Jun 18 '18 at 13:03
  • @SuperHyperMegaSomething, check edits. I provided some sample code. – Rodrigo Rodrigues Jun 20 '18 at 08:51
  • @RodrigoRodrigues I've been a bit tied up and haven't had the time to test the code. But from a glance it seems to be what I need. Also the bounty will expire in an hour, so I'll award you the bounty in good faith that we'll be able to work through any small changes that might be needed :) – SuperHyperMegaSomething Jun 21 '18 at 08:55
  • 1
    @RodrigoRodrigues Wow, this is amazing. I just tested it out, thanks so much. There are just two things 1. The curve itself will have a number of properties (in `PathCreator`) that I would need to access. The PropertyDrawer hides all these properties. I plan to add more properties for the control points too. I think being able to select a control point (handle) to change from the default inspector to a PropertyDrawer for an individual control point would work better. 1/2 (continued in next comment) – SuperHyperMegaSomething Jun 21 '18 at 14:42
  • @RodrigoRodrigues 2. I’ve been thinking of a way to keep the `movePoint`, `MoveTangentBack` and `MoveTangentFront` combined, similar to how it was originally. Do you foresee a way of maintaining them all in one? Thanks once again. I am really grateful for how you’ve taken the time to add a lot of stuff that I need that wasn’t in the original post. I hope this is not a bother, but I’ll be glad if you could please address the things I just mentioned. 2/2 – SuperHyperMegaSomething Jun 21 '18 at 14:42
  • 1/2 For having a single point selectable and it's properties shown in the inspector, the most straightforward way would be to make each ControlPoint a gameObject by itself, instead of a serialized data class. Other way would be to write a pickabke gizmo on PathCreatorEditor that store the current point in a static field, and make the custom inspector show info on that point. – Rodrigo Rodrigues Jun 21 '18 at 18:02
  • 1/2 cont. If you want your custom inspector to show the other fields you can: a) draw the new properties in your custom `OnInspectorGUI` or b) call `base.OnInspectorGUI` to show default drawers for all the public fields. 2/2 you can change the function to receive all the three values as arguments or to receive a new `ControlPoint` as argument (in this case, you should consider making it a `struct` instead of a class). – Rodrigo Rodrigues Jun 22 '18 at 00:37
  • 5
    @RodrigoRodrigues I don't think I understand how `Other way would be to write a pickabke gizmo on PathCreatorEditor that store the current point in a static field, and make the custom inspector show info on that point` will work. Isn't there a way I can make the `ControlPoint`s selectable so that it shows its properties for individual control points in the inspector when clicked without making the `ControlPoint`s game objects? – SuperHyperMegaSomething Jun 25 '18 at 18:17
  • 1
    @SuperHyperMegaSomething have you got this "select handle" to see properties in inspector working? Was trying to do something along these lines a while a go, but didn't go far. – NSSwift Jun 29 '18 at 04:51
  • @RodrigoRodrigues Would really appreciate if you could address my last comment. – SuperHyperMegaSomething Jul 04 '18 at 19:48
  • @NSSwift haven't made much progress since – SuperHyperMegaSomething Jul 04 '18 at 19:48
  • @RodrigoRodrigues I know that the other stuff Superhypermegasomething stated exceeds the original scope of this question but it has drawn a lot of interest including mine and is very closely related. I will award you 50 points for this answer if you’ll include: 1. a way to select handles that shows the properties for the unique selected handle in the inspector window (without making it a game object) 2. Changing `movePoint` so it manages the movement of all points (similar to it’s original behaviour as Superhypermegasomething stated). – Hilarious404 Jul 06 '18 at 13:47
  • Sorry for the absense, guys. I've been busy but i'm stil interested in the question. I will return to the code tonight. But, I think this question is already saturated of information, this could continue in a new question, no? – Rodrigo Rodrigues Jul 08 '18 at 05:43
  • @Hilarious404 I really appreciate the kind gesture – SuperHyperMegaSomething Jul 08 '18 at 19:58
  • @RodrigoRodrigues okay, I'll ask a new question though it will be closely related to this. I'll provide the link here once I'm done – SuperHyperMegaSomething Jul 08 '18 at 20:00
  • SuperHyperMegaSomething: try to filter out all the thing you got solved in this question. You can use @Hilarious404 comment as a guidance for your new Q, something like: how to click / select handles in Scene GUI and show a custom inspector for non-GameObject entity – Rodrigo Rodrigues Jul 08 '18 at 20:36
  • @RodrigoRodrigues here's the link https://stackoverflow.com/questions/51238340/how-to-make-editor-handles-selectable-to-display-properties-inspector-window – SuperHyperMegaSomething Jul 09 '18 at 04:45
  • @RodrigoRodrigues Please note that (for the linked question above) I'd also want the properties of only the selected control point/handle to be visible in the inspector window or embedded within the bezier curves custom inspector. This is because I'll be adding a ton of properties down the line for the control points so showing properties for all of them at a go wouldn't work. – SuperHyperMegaSomething Jul 09 '18 at 13:46
  • @RodrigoRodrigues hope you've seen the new question I posted? – SuperHyperMegaSomething Jul 12 '18 at 20:25
  • @RodrigoRodrigues my offer stands for the new question SuperHyperMegaSomething posted – Hilarious404 Jul 13 '18 at 14:50
  • @SuperHyperMegaSomething you are welcome. Are you displaying your curves using gizmos as RodrigoRodrigues provided in his solution or are you using a LineRenderer? – Hilarious404 Jul 13 '18 at 14:51
  • @RodrigoRodrigues it's now official. I've added the bounty. :) – Hilarious404 Jul 13 '18 at 14:59
  • 1
    @RodrigoRodrigues This is a great answer. I desperately need something like this. My only issue is that personally, I would have preferred using a struct (as you mentioned in one comment) instead and preserved the way points are moved as in the original question. I feel that is what SuperHyperMegaSomething wanted, just saying. This answer is already long. Do you have plans of providing something like that on say Github or Paste Bin? If not, I plead with you to. – TenOutOfTen Jul 17 '18 at 15:04
  • @RodrigoRodrigues the bounty I placed on SuperHyperMegaSomething question would expire in a few hours – Hilarious404 Jul 20 '18 at 04:26
  • Thank you for the reminder, @Hilarious404. Folks, I posted an answer there! – Rodrigo Rodrigues Jul 20 '18 at 08:57
  • @RodrigoRodrigues Please would you take a look at my question https://stackoverflow.com/questions/51089098/how-to-clone-several-game-objects-in-a-way-that-clone-properties-of-one-can-be-a someone put up a bounty for it. – NSSwift Oct 31 '18 at 09:28
  • @RodrigoRodrigues I asked a question that is related. Would be glad if you could help https://stackoverflow.com/questions/54201824/prefab-mode-only-adjusts-one-game-object-until-the-others-are-manually-selected – TenOutOfTen Jan 16 '19 at 11:01
3

The basic question in your post is: 'Is it a good idea to a have a separate Class for the points of a bezier curve?'

Since the curve will be made up of such points and these are more than just two coordinates imo it most certainly is a good idea.

But, as usual when doing class design, let's collect a few use cases, i.e. things a point will be used for or things we expect to do to a point..:

  • A point can be added or removed from a curve
  • A point can be moved
  • Its control point(s) can be moved

Besides the mere location, a point, i.e. an 'anchor point' should have more properties and abilities/methods..:

  • It has control points; how these are related to the points is sometimes not exactly the same. Looking at the Unity docs we see that the Handles.DrawLine looks at two points and their 'inner' control poiints. Coming from GDI+ GraphicsPath I see a sequence of points, altenrating between 1 anchor and 2 control points. Imo, this makes an even stronger case for treating the two control points as properties of the anchor point. Since both must be movable they may have a common ancestor or be hooked up to movecontroller class; but I trust you know best how to do that in Unity..

  • The property the question really started with was something like bool IsContinuous. When true we need to couple

    • moving a control point to moving the other one in 'the opposite' way.
    • moving the anchor to moving both control points in parallel
  • Maybe a property bool IsLocked to prevent moving it
  • Maybe a property bool IsProtected to prevent removing it when reducing/simplifying the curve. (Which is hardly needed for constructed curves but very much so for curves from free-hand drawing or tracing with the mouse)
  • Maybe a property to know that the point in a group of points which can be edited together.
  • Maybe a general marker.
  • Maybe a text annotation
  • Maybe a type indicator that denotes a break/split in the curve.
  • Maybe methods to increase or decrease smoothness vs. pointiness.

Some use cases clearly mostly involve the curve but others don't; and some are useful for both.

So, clearly we have a lot of good reasons to create a clever ÀnchPoint` class..

((I'm a bit tied up but still plan to write my own editor for the GraphicsPath bezier curves. If and when this happens, I'll update the post with things I learned including the class design I come up with..))

TaW
  • 53,122
  • 8
  • 69
  • 111