Node resizing reverse after mirroring node or flipping node

Hi, I have created a Template which i am rotating vertically or mirroring then the resize behaviour of the node is also mirrored.

            <Behaviors:Interaction.Triggers>
                <Behaviors:EventTrigger EventName="MouseLeftButtonDown">
                    <Behaviors:InvokeCommandAction Command="{Binding Path=Data.MouseLeftButtonDown}" PassEventArgsToCommand="True" />
                </Behaviors:EventTrigger>
            </Behaviors:Interaction.Triggers>
            <Grid Name="ItemData" MinHeight="10" MinWidth="15" Height="{Binding Path=Data.Height, Mode=TwoWay}" 
                  Width="{Binding Path=Data.Width, Mode=TwoWay}">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="5"/>
                    <ColumnDefinition Width="45*"/>
                    <ColumnDefinition Width="10*"/>
                    <ColumnDefinition Width="45*" />
                    <ColumnDefinition Width="5"/>
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition Height="5"/>
                    <RowDefinition Height="40*"/>
                    <RowDefinition Height="20*"/>
                    <RowDefinition Height="40*"/>
                </Grid.RowDefinitions>
                <!--Right port-->
                <flowsheeter:CustomPort PortColor="Red" PortType="NodeH2O" x:Name="In" go:Node.LinkableMaximum="1" ToolTip="{x:Static prop:Resources.In}" go:Node.PortId="{x:Static prop:Resources.In}" Grid.Row="2"  Grid.Column="3" Grid.ColumnSpan="2"  HorizontalAlignment="Right" VerticalAlignment="Center" go:Node.LinkableFrom="False" go:Node.LinkableTo="True"  Margin="0,0,2.7,0" Panel.ZIndex="1"/>
                <!--Left port-->
                <flowsheeter:CustomPort PortColor="Red" PortType="NodeH2O" x:Name="Out" go:Node.LinkableMaximum="1" ToolTip="{x:Static prop:Resources.Out}" go:Node.PortId="{x:Static prop:Resources.Out}" Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2" HorizontalAlignment="Left" VerticalAlignment="Center" go:Node.LinkableFrom="True" go:Node.LinkableTo="False" Margin="2.7,0,0,0" Panel.ZIndex="1" />
                <!--Top port-->
                <flowsheeter:CustomPort PortColor="Black" PortType="NodeSignal" x:Name="SignalInPort" go:Node.LinkableMaximum="1" ToolTip="{x:Static prop:Resources.Signal}" go:Node.PortId="{x:Static prop:Resources.Signal}" Grid.Row="0" Grid.RowSpan="2" Grid.Column="2"  HorizontalAlignment="Center" VerticalAlignment="Top" go:Node.LinkableFrom="False" go:Node.LinkableTo="True" Margin="0,2.7,0,0" Panel.ZIndex="1" />

                <Path Grid.Row="1" Grid.RowSpan="3" Grid.Column="1" Grid.ColumnSpan="3" Stretch="Fill" HorizontalAlignment="Center" VerticalAlignment="Center" Fill="White" Data="M 8 4 L 8 0 L 0 4 L 0 0 L 8 4 M 2 1 L 4 0 L 6 1" Stroke="Black" StrokeThickness="1"></Path>
                <Ellipse  Grid.Row="2"  Grid.Column="2"  Stroke="Black" StrokeThickness="1" Stretch="Fill"  Fill="Black" Panel.ZIndex="1"/>
            </Grid>
            <TextBlock Name="AggregateName" HorizontalAlignment="Center" Text="Control Valve" />
        </StackPanel>
        <DataTemplate.Triggers>
            <DataTrigger Binding="{Binding Path=Data.IsAggregateNameHidden}" Value="True">
                <Setter TargetName="AggregateName" Property="Visibility" Value="Collapsed" />
                <Setter TargetName="ShapeLayout" Property="ForceCursor" Value="False"/>
            </DataTrigger>
        </DataTemplate.Triggers>
    </DataTemplate>

private void MirrorAggregate(bool isVertical)
{
GoXamModel.StartTransaction(“MirrorAggregates”);

        GetSelectedNodes()?.ToList().ForEach(node =>
        {
            ShapeData data = (ShapeData)node.Data;


            ScaleTransform scaleTransform = isVertical ? GetScaleTransformForVertical(data.MirrorX, data.MirrorY, node.RotationAngle) : GetScaleTransformForHorizontal(data.MirrorX, data.MirrorY, node.RotationAngle);
            if (!data.MirrorX.Equals(scaleTransform.ScaleX))
                data.MirrorX = scaleTransform.ScaleX;
            if (!data.MirrorY.Equals(scaleTransform.ScaleY))
                data.MirrorY = scaleTransform.ScaleY;

            if (IsCustomNodeEnabled)
            {
                ((CustomNode)node).SetScale((ShapeData)node.Data);

                node.LinksConnected.ToList().ForEach(link => { link.Route.UpdateLayout(); link.Route.RecomputePoints(); });
                node.UpdateLayout();
                node.UpdateAdornments();
            }
        });
        GoXamModel.CommitTransaction("MirrorAggregates");

    }

private ScaleTransform GetScaleTransformForHorizontal(double scaleX, double scaleY, double angle)
{
if (angle == 0 || angle == 180)
return new ScaleTransform(-scaleX, scaleY);
return new ScaleTransform(scaleX, -scaleY);
}

    private ScaleTransform GetScaleTransformForVertical(double scaleX, double scaleY, double angle)
    {
        if (angle == 0 || angle == 180)
            return new ScaleTransform(scaleX, -scaleY);
        return new ScaleTransform(-scaleX, scaleY);
    }

