LocationSpot not what it should be

Hi,
I didn’t realize it before, but now I want to rotate my nodes and the behavior shows up.

The LocationSpot of my node seams to be mirrored. The default should be TopLeft, but the node turns around BottomRight. The Location is also set on this “wrong” place. I want my nodes to have their location set on TopLeft, but they are set on BottomRight.

With the other LocationSpots It’s the same:
TopLeft -> BottomRight
TopRight -> BottomLeft
BottomLeft -> TopRight
BottomRight -> TopLeft

Did I make a mistake, and where should I look for it?

If you modify one of the regular samples to make the Nodes go:Node.Rotatable="True", do you get that behavior? I would not think so. Furthermore you can control the rotation point by setting Node.RotationSpot.

You are right! It’s working in the examples.
But I have no idea what I have programmed to change this behavior.
The only thing I have is a converter to convert my position stored in two values (x and y) to the location point and back.

Also I have strange effects setting the RotationSpot - it also changes the position of the little yellow grip.

The rotation handle (the small yellow circle that is part of the rotation Adornment) should be to the right of the rotation point if the angle is zero.

I have uploaded two little videos:
Part1
Part2

Where in Part1 LocationSpot should be TopLeft, but it is Center.
And in Part2 after changing RotationSpot to Center LocationSpot is TopLeft, but the rotation handle was moved to another position.

I have no idea.

I just tried a bunch of different values for go:Node.RotationSpot, and rotation worked consistently. I also tried a bunch of RotationSpot values when the go:Part.SelectionElementName and/or the go:Node.LocationSpot were specified (i.e. not the whole Node).

Everything seemed to work as I would have expected.

Did you remember the following issue?
Insert point for collection of nodes

I think we have similar problems here.

I will soon post a video to show you.

In Part3 you can see 3 nodes. Each node has LocationSpot=“TopLeft” and RotationSpot=“Center”.
As you can see the Location of all nodes is 0, 0 but since the LocationSpot is TopLeft and not Center there should be other values to see.

In Part4 you can see 5 nodes in a row. Now I select different count of nodes. As you can see the rotationpoint isn’t in the center of the collection. It’s always on the bottomline and it seems the the calculation is ignoring the height and width of the nodes and uses bottomright instead of topleft.
This should be posted on my other thread, but I think it’s all about the same issues.

Rotation modifies the RenderTransform, not the LayoutTransform, so what you are seeing in Part3 is quite plausibly correct. It depends on your template.

For Part4, I guess you have implemented a custom rotation tool, as I may have suggested in another topic. I’m wondering if the difference in apparent position and actual Position is causing a problem in the calculations.

I have other strange effects as you can see in Part5.
After the rotation I can’t resize the node as expected.

My have no idea how my template can be responsible for this.

Since I have many different elements to display in the nodes, I’ve made a style with all the major bindings.

