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