image

Before Mirroring :

image

If i select on top right and move down then it becomes smaller which is right.

After Mirroring :

image

If i select on top right and move down then it becomes larger which is wrong.

After mirroring the behaviour of resize is also mirrored. don’t understand why? What can be done to resolve this problem?

          entire template
      <StackPanel Tag="ControlValve" 
              go:Node.Location="{Binding Path=Data.Location, Mode=TwoWay}"
                    go:Node.RotationAngle="{Binding Path=Data.RotationAngle, Mode=TwoWay}"
              go:Part.Rotatable="True"
              go:Part.RotateAdornmentTemplate="{StaticResource NodeRotateAdornmentTemplate}"
              go:Node.LocationSpot="Center"  ToolTip="{Binding Path=Data.Key}"
              go:Node.SelectionElementName="ItemData" Name="ShapeLayout" ForceCursor="True"
              go:Node.Resizable="True" Cursor="SizeAll"
              go:Part.LayerName="{Binding Path=Part.IsSelected, Converter={StaticResource selectedLayerConverter}}"  
               HorizontalAlignment="Center" VerticalAlignment="Center">
            <Behaviors:Interaction.Triggers>
                <Behaviors:EventTrigger EventName="MouseLeftButtonDown">
                    <Behaviors:InvokeCommandAction Command="{Binding Path=Data.MouseLeftButtonDown}" PassEventArgsToCommand="True" />
                </Behaviors:EventTrigger>
            </Behaviors:Interaction.Triggers>
            <Grid Name="ItemData" MinHeight="10" MinWidth="15" Height="{Binding Path=Data.Height, Mode=TwoWay}" 
                  Width="{Binding Path=Data.Width, Mode=TwoWay}">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="5"/>
                    <ColumnDefinition Width="45*"/>
                    <ColumnDefinition Width="10*"/>
                    <ColumnDefinition Width="45*" />
                    <ColumnDefinition Width="5"/>
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition Height="5"/>
                    <RowDefinition Height="40*"/>
                    <RowDefinition Height="20*"/>
                    <RowDefinition Height="40*"/>
                </Grid.RowDefinitions>
                <!--Right port-->
                <flowsheeter:CustomPort PortColor="Red" PortType="NodeH2O" x:Name="In" go:Node.LinkableMaximum="1" ToolTip="{x:Static prop:Resources.In}" go:Node.PortId="{x:Static prop:Resources.In}" Grid.Row="2"  Grid.Column="3" Grid.ColumnSpan="2"  HorizontalAlignment="Right" VerticalAlignment="Center" go:Node.LinkableFrom="False" go:Node.LinkableTo="True"  Margin="0,0,2.7,0" Panel.ZIndex="1"/>
                <!--Left port-->
                <flowsheeter:CustomPort PortColor="Red" PortType="NodeH2O" x:Name="Out" go:Node.LinkableMaximum="1" ToolTip="{x:Static prop:Resources.Out}" go:Node.PortId="{x:Static prop:Resources.Out}" Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2" HorizontalAlignment="Left" VerticalAlignment="Center" go:Node.LinkableFrom="True" go:Node.LinkableTo="False" Margin="2.7,0,0,0" Panel.ZIndex="1" />
                <!--Top port-->
                <flowsheeter:CustomPort PortColor="Black" PortType="NodeSignal" x:Name="SignalInPort" go:Node.LinkableMaximum="1" ToolTip="{x:Static prop:Resources.Signal}" go:Node.PortId="{x:Static prop:Resources.Signal}" Grid.Row="0" Grid.RowSpan="2" Grid.Column="2"  HorizontalAlignment="Center" VerticalAlignment="Top" go:Node.LinkableFrom="False" go:Node.LinkableTo="True" Margin="0,2.7,0,0" Panel.ZIndex="1" />

                <Path Grid.Row="1" Grid.RowSpan="3" Grid.Column="1" Grid.ColumnSpan="3" Stretch="Fill" HorizontalAlignment="Center" VerticalAlignment="Center" Fill="White" Data="M 8 4 L 8 0 L 0 4 L 0 0 L 8 4 M 2 1 L 4 0 L 6 1" Stroke="Black" StrokeThickness="1"></Path>
                <Ellipse  Grid.Row="2"  Grid.Column="2"  Stroke="Black" StrokeThickness="1" Stretch="Fill"  Fill="Black" Panel.ZIndex="1"/>
            </Grid>
            <TextBlock Name="AggregateName" HorizontalAlignment="Center" Text="Control Valve" />
        </StackPanel>

I cannot tell from the code that you have supplied how the ScaleTransform is used. Are you setting the LayoutTransform or the RenderTransform?

My guess is that the ResizingTool.DoActivate is calling DiagramTool.FindToolHandleAt in order to decide what to do, but it is getting the top-left handle because your mirroring has changed the apparent location of the handle, but it really still is at its original location.

Hi Walter, I am applying the RenderTransform. Could you please suggest to fix this? or any sample would be helpful.

Thanks!!

Best Regards,
Darshit

The problem with RenderTransform is that elements appear elsewhere from where they actually are, which affects hit testing and geometric computations. You will need to override various ResizingTool methods in order for it to get the points in the correct coordinate system.