<Style x:Key="ElementGridStyle" TargetType="{x:Type Grid}">
    <Setter Property="go:Part.Resizable" Value="{Binding Path=Part.Diagram.DataContext.EditMode}"/>
    <Setter Property="go:Part.SelectionAdorned" Value="False" />
    <Setter Property="go:Node.ZOrder" Value="{Binding Path=Data.Z}" />
    <Setter Property="go:Part.Movable" Value="{Binding Path=Data.IsMovable}" />
    <Setter Property="go:Part.Selectable" Value="{Binding Path=Data.IsSelectable}" />
    <Setter Property="go:Part.Deletable" Value="{Binding Path=Data.IsDeletable}" />
    <Setter Property="go:Part.Rotatable" Value="{Binding Path=Data.IsRotatable}" />
    <Setter Property="Width" Value="{Binding Path=Data.Width, Mode=TwoWay}" />
    <Setter Property="Height" Value="{Binding Path=Data.Height, Mode=TwoWay}" />
    <Setter Property="Opacity" Value="{Binding Path=Data.Opacity}" />
    <Setter Property="Visibility" Value="{Binding Path=Data.Visibility}" />
    <Setter Property="Background" Value="{Binding Path=Part.Diagram.DataContext.EditMode, Converter={StaticResource IsEditModeBackgroundConverter}}" />
    <Setter Property="Tag" Value="{Binding Part.Diagram}" />
    <Setter Property="go:Node.LocationSpot" Value="TopLeft" />
    <Setter Property="go:Node.RotationSpot" Value="Center" />
    <Setter Property="go:Node.Location">
        <Setter.Value>
            <MultiBinding Converter="{StaticResource PosToPointConverter}" Mode="TwoWay">
                <Binding Path="Data.X" Mode="TwoWay" />
                <Binding Path="Data.Y" Mode="TwoWay" />
            </MultiBinding>
        </Setter.Value>
    </Setter>
    <Setter Property="go:Node.RotationAngle" Value="{Binding Path=Data.Angle, Mode=TwoWay}" />
    <Setter Property="ContextMenu" Value="{StaticResource ElementContextMenu}" />
    <Setter Property="ToolTip" Value="{x:Null}" />
    <Style.Triggers>
        <DataTrigger Binding="{Binding Path=Part.Diagram.DataContext.EditMode}" Value="True">
            <Setter Property="ToolTip" Value="{Binding Path=Data.Description}" />
        </DataTrigger>
        <DataTrigger Binding="{Binding Path=Part.Diagram.DataContext.EditMode}" Value="False">
            <Setter Property="ContextMenu" Value="{x:Null}" />
        </DataTrigger>
        <Trigger Property="ToolTip" Value="{x:Static system:String.Empty}">
            <Setter Property="ToolTipService.IsEnabled" Value="False" />
        </Trigger>
    </Style.Triggers>
</Style>

Then for the element you can see in the videos there is a datatemplate:

<DataTemplate x:Key="SimpleButton" DataType="{x:Type classes:Element}">
    <Grid Style="{StaticResource ElementGridStyle}">
        <Button x:Name="button" Style="{StaticResource KeyboardButtonStyle}"
                Visibility="{Binding Path=Data.NoDisplay, Converter={StaticResource BooleanToVisibilityConverterInverse}}"
                IsHitTestVisible="{Binding Path=Part.Diagram.DataContext.EditMode, Converter={StaticResource BooleanNegationConverter}}"
                Background="{Binding Path=Data.BackgroundBrush}"
                BorderBrush="{Binding Path=Data.BorderColor, Converter={StaticResource ColorToBrushConverter}}"
                Foreground="{Binding Path=Data.ForegroundColor, Converter={StaticResource ColorToBrushConverter}}"
                HorizontalContentAlignment="{Binding Path=Data.Horizontal}"
                VerticalContentAlignment="{Binding Path=Data.Vertical}"
                Padding="2"
                FontSize="{Binding Path=Data.FontSize}"
                FontFamily="{Binding Path=Data.FontFamily, TargetNullValue='Segoe UI'}"
                >
            <TextBlock Text="{Binding Path=Data.Text}"  TextWrapping="{Binding Data.Wrap, Converter={StaticResource WrapConverter}}" />
        </Button>
        <ContentControl Template="{StaticResource PlaceholderTemplate}" />
        <ToggleButton x:Name="PinnedButton" Style="{StaticResource PinnedToggleButton}" Opacity="0" />
        <ToggleButton x:Name="QuickZoomButton" Style="{StaticResource QuickZoomToggleButton}" Opacity="0" />
        <Button x:Name="SettingButton" Style="{StaticResource SettingButton}" Opacity="0" />
        <Border Style="{StaticResource MarkerBorderStyle}" Visibility="{Binding Path=Data.IsMarked, Converter={StaticResource BooleanToVisibilityConverter}}"/>
    </Grid>
    <DataTemplate.Triggers>
        <DataTrigger Binding="{Binding Path=Data.IsSelected}" Value="True">
            <Setter TargetName="button" Property="Background" Value="{Binding Path=Data.BackgroundBrushSelected}" />
            <Setter TargetName="button" Property="BorderBrush" Value="{Binding Path=Data.BorderColorSelected, Converter={StaticResource ColorToBrushConverter}}" />
            <Setter TargetName="button" Property="Foreground" Value="{Binding Path=Data.ForegroundColorSelected, Converter={StaticResource ColorToBrushConverter}}" />
        </DataTrigger>
        <Trigger Property="Button.IsMouseOver" Value="True">
            <Setter TargetName="PinnedButton" Property="Opacity" Value="1"></Setter>
            <Setter TargetName="QuickZoomButton" Property="Opacity" Value="1"></Setter>
            <Setter TargetName="SettingButton" Property="Opacity" Value="1"></Setter>
        </Trigger>
    </DataTemplate.Triggers>
