How to show "Snap Lines" or Alignment Guides?

Hi Walter,

I want to enable the visual alignment lines that appear dynamically to help you align shapes (nodes) relative to others on the diagram canvas. They help indicate when edges, centers, or midpoints of shapes are aligned horizontally or vertically.

I see similar possibility using GuidedDraggingTool in GoJs

can this be done in Goxam?

Yes, I believe that GoJS code could be ported to GoXam, although that design will not show three horizontal guidelines at the same time, but just one.

is this directly not possible in goxam? without porting GoJs in goxam.

This is ancient code that I haven’t tried in years, but you can try it:

<!-- Copyright © Northwoods Software Corporation, 2008-2023. All Rights Reserved. -->

<Window x:Class="Guidelines.Guidelines"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:go="http://schemas.nwoods.com/GoXam"
    Title="Guidelines" Height="600" Width="600">

  <Window.Resources>
    <DataTemplate x:Key="NodeTemplate">
      <go:NodePanel Width="{Binding Path=Data.Width, Mode=TwoWay}"
                    Height="{Binding Path=Data.Height, Mode=TwoWay}"
                    Background="{Binding Path=Data.Color}"
                    go:Part.SelectionAdorned="True"
                    go:Part.Resizable="True"
                    go:Node.LocationSpot="Center"
                    go:Node.Location="{Binding Path=Data.Location, Mode=TwoWay}">
        <TextBlock Text="{Binding Path=Data.Key}" />
      </go:NodePanel>
    </DataTemplate>
    <DataTemplate x:Key="LinkTemplate">
      <go:LinkPanel>
        <go:Link.Route>
          <go:Route Routing="AvoidsNodes" Corner="5" />
        </go:Link.Route>
        <go:LinkShape Stroke="Blue" StrokeThickness="2" />
        <Path Fill="Blue" go:LinkPanel.ToArrow="Standard" />
      </go:LinkPanel>
    </DataTemplate>
  </Window.Resources>

  <Grid>
    <go:Diagram x:Name="myDiagram"
                HorizontalContentAlignment="Stretch"
                VerticalContentAlignment="Stretch"
                NodeTemplate="{StaticResource NodeTemplate}"
                LinkTemplate="{StaticResource LinkTemplate}">
    </go:Diagram>
  </Grid>
</Window>

/* Copyright © Northwoods Software Corporation, 2008-2023. All Rights Reserved. */

using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using System.Windows.Media;
using System.Windows.Shapes;
using Northwoods.GoXam;
using Northwoods.GoXam.Model;
using Northwoods.GoXam.Tool;

namespace Guidelines {
  public partial class Guidelines : Window {
    public Guidelines() {
      InitializeComponent();

      var model = new GraphLinksModel<NodeInfo, String, String, UniversalLinkData>();
      model.NodesSource = new ObservableCollection<NodeInfo>() {
        new NodeInfo() { Key="A", Color="LightBlue" },
        new NodeInfo() { Key="B", Color="LightYellow" },
        new NodeInfo() { Key="C", Color="LightGreen" },
        new NodeInfo() { Key="D", Color="Pink" },
        new NodeInfo() { Key="E" },
      };
      model.LinksSource = new ObservableCollection<UniversalLinkData>() {
        new UniversalLinkData() { From="A", To="B" },
        new UniversalLinkData() { From="C", To="D" },
        new UniversalLinkData() { From="A", To="C" }
      };
      model.Modifiable = true;
      model.HasUndoManager = true;
      myDiagram.Model = model;

      myDiagram.DraggingTool = new GuidedDraggingTool();
    }
  }

  public class NodeInfo : GraphLinksModelNodeData<String> {
    public String Color {
      get { return _Color; }
      set { if (_Color != value) { String old = _Color; _Color = value; RaisePropertyChanged("Color", old, value); } }
    }
    private String _Color = "Fuchsia";

    public double Width {
      get { return _Width; }
      set { if (_Width != value) { double old = _Width; _Width = value; RaisePropertyChanged("Width", old, value); } }
    }
    private double _Width = 60;

    public double Height {
      get { return _Height; }
      set { if (_Height != value) { double old = _Height; _Height = value; RaisePropertyChanged("Height", old, value); } }
    }
    private double _Height = 60;
  }


  public class GuidedDraggingTool : DraggingTool {
    public GuidedDraggingTool() {
      this.GuidelineSnapDistance = 6;
      this.GuidelineVicinity = 1000;
    }

    public override void DoActivate() {
      if (this.HorizontalGuideline == null) CreateGuidelines();
      base.DoActivate();
    }
    public override void DoDeactivate() {
      this.Diagram.PartsModel.RemoveNode(this.HorizontalGuideline);
      this.Diagram.PartsModel.RemoveNode(this.VerticalGuideline);
      this.HorizontallyAlignedNode = null;
      this.VerticallyAlignedNode = null;
      base.DoDeactivate();
    }

    public double GuidelineSnapDistance { get; set; }
    public double GuidelineVicinity { get; set; }

    private Node HorizontalGuideline { get; set; }
    private Node VerticalGuideline { get; set; }

    private Node HorizontallyAlignedNode { get; set; }
    private Node VerticallyAlignedNode { get; set; }

    private void CreateGuidelines() {
      Node n = new Node();
      var line = new Line();
      line.Stroke = new SolidColorBrush(Colors.Red);
      line.StrokeThickness = 1;
      line.X1 = 0;
      line.Y1 = 0;
      line.X2 = this.GuidelineVicinity;
      line.Y2 = 0;
      n.Content = line;
      n.Id = "HorizontalGuideline";
      n.LayerName = "Tool";
      n.InDiagramBounds = false;
      this.HorizontalGuideline = n;

      n = new Node();
      line = new Line();
      line.Stroke = new SolidColorBrush(Colors.Red);
      line.StrokeThickness = 1;
      line.X1 = 0;
      line.Y1 = 0;
      line.X2 = 0;
      line.Y2 = this.GuidelineVicinity;
      n.Content = line;
      n.Id = "VerticalGuideline";
      n.LayerName = "Tool";
      n.InDiagramBounds = false;
      this.VerticalGuideline = n;
    }