I’ll provide you with the implementation of the ResizingTool so that you can see how it works and figure out how to handle your sometimes-weird node.

I am using below method to override using new scale after mirroring.

public override void UpdateAdornments(Part part)
{
base.UpdateAdornments(part);
double scaleX = 1, scaleY = 1;
if (part.GetAdornment(ToolCategory) is CustomAdornment adornment)
{
Node node = (Node)part;

            var elt = node.SelectionElement;
            if (elt.RenderTransform != null && elt.RenderTransform != Transform.Identity)
            {
                Transform transform = elt.RenderTransform;
                ScaleTransform st = getScaleTransform(transform);
                if (st != null && !st.IsFrozen)
                {
                    scaleX = st.ScaleX;
                    scaleY = st.ScaleY;
                }
            }
            FrameworkElement frameworkElement = adornment.SelectionElement;
            adornment.SetScale(frameworkElement, scaleX, scaleY);
        }
    }

    private ScaleTransform getScaleTransform(Transform transform)
    {
        ScaleTransform st = transform as ScaleTransform;
        if (st == null && transform is TransformGroup tg)
        {
            foreach (Transform t in tg.Children)
            {
                st = t as ScaleTransform;
                if (st != null) break;
            }
        }

        return st;
    }

If that code is correct, it will help position the resizing handles where they expect to be. However, when dragging the handle, the points are still in the wrong coordinate system, since the code doesn’t know about that RenderTransform.

The RenderTransform only transforms coordinates when rendering – not for hit testing or any other operation.

Hi Walter,

I am able to resize in proper direction after mirroring the node as i am creating the adornments again but adornments are not in proper place as we need to change the adornment location spot.

Before mirroring - Spot is TopLeft
What will be the adornment Spot after mirroring since node’s position are getting changed? How to change the adornment spot to get in right place?

image

If you are still using a RenderTransform, I cannot help you.

I suggest that your flipping/mirroring operations not change the location of the node.

Yes, after mirroring the node points is different compare to original. How to handle this that after mirroring/rendering the node point are same and doesn’t change? So it will behave properly.

What is your LayoutTransform and how do you replace it?

Hi walter,

Based on the rotation angel i change the x and y axis to get it mirrored horizontally or vertically and set the scale for node and adornment using rendertransform.

Do u wish to look the code of setting of scale?

Here’s what I have so far. Alas, resizing doesn’t work when the node is rotated. I don’t know if that is important to you or not.

The needed code is in a custom ResizingTool. The CustomResizingTool doesn’t resize the Part.ResizeElementName but the Part.SelectionElementName.

The design also depends on a new data property, Flip, which has four values:

  • 0: normal
  • 1: flipped horizontally
  • 2: flipped vertically
  • 3: flipped both ways

I then defined two converters so that the LayoutTransform is bound to the “Flip” property, and so that the FromSpot and ToSpot are bound to the “Flip” property.

Flip.xaml:

