Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ public async Task BatchedMode_InvalidatesLayerWhenAutomaticEdgeGeometryChanges()
var layer = (BatchedEdgeLayer)graphArea.Children[0];
var refreshCount = layer.RefreshCount;
var initialGeometry = edge.GetLineGeometry();
((TestEdge)edge.Edge!).RoutingPoints = [new(140, 120)];
((TestEdge)edge.Edge!).RoutingPoints = [new(140, 120), new(130, 120), new(140, 130)];

edge.InvalidateMeasure();
UpdateEdgeGeometry(edge);
Expand Down Expand Up @@ -238,4 +238,4 @@ private static ControlTemplate CreateCustomEdgeTemplate()
Content = new Func<IServiceProvider?, object?>(_ => new TemplateResult<Control>(content, nameScope))
};
}
}
}
16 changes: 0 additions & 16 deletions Westermo.GraphX.Controls.Avalonia.Tests/GeometryCachingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,20 +176,4 @@ public async Task Geometry_IsReused_WhenMeasureIsInvalidatedWithoutInputChanges(

await Assert.That(edge.GetLineGeometry()).IsSameReferenceAs(initialGeometry);
}

[Test]
public async Task Geometry_IsRebuilt_WhenEndpointVertexShapeChanges()
{
var (_, source, _, edge) = CreateSimpleGraph();
edge.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
edge.Arrange(new Rect(0, 0, edge.DesiredSize.Width, edge.DesiredSize.Height));
var initialGeometry = edge.GetLineGeometry();

source.VertexShape = VertexShape.Circle;
edge.InvalidateMeasure();
edge.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
edge.Arrange(new Rect(0, 0, edge.DesiredSize.Width, edge.DesiredSize.Height));

await Assert.That(edge.GetLineGeometry()).IsNotSameReferenceAs(initialGeometry);
}
}
206 changes: 51 additions & 155 deletions Westermo.GraphX.Controls.Avalonia/Controls/EdgeControlBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -392,21 +392,16 @@ public bool ShowArrows
set => SetValue(ShowArrowsProperty, value);
}

/// <summary>
/// Gets or Sets that user controls the path geometry object or it is generated automatically
/// </summary>
private bool _manualDrawing;

/// <summary>
/// Gets or sets whether the user controls the path geometry instead of automatic generation.
/// </summary>
public bool ManualDrawing
{
get => _manualDrawing;
get;
set
{
if (_manualDrawing == value) return;
_manualDrawing = value;
if (field == value) return;
field = value;
RootArea?.NotifyBatchedEdgeChanged(this);
}
}
Expand Down Expand Up @@ -438,6 +433,7 @@ public bool IsSelected
/// Templated Path object to operate with routed path
/// </summary>
protected Path? LinePathObject;

private double? _batchedPathOpacity;

private IList<IEdgeLabelControl> _edgeLabelControls = [];
Expand Down Expand Up @@ -631,7 +627,6 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
EdgePointerForTarget = GetTemplatePart(e, "PART_EdgePointerForTarget") as IEdgePointer;

SelfLoopIndicator = GetTemplatePart(e, "PART_SelfLoopedEdge") as Control;
// var x = ShowLabel;
MeasureChild(EdgePointerForSource as Control);
MeasureChild(EdgePointerForTarget as Control);
MeasureChild(SelfLoopIndicator);
Expand Down Expand Up @@ -688,6 +683,8 @@ private static Size Union(params ReadOnlySpan<Size> sizes)
return new Size(width, height);
}

private int _oldSignature;

