Draggable VisualElement Position Relating to Parent and Event Handling

Greetings! I’m trying to implement a custom VisualElement where, given two distinct points, I can draw a line that “link” them up. The user should be able to move these points around and the line should always connect the points. As you can see in the figure, I’m able to position the points inside another VisualElement container just fine.

link

For the movement part, I’ve followed this example from Unity’s documentation: Unity - Manual: Create a drag-and-drop UI inside a custom Editor window. It works as expected, but I’m having difficulty with the line drawing part. Here’s how I’ve structured the code so far:

Point

public class Point : VisualElement
{
    public Color Color;
    public Color OutlineColor;
    public float Radius;
    public float OutlineThickness;

    public Point(Color color, Color outlineColor, float radius, float outlineThickness)
    {   
        name = "Point";
            
        Color = color;
        OutlineColor = outlineColor;
        Radius = radius;
        OutlineThickness = outlineThickness;

        style.width = 2 * Radius;
        style.height = 2 * Radius;
    
         generateVisualContent += OnGenerateVisualContent;
    }

    private void OnGenerateVisualContent(MeshGenerationContext context)
    {
        Painter2D painter = context.painter2D;

        painter.fillColor = OutlineColor;
        painter.BeginPath();
        painter.Arc(new Vector2(Radius, Radius), Radius + OutlineThickness, 0.0f, 360.0f);
        painter.Fill();

        painter.fillColor = Color;
        painter.BeginPath();
        painter.Arc(new Vector2(Radius, Radius), Radius, 0.0f, 360.0f);
        painter.Fill();
    }
}

As can be seen, the point itself is just an overlay drawn on-top of the VisualElement, positioned in the middle of it (the diameter is the same size as the VE’s width and height). For the Line, I have the following:

Line

public class Line: VisualElement
{
    public Color StartPointColor { get; set; }
    public Color EndPointColor { get; set; } 
    public Color OutlineColor { get; set; }
    public float Radius { get; set; }
    public float Thickness { get; set; }

    public Point StartPoint;
    public Point EndPoint;

    private const string styleSheet = "UI/VisualElements/SliceViewer/LinkStyleSheet";
    private const string styleClass = "line";
    private const string startPointStyleClass = "startPoint";
    private const string endPointStyleClass = "endPoint";

    public Line()
    {
        styleSheets.Add(Resources.Load<StyleSheet>(styleSheet));
        AddToClassList(styleClass);

        generateVisualContent += OnGenerateVisualContent;
    }

    public void SetVisualElements()
    {
        StartPoint = new Point(StartPointColor, OutlineColor, Radius, Thickness);
        EndPoint = new Point(EndPointColor, OutlineColor, Radius, Thickness);

        StartPoint.AddManipulator(new DragAndDropManipulator(StartPoint));
        EndPoint.AddManipulator(new DragAndDropManipulator(EndPoint));

        StartPoint.AddToClassList(startPointStyleClass);
        EndPoint.AddToClassList(endPointStyleClass);

        Add(StartPoint);
        Add(EndPoint);
    }
            
    private void OnGenerateVisualContent(MeshGenerationContext context)
    {
        Painter2D painter = context.painter2D;

        painter.strokeColor = OutlineColor;
        painter.lineWidth = Thickness;
        painter.lineJoin = LineJoin.Miter;
        painter.lineCap = LineCap.Butt;

        // PROBLEM HERE
        painter.BeginPath();
        painter.MoveTo(???); // StartPoint position
        painter.LineTo(???); // EndPoint position
        painter.Stroke();
    }

    public new class UxmlFactory : UxmlFactory<Line, UxmlTraits> { }

    public new class UxmlTraits : VisualElement.UxmlTraits
    {
        UxmlColorAttributeDescription startPointColor = new UxmlColorAttributeDescription { name = "start-point-color", defaultValue = Color.red };
        UxmlColorAttributeDescription endPointColor = new UxmlColorAttributeDescription { name = "end-point-color", defaultValue = Color.blue };
        UxmlColorAttributeDescription outlineColor = new UxmlColorAttributeDescription { name = "outline-color", defaultValue = Color.white };
        UxmlFloatAttributeDescription radius = new UxmlFloatAttributeDescription { name = "radius", defaultValue = 10.0f };
        UxmlFloatAttributeDescription thickness = new UxmlFloatAttributeDescription { name = "thickness", defaultValue = 3.0f };

        public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
        {
            base.Init(ve, bag, cc);
            Line line = ve as Line;
            line.Clear();

            line.name = "Line";
            line.StartPointColor = startPointColor.GetValueFromBag(bag, cc);
            line.EndPointColor = endPointColor.GetValueFromBag(bag, cc);
            line.OutlineColor = outlineColor.GetValueFromBag(bag, cc);
            line.Radius = radius.GetValueFromBag(bag, cc);
            line.Thickness = thickness.GetValueFromBag(bag, cc);

            line.SetVisualElements();
        }
    }
}

I believe I can accomplish the drawing portion by stroking a line from the StartPoint location to the EndPoint location. My question is: how can I retrive their positions relative to their parent (the black box container)? I tried using their transform.position but it returns the zero vector. What I want is their position relative to how far they are positioned inside the parent element. And while we are at it, how can I trigger an event from within the child, so the parent can be notified and handle it? I imagine that’s how I could call MarkDirtyRepaint in the parent’s class, and have the line be redrawn.

Any help is much appretiated. Thank you all!

I believe you want to use the rect returned by VisualElement.layout, as that should be its position/size with respect to its parent.

Though if you want to use the 2D painter to draw lines between points, the painter is constrained by the bounding box of its respective visual element. Meaning to draw lines between points, the parent visual element containing the points will need to be the one handling this drawing.

Unfortunately VisualElement.layout returns an empty vector, just like transform.position.

Though if you want to use the 2D painter to draw lines between points, the painter is constrained by the bounding box of its respective visual element. Meaning to draw lines between points, the parent visual element containing the points will need to be the one handling this drawing.

The way I’m handling things right now, I have a parent element with a defined size and two distinct elements added into it (the points). Having the parent handle the drawing would indeed solve the problem, if I could retrieve the correct positions of its children.

When are you grabbing the properties?

I believe layouting may take a frame or two to correctly calculate. Unity’s magic methods like Awake, OnEnable, Update and even LateUpdate all happen before visual elements are calculated by the PlayerLoop.