    protected override void DragOver(Point pt, bool moving, bool copying) {
      // Just guide the first dragged Node
      Node node = null;
      if (moving && this.DraggedParts != null) node = this.DraggedParts.Keys.First(p => p is Node) as Node;
      if (copying && this.CopiedParts != null) node = this.CopiedParts.Keys.First(p => p is Node) as Node;
      ShowGuidelines(node);
      base.DragOver(pt, moving, copying);
    }

    protected override void DropOnto(Point pt) {
      base.DropOnto(pt);
      // Just shift the first dragged Node
      Node node = this.Diagram.SelectedParts.OfType<Node>().FirstOrDefault();
      if (node != null) {
        double dx = 0;
        double dy = 0;
        if (this.VerticallyAlignedNode != null) {
          dx = this.VerticallyAlignedNode.Bounds.X + this.VerticallyAlignedNode.Bounds.Width/2 - (node.Bounds.X + node.Bounds.Width/2);
        }
        if (this.HorizontallyAlignedNode != null) {
          dy = this.HorizontallyAlignedNode.Bounds.Y + this.HorizontallyAlignedNode.Bounds.Height/2 - (node.Bounds.Y + node.Bounds.Height/2);
        }
        var pos = node.Position;
        pos.Offset(dx, dy);
        node.Move(pos, true);
      }
    }

    private void ShowGuidelines(Node node) {
      if (node == null) return;
      // just align centers, for now
      double cx = node.Bounds.X + node.Bounds.Width/2;
      double cy = node.Bounds.Y + node.Bounds.Height/2;
      this.HorizontallyAlignedNode = null;
      this.VerticallyAlignedNode = null;
      double bestdx = this.GuidelineSnapDistance + 0.5;
      double bestdy = this.GuidelineSnapDistance + 0.5;
      foreach (Node n in this.Diagram.Nodes) {  // this could be more efficient by only looking at nodes within the viewport?
        // ignore invisible and dragged Nodes
        if (!n.Visible || !n.IsBoundToData) continue;
        if (this.DraggedParts != null && this.DraggedParts.ContainsKey(n)) continue;
        if (this.CopiedParts != null && this.CopiedParts.ContainsKey(n)) continue;
        // now see if it's the best aligned within GuidelineSnapDistance
        Rect b = n.Bounds;
        double nx = b.X + b.Width/2;
        double ny = b.Y + b.Height/2;
        double dx = Math.Abs(nx - cx);
        double dy = Math.Abs(ny - cy);
        if (dx < bestdx && dy < this.GuidelineVicinity) {
          this.VerticallyAlignedNode = n;
          bestdx = dx;
        }
        if (dy < bestdy && dx < this.GuidelineVicinity) {
          this.HorizontallyAlignedNode = n;
          bestdy = dy;
        }
      }
      if (this.HorizontallyAlignedNode != null) {
        double minx = Math.Min(this.HorizontallyAlignedNode.Bounds.X, node.Bounds.X) - 10;
        double maxx = Math.Max(this.HorizontallyAlignedNode.Bounds.Right, node.Bounds.Right) + 10;
        this.HorizontalGuideline.Position = new Point(minx, this.HorizontallyAlignedNode.Bounds.Y + this.HorizontallyAlignedNode.Bounds.Height/2);
        ((Line)this.HorizontalGuideline.Content).X2 = maxx-minx;
        this.Diagram.PartsModel.AddNode(this.HorizontalGuideline);
      } else {
        this.Diagram.PartsModel.RemoveNode(this.HorizontalGuideline);
      }

      if (this.VerticallyAlignedNode != null) {
        double miny = Math.Min(this.VerticallyAlignedNode.Bounds.Y, node.Bounds.Y) - 10;
        double maxy = Math.Max(this.VerticallyAlignedNode.Bounds.Bottom, node.Bounds.Bottom) + 10;
        this.VerticalGuideline.Position = new Point(this.VerticallyAlignedNode.Bounds.X + this.VerticallyAlignedNode.Bounds.Width/2, miny);
        ((Line)this.VerticalGuideline.Content).Y2 = maxy-miny;
        this.Diagram.PartsModel.AddNode(this.VerticalGuideline);
      } else {
        this.Diagram.PartsModel.RemoveNode(this.VerticalGuideline);
      }
    }
  }
}

I used the above implementation, and it gives me a center alignment line, but I don’t see any guiding line for left or right alignment. Any possibility on how we can enhance it for such usecase?

I suggest that you port the GoJS code to GoDiagram:

GoJS/extensionsJSM/GuidedDraggingTool.ts at master · NorthwoodsSoftware/GoJS · GitHub

I finally found some time to do the port. I have not tested this thoroughly. This is in two parts. Part #1, C# code:

/* Copyright © Northwoods Software Corporation, 2008-2025. All Rights Reserved. */

using Northwoods.GoXam;
using Northwoods.GoXam.Model;
using Northwoods.GoXam.Tool;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Ink;
using System.Windows.Media;
using System.Windows.Media.TextFormatting;
using System.Windows.Shapes;
using WinRT;
//using static System.Runtime.InteropServices.JavaScript.JSType;

namespace Guidelines {
  public partial class Guidelines : Window {
    public Guidelines() {
      InitializeComponent();

      var model = new GraphLinksModel<NodeInfo, String, String, UniversalLinkData>();
      model.NodesSource = new ObservableCollection<NodeInfo>() {
        new NodeInfo() { Key="A", Color="LightBlue", Location=new Point(0, 0) },
        new NodeInfo() { Key="B", Color="Yellow", Location=new Point(200, 0) },
        new NodeInfo() { Key="C", Color="LightGreen", Location=new Point(400, 0) },
        new NodeInfo() { Key="D", Color="Pink", Location=new Point(0, 200) },
        new NodeInfo() { Key="E", Color="Orange", Location=new Point(200, 200) },
        new NodeInfo() { Key="F", Color="Tomato", Location=new Point(400, 210) },
        new NodeInfo() { Key="G", Color="Lavender", Location=new Point(0, 400) },
        new NodeInfo() { Key="H", Color="Aqua", Location=new Point(210, 400) },
        new NodeInfo() { Key="I", Color="Teal", Location=new Point(400, 400) },
      };
      model.Modifiable = true;
      model.HasUndoManager = true;
      myDiagram.Model = model;

      myDiagram.DraggingTool = new GuidedDraggingTool();
    }
  }