// Provide a desired size for layout based on current geometry bounds so edge is not collapsed to 0x0.
protected override Size MeasureOverride(Size availableSize)
{
Expand All @@ -696,12 +693,7 @@ protected override Size MeasureOverride(Size availableSize)

if (!TryGetSourcePoints(false, out var sourceRect) || !TryGetTargetPoints(false, out var targetRect))
{
// Source/Target isn't ready yet (e.g. added to the graph before the vertex has been
// assigned a position). Explicitly flag the geometry as dirty and drop the cached
// signature so that the *next* successful Measure - whenever it happens - is guaranteed
// to rebuild the geometry instead of being silently treated as unchanged.
_isGeometryDirty = true;
_hasGeometryInputSignature = false;
return default;
}

Expand All @@ -720,10 +712,6 @@ protected override Size MeasureOverride(Size availableSize)
//get the route informations
var routeInformation = routedEdge.RoutingPoints;
var gEdge = Edge as IGraphXCommonEdge;
var geometryInputSignature = GetGeometryInputSignature(sourceRect, targetRect, routeInformation, gEdge);
_isGeometryDirty = !_hasGeometryInputSignature || _lastGeometryInputSignature != geometryInputSignature;
_lastGeometryInputSignature = geometryInputSignature;
_hasGeometryInputSignature = true;
UpdateConnectionPoints(gEdge, routeInformation, sourceRect, targetRect);

// If the logic above is working correctly, both the source and target connection points will exist.
Expand All @@ -732,7 +720,7 @@ protected override Size MeasureOverride(Size availableSize)

var p1 = SourceConnectionPoint.Value;
var p2 = TargetConnectionPoint.Value;
GetPoints(p1, p2, routedEdge.RoutingPoints);
UpdatePoints(p1, p2, routedEdge.RoutingPoints);

// Cache layout info for pointer arrangement after base.ArrangeOverride
_sourcePointerLayout = new EdgePointerLayoutInfo
Expand Down Expand Up @@ -807,10 +795,10 @@ protected override Size MeasureOverride(Size availableSize)
selfLoopSize = Union(selfLoopSize, ctrl.DesiredSize);
}

// For self-looped edges _pathBounds was set to the actual indicator rect above; the legacy
// selfLoopSize estimate (radius * 2 + offset) would inflate the EdgeControl past the
// indicator and let Avalonia's Grid centring push the visible part behind the source
// vertex. Use the precise indicator rect instead so DesiredSize matches what we render.
var pointSignature = GetSignature(_points);
var changed = _oldSignature != pointSignature;
_isGeometryDirty = LineGeometry is null || changed;
Comment thread
carl-andersson-at-westermo marked this conversation as resolved.
_oldSignature = pointSignature;
return IsSelfLooped
? _pathBounds.Size
: Union(spanningRect.Size, selfLoopSize, _pathBounds.Size);
Expand All @@ -831,11 +819,8 @@ protected override Size ArrangeOverride(Size finalSize)
_isGeometryDirty = false;
RootArea?.NotifyBatchedEdgeChanged(this);
}
// else: Source/Target position or size still isn't available. Leave `_isGeometryDirty`
// set so the geometry is not incorrectly treated as "up to date" - the next Measure that
// succeeds (see MeasureOverride) will force a rebuild instead of this edge silently
// staying blank until something unrelated (e.g. dragging the vertex) invalidates it again.
}