<UserControl x:Class="WpfWindow1.Flip"
             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"
             xmlns:local="clr-namespace:WpfWindow1">
  <UserControl.Resources>
    <local:FlipToTransformConverter x:Key="theFlipToTransformConverter"/>
    <local:FlipToSpotConverter x:Key="theFlipToSpotConverter"/>

    <DataTemplate x:Key="NodeTemplate">
      <Grid
            Background="Transparent"
            go:Node.LocationSpot="Center"
            go:Node.Location="{Binding Path=Data.Location, Mode=TwoWay}"
            go:Part.Resizable="True" go:Part.ResizeElementName="PANEL"
            go:Part.Rotatable="True">
          <go:SpotPanel x:Name="PANEL" Grid.Row="0" Grid.Column="0" Background="Transparent"
                        LayoutTransform="{Binding Path=Data.Flip, Converter={StaticResource theFlipToTransformConverter}}">
            <Path x:Name="SHAPE" Data="M0 80 L0 0 50 0 M0 40 L50 40"
                  Stroke="Green" StrokeThickness="4" Margin="2 2 2 2"
                  Stretch="Fill" />
            <Rectangle x:Name="TR" Width="8" Height="8" go:Node.PortId="TR" Fill="LightBlue" StrokeThickness="0"
                         go:SpotPanel.Spot="TopRight" go:SpotPanel.Alignment="1 0 0 0"
                         go:Node.FromSpot="{Binding Path=Data.Flip, Converter={StaticResource theFlipToSpotConverter}, ConverterParameter=R}"
                         go:Node.ToSpot="{Binding Path=Data.Flip, Converter={StaticResource theFlipToSpotConverter}, ConverterParameter=R}"
                         go:Node.LinkableFrom="True" go:Node.LinkableTo="True" />
            <Rectangle x:Name="R" Width="8" Height="8" go:Node.PortId="R" Fill="Pink" StrokeThickness="0"
                         go:SpotPanel.Spot="MiddleRight" go:SpotPanel.Alignment="1 0.5 0 0"
                         go:Node.FromSpot="{Binding Path=Data.Flip, Converter={StaticResource theFlipToSpotConverter}, ConverterParameter=R}"
                         go:Node.ToSpot="{Binding Path=Data.Flip, Converter={StaticResource theFlipToSpotConverter}, ConverterParameter=R}"
                         go:Node.LinkableFrom="True" go:Node.LinkableTo="True" />
            <Rectangle x:Name="B" Width="8" Height="8" go:Node.PortId="BL" Fill="LightGreen" StrokeThickness="0"
                         go:SpotPanel.Spot="BottomLeft" go:SpotPanel.Alignment="0 1 0 0"
                         go:Node.FromSpot="{Binding Path=Data.Flip, Converter={StaticResource theFlipToSpotConverter}, ConverterParameter=B}"
                         go:Node.ToSpot="{Binding Path=Data.Flip, Converter={StaticResource theFlipToSpotConverter}, ConverterParameter=B}"
                         go:Node.LinkableFrom="True" go:Node.LinkableTo="True" />
            <Rectangle x:Name="TL" Width="8" Height="8" go:Node.PortId="TL" Fill="Yellow" StrokeThickness="0"
                         go:SpotPanel.Spot="TopLeft" go:SpotPanel.Alignment="0 0 0 0"
                         go:Node.FromSpot="{Binding Path=Data.Flip, Converter={StaticResource theFlipToSpotConverter}, ConverterParameter=L}"
                         go:Node.ToSpot="{Binding Path=Data.Flip, Converter={StaticResource theFlipToSpotConverter}, ConverterParameter=L}"
                         go:Node.LinkableFrom="True" go:Node.LinkableTo="True" />
          <ContextMenuService.ContextMenu>
              <ContextMenu>
                <MenuItem Header="Flip Horizontally" Click="FlipH_Click" />
                <MenuItem Header="Flip Vertically" Click="FlipV_Click" />
              </ContextMenu>
            </ContextMenuService.ContextMenu>
          </go:SpotPanel>
      </Grid>
    </DataTemplate>
    <DataTemplate x:Key="LinkTemplate">
      <go:LinkShape Stroke="Black" StrokeThickness="2"
                    go:Part.SelectionAdorned="True" go:Part.Reshapable="True">
        <go:Link.Route>
          <go:Route Routing="Orthogonal" Corner="4" Curve="JumpOver" 
                    RelinkableFrom="True" RelinkableTo="True" />
        </go:Link.Route>
      </go:LinkShape>
    </DataTemplate>
  </UserControl.Resources>
  <Grid>
    <go:Diagram x:Name="myDiagram"
                Padding="10"
                HorizontalContentAlignment="Center"
                VerticalContentAlignment="Center"
                InitialScale="2"
                NodeTemplate="{StaticResource NodeTemplate}"
                LinkTemplate="{StaticResource LinkTemplate}">
      <go:Diagram.ResizingTool>
        <local:CustomResizingTool />
      </go:Diagram.ResizingTool>      
    </go:Diagram>
  </Grid>
</UserControl>

Flip.xaml.cs:

using Northwoods.GoXam;
using Northwoods.GoXam.Model;
using Northwoods.GoXam.Tool;
using System;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;

namespace WpfWindow1 {
  /// <summary>
  /// Interaction logic for Flip.xaml
  /// </summary>
  public partial class Flip : UserControl {
    public Flip() {
      InitializeComponent();

      // model is a GraphLinksModel using instances of MyNodeData as the node data
      // and MyLinkData as the link data
      var model = new GraphLinksModel<MyNodeData, String, String, MyLinkData>();

      // Flip: 0 normal, 1 flipped horizontally, 2 flipped vertically, 3 flipped both ways 
      model.NodesSource = new ObservableCollection<MyNodeData>() {
          new MyNodeData() { Key="Alpha", Location=new Point(0, 0) },
          new MyNodeData() { Key="Beta", Location=new Point(150, 50), Flip=1 },
          new MyNodeData() { Key="Gamma", Location=new Point(-80, 120), Flip=2 },
          new MyNodeData() { Key="Delta", Location=new Point(100, 200), Flip=3 }
        };

      model.LinksSource = new ObservableCollection<MyLinkData>() {
          new MyLinkData() { From="Alpha", FromPort="TR", To="Beta", ToPort="TR" },
          new MyLinkData() { From="Alpha", FromPort="BL", To="Gamma", ToPort="TR" },
          new MyLinkData() { From="Gamma", FromPort="R", To="Delta", ToPort="R" },
          new MyLinkData() { From="Beta", FromPort="BL", To="Delta", ToPort="TL" }
      };

      model.Modifiable = true;
      model.HasUndoManager = true;

      myDiagram.Model = model;
    }

    private void FlipH_Click(object sender, RoutedEventArgs e) {
      var elt = sender as FrameworkElement;
      if (elt != null && elt.DataContext != null) {
        var data = ((PartManager.PartBinding)elt.DataContext).Data as MyNodeData;
        if (data != null) {
          myDiagram.StartTransaction("FlippedH");
          data.Flip ^= 1;
          myDiagram.CommitTransaction("FlippedH");
        }
      }
    }
    private void FlipV_Click(object sender, RoutedEventArgs e) {
      var elt = sender as FrameworkElement;
      if (elt != null && elt.DataContext != null) {
        var data = ((PartManager.PartBinding)elt.DataContext).Data as MyNodeData;
        if (data != null) {
          myDiagram.StartTransaction("FlippedV");
          data.Flip ^= 2;
          myDiagram.CommitTransaction("FlippedV");
        }
      }
    }

  }

  // Define custom node data; the node key is of type String.
  // Add a property named Color that might change.
  [Serializable]  // serializable in WPF to support the clipboard
  public class MyNodeData : GraphLinksModelNodeData<String> {
    public String Color {
      get { return _Color; }
      set {
        if (_Color != value) {
          String old = _Color;
          _Color = value;
          RaisePropertyChanged("Color", old, value);
        }
      }
    }
    private String _Color = "White";