  public class NodeInfo : GraphLinksModelNodeData<String> {
    public String Color {
      get { return _Color; }
      set { if (_Color != value) { String old = _Color; _Color = value; RaisePropertyChanged("Color", old, value); } }
    }
    private String _Color = "Fuchsia";

    public double Width {
      get { return _Width; }
      set { if (_Width != value) { double old = _Width; _Width = value; RaisePropertyChanged("Width", old, value); } }
    }
    private double _Width = 60;

    public double Height {
      get { return _Height; }
      set { if (_Height != value) { double old = _Height; _Height = value; RaisePropertyChanged("Height", old, value); } }
    }
    private double _Height = 60;
  }


  /**
   * The GuidedDraggingTool class makes guidelines visible as the parts are dragged around a diagram
   * when the selected part is nearly aligned with another part, or when a part is nearly positioned
   * between two other parts with equal space on both sides.
   *
   * During dragging, this tool will show temporary Parts named "guide..." to indicate what the dragged Node will align with.
   * You can customize the appearance of those Parts by setting those properties.
   * By default they are Parts in the "Tool" Layer holding magenta or cyan dashed lines.
   * You may also set some of those "guide..." properties to null if you do not want those guides to be shown
   * and do not want to snap to those alignments.
   *
   * Normally as the user drags a Node, that Node will snap to center itself with equal spacing on both sides,
   * or it will line up with a nearby Node.  If you do not want that snapping behavior during a drag,
   * the user can hold down the Shift modifier key in order to move the Node smoothly,
   * or you can set {@link IsRealtimeSnapEnabled} to false.
   * When that property is set to false, snapping will still happen upon mouse-up.
   *
   * You can set the {@link IsGuidelineSnapEnabled} or {@link IsEqualSpacingSnapEnabled} property to false
   * to avoid that kind of snapping behavior.
   * When both those properties are true, as they are by default,
   * and when a Node is near a point that provides both equal spacing between two Parts and alignment with a nearby Node,
   * the equal spacing snapping takes precedence.
   *
   * The maximum distance from perfect algnment that a dragged Node will snap to is controlled by the
   * {@link guideSnapDistance} property.  This tends to be a small value.
   *
   * The maximum distance at which another Node might affect the alignment of the dragged Node is controlled by the
   * {@link searchDistance} property.  This tends to be a large value.
   *
   * If you want to experiment with this extension, try the <a href="../../samples/GuidedDragging.html">Guided Dragging</a> sample.
   * @category Tool Extension
   */
  public class GuidedDraggingTool : DraggingTool {
    /**
     * Constructs a GuidedDraggingTool and sets up the temporary guideline parts.
     */
    public GuidedDraggingTool() {
      this.Name = "GuidedDragging";
      this.IsRealtimeSnapEnabled = true;
      this.IsGuidelineSnapEnabled = true;
      this.IsEqualSpacingSnapEnabled = true;

      this.GuideSnapDistance = 6;
      this.SearchDistance = 2000;
      this.ShowsGuides = true;

      // temporary parts for horizonal guidelines
      this.GuidelineHTop = new Node() { LayerName = "Tool", InDiagramBounds = false };
      this.GuidelineHTop.Content = new Canvas();
      (this.GuidelineHTop.Content as Canvas).Children
        .Add(
          new Line() { Name = "H", Stroke = Brushes.Magenta, StrokeDashArray = [4, 4], X1 = 0, Y1 = 0, X2 = 100, Y2 = 0 }
        );
      this.GuidelineHCenter = new Node() { LayerName = "Tool", InDiagramBounds = false };
      this.GuidelineHCenter.Content = new Canvas();
      (this.GuidelineHCenter.Content as Canvas).Children
        .Add(
          new Line() { Name = "H", Stroke = Brushes.Magenta, StrokeDashArray = [4, 4], X1 = 0, Y1 = 0, X2 = 100, Y2 = 0 }
        );
      this.GuidelineHBottom = new Node() { LayerName = "Tool", InDiagramBounds = false };
      this.GuidelineHBottom.Content = new Canvas();
      (this.GuidelineHBottom.Content as Canvas).Children
        .Add(
          new Line() { Name = "H", Stroke = Brushes.Magenta, StrokeDashArray = [4, 4], X1 = 0, Y1 = 0, X2 = 100, Y2 = 0 }
        );

      // temporary parts for vertical guidelines
      this.GuidelineVLeft = new Node() { LayerName = "Tool", InDiagramBounds = false };
      this.GuidelineVLeft.Content = new Canvas();
      (this.GuidelineVLeft.Content as Canvas).Children
        .Add(
          new Line() { Name = "V", Stroke = Brushes.Magenta, StrokeDashArray = [4, 4], X1 = 0, Y1 = 0, X2 = 0, Y2 = 100 }
        );
      this.GuidelineVCenter = new Node() { LayerName = "Tool", InDiagramBounds = false };
      this.GuidelineVCenter.Content = new Canvas();
      (this.GuidelineVCenter.Content as Canvas).Children
        .Add(
          new Line() { Name = "V", Stroke = Brushes.Magenta, StrokeDashArray = [4, 4], X1 = 0, Y1 = 0, X2 = 0, Y2 = 100 }
        );
      this.GuidelineVRight = new Node() { LayerName = "Tool", InDiagramBounds = false };
      this.GuidelineVRight.Content = new Canvas();
      (this.GuidelineVRight.Content as Canvas).Children
        .Add(
          new Line() { Name = "V", Stroke = Brushes.Magenta, StrokeDashArray = [4, 4], X1 = 0, Y1 = 0, X2 = 0, Y2 = 100 }
        );

      // temporary parts for spacing guides
      this.GuideHSpacingLeft = new Node() { LayerName = "Tool", InDiagramBounds = false };
      this.GuideHSpacingLeft.Content = new Canvas();
      (this.GuideHSpacingLeft.Content as Canvas).Children
        .Add(
          new Line() { Name = "H", Stroke = Brushes.DarkCyan, StrokeDashArray = [4, 4], X1 = 0, Y1 = 0, X2 = 100, Y2 = 0 }
        );
      (this.GuideHSpacingLeft.Content as Canvas).Children
        .Add(
          new Line() { Name = "V", Stroke = Brushes.DarkCyan, StrokeDashArray = [4, 4], X1 = 0, Y1 = 0, X2 = 0, Y2 = 100 }
        );
      this.GuideHSpacingRight = new Node() { LayerName = "Tool", InDiagramBounds = false };
      this.GuideHSpacingRight.Content = new Canvas();
      (this.GuideHSpacingRight.Content as Canvas).Children
        .Add(
          new Line() { Name = "H", Stroke = Brushes.DarkCyan, StrokeDashArray = [4, 4], X1 = 0, Y1 = 0, X2 = 100, Y2 = 0 }
        );
      (this.GuideHSpacingRight.Content as Canvas).Children
        .Add(
          new Line() { Name = "V", Stroke = Brushes.DarkCyan, StrokeDashArray = [4, 4], X1 = 0, Y1 = 0, X2 = 0, Y2 = 100 }
        );
      this.GuideVSpacingTop = new Node() { LayerName = "Tool", InDiagramBounds = false };
      this.GuideVSpacingTop.Content = new Canvas();
      (this.GuideVSpacingTop.Content as Canvas).Children
        .Add(
          new Line() { Name = "H", Stroke = Brushes.DarkCyan, StrokeDashArray = [4, 4], X1 = 0, Y1 = 0, X2 = 100, Y2 = 0 }
        );
      (this.GuideVSpacingTop.Content as Canvas).Children
        .Add(
          new Line() { Name = "V", Stroke = Brushes.DarkCyan, StrokeDashArray = [4, 4], X1 = 0, Y1 = 0, X2 = 0, Y2 = 100 }
        );
      this.GuideVSpacingBottom = new Node() { LayerName = "Tool", InDiagramBounds = false };
      this.GuideVSpacingBottom.Content = new Canvas();
      (this.GuideVSpacingBottom.Content as Canvas).Children
        .Add(
          new Line() { Name = "H", Stroke = Brushes.DarkCyan, StrokeDashArray = [4, 4], X1 = 0, Y1 = 0, X2 = 100, Y2 = 0 }
        );
      (this.GuideVSpacingBottom.Content as Canvas).Children
        .Add(
          new Line() { Name = "V", Stroke = Brushes.DarkCyan, StrokeDashArray = [4, 4], X1 = 0, Y1 = 0, X2 = 0, Y2 = 100 }
        );
    }