if (LinePathObject == null) return base.ArrangeOverride(finalSize);
LinePathObject.Data = LineGeometry;
LinePathObject.StrokeDashArray = StrokeDashArray;
Expand Down Expand Up @@ -933,9 +918,8 @@ protected virtual Point GetParallelOffset(Point sourceCenter, Point targetCenter
protected internal Point? TargetConnectionPoint;

private readonly List<Point> _points = [];
private Rect _pathBounds;
private ulong _lastGeometryInputSignature;
private bool _hasGeometryInputSignature;

private Rect _pathBounds;
private bool _isGeometryDirty = true;

internal bool CanRenderInBatchedLayer =>
Expand Down Expand Up @@ -1067,51 +1051,57 @@ protected virtual void UpdateSelfLoopedEdgeData()

//if self looped edge
UpdateSelfLoopedEdgeData();
if (!IsSelfLooped) return CreateEdgeGeometry(_points, routedEdge is IGraphXCommonEdge { ReversePath: true });

// Self-looped edges have no edge pointers to position
_sourcePointerLayout = default;
_targetPointerLayout = default;
var geom = PrepareSelfLoopedEdge(sourceRect.TopLeft);
// PrepareSelfLoopedEdge returns geometry in graph-area coordinates; shift it into
// the EdgeControl's local space so it lines up with the (now self-loop-sized) bounds.
if (geom is { } ellipse)
if (IsSelfLooped)
{
ellipse.Center = new Point(ellipse.Center.X - _pathBounds.X, ellipse.Center.Y - _pathBounds.Y);
_sourcePointerLayout = default;
_targetPointerLayout = default;
var geom = PrepareSelfLoopedEdge(sourceRect.TopLeft);
geom?.Center = new Point(geom.Center.X - _pathBounds.X, geom.Center.Y - _pathBounds.Y);
return geom;
}

return geom;
return CreateEdgeGeometry(_points, routedEdge is IGraphXCommonEdge { ReversePath: true });
}

private void GetPoints(Point p1, Point p2, Measure.Point[]? routeInformation)

private void UpdatePoints(Point p1, Point p2, Measure.Point[]? routeInformation)
{
_points.Clear();
if (routeInformation is null || routeInformation.Length <= 2)
if (routeInformation is not null && routeInformation.Length > 2)
{
_points.Add(p1);
_points.Add(p2);
return;
}
var pointsLength = routeInformation.Length;
_points.EnsureCapacity(pointsLength);
foreach (var t in routeInformation)
{
_points.Add(t.ToAvalonia());
}

var pointsLength = routeInformation.Length;
_points.EnsureCapacity(pointsLength);
foreach (var t in routeInformation)
{
_points.Add(t.ToAvalonia());
}
_points[0] = p1;
_points[^1] = p2;

_points[0] = p1;
_points[^1] = p2;
if (RootArea is not { IsEdgeRoutingEnabled: true }) return;
Span<Point> copy = stackalloc Point[_points.Count];
_points.CopyTo(copy);

if (RootArea is not { IsEdgeRoutingEnabled: true })
copy.GetCurveThroughPoints(_points, 0.5, RootArea.EdgeCurvingTolerance);
}
else
{
return;
_points.Add(p1);
_points.Add(p2);
}
}

Span<Point> copy = stackalloc Point[_points.Count];
_points.CopyTo(copy);
private static int GetSignature(List<Point> points)
{
var hash = new HashCode();
foreach (var p in points)
{
hash.Add(p.X);
hash.Add(p.Y);
}

copy.GetCurveThroughPoints(_points, 0.5, RootArea.EdgeCurvingTolerance);
return hash.ToHashCode();
}

/// <summary>
Expand Down Expand Up @@ -1155,15 +1145,6 @@ private static bool TryGetPoints(bool useCurrentCoords, VertexControl? control,
if (double.IsNaN(y)) y = GraphAreaBase.GetY(control);
if (double.IsNaN(x) ||
double.IsNaN(y)) return false;
// Defend against Panel child-ordering: GraphAreaBase measures its Children in collection
// order, and edges can be positioned before their own Source/Target vertices there (e.g. the
// default ControlDrawOrder.VerticesOnTop inserts new edges at index 0). If this vertex hasn't
// been measured yet, DesiredSize would still be the zero-size default, producing an edge that
// is anchored at the vertex's top-left corner instead of its real bounds. Measuring it here
// (idempotent - Avalonia skips the work if it's already valid) guarantees a correct size
// regardless of visual-tree ordering.
if (!control.IsMeasureValid)
control.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
result = new Rect(new Point(x, y), control.DesiredSize);
Comment thread
carl-andersson-at-westermo marked this conversation as resolved.
return true;
}
Expand Down Expand Up @@ -1267,91 +1248,6 @@ private bool NeedParallelCalc(bool hasRouteInfo)
return !hasRouteInfo && RootArea.EnableParallelEdges && IsParallel;
}

