Hi there, I have a custom VisualElement with its own OnGenerateVisualContent() method using Painter2D to draw a series of lines. I want to be able to click on the lines, however, by default, Painter2D lines don’t appear to be clickable. To solve this, I overrode ContainsPoint() to test whether my lines contain a given point, but the method appears to never be called? Neither the does debug output appear, nor does a breakpoint within that method trigger.
Do I need to do something else to ensure that ContainsPoint() is actually called?
Here’s the code for the VisualElement:
using UnityEngine;
using UnityEngine.UIElements;
public class EdgeView : VisualElement
{
private const int STROKE_WIDTH = 2;
// Need a state for tracking mouse movement when connecting via drag
// and one of source/target is null
public PortView sourcePortView;
public PortView targetPortView;
private Edge edgeData;
public bool selected;
public EdgeView()
{
name = "node-connection-edge";
generateVisualContent += OnGenerateVisualContent;
this.AddManipulator(new Clickable(OnClick));
}
void OnClick()
{
selected = !selected;
MarkDirtyRepaint();
}
public void OnGeometryChanged(GeometryChangedEvent evt)
{
MarkDirtyRepaint();
}
public void Bind(Edge boundEdge)
{
edgeData = boundEdge;
var sourcePort = edgeData.source;
var targetPort = edgeData.target;
sourcePortView = sourcePort.view;
targetPortView = targetPort.view;
sourcePort.parentNode.view.RegisterCallback<GeometryChangedEvent>(OnGeometryChanged);
targetPort.parentNode.view.RegisterCallback<GeometryChangedEvent>(OnGeometryChanged);
}
public Vector2[] EdgePathPoints()
{
Vector2[] points = new Vector2[4];
var source = NodeCanvasView.instance.PanelToCanvas(sourcePortView.worldBound.center);
var target = NodeCanvasView.instance.PanelToCanvas(targetPortView.worldBound.center);
var sourceEdgeExitOffset = sourcePortView.portData.side == PortSide.Bottom ?
new Vector2(0, 30) : // Offset bottom outputs down
new Vector2(30, 0); // Offset right outputs right
var targetEdgeExitOffset = targetPortView.portData.side == PortSide.Top ?
new Vector2(0, -30) : // Offset top inputs up
new Vector2(-30, 0); // Offset left outputs left
points[0] = source;
points[1] = source + sourceEdgeExitOffset;
points[2] = target + targetEdgeExitOffset;
points[3] = target;
return points;
}
private bool IsPointInRectangle(Vector2 start, Vector2 end, float width, Vector2 point)
{
// Vector AB = B - A
Vector2 line = end - start;
float length = line.magnitude;
if (Mathf.Approximately(length, 0f))
{
// Degenerate case: A and B are the same point.
return false;
}
// Vector AP = P - A
Vector2 AP = point - start;
// Dot product (AP · AB)
float dot = Vector2.Dot(AP, line);
// The "along-axis" distance t from start to the projection of P on line
float t = dot / length; // measured in the same scale as AB’s magnitude
// Check if the projection is between start and end: 0 <= t <= L
if (t < 0f || t > length)
{
// Outside rectangle along the spine
return false;
}
// Compute the perpendicular distance from P to the line A->B
// Vector2.Project(AP, AB) projects AP onto AB.
Vector2 proj = Vector3.Project(AP, line);
Vector2 perp = AP - proj; // This is the perpendicular vector from the line to P
float d_perp = perp.magnitude;
// Check if within half-width
return d_perp <= (width * 0.5f);
}
public override bool ContainsPoint(Vector2 localPoint)
{
Debug.Log("Testing edge hit");
var points = EdgePathPoints();
for (int i = 1; i < points.Length; i++)
{
if (IsPointInRectangle(points[i-1], points[i], STROKE_WIDTH, localPoint))
{
return true;
}
}
return false;
}
void OnGenerateVisualContent(MeshGenerationContext meshGenerationContext)
{
var painter = meshGenerationContext.painter2D;
painter.lineWidth = STROKE_WIDTH;
painter.lineCap = LineCap.Round;
painter.strokeGradient = new Gradient()
{
colorKeys = !selected ? new GradientColorKey[]
{
new GradientColorKey(Color.red, 0),
new GradientColorKey(Color.blue, 1)
} : new GradientColorKey[]
{
new GradientColorKey(Color.cyan, 0),
new GradientColorKey(Color.cyan, 1)
},
};
var points = EdgePathPoints();
painter.BeginPath();
painter.lineJoin = LineJoin.Round;
painter.MoveTo(points[0]);
for (int i = 1; i < points.Length; i++)
{
painter.LineTo(points[i]);
}
painter.Stroke();
}
}