</DataTemplate>

I’ve spent some time trying to get your XAML to work, and eventually I got something going, but I was unable to produce any problems with rotating the nodes. But maybe I still don’t understand exactly what circumstances you have that aren’t working for you.

If you could modify the Minimal sample to demonstrate the problem, without any unrelated XAML, I might be able to help you.

I will try it!

In the meanwhile I have cleared the template for my simpleButton to this, but it shows the same issues:

<DataTemplate x:Key="SimpleButton">
        <Button x:Name="button"
                        go:Part.Resizable="True"
                        go:Part.Movable="True"
                        go:Part.Selectable="True"
                        go:Part.Deletable="True"
                        go:Part.Rotatable="True"
                >
        </Button>
</DataTemplate>

I also disabled the CustomResizingTool and the CustomRotationTool.

Hi Walter,
I modified your minimal example and the effect of video Part5 is reproducible:

<DataTemplate x:Key="MyNodeTemplate">
  <Border BorderBrush="Black" BorderThickness="1"
          Padding="3" CornerRadius="3"
          Background="{Binding Path=Data.Color}"
          go:Part.SelectionAdorned="True"
          go:Node.Location="{Binding Path=Data.Location, Mode=TwoWay}"
          go:Part.Resizable="True"   <---
          go:Part.Rotatable="True"   <---
          >
    <TextBlock Text="{Binding Path=Data.Key}" />
  </Border>
</DataTemplate>

If you turn the “delta” node about 90° and than try to resize it, it behaves complete different from the resize on 0°.
The other issue with the “moving” rotationpoint I haven’t reproduced so far.

If you want to change the behavior, you can override ResizingTool.DoResize. Here’s the code:

    protected virtual void DoResize(Rect newr) {
      Node node = this.AdornedNode;
      if (node != null) {
        Rect oldr = this.OriginalBounds;
        Point oldloc = this.OriginalLocation;
        Point newloc = oldloc;
        if (oldloc.X > oldr.Right) {
          newloc.X = newr.X + newr.Width + oldloc.X - oldr.Right;
        } else if (oldloc.X > oldr.X && oldr.Width > 0) {
          double fx = (oldloc.X-oldr.X)/oldr.Width;
          newloc.X = newr.X + fx*newr.Width;
        } else {
          newloc.X = newr.X + (oldloc.X - oldr.X);
        }
        if (oldloc.Y > oldr.Bottom) {
          newloc.Y = newr.Y + newr.Height + oldloc.Y - oldr.Bottom;
        } else if (oldloc.Y > oldr.Y && oldr.Height > 0) {
          double fx = (oldloc.Y-oldr.Y)/oldr.Height;
          newloc.Y = newr.Y + fx*newr.Height;
        } else {
          newloc.Y = newr.Y + (oldloc.Y - oldr.Y);
        }
        Point rotloc = Geo.RotatePoint(newloc, oldloc, this.Angle);
        node.Location = rotloc;
        FrameworkElement elt = this.AdornedElement;
        if (elt != null) {
          elt.Width = newr.Width;
          elt.Height = newr.Height;
          node.InvalidateVisual(elt);
        }
      }
    }

InvalidateVisual is an internal method; I’m not sure what you can do there instead.

Even Geo and this.Angle are unknown.

    public static Point RotatePoint(Point p, Point c, double angle) {
      if (angle == 0 || p == c)
        return p;
      double rad = angle * Math.PI / 180;
      double cosine = Math.Cos(rad);
      double sine = Math.Sin(rad);
      double dx = p.X - c.X;
      double dy = p.Y - c.Y;
      return new Point((c.X + cosine * dx - sine * dy),
                       (c.Y + sine * dx + cosine * dy));
    }

this.Angle is the value of Node.GetAngle saved during DoActivate.

Hi Walter,
did you try this? Of course I think - but in the Minimal-Example still doesn’t behave as expected.

Maybe it’s the Node.InvalidateVisual?