    /**
     * Gets or sets the margin of error for which guidelines show up.
     *
     * The default value is 6.
     * Guidelines will show up when the aligned nodes are ± 6px away from perfect alignment.
     */
    public double GuideSnapDistance { get; set; }

    /**
     * Gets or sets the distance around the selected part to search for aligned parts.
     *
     * The default value is 2000.
     * Set this to Infinity if you want to search the entire diagram no matter how far away.
     */
    public double SearchDistance { get; set; }

    /**
     * Gets or sets whether snapping for equal spacing or to guidelines is enabled during a drag.
     * This property is useless when both {@link IsGuidelineSnapEnabled} and {@link isEqualSpacingSnapEnabled} are false.
     *
     * The default value is true.
     */
    public bool IsRealtimeSnapEnabled{ get; set; }

    /**
     * Gets or sets whether snapping to guidelines provided by nearby Parts is enabled.
     *
     * The default value is true.
     */
    public bool IsGuidelineSnapEnabled { get; set; }

    /**
     * Gets or sets whether snapping to have equal space on both sides of the moved Node is enabled.
     *
     * The default value is true.
     */
    public bool IsEqualSpacingSnapEnabled { get; set; }


    /**
     * Gets or sets whether the guidelines are shown or not.
     * Setting this to false causes no horizontal or vertical
     * guidelines, nor the spacing guides, to be shown.
     * However, snapping may still take place, during the drag if {@link IsRealtimeSnapEnabled} is true,
     * or at the end on mouse-up.
     *
     * The default value is true.
     */
    public bool ShowsGuides { get; set; }

    /**
     * Gets or sets the Node to show when the moved Node's top lines up with a stationary Node.
     * This defaults to a horizontal magenta dashed line.  Set this to null to not show anything for this case.
     * */
    public Node GuidelineHTop { get; set; }

    /**
     * Gets or sets the Node to show when the moved Node's center lines up with a stationary Node.
     * This defaults to a horizontal magenta dashed line.  Set this to null to not show anything for this case.
     * */
    public Node GuidelineHCenter { get; set; }

    /**
     * Gets or sets the Node to show when the moved Node's bottom lines up with a stationary Node.
     * This defaults to a horizontal magenta dashed line.  Set this to null to not show anything for this case.
     * */
    public Node GuidelineHBottom { get; set; }

    /**
     * Gets or sets the Node to show when the moved Node's left side lines up with a stationary Node.
     * This defaults to a vertical magenta dashed line.  Set this to null to not show anything for this case.
     * */
    public Node GuidelineVLeft { get; set; }

    /**
     * Gets or sets the Node to show when the moved Node's center lines up with a stationary Node.
     * This defaults to a vertical magenta dashed line.  Set this to null to not show anything for this case.
     * */
    public Node GuidelineVCenter { get; set; }

    /**
     * Gets or sets the Node to show when the moved Node's right side lines up with a stationary Node.
     * This defaults to a vertical magenta dashed line.  Set this to null to not show anything for this case.
     * */
    public Node GuidelineVRight { get; set; }

    /**
     * Gets or sets the Node to show on the left side when the moved Node leaves nearly the same spacing left and right
     * between this Node and the nearest Parts on either side of it.
     * This defaults to a vertical cyan dashed line.  Set this to null to not show anything for this case.
     * */
    public Node GuideHSpacingLeft { get; set; }

