Link anchors

I am trying to control where the reshaping anchors are placed on my links, and which direction they will reshape… Where would I need to implement this and how would I go about doing it?

Thanks
Ryan

The link reshaping handles are created and positioned by LinkReshapingTool.UpdateAdornments. That in turn depends on the value of Link.Route.LinkReshapeHandleTemplate, if you just want to make simple changes to the appearance of the reshape handles.

The rest of the methods of LinkReshapingTool implement the reshaping functionality.

I am trying to manually add handles along the route and control the direction of the reshaping… do you have any samples of how that may work?

For example, if I have a right angle link with 3 points - (10, 10), (10, 100), (100, 100), I would like to be able to add a reshape in all direction anchor at (10, 50) and another at (50, 100)…

It’s not exactly the same thing, but it’s very similar: try the PolylineReshapingTool that I believe is talked about in Drawing Tool.

The source files don’t seem to be available any more… do you have another copy somewhere else?

Sorry about that – it must have been lost when we moved web servers. I have fixed the link in that forum topic.

Although we really ought to put out a newer version of that code.

Thanks for getting that back up… I’ve gone through and tried to pull some of it out, but the handles don’t seem to be showing up (which I think is because I’m misusing the sample)… This is my overridden method in my LinkReshapingTool:


// add reshape handles for each Point in the Shape
        public override void UpdateAdornments(Part part)
        {
            Node node = part as Node;
            if (node != null)
            {
                base.UpdateAdornments(part);
                return;
            }

            Link link = part as Link;
            if (link != null)
            {
                IEnumerable<Point> pts = link.Route.Points;

                if (pts != null && pts.Count() > 4)
                {
                    Adornment adornment;
                    SpotPanel panel = new SpotPanel();

                    for (int i = link.Route.FirstPickIndex; i < link.Route.LastPickIndex; i++)
                    {
                        // create a reshape handle
                        // (ideally this would be in a DataTemplate, but for now it's hardcoded)
                        var h = new ToolHandle();
                        NodePanel.SetFigure(h, HandleFigure);
                        h.Fill = FillBrush;
                        h.Stroke = StrokeBrush;
                        h.StrokeThickness = StrokeThickness;
                        h.Width = HandleSize.Width;
                        h.Height = HandleSize.Height;
                        h.Cursor = Cursors.Hand;
                        // identify this particular handle within the SpotPanel
                        h.Tag = i;

                        // position the handle
                        Point pt = pts.ElementAt(i);
                        SpotPanel.SetSpot(h, new Spot(0, 0, pt.X, pt.Y));
                        // add the reshape handle to the panel
                        panel.Children.Add(h);
                    }

                    adornment = new Adornment();
                    adornment.AdornedElement = link;
                    adornment.Category = ToolCategory;
                    adornment.Content = panel;  // just provide the FrameworkElement as the Content and as the Visual Child
                    adornment.LocationSpot = Spot.TopLeft;

                    link.SetAdornment(ToolCategory, adornment);
                }
            }            
        }

What do I need to change to get this working?

I think you need to use a LinkPanel as an Adornment for a Link, not a SpotPanel. Also the AdornedElement should be the LinkShape, not the whole Link.

Here’s LinkReshapingTool.UpdateAdornments:

[code] public override void UpdateAdornments(Part part) {
Link link = part as Link;
if (link == null) return; // no Nodes

  Adornment adornment = null;
  if (link.IsSelected) {
    FrameworkElement selelt = link.Path;
    if (selelt != null && link.CanReshape() && Part.IsVisibleElement(selelt)) {
      adornment = link.GetAdornment(ToolCategory);
      if (adornment == null) {
        Route route = link.Route;
        if (route != null) {
          IEnumerable<Point> pts = route.Points;
          int numpts = route.PointsCount;
          bool ortho = route.Orthogonal;
          if (pts != null && numpts > 2) {
            // LinkReshapeHandleTemplate: for each reshape handle, not for whole adornment
            DataTemplate template = link.LinkReshapeHandleTemplate;
            if (template == null) template = Diagram.FindDefault<DataTemplate>("DefaultLinkReshapeHandleTemplate");
            
            LinkPanel panel = new LinkPanel();
            int firstindex = route.FirstPickIndex;
            int lastindex = route.LastPickIndex;
            // don't bother creating handles for firstindex or lastindex
            for (int i = firstindex+1; i <= lastindex-1; i++) {
              // expand the DataTemplate to make a copy of a reshape handle
              FrameworkElement h = template.LoadContent() as FrameworkElement;
              // needs to be a FrameworkElement so we can set its Cursor
              if (h == null) continue;
              // identify this particular handle within the LinkPanel
              LinkPanel.SetIndex(h, i);
              // now determines its reshape behavior and cursor, depending on whether Orthogonal et al.
              if (i == firstindex) {
                // default ReshapeBehavior.None
              } else if (i == firstindex+1 && ortho) {
                Point a = route.GetPoint(firstindex);
                Point b = route.GetPoint(firstindex+1);
                if (Geo.IsApprox(a.X, b.X)) {
                  SetReshapeBehavior(h, ReshapeBehavior.Vertical);
                  h.Cursor = Part.SizeNSCursor;
                } else if (Geo.IsApprox(a.Y, b.Y)) {
                  SetReshapeBehavior(h, ReshapeBehavior.Horizontal);
                  h.Cursor = Part.SizeWECursor;
                }
              } else if (i == lastindex-1 && ortho) {
                Point a = route.GetPoint(lastindex-1);
                Point b = route.GetPoint(lastindex);
                if (Geo.IsApprox(a.X, b.X)) {
                  SetReshapeBehavior(h, ReshapeBehavior.Vertical);
                  h.Cursor = Part.SizeNSCursor;
                } else if (Geo.IsApprox(a.Y, b.Y)) {
                  SetReshapeBehavior(h, ReshapeBehavior.Horizontal);
                  h.Cursor = Part.SizeWECursor;
                }
              } else if (i == lastindex) {
                // default ReshapeBehavior.None
              } else {
                SetReshapeBehavior(h, ReshapeBehavior.All);
                h.Cursor = Part.SizeAllCursor;
              }
              panel.Children.Add(h);
            }
            adornment = new Adornment();  // for LinkReshapingTool.UpdateAdornments
            adornment.AdornedElement = selelt;
            adornment.Category = ToolCategory;
            adornment.Content = panel;  // just provide the FrameworkElement as the Content and as the Visual Child
            adornment.LocationSpot = Spot.TopLeft;
          }
        }
      }
      if (adornment != null) {
        Point loc = link.GetElementPoint(selelt, Spot.TopLeft);
        adornment.Location = loc;
        adornment.RotationAngle = link.GetAngle(selelt);
        adornment.Remeasure();
      }
    }
  }
  link.SetAdornment(ToolCategory, adornment);
}[/code]

Pardon me if there are any unresolved dependencies in this code – I’m sure you can figure them out if there are.

Hi Walter,

I’m sorry to say that I can’t resolve some of the symbols in the code you posted: ToolCategory, Geo (used for Geo.IsApprox), and Part.SizeAllCursor (along with the other Part cursors). Where can I find these?

And speaking of cursors, is there a way to change the cursor the PanningTool uses while panning?

Thanks,
Bob

ToolCategory is “ReshapeLink”.

public static bool IsApprox(double x, double y) {
  double d = x - y;
  return d < 0.5 && d > -0.5;
}

Part.SizeAllCursor is Cursors.SizeAll in WPF and is Cursors.Hand in Silverlight.

PanningTool defines these methods:

public override void DoActivate() {
  Diagram diagram = this.Diagram;
  if (diagram == null) return;
  diagram.Cursor = Part.ScrollAllCursor;
  this.OriginalPosition = diagram.Panel.Position;
  this.Active = true;
}

public override void DoDeactivate() {
  Diagram diagram = this.Diagram;
  if (diagram != null) {
    diagram.Cursor = null;
  }
  this.Active = false;
}

where Part.ScrollAllCursor is Cursors.ScrollAll (WPF) or Cursors.Hand (Silverlight).

Thank you, Walter.

  • Bob

Hi Walter,

I’m still not clear on how to add drag points to a link, which I think is what rwdial was also trying to do.

UpdateAdornments assigns behavior to existing points, but does not add points. The documentation for DoReshape says “For handles that are near either end of the route, the movement may be
constrained to be only vertical or only horizontal, in order to maintain
orthogonality”.

But ideally, the handles would not be constrained and new segments would be created as needed to maintain orthogonality. As segments are created, I suppose UpdateAdornments-like code would need to be run on the new inflection points.