Do you agree that this is not the behavior someone expected when rotating a node?
Is it possible for you to fix this in a next version?

I must disable the rotation of nodes in my application, which is very sad, cause I can’t explain the customer why the node is changing it’s position on resize after rotating,

Perhaps you don’t want to set the Node.Location in DoResize?

Hi Walter,
I don’t get it!
Did you look at the resize behavior of an unrotated node and compare it with the resize behavior of a node rotated 90°?
If you use your minimal example - no customResizingTool, just adding Resizable=“True” and Rotatable=“True” you must be able to see the difference between the behaviors!

The CustomResizingTool makes it only worse. Maybe caused by the lack of Node.InvalidateVisual() - I don’t know.

Even if this were a suitable solution, I have to rewrite a lot of code since I already use a customResizeTool for resizing collections and so on (which I have disabled for the tests of this rotating stuff of course).

I still think it would be better to fix this in the goxam code - I think nobody relied on the existing behavior.

Here’s the code that I tried and that didn’t work:
I even tried to comment out the line node.Location = rotloc;

public class CustomResizingTool : ResizingTool
{
    private double Angle;
    public override void DoActivate()
    {
        base.DoActivate();
        Angle = AdornedNode.GetAngle(AdornedElement);
    }

    protected override void DoResize(Rect newr)
    {
        Node node = this.AdornedNode;
        if (node != null)
        {
            Rect oldr = this.OriginalBounds;
            Point oldloc = this.OriginalLocation;
            Point newloc = oldloc;
            if (oldloc.X > oldr.Right)
            {
                newloc.X = newr.X + newr.Width + oldloc.X - oldr.Right;
            }
            else if (oldloc.X > oldr.X && oldr.Width > 0)
            {
                double fx = (oldloc.X - oldr.X) / oldr.Width;
                newloc.X = newr.X + fx * newr.Width;
            }
            else
            {
                newloc.X = newr.X + (oldloc.X - oldr.X);
            }
            if (oldloc.Y > oldr.Bottom)
            {
                newloc.Y = newr.Y + newr.Height + oldloc.Y - oldr.Bottom;
            }
            else if (oldloc.Y > oldr.Y && oldr.Height > 0)
            {
                double fx = (oldloc.Y - oldr.Y) / oldr.Height;
                newloc.Y = newr.Y + fx * newr.Height;
            }
            else
            {
                newloc.Y = newr.Y + (oldloc.Y - oldr.Y);
            }
            Point rotloc = RotatePoint(newloc, oldloc, this.Angle);
            node.Location = rotloc;
            FrameworkElement elt = this.AdornedElement;
            if (elt != null)
            {
                elt.Width = newr.Width;
                elt.Height = newr.Height;
                //node.InvalidateVisual(elt);
                elt.InvalidateVisual();
            }
        }
    }

    public static Point RotatePoint(Point p, Point c, double angle)
    {
        if (angle == 0 || p == c)
            return p;
        double rad = angle * Math.PI / 180;
        double cosine = Math.Cos(rad);
        double sine = Math.Sin(rad);
        double dx = p.X - c.X;
        double dy = p.Y - c.Y;
        return new Point((c.X + cosine * dx - sine * dy),
                         (c.Y + sine * dx + cosine * dy));
    }
}

It is certainly possible to have situations where the results are not what you want. In your minimal example, you had:

<DataTemplate x:Key="MyNodeTemplate">
  <Border BorderBrush="Black" BorderThickness="1"
          Padding="3" CornerRadius="3"
          Background="{Binding Path=Data.Color}"
          go:Part.SelectionAdorned="True"
          go:Node.Location="{Binding Path=Data.Location, Mode=TwoWay}"
          go:Part.Resizable="True"   <---
          go:Part.Rotatable="True"   <---
          >
    <TextBlock Text="{Binding Path=Data.Key}" />
  </Border>
</DataTemplate>

If you add go:Node.LocationSpot="Center", does rotating and resizing then work the way that you want?

If so, then it appears that the behavior of ResizingTool.DoResize isn’t handling the case for rotated nodes where the LocationSpot isn’t Spot.Center. I don’t have a general solution for you. That’s why I gave you the implementation of DoResize so you can implement what you want.

I think the unavailability of the internal Node.invalidateVisual does not affect this issue.