    /**
     * Gets or sets the Node to show on the right side when the moved Node leaves nearly the same spacing left and right
     * between this Node and the nearest Parts on either side of it.
     * This defaults to a vertical cyan dashed line.  Set this to null to not show anything for this case.
     * */
    public Node GuideHSpacingRight { get; set; }

    /**
     * Gets or sets the Node to show above when the moved Node leaves nearly the same spacing above and below
     * between this Node and the nearest Parts above and below it.
     * This defaults to a vertical cyan dashed line.  Set this to null to not show anything for this case.
     * */
    public Node GuideVSpacingTop { get; set; }

    /**
     * Gets or sets the Node to show below when the moved Node leaves nearly the same spacing above and below
     * between this Node and the nearest Parts above and below it.
     * This defaults to a vertical cyan dashed line.  Set this to null to not show anything for this case.
     * */
    public Node GuideVSpacingBottom { get; set; }

    /**
     * Removes all of the guidelines from the grid.
     */
    public void ClearGuidelines() {
      if (this.GuidelineHTop != null) this.Diagram.PartsModel.RemoveNode(this.GuidelineHTop);
      if (this.GuidelineHCenter != null) this.Diagram.PartsModel.RemoveNode(this.GuidelineHCenter);
      if (this.GuidelineHBottom != null) this.Diagram.PartsModel.RemoveNode(this.GuidelineHBottom);
      if (this.GuidelineVLeft != null) this.Diagram.PartsModel.RemoveNode(this.GuidelineVLeft);
      if (this.GuidelineVCenter != null) this.Diagram.PartsModel.RemoveNode(this.GuidelineVCenter);
      if (this.GuidelineVRight != null) this.Diagram.PartsModel.RemoveNode(this.GuidelineVRight);
      if (this.GuideHSpacingLeft != null) this.Diagram.PartsModel.RemoveNode(this.GuideHSpacingLeft);
      if (this.GuideHSpacingRight != null) this.Diagram.PartsModel.RemoveNode(this.GuideHSpacingRight);
      if (this.GuideVSpacingTop != null) this.Diagram.PartsModel.RemoveNode(this.GuideVSpacingTop);
      if (this.GuideVSpacingBottom != null) this.Diagram.PartsModel.RemoveNode(this.GuideVSpacingBottom);
    }

    /**
     * Calls the base method and removes the guidelines from the graph.
     */
    override public void DoDeactivate() {
      // clear any guidelines when dragging is done
      this.ClearGuidelines();
      base.DoDeactivate();
    }

    /**
     * Shows vertical and horizontal guidelines for the dragged part.
     */
    override protected void DragOver(Point pt, bool moving, bool copying) {
      // clear all existing guidelines in case either show... method decides to show a guideline
      this.ClearGuidelines();

      // gets the selected part
      var draggingParts = this.CopiedParts;
      if (draggingParts == null) draggingParts = this.DraggedParts;
      if (draggingParts == null) return;
      foreach (Part part in draggingParts.Keys) {
        var node = part as Node;
        if (node == null) continue;
        // maybe snaps during drag
        var snap = this.IsRealtimeSnapEnabled && !IsShiftKeyDown();
        this.ShowMatches(node, this.ShowsGuides, snap);
      }
    }

    /**
     * On a mouse-up, snaps the selected part to the nearest guideline.
     * If not snapping, the part remains at its position.
     */
    override protected void DropOnto(Point pt) {
      this.ClearGuidelines();

      // gets the selected (perhaps copied) Node
      var draggingParts = this.CopiedParts;
      if (draggingParts == null) draggingParts = this.DraggedParts;
      if (draggingParts == null) return;
      foreach (Part part in draggingParts.Keys) {
        var node = part as Node;
        if (node == null) continue;
        // snaps only when the mouse is released without shift modifier
        var snap = !IsShiftKeyDown();
        this.ShowMatches(node, false, snap); // false means don't show guidelines
      }
    }

    /**
     * When nodes are shifted due to being guided upon a drop, make sure all connected link routes are invalidated,
     * since the node is likely to have moved a different amount than all its connected links in the regular
     * operation of the DraggingTool.
     */
    void InvalidateLinks(Node node){
      if (node is Node) node.InvalidateRelationships();
    }

    /**
     * This predicate decides whether or not the given Node should guide the dragged part.
     * @param part -  a stationary Node to which the dragged part might be aligned
     * @param guidedpart -  the Node being dragged
     */
    protected bool IsGuiding(Node part, Node guidedpart) {
      return (
        !part.IsSelected &&
        guidedpart is Node &&
        part.ContainingSubGraph == guidedpart.ContainingSubGraph &&
        part.Layer != null &&
        !part.Layer.IsTemporary
      );
    }

Part #2, end of the C# class, and the .XAML:

    /**
     * This finds parts that are aligned near the selected part along horizontal and vertical lines.
     * It compares the selected part to all parts within a rectangle approximately twice the {@link searchDistance} wide.
     * The guidelines appear when a part is aligned within a margin-of-error equal to {@link guideSnapDistance}.
     * @param part the Node being moved
     * @param guide - if true, show guideline
     * @param snap - if true, snap the part to where the guideline would be
     */
    protected void ShowMatches(Node part, bool guide, bool snap) {
      var marginOfError = this.GuideSnapDistance;
      var distance = this.SearchDistance;
      if (distance == Double.PositiveInfinity) distance = this.Diagram.Panel.DiagramBounds.Width;

      var objBounds = part.GetElementBounds(part.LocationElement);
      var p0x = objBounds.X;
      var p1x = objBounds.X + objBounds.Width / 2;
      var p2x = objBounds.X + objBounds.Width;
      var p0y = objBounds.Y;
      var p1y = objBounds.Y + objBounds.Height / 2;
      var p2y = objBounds.Y + objBounds.Height;

      // compares with parts within narrow vertical area
      var rowArea = objBounds;
      rowArea.Inflate(distance, marginOfError + 1);
      var rowParts = this.Diagram.Panel.FindElementsIn(
        rowArea,
        (obj) => Part.FindAncestor<Node>(obj),
        (p) => this.IsGuiding(p as Node, part),
        SearchLayers.Nodes
      );

      double bestVDiff = marginOfError;
      Node bestVPart = null;
      Spot bestVSpot = Spot.Default;
      Spot bestVOtherSpot = Spot.Default;
      Node closestLeft = null;
      double closestLeftX = Double.NegativeInfinity;
      Node closestRight = null;
      double closestRightX = Double.PositiveInfinity;
      // horizontal line -- comparing y-values
      foreach (Node other in rowParts) {
        if (other == part) return; // ignore itself

        var otherBounds = other.GetElementBounds(other.LocationElement);
        if (this.IsGuidelineSnapEnabled) {
          var q0y = otherBounds.Y;
          var q1y = otherBounds.Y + otherBounds.Height / 2;
          var q2y = otherBounds.Y + otherBounds.Height;

          // compare center with center of OTHER part
          if (this.GuidelineHCenter != null && Math.Abs(p1y - q1y) < bestVDiff) {
            bestVDiff = Math.Abs(p1y - q1y);
            bestVPart = other;
            bestVSpot = Spot.Center;
            bestVOtherSpot = Spot.Center;
          }
          // compare top side with top and bottom sides of OTHER part
          if (this.GuidelineHTop != null && Math.Abs(p0y - q0y) < bestVDiff) {
            bestVDiff = Math.Abs(p0y - q0y);
            bestVPart = other;
            bestVSpot = Spot.TopCenter;
            bestVOtherSpot = Spot.TopCenter;
          } else if (this.GuidelineHTop != null&& Math.Abs(p0y - q2y) < bestVDiff) {
            bestVDiff = Math.Abs(p0y - q2y);
            bestVPart = other;
            bestVSpot = Spot.TopCenter;
            bestVOtherSpot = Spot.BottomCenter;
          }
          // compare bottom side with top and bottom sides of OTHER part
          if (this.GuidelineHBottom != null && Math.Abs(p2y - q0y) < bestVDiff) {
            bestVDiff = Math.Abs(p2y - q0y);
            bestVPart = other;
            bestVSpot = Spot.BottomCenter;
            bestVOtherSpot = Spot.TopCenter;
          } else if (this.GuidelineHBottom != null && Math.Abs(p2y - q2y) < bestVDiff) {
            bestVDiff = Math.Abs(p2y - q2y);
            bestVPart = other;
            bestVSpot = Spot.BottomCenter;
            bestVOtherSpot = Spot.BottomCenter;
          }
        }
        if (this.IsEqualSpacingSnapEnabled) {
          // look for something on the left side that overlaps vertically
          if (otherBounds.Right <= objBounds.X && (closestLeft == null || otherBounds.Right > closestLeftX) && otherBounds.Y < objBounds.Bottom && otherBounds.Bottom > objBounds.Top) {
            closestLeft = other;
            closestLeftX = otherBounds.Right;
          }
          // look for something on the right side that overlaps vertically
          if (otherBounds.X >= objBounds.Right && (closestRight == null || otherBounds.X < closestRightX) && otherBounds.Y < objBounds.Bottom && otherBounds.Bottom > objBounds.Top) {
            closestRight = other;
            closestRightX = otherBounds.X;
          }
        }
      }

      // compares with parts within narrow vertical area
      var colArea = objBounds;
      colArea.Inflate(marginOfError + 1, distance);
      var colParts = this.Diagram.Panel.FindElementsIn(
        colArea,
        (obj) => Part.FindAncestor<Node>(obj),
        (p) => this.IsGuiding(p as Node, part),
        SearchLayers.Nodes
      );

      double bestHDiff = marginOfError;
      Node bestHPart = null;
      Spot bestHSpot = Spot.Default;
      Spot bestHOtherSpot = Spot.Default;
      Node closestTop = null;
      double closestTopY = Double.NegativeInfinity;
      Node closestBottom = null;
      double closestBottomY = Double.PositiveInfinity;
      // vertical line -- comparing x-values
      foreach (Node other in colParts) {
        if (other == part) return; // ignore itself

        var otherBounds = other.GetElementBounds(other.LocationElement);
        if (this.IsGuidelineSnapEnabled) {
          var q0x = otherBounds.X;
          var q1x = otherBounds.X + otherBounds.Width / 2;
          var q2x = otherBounds.X + otherBounds.Width;

          // compare center with center of OTHER part
          if (this.GuidelineVCenter != null && Math.Abs(p1x - q1x) < bestHDiff) {
            bestHDiff = Math.Abs(p1x - q1x);
            bestHPart = other;
            bestHSpot = Spot.Center;
            bestHOtherSpot = Spot.Center;
          }
          // compare left side with left and right sides of OTHER part
          if (this.GuidelineVLeft != null && Math.Abs(p0x - q0x) < bestHDiff) {
            bestHDiff = Math.Abs(p0x - q0x);
            bestHPart = other;
            bestHSpot = Spot.MiddleLeft;
            bestHOtherSpot = Spot.MiddleLeft;
          } else if (this.GuidelineVLeft != null && Math.Abs(p0x - q2x) < bestHDiff) {
            bestHDiff = Math.Abs(p0x - q2x);
            bestHPart = other;
            bestHSpot = Spot.MiddleLeft;
            bestHOtherSpot = Spot.MiddleRight;
          }
          // compare right side with left and right sides of OTHER part
          if (this.GuidelineVRight != null && Math.Abs(p2x - q0x) < bestHDiff) {
            bestHDiff = Math.Abs(p2x - q0x);
            bestHPart = other;
            bestHSpot = Spot.MiddleRight;
            bestHOtherSpot = Spot.MiddleLeft;
          } else if (this.GuidelineVRight != null && Math.Abs(p2x - q2x) < bestHDiff) {
            bestHDiff = Math.Abs(p2x - q2x);
            bestHPart = other;
            bestHSpot = Spot.MiddleRight;
            bestHOtherSpot = Spot.MiddleRight;
          }
        }
        if (this.IsEqualSpacingSnapEnabled) {
          // look for something on the left side that overlaps vertically
          if (this.GuideVSpacingTop != null && otherBounds.Bottom <= objBounds.Y &&
              (closestTop == null || otherBounds.Bottom > closestTopY) && otherBounds.X < objBounds.Right && otherBounds.Right > objBounds.X) {
            closestTop = other;
            closestTopY = otherBounds.Bottom;
          }
          // look for something on the right side that overlaps vertically
          if (this.GuideVSpacingBottom != null && otherBounds.Y >= objBounds.Bottom &&
              (closestBottom == null || otherBounds.Y < closestBottomY) && otherBounds.X < objBounds.Right && otherBounds.Right > objBounds.X) {
            closestBottom = other;
            closestBottomY = otherBounds.Y;
          }
        }
      }

      // figure out whether to snap, and where to
      var snapx = Double.NaN;
      var snapy = Double.NaN;

      // vertical equal spacing takes precedence over guideline snapping
      var verticalSpacing = false;
      if (closestTop != null && closestBottom != null) {
        var dxTop = objBounds.Y - closestTopY;
        var dxBottom = closestBottomY - objBounds.Bottom;
        if (dxTop >= 0 && dxBottom >= 0 && Math.Abs(dxBottom - dxTop) < 2*marginOfError) {
          verticalSpacing = true;
          if (snap) {
            snapy = part.Bounds.Y + (dxBottom - dxTop)/2;
          }
          if (guide) {  // show equal vertical spacing guidelines
            if (this.GuideVSpacingTop != null) {
              var minx = Math.Min(closestTop.Bounds.X, objBounds.X);
              var maxx = Math.Max(closestTop.Bounds.Right, objBounds.Right);
              this.GuideVSpacingTop.Position = new Point(minx - 10, closestTopY);
              (this.GuideVSpacingTop.FindNamedDescendant("V") as Line).Y2 = dxTop;
              (this.GuideVSpacingTop.FindNamedDescendant("H") as Line).X2 = maxx - minx + 20;
              Canvas.SetLeft(this.GuideVSpacingTop.FindNamedDescendant("V"), objBounds.X + objBounds.Width * 3 / 4 - minx + 10);
              Canvas.SetTop(this.GuideVSpacingTop.FindNamedDescendant("V"), 0);
              this.Diagram.PartsModel.AddNode(this.GuideVSpacingTop);
            }
            if (this.GuideVSpacingBottom != null) {
              var minx = Math.Min(closestBottom.Bounds.X, objBounds.X);
              var maxx = Math.Max(closestBottom.Bounds.Right, objBounds.Right);
              this.GuideVSpacingBottom.Position = new Point(minx - 10, objBounds.Bottom);
              (this.GuideVSpacingBottom.FindNamedDescendant("V") as Line).Y2 = dxBottom;
              (this.GuideVSpacingBottom.FindNamedDescendant("H") as Line).X2 = maxx - minx + 20;
              Canvas.SetLeft(this.GuideVSpacingBottom.FindNamedDescendant("V"), objBounds.X + objBounds.Width * 3 / 4 - minx + 10);
              Canvas.SetTop(this.GuideVSpacingBottom.FindNamedDescendant("V"), 0);
              Canvas.SetLeft(this.GuideVSpacingBottom.FindNamedDescendant("H"), 0);
              Canvas.SetTop(this.GuideVSpacingBottom.FindNamedDescendant("H"), dxBottom);
              this.Diagram.PartsModel.AddNode(this.GuideVSpacingBottom);
            }
          }
        }
      }
      if (!verticalSpacing && bestVPart != null) {
        var offsetY = objBounds.Y - part.Bounds.Y;
        var bestBounds = bestVPart.GetElementBounds(bestVPart.LocationElement);
        // line extends from x0 to x2
        var x0 = Math.Min(objBounds.X, bestBounds.X) - 10;
        var x2 = Math.Max(objBounds.X + objBounds.Width, bestBounds.X + bestBounds.Width) + 10;
        // find bestObj's desired Y
        var bestPoint = bestVOtherSpot.PointInRect(bestBounds);
        if (bestVSpot == Spot.Center) {
          if (snap) {
            // call Node.Move in order to automatically move member Parts of Groups
            snapy = bestPoint.Y - objBounds.Height / 2 - offsetY;
          }
          if (guide && this.GuidelineHCenter != null) {
            this.GuidelineHCenter.Position = new Point(x0, bestPoint.Y);
            (this.GuidelineHCenter.FindNamedDescendant("H") as Line).X2 = x2 - x0;
            this.Diagram.PartsModel.AddNode(this.GuidelineHCenter);
          }
        } else if (bestVSpot == Spot.TopCenter) {
          if (snap) {
            snapy = bestPoint.Y - offsetY;
          }
          if (guide && this.GuidelineHTop != null) {
            this.GuidelineHTop.Position = new Point(x0, bestPoint.Y);
            (this.GuidelineHTop.FindNamedDescendant("H") as Line).X2 = x2 - x0;
            this.Diagram.PartsModel.AddNode(this.GuidelineHTop);
          }
        } else if (bestVSpot == Spot.BottomCenter) {
          if (snap) {
            snapy = bestPoint.Y - objBounds.Height - offsetY;
          }
          if (guide && this.GuidelineHBottom != null) {
            this.GuidelineHBottom.Position = new Point(x0, bestPoint.Y);
            (this.GuidelineHBottom.FindNamedDescendant("H") as Line).X2 = x2 - x0;
            this.Diagram.PartsModel.AddNode(this.GuidelineHBottom);
          }
        }
      }

      // horizontal equal spacing takes precedence over guideline snapping
      var horizontalSpacing = false;
      if (closestLeft != null & closestRight != null) {
        var dxLeft = objBounds.X - closestLeftX;
        var dxRight = closestRightX - objBounds.Right;
        if (dxLeft >= 0 && dxRight >= 0 && Math.Abs(dxRight - dxLeft) < 2*marginOfError) {
          horizontalSpacing = true;
          if (snap) {
            snapx = part.Bounds.X + (dxRight - dxLeft)/2;
          }
          if (guide) {  // show equal horizontal spacing guidelines
            if (this.GuideHSpacingLeft != null) {
              var miny = Math.Min(closestLeft.Bounds.Y, objBounds.Y);
              var maxy = Math.Max(closestLeft.Bounds.Bottom, objBounds.Bottom);
              this.GuideHSpacingLeft.Position = new Point(closestLeftX, miny - 10);
              (this.GuideHSpacingLeft.FindNamedDescendant("H") as Line).X2 = dxLeft;
              (this.GuideHSpacingLeft.FindNamedDescendant("V") as Line).Y2 = maxy - miny + 20;
              Canvas.SetLeft(this.GuideHSpacingLeft.FindNamedDescendant("H"), 0);
              Canvas.SetTop(this.GuideHSpacingLeft.FindNamedDescendant("H"), objBounds.Y + objBounds.Height * 3 / 4 - miny + 10);
              this.Diagram.PartsModel.AddNode(this.GuideHSpacingLeft);
            }
            if (this.GuideHSpacingRight != null) {
              var miny = Math.Min(closestRight.Bounds.Y, objBounds.Y);
              var maxy = Math.Max(closestRight.Bounds.Bottom, objBounds.Bottom);
              this.GuideHSpacingRight.Position = new Point(objBounds.Right, miny - 10);
              (this.GuideHSpacingRight.FindNamedDescendant("H") as Line).X2 = dxRight;
              (this.GuideHSpacingRight.FindNamedDescendant("V") as Line).Y2 = maxy - miny + 20;
              Canvas.SetLeft(this.GuideHSpacingRight.FindNamedDescendant("H"), 0);
              Canvas.SetTop(this.GuideHSpacingRight.FindNamedDescendant("H"), objBounds.Y + objBounds.Height * 3 / 4 - miny + 10);
              Canvas.SetLeft(this.GuideHSpacingRight.FindNamedDescendant("V"), dxRight);
              Canvas.SetTop(this.GuideHSpacingRight.FindNamedDescendant("V"), 0);
              this.Diagram.PartsModel.AddNode(this.GuideHSpacingRight);
            }
          }
        }
      }
      if (!horizontalSpacing && bestHPart != null) {
        var offsetX = objBounds.X - part.Bounds.X;
        var bestBounds = bestHPart.GetElementBounds(bestHPart.LocationElement);
        // line extends from y0 to y2
        var y0 = Math.Min(objBounds.Y, bestBounds.Y) - 10;
        var y2 = Math.Max(objBounds.Y + objBounds.Height, bestBounds.Y + bestBounds.Height) + 10;
        // find bestObj's desired X
        var bestPoint = bestHOtherSpot.PointInRect(bestBounds);
        if (bestHSpot == Spot.Center) {
          if (snap) {
            // call Node.Move in order to automatically move member Parts of Groups
            snapx = bestPoint.X - objBounds.Width / 2 - offsetX;
          }
          if (guide && this.GuidelineVCenter != null) {
            this.GuidelineVCenter.Position = new Point(bestPoint.X, y0);
            (this.GuidelineVCenter.FindNamedDescendant("V") as Line).Y2 = y2 - y0;
            this.Diagram.PartsModel.AddNode(this.GuidelineVCenter);
          }
        } else if (bestHSpot == Spot.MiddleLeft) {
          if (snap) {
            snapx = bestPoint.X - offsetX;
          }
          if (guide && this.GuidelineVLeft != null) {
            this.GuidelineVLeft.Position = new Point(bestPoint.X, y0);
            (this.GuidelineVLeft.FindNamedDescendant("V") as Line).Y2 = y2 - y0;
            this.Diagram.PartsModel.AddNode(this.GuidelineVLeft);
          }
        } else if (bestHSpot == Spot.MiddleRight) {
          if (snap) {
            snapx = bestPoint.X - objBounds.Width - offsetX;
          }
          if (guide && this.GuidelineVRight != null) {
            this.GuidelineVRight.Position = new Point(bestPoint.X, y0);
            (this.GuidelineVRight.FindNamedDescendant("V") as Line).Y2 = y2 - y0;
            this.Diagram.PartsModel.AddNode(this.GuidelineVRight);
          }
        }
      }

      // if either snapx and/or snapy have been set, snap move the part
      if (!Double.IsNaN(snapx) || !Double.IsNaN(snapy)) {
        if (Double.IsNaN(snapx)) snapx = part.Bounds.X;
        if (Double.IsNaN(snapy)) snapy = part.Bounds.Y;
        part.Move(new Point(snapx, snapy), false);
        this.InvalidateLinks(part);
      }

    }
  }

}
<!-- Copyright © Northwoods Software Corporation, 2008-2025. All Rights Reserved. -->