    public int Flip {
      get { return _Flip; }
      set {
        if (_Flip != value) {
          int old = _Flip;
          _Flip = value;
          RaisePropertyChanged("Flip", old, value);
        }
      }
    }
    private int _Flip = 0;
  }

  // Define custom link data; the node key is of type String,
  // the port key should be of type String but is unused in this app.
  [Serializable]  // serializable in WPF to support the clipboard
  public class MyLinkData : GraphLinksModelLinkData<String, String> {
    // nothing to add
  }

  // Flip: 0 normal, 1 flipped horizontally, 2 flipped vertically, 3 flipped both ways 

  public class FlipToTransformConverter : IValueConverter {
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
      switch (value) {
        case 1: return new ScaleTransform(-1, 1);
        case 2: return new ScaleTransform(1, -1);
        case 3: return new ScaleTransform(-1, -1);
        default: return new ScaleTransform(1, 1);
      }
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
      throw new NotImplementedException();
    }
  }

  public class FlipToSpotConverter : IValueConverter {
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
      var name = parameter == null ? "R" : parameter.ToString();
      if (name.Length == 0) name = "R";
      switch (name[0]) {
        case 'T':  // top
          switch (value) {
            case 1: return new Spot(0.5, 0);  // flipped horizontally
            case 2: return new Spot(0.5, 1, 0, -8);  // flipped vertically
            case 3: return new Spot(0.5, 1, 0, -8);  // flipped both
            default: return new Spot(0.5, 0); // normal
          }
        case 'L':  // left
          switch (value) {
            case 1: return new Spot(1, 0.5, -8, 0);  // flipped horizontally
            case 2: return new Spot(0, 0.5);  // flipped vertically
            case 3: return new Spot(1, 0.5, -8, 0);  // flipped both
            default: return new Spot(0, 0.5); // normal
          }
        case 'R':  // right
          switch (value) {
            case 1: return new Spot(0, 0.5, 8, 0);  // flipped horizontally
            case 2: return new Spot(1, 0.5);  // flipped vertically
            case 3: return new Spot(0, 0.5, 8, 0);  // flipped both
            default: return new Spot(1, 0.5); // normal
          }
        default:  // bottom
          switch (value) {
            case 1: return new Spot(0.5, 1);  // flipped horizontally
            case 2: return new Spot(0.5, 0, 0, 8);  // flipped vertically
            case 3: return new Spot(0.5, 0, 0, 8);  // flipped both
            default: return new Spot(0.5, 1); // normal
          }
      }
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
      throw new NotImplementedException();
    }
  }

  public class CustomResizingTool : ResizingTool {
    public override void UpdateAdornments(Part part) {
      if (part == null || part is Link) return;  // can't resize links
      var ToolCategory = "Resize";
      Adornment adornment = null;
      if (part.IsSelected) {
        FrameworkElement selelt = null;
        //String eltname = part.ResizeElementName;
        //if (eltname != null) selelt = part.FindNamedDescendant(eltname);
        if (selelt == null) selelt = part.SelectionElement;
        if (selelt != null && part.CanResize() && Part.IsVisibleElement(selelt)) {
          adornment = part.GetAdornment(ToolCategory);
          if (adornment == null) {
            DataTemplate template = part.ResizeAdornmentTemplate;
            if (template == null) template = Diagram.FindDefault<DataTemplate>("DefaultResizeAdornmentTemplate");
            adornment = part.MakeAdornment(selelt, template);
            if (adornment != null) {
              adornment.Category = ToolCategory;
              adornment.LocationSpot = Spot.TopLeft;
            }
          }
          if (adornment != null) {
            Node node = (Node)part;
            Point loc = part.GetElementPoint(selelt, Spot.TopLeft);
            var shape = selelt as Shape;
            if (shape != null) loc.Offset(3, 3);
            // use the node's angle, not part.GetAngle(selelt)
            //?? this can be wrong if there are additional rotational transforms in the tree
            double angle = node.RotationAngle;
            UpdateResizeHandles(adornment.VisualElement, angle);
            adornment.Location = loc;
            adornment.RotationAngle = angle;
            adornment.Remeasure();
          }
        }
      }
      part.SetAdornment(ToolCategory, adornment);
    }

    // copied from ResizingTool.cs

    private void UpdateResizeHandles(FrameworkElement elt, double angle) {
      if (elt == null) return;
      if (NodePanel.GetFigure(elt) != NodeFigure.None) {
        SetResizeCursor(elt, angle);
      } else {
        int count = VisualTreeHelper.GetChildrenCount(elt);
        for (int i = 0; i < count; i++) {
          UpdateResizeHandles(VisualTreeHelper.GetChild(elt, i) as FrameworkElement, angle);
        }
      }
    }

    private void SetResizeCursor(FrameworkElement h, double angle) {
      Spot spot = SpotPanel.GetSpot(h);
      if (spot.IsNoSpot) spot = Spot.Center;
      double a = angle;
      if (spot.X <= 0) {  // left
        if (spot.Y <= 0) {  // top-left
          a += 225;
        } else if (spot.Y >= 1) {  // bottom-left
          a += 135;
        } else {  // middle-left
          a += 180;
        }
      } else if (spot.X >= 1) {  // right
        if (spot.Y <= 0) {  // top-right
          a += 315;
        } else if (spot.Y >= 1) {  // bottom-right
          a += 45;
        } else {  // middle-right
          // a += 0;
        }
      } else {  // middle-X
        if (spot.Y <= 0) {  // top-middle
          a += 270;
        } else if (spot.Y >= 1) {  // bottom-middle
          a += 90;
        } else {
          // handle is in the middle-middle -- don't do anything
          return;
        }
      }
      if (a < 0) a += 360;
      else if (a >= 360) a -= 360;
      if (a < 22.5)
        h.Cursor = Cursors.SizeWE;
      else if (a < 67.5)
        h.Cursor = Cursors.SizeNWSE;
      else if (a < 112.5)
        h.Cursor = Cursors.SizeNS;
      else if (a < 157.5)
        h.Cursor = Cursors.SizeNESW;
      else if (a < 202.5)
        h.Cursor = Cursors.SizeWE;
      else if (a < 247.5)
        h.Cursor = Cursors.SizeNWSE;
      else if (a < 292.5)
        h.Cursor = Cursors.SizeNS;
      else if (a < 337.5)
        h.Cursor = Cursors.SizeNESW;
      else
        h.Cursor = Cursors.SizeWE;
    }

    private Spot ComputeResizeSpot(FrameworkElement h) {
      int flip = 0;
      var data = this.AdornedNode.Data as MyNodeData;
      if (data != null) flip = data.Flip;

      Spot spot = SpotPanel.GetSpot(h);
      if (spot.IsNoSpot) spot = Spot.Center;
      double a = 0;
      if (spot.X <= 0) {  // left
        if (spot.Y <= 0) {  // top-left
          a += 225;
        } else if (spot.Y >= 1) {  // bottom-left
          a += 135;
        } else {  // middle-left
          a += 180;
        }
      } else if (spot.X >= 1) {  // right
        if (spot.Y <= 0) {  // top-right
          a += 315;
        } else if (spot.Y >= 1) {  // bottom-right
          a += 45;
        } else {  // middle-right
          // a += 0;
        }
      } else {  // middle-X
        if (spot.Y <= 0) {  // top-middle
          a += 270;
        } else if (spot.Y >= 1) {  // bottom-middle
          a += 90;
        } else {
          // handle is in the middle-middle -- don't do anything
          return spot;
        }
      }
      if (a < 0) a += 360;
      else if (a >= 360) a -= 360;
      var angle = a;
      if (flip == 1) {  // flip horizontally
        a = 180 - a;
      } else if (flip == 2) {  // flip vertically
        a = -a;
      } else if (flip == 3) {  // flipping both is same as rotating 180
        a += 180;
      }
      if (a < 0) a += 360;
      else if (a >= 360) a -= 360;
      Spot newspot = spot;
      if (a < 22.5)
        newspot = Spot.MiddleRight;
      else if (a < 67.5)
        newspot = Spot.BottomRight;
      else if (a < 112.5)
        newspot = Spot.BottomCenter;
      else if (a < 157.5)
        newspot = Spot.BottomLeft;
      else if (a < 202.5)
        newspot = Spot.MiddleLeft;
      else if (a < 247.5)
        newspot = Spot.TopLeft;
      else if (a < 292.5)
        newspot = Spot.TopCenter;
      else if (a < 337.5)
        newspot = Spot.TopRight;
      else
        newspot = Spot.MiddleRight;
      return newspot;
    }

    public override void DoActivate() {
      base.DoActivate();
      //var spot = SpotPanel.GetSpot(this.Handle);
      var spot = ComputeResizeSpot(this.Handle);
      var pt = this.AdornedNode.GetElementPoint(this.AdornedElement, spot.Opposite);

      int flip = 0;
      var data = this.AdornedNode.Data as MyNodeData;
      if (data != null) flip = data.Flip;
      switch (flip) {
        case 1:
          if (spot.X == 1) pt.X += this.OriginalBounds.Width;
          else if (spot.X == 0) pt.X -= this.OriginalBounds.Width;
          break;
        case 2:
          if (spot.Y == 1) pt.Y += this.OriginalBounds.Height;
          else if (spot.Y == 0) pt.Y -= this.OriginalBounds.Height;
          break;
        case 3:
          if (spot.X == 1) pt.X += this.OriginalBounds.Width;
          else if (spot.X == 0) pt.X -= this.OriginalBounds.Width;
          if (spot.Y == 1) pt.Y += this.OriginalBounds.Height;
          else if (spot.Y == 0) pt.Y -= this.OriginalBounds.Height;
          break;
        default: break;
      }
      this.OriginalPoint = pt;
    }

    private Point OriginalPoint;

    protected override void DoResize(Rect newr) {
      Node node = this.AdornedNode;
      FrameworkElement elt = node.FindNamedDescendant("SHAPE");

      //var spot = SpotPanel.GetSpot(this.Handle);
      var spot = ComputeResizeSpot(this.Handle);

      elt.Width = newr.Width;
      elt.Height = newr.Height;
      node.Remeasure();

      var pt = node.GetElementPoint(elt, spot.Opposite);
      var newloc = node.Location;
      newloc.X = newloc.X - pt.X + this.OriginalPoint.X;
      newloc.Y = newloc.Y - pt.Y + this.OriginalPoint.Y;
      node.Location = newloc;
    }
  }
}