I presume this would be a “do-it-myself” project. Any hints as to how to pull it off?

Thanks,
Bob

I know we have done that in GoDiagram, but I don’t recall seeing that in GoXam.

However, in searching through random code that we have, I did find this which is related to this topic, although it isn’t exactly what you are looking for. It also assumes non-orthogonal routes.

[code] public class CustomLinkReshapingTool : LinkReshapingTool {
private const String ToolCategory = “ReshapeLink”;

public override void UpdateAdornments(Part part) {
  Link link = part as Link;
  if (link == null) return;  // no Nodes

  Adornment adornment = null;
  if (link.IsSelected) {
    FrameworkElement selelt = link.Path;
    if (selelt != null && link.CanReshape() && Part.IsVisibleElement(selelt)) {
      adornment = link.GetAdornment(ToolCategory);
      if (adornment == null) {
        Route route = link.Route;
        IEnumerable<Point> pts = route.Points;
        int numpts = route.PointsCount;
        if (numpts < 2) return;

        // this Adornment consists of a LinkPanel holding a bunch of ToolHandles
        LinkPanel panel = new LinkPanel();
        int firstindex = route.FirstPickIndex;
        int lastindex = route.LastPickIndex;
        // don't bother creating handles for firstindex or lastindex
        for (int i = firstindex; i <= lastindex-1; i++) {
          if (i > firstindex) {
            // the handle at each vertex point:
            FrameworkElement h = new ToolHandle() {
              Width=6,
              Height=6,
              Fill=Brushes.Yellow,
              Stroke=Brushes.Black,
              StrokeThickness=1
            };
            NodePanel.SetFigure(h, NodeFigure.Rectangle);
            // identify the segment for this particular handle
            LinkPanel.SetIndex(h, i);
            // allow reshape behavior if not at either end
            SetReshapeBehavior(h, ReshapeBehavior.All);
            h.Cursor = Cursors.Hand;
            panel.Children.Add(h);
          }

          // now the handle at the middle of each segment:
          FrameworkElement mh = new ToolHandle() {
            Width=6,
            Height=6,
            Fill=Brushes.Yellow,
            Stroke=Brushes.Black,
            StrokeThickness=1
          };
          NodePanel.SetFigure(mh, NodeFigure.Ellipse);
          // identify this particular handle within the LinkPanel
          LinkPanel.SetIndex(mh, i);  // the segment
          LinkPanel.SetFraction(mh, 0.5);  // how far along the segment
          SetReshapeBehavior(mh, ReshapeBehavior.All);
          mh.Cursor = Cursors.Hand;
          panel.Children.Add(mh);
        }

        adornment = new Adornment();
        adornment.AdornedElement = selelt;
        adornment.Category = ToolCategory;
        adornment.Content = panel;  // just provide the FrameworkElement as the Content and as the Visual Child
        adornment.LocationSpot = Spot.TopLeft;
      }
    }
    if (adornment != null) {
      Point loc = link.GetElementPoint(selelt, Spot.TopLeft);
      adornment.Position = loc;
      //adornment.RotationAngle = link.GetAngle(selelt);
      adornment.Remeasure();
    }
  }
  link.SetAdornment(ToolCategory, adornment);
}

protected override void DoReshape(Point newPoint) {
  Link link = this.AdornedLink;
  Route route = link.Route;
  ReshapeBehavior behavior = GetReshapeBehavior(this.Handle);
  if (behavior != ReshapeBehavior.None) {
    int idx = this.HandleIndex;
    double frac = LinkPanel.GetFraction(this.Handle);
    if (frac == 0) {  // dragging a vertex point reshape handle
      route.SetPoint(idx, newPoint);
      if (this.Diagram.LastMouseEventArgs.LeftButton == MouseButtonState.Released &&
          idx > route.FirstPickIndex && idx < route.LastPickIndex) {
        Point a = route.GetPoint(idx-1);
        Point b = route.GetPoint(idx);
        Point c = route.GetPoint(idx+1);
        Point q;  // the closest point to B on the line between A and C
        bool between = NearestPointOnLine(a, c, b, out q);
        // if B is within 3 units of the line from A to C, and if Q is between A and C,
        // remove that point from the stroke
        if (between && ((q.X-b.X)*(q.X-b.X) + (q.Y-b.Y)*(q.Y-b.Y)) < 3*3) {
          route.RemovePoint(idx);
          // force the reshape handles to be re-created
          link.SetAdornment(ToolCategory, null);
          UpdateAdornments(link);
        }
      }
    } else {  // dragging a mid-segment reshape handle
      idx++;  // insert new point after the current index
      route.InsertPoint(idx, newPoint);
      this.InsertionIndex = idx;
      // force the reshape handles to be re-created
      link.SetAdornment(ToolCategory, null);
      UpdateAdornments(link);
      // find the new vertex point reshape handle
      Adornment ad = link.GetAdornment(ToolCategory);
      if (ad == null) return;
      LinkPanel lp = ad.FindDescendant(e => e is LinkPanel) as LinkPanel;
      if (lp == null) return;
      this.Handle = lp.Children.OfType<ToolHandle>().FirstOrDefault(h => LinkPanel.GetIndex(h) == idx && LinkPanel.GetFraction(h) == 0);
      this.HandleIndex = idx;
      // now continue reshaping with the new vertex point handle
    }
  }
}

private int InsertionIndex { get; set; }

public override void DoStart() {
  base.DoStart();
  this.InsertionIndex = 0;
}

public override void DoCancel() {
  if (this.InsertionIndex > 0) {
    this.AdornedLink.Route.RemovePoint(this.InsertionIndex);
    // force the reshape handles to be re-created
    this.AdornedLink.SetAdornment(ToolCategory, null);
    UpdateAdornments(this.AdornedLink);
    StopTool();
  } else {
    base.DoCancel();
  }
}

public static bool NearestPointOnLine(Point a, Point b, Point p, out Point result) {
  // A and B are the endpoints of this segment
  double Ax = a.X;
  double Ay = a.Y;
  double Bx = b.X;
  double By = b.Y;
  // P is the point we want to get closest to
  double Px = p.X;
  double Py = p.Y;

  // handle vertical and horizontal lines specially
  if (Ax == Bx) {
    double min, max;
    if (Ay < By) {
      min = Ay;
      max = By;
    } else {
      min = By;
      max = Ay;
    }
    double newp = Py;
    if (newp < min) {
      result = new Point(Ax, min);
      return false;
    }
    if (newp > max) {
      result = new Point(Ax, max);
      return false;
    }
    result = new Point(Ax, newp);
    return true;
  } else if (Ay == By) {
    double min, max;
    if (Ax < Bx) {
      min = Ax;
      max = Bx;
    } else {
      min = Bx;
      max = Ax;
    }
    double newp = Px;
    if (newp < min) {
      result = new Point(min, Ay);
      return false;
    }
    if (newp > max) {
      result = new Point(max, Ay);
      return false;
    }
    result = new Point(newp, Ay);
    return true;
  } else {
    // ought to take sqrt to get real length, but don't bother...
    double L = (Bx-Ax) * (Bx-Ax) + (By-Ay) * (By-Ay);
    // ought to be dividing By L^2, but didn't bother to sqrt!
    double Q = ((Ax-Px) * (Ax-Bx) + (Ay-Py) * (Ay-By)) / L;

    if (Q < 0) {
      result = a;
      return false;
    } else if (Q > 1) {
      result = b;
      return false;
    } else {
      // OK to use point on line between A and B
      double x = Ax + Q * (Bx-Ax);
      double y = Ay + Q * (By-Ay);
      result = new Point(x, y);
      return true;
    }
  }
}

}[/code]
Install this in your Diagram with:

<go:Diagram.LinkReshapingTool> <local:CustomLinkReshapingTool /> </go:Diagram.LinkReshapingTool>
and don’t forget to set go:Part.Reshapable=“True” on your Link DataTemplate’s root visual element.

So I added this to the GoWpfBasic demo. After moving a node and selecting a link:

After dragging the middle handle:

Note how it inserts a Point into the Route. There are now 2 reshape handles that insert Points into the Route.

After dragging the second insertion handle:

One can also remove Points from the Route. In this tool, one does it by moving a regular reshaping handle so that the two segments are nearly straight. In this case after I move the left-most square handle (a standard reshaping handle) directly between the Gamma node and the right-most square reshaping handle, we get:

And you can see that the number of Points in the Route has decreased.

You could also accomplish such removals more directly by having a context menu command that removed the Point associated with a particular reshape handle.