<Window x:Class="Guidelines.Guidelines"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:go="http://schemas.nwoods.com/GoXam"
    Title="Guidelines" Height="600" Width="600">

  <Window.Resources>
    <DataTemplate x:Key="NodeTemplate">
      <go:NodePanel Width="{Binding Path=Data.Width, Mode=TwoWay}"
                    Height="{Binding Path=Data.Height, Mode=TwoWay}"
                    Background="{Binding Path=Data.Color}"
                    go:Part.SelectionAdorned="True"
                    go:Part.Resizable="True"
                    go:Node.LocationSpot="Center"
                    go:Node.Location="{Binding Path=Data.Location, Mode=TwoWay}">
        <TextBlock Text="{Binding Path=Data.Key}" />
      </go:NodePanel>
    </DataTemplate>
    <DataTemplate x:Key="LinkTemplate">
      <go:LinkPanel>
        <go:Link.Route>
          <go:Route Routing="AvoidsNodes" Corner="5" />
        </go:Link.Route>
        <go:LinkShape Stroke="Blue" StrokeThickness="2" />
        <Path Fill="Blue" go:LinkPanel.ToArrow="Standard" />
      </go:LinkPanel>
    </DataTemplate>
  </Window.Resources>

  <Grid>
    <go:Diagram x:Name="myDiagram"
                HorizontalContentAlignment="Stretch"
                VerticalContentAlignment="Stretch"
                NodeTemplate="{StaticResource NodeTemplate}"
                LinkTemplate="{StaticResource LinkTemplate}">
    </go:Diagram>
  </Grid>
</Window>

Thank you for the code, will integrate this soon and let you know.