Hi Walter, Thanks for your template. above code works fine in sample application after flipping the node.
I have tried same way but having issue in resizing like adornments are not resizing but the shape of node is resizing. Could you please let me know how to use spotpanel in my datatemplate and to and from spot as i have divided grid into rows and columns.

   <DataTemplate x:Key="{x:Static prop:Resources.ControlValve}">
        <StackPanel Tag="ControlValve" 
              go:Node.Location="{Binding Path=Data.Location, Mode=TwoWay}"
                    go:Node.RotationAngle="{Binding Path=Data.RotationAngle, Mode=TwoWay}"
              go:Part.Rotatable="True"
              go:Part.RotateAdornmentTemplate="{StaticResource NodeRotateAdornmentTemplate}"
              go:Node.LocationSpot="Center"  ToolTip="{Binding Path=Data.Key}"
              go:Node.SelectionElementName="ItemData" Name="ShapeLayout" ForceCursor="True"
              go:Node.Resizable="True" Cursor="SizeAll"
              go:Part.LayerName="{Binding Path=Part.IsSelected, Converter={StaticResource selectedLayerConverter}}"  
               HorizontalAlignment="Center" VerticalAlignment="Center">

            <Behaviors:Interaction.Triggers>
                <Behaviors:EventTrigger EventName="MouseLeftButtonDown">
                    <Behaviors:InvokeCommandAction Command="{Binding Path=Data.MouseLeftButtonDown}" PassEventArgsToCommand="True" />
                </Behaviors:EventTrigger>
            </Behaviors:Interaction.Triggers>
            <Grid Name="ItemData" MinHeight="10" MinWidth="15" Height="{Binding 
                Path=Data.Height, Mode=TwoWay}" 
                  Width="{Binding Path=Data.Width, Mode=TwoWay}">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="5"/>
                    <ColumnDefinition Width="45*"/>
                    <ColumnDefinition Width="10*"/>
                    <ColumnDefinition Width="45*" />
                    <ColumnDefinition Width="5"/>
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition Height="5"/>
                    <RowDefinition Height="40*"/>
                    <RowDefinition Height="20*"/>
                    <RowDefinition Height="40*"/>
                </Grid.RowDefinitions>
                <!--Right port-->
                <flowsheeter:CustomPort PortColor="Red" PortType="NodeH2O" x:Name="In" go:Node.LinkableMaximum="1" ToolTip="{x:Static prop:Resources.In}" go:Node.PortId="{x:Static prop:Resources.In}" Grid.Row="2"  Grid.Column="3" Grid.ColumnSpan="2"  HorizontalAlignment="Right" VerticalAlignment="Center" go:Node.LinkableFrom="False" go:Node.LinkableTo="True"  Margin="0,0,2.7,0" Panel.ZIndex="1"/>
                <!--Left port-->
                <flowsheeter:CustomPort PortColor="Red" PortType="NodeH2O" x:Name="Out" go:Node.LinkableMaximum="1" ToolTip="{x:Static prop:Resources.Out}" go:Node.PortId="{x:Static prop:Resources.Out}" Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2" HorizontalAlignment="Left" VerticalAlignment="Center" go:Node.LinkableFrom="True" go:Node.LinkableTo="False" Margin="2.7,0,0,0" Panel.ZIndex="1" />
                <!--Top port-->
                <flowsheeter:CustomPort PortColor="Black" PortType="NodeSignal" 