/// <summary>
/// Captures every value that changes automatic edge geometry. The signature allows a layout
/// invalidation with unchanged inputs to retain the existing immutable StreamGeometry.
/// </summary>
private ulong GetGeometryInputSignature(Rect sourceRect, Rect targetRect, Measure.Point[]? routingPoints,
IGraphXCommonEdge? edge)
{
const ulong offsetBasis = 14695981039346656037;
var signature = offsetBasis;

AddSignature(ref signature, sourceRect.X);
AddSignature(ref signature, sourceRect.Y);
AddSignature(ref signature, sourceRect.Width);
AddSignature(ref signature, sourceRect.Height);
AddSignature(ref signature, targetRect.X);
AddSignature(ref signature, targetRect.Y);
AddSignature(ref signature, targetRect.Width);
AddSignature(ref signature, targetRect.Height);
AddSignature(ref signature, (int)Source!.VertexShape);
AddSignature(ref signature, (int)Target!.VertexShape);
AddSignature(ref signature, IsSelfLooped);
AddSignature(ref signature, IsParallel);
AddSignature(ref signature, ParallelEdgeOffset);
AddSignature(ref signature, ShowArrows);
AddSignature(ref signature, ShowSelfLoopIndicator);
AddSignature(ref signature, SelfLoopIndicatorRadius);
AddSignature(ref signature, SelfLoopIndicatorOffset.X);
AddSignature(ref signature, SelfLoopIndicatorOffset.Y);
AddControlSizeSignature(ref signature, EdgePointerForSource as Control);
AddControlSizeSignature(ref signature, EdgePointerForTarget as Control);
AddControlSizeSignature(ref signature, SelfLoopIndicator);
AddSignature(ref signature, edge?.SourceConnectionPointId ?? -1);
AddSignature(ref signature, edge?.TargetConnectionPointId ?? -1);
AddSignature(ref signature, edge?.ReversePath ?? false);
AddSignature(ref signature, RootArea?.IsEdgeRoutingEnabled ?? false);
AddSignature(ref signature, RootArea?.EnableParallelEdges ?? false);
AddSignature(ref signature, RootArea?.EdgeCurvingTolerance ?? 0);
AddSignature(ref signature, routingPoints?.Length ?? 0);

if (routingPoints is not null)
{
foreach (var point in routingPoints)
{
AddSignature(ref signature, point.X);
AddSignature(ref signature, point.Y);
}
}

return signature;
}

private static void AddControlSizeSignature(ref ulong signature, Control? control)
{
if (control is null)
{
AddSignature(ref signature, 0);
return;
}

AddSignature(ref signature, control.DesiredSize.Width);
AddSignature(ref signature, control.DesiredSize.Height);
}

private static void AddSignature(ref ulong signature, bool value)
{
AddSignature(ref signature, value ? 1 : 0);
}

private static void AddSignature(ref ulong signature, int value)
{
AddSignature(ref signature, unchecked((ulong)(uint)value));
}

private static void AddSignature(ref ulong signature, double value)
{
AddSignature(ref signature, unchecked((ulong)BitConverter.DoubleToInt64Bits(value)));
}

private static void AddSignature(ref ulong signature, ulong value)
{
const ulong prime = 1099511628211;
signature ^= value;
signature *= prime;
}

private IVertexConnectionPoint GetTargetCpOrThrow(int id)
{
return Target?.GetConnectionPointById(id, true) ?? throw new GX_ObjectNotFoundException(string.Format(
Expand Down Expand Up @@ -1390,4 +1286,4 @@ public IList<IEdgeLabelControl> GetLabelControls()
{
return [.. EdgeLabelControls];
}
}
}
Loading