x:Name="SignalInPort" go:Node.LinkableMaximum="1" ToolTip="{x:Static prop:Resources.Signal}" 
go:Node.PortId="{x:Static prop:Resources.Signal}" Grid.Row="0" Grid.RowSpan="2" 
 Grid.Column="2"  HorizontalAlignment="Center" VerticalAlignment="Top" 
 go:Node.LinkableFrom="False" go:Node.LinkableTo="True" Margin="0,2.7,0,0" Panel.ZIndex="1" />

                <Path Grid.Row="1" Grid.RowSpan="3" Grid.Column="1" Grid.ColumnSpan="3" 
           Stretch="Fill" HorizontalAlignment="Center" VerticalAlignment="Center" Fill="White" 
       Data="M 8 4 L 8 0 L 0 4 L 0 0 L 8 4 M 2 1 L 4 0 L 6 1" Stroke="Black" StrokeThickness="1"></Path>
                <Ellipse  Grid.Row="2"  Grid.Column="2"  Stroke="Black" StrokeThickness="1" 
          Stretch="Fill"  Fill="Black" Panel.ZIndex="1"/>
            </Grid>
            <TextBlock Name="AggregateName" HorizontalAlignment="Center" Text="Control Valve" />
        </StackPanel>
        <DataTemplate.Triggers>
            <DataTrigger Binding="{Binding Path=Data.IsAggregateNameHidden}" Value="True">
                <Setter TargetName="AggregateName" Property="Visibility" Value="Collapsed" />
                <Setter TargetName="ShapeLayout" Property="ForceCursor" Value="False"/>
            </DataTrigger>
        </DataTemplate.Triggers>
    </DataTemplate>

UI:
image

You don’t have to use a SpotPanel if you don’t want to. But resizing a SpotPanel is much more difficult than just resizing the main element of a SpotPanel such as when it is a Shape or whatever else you want it to be.

The CustomResizingTool resizes the element that is the SelectionElementName. In my example it is the element named “SHAPE”, in your code it is the Grid Panel. Maybe rename your Grid Panel to be “SHAPE”, or change the DoResize code?

Hi Walter,

I have changed the Grid panel name and i can see after flipping the node vertically handle are correct and resize is also working in proper direction but adornments are getting flipped as well along with the node. This might be because of tospot and fromspot is changed. Do we need to change tospot and fromspot location? If yes, could you please guide for datatemplate where to call the converter with parameter to update adornments properly.

 <DataTemplate x:Key="{x:Static prop:Resources.ControlValve}">
        <StackPanel Tag="ControlValve" 
              go:Node.Location="{Binding Path=Data.Location, Mode=TwoWay}"
                    go:Node.RotationAngle="{Binding Path=Data.RotationAngle, Mode=TwoWay}"
              go:Part.Rotatable="True"
              go:Part.RotateAdornmentTemplate="{StaticResource NodeRotateAdornmentTemplate}"
              go:Node.LocationSpot="Center"  ToolTip="{Binding Path=Data.Key}"
              go:Node.SelectionElementName="SHAPE" Name="ShapeLayout" ForceCursor="True"
              go:Node.Resizable="True" Cursor="SizeAll" go:Part.ResizeElementName="SHAPE"
              go:Part.LayerName="{Binding Path=Part.IsSelected, Converter={StaticResource 
selectedLayerConverter}}"  
               HorizontalAlignment="Center" VerticalAlignment="Center">

            <Behaviors:Interaction.Triggers>
                <Behaviors:EventTrigger EventName="MouseLeftButtonDown">
                    <Behaviors:InvokeCommandAction Command="{Binding Path=Data.MouseLeftButtonDown}" PassEventArgsToCommand="True" />
                </Behaviors:EventTrigger>
            </Behaviors:Interaction.Triggers>
            <Grid x:Name="SHAPE" MinHeight="10" MinWidth="15" Height="{Binding Path=Data.Height, Mode=TwoWay}" 
                  Width="{Binding Path=Data.Width, Mode=TwoWay}"
                  LayoutTransform="{Binding Path=Data.Flip, Converter={StaticResource theFlipToTransformConverter}}"
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="5"/>
                    <ColumnDefinition Width="45*"/>
                    <ColumnDefinition Width="10*"/>
                    <ColumnDefinition Width="45*" />
                    <ColumnDefinition Width="5"/>
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition Height="5"/>
                    <RowDefinition Height="40*"/>
                    <RowDefinition Height="20*"/>
                    <RowDefinition Height="40*"/>
                </Grid.RowDefinitions>
                <!--Right port-->
                <flowsheeter:CustomPort PortColor="Red" PortType="NodeH2O" x:Name="In" go:Node.LinkableMaximum="1" ToolTip="{x:Static prop:Resources.In}" go:Node.PortId="{x:Static prop:Resources.In}" Grid.Row="2"  Grid.Column="3" Grid.ColumnSpan="2"  HorizontalAlignment="Right" VerticalAlignment="Center" go:Node.LinkableFrom="False" go:Node.LinkableTo="True"  Margin="0,0,2.7,0" Panel.ZIndex="1"/>
                <!--Left port-->
                <flowsheeter:CustomPort PortColor="Red" PortType="NodeH2O" x:Name="Out" go:Node.LinkableMaximum="1" ToolTip="{x:Static prop:Resources.Out}" go:Node.PortId="{x:Static prop:Resources.Out}" Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2" HorizontalAlignment="Left" VerticalAlignment="Center" go:Node.LinkableFrom="True" go:Node.LinkableTo="False" Margin="2.7,0,0,0" Panel.ZIndex="1" />
                <!--Top port-->
                <flowsheeter:CustomPort PortColor="Black" PortType="NodeSignal" x:Name="SignalInPort" go:Node.LinkableMaximum="1" ToolTip="{x:Static prop:Resources.Signal}" go:Node.PortId="{x:Static prop:Resources.Signal}" Grid.Row="0" Grid.RowSpan="2" Grid.Column="2"  HorizontalAlignment="Center" VerticalAlignment="Top" go:Node.LinkableFrom="False" go:Node.LinkableTo="True" Margin="0,2.7,0,0" Panel.ZIndex="1" />

                <Path Grid.Row="1" Grid.RowSpan="3" Grid.Column="1" Grid.ColumnSpan="3" Stretch="Fill" HorizontalAlignment="Center" VerticalAlignment="Center" Fill="White" Data="M 8 4 L 8 0 L 0 4 L 0 0 L 8 4 M 2 1 L 4 0 L 6 1" Stroke="Black" StrokeThickness="1"></Path>
                <Ellipse  Grid.Row="2"  Grid.Column="2"  Stroke="Black" StrokeThickness="1" Stretch="Fill"  Fill="Black" Panel.ZIndex="1"/>
            </Grid>
            <TextBlock Name="AggregateName" HorizontalAlignment="Center" Text="Control Valve" />
        </StackPanel>
        <DataTemplate.Triggers>
            <DataTrigger Binding="{Binding Path=Data.IsAggregateNameHidden}" Value="True">
                <Setter TargetName="AggregateName" Property="Visibility" Value="Collapsed" />
                <Setter TargetName="ShapeLayout" Property="ForceCursor" Value="False"/>
            </DataTrigger>
        </DataTemplate.Triggers>
    </DataTemplate>

image

Sorry, but I don’t have time now to modify your template. Could you just adapt the template I gave you above? Make changes incrementally so that you can notice when something breaks.