Rotating RadDiagramShape inside of RadDiagramContainer

10 posts, 1 answers
  1. Tim
    Tim avatar
    14 posts
    Member since:
    Apr 2013

    Posted 29 Oct 2013 Link to this post

    Is there a way to rotate child shapes inside of a rotating container? The idea is to keep the children of the container laid out exactly the same as the container is rotated. An additional complication is that I need to be able to select the child shapes.

    This second requirement is tricky because it essentially eliminates the easy solutions -- i.e. Ellipses/Rects inside the container instead of RadDiagramShapes. 

    Right now, my idea is to hook the OnRotationAngleChanged method in the RadDiagramContainer and then iterate through the children and apply a RotateTransform to the children.
     
    protected override void OnRotationAngleChanged(double newValue, double oldValue)
            {
                base.OnRotationAngleChanged(newValue, oldValue);
     
                List<RadDiagramShape> childrenList = Children.OfType<RadDiagramShape>().ToList();
     
                // get rotation center
                double centerX = (ActualWidth / 2);
                double centerY = (ActualHeight / 2);
     
                foreach (RadDiagramShape item in childrenList)
                {
                    RotateTransform transform = new RotateTransform(newValue, centerX, centerY);
                    item.LayoutTransform = transform;
                }
            }

    The idea here is that I would use the container center as the rotation origin and just have the children rotate around that point. That didn't work - in fact, it seems any values supplied as centerX/centerY produce the same behavior, which makes me think that the container might be doing some coercing of the child during layout. 

    Am I making this way too hard? 



  2. Tim
    Tim avatar
    14 posts
    Member since:
    Apr 2013

    Posted 29 Oct 2013 Link to this post

    I realized that what I'm trying to do is very similar to what is happening to connectors during the rotation process. I peeked at that code and modified my own code to this:

    protected override void OnRotationAngleChanged(double newValue, double oldValue)
           {
               base.OnRotationAngleChanged(newValue, oldValue);
     
               List<RadDiagramShape> childrenList = Children.OfType<RadDiagramShape>().ToList();
     
               // get rotation center
               double centerX = ActualBounds.Center().X;
               double centerY = ActualBounds.Center().Y;
               RotateTransform transform = new RotateTransform(newValue - oldValue, centerX, centerY);
                
               foreach (RadDiagramShape item in childrenList)
               {
                   item.Position = transform.Transform(item.Position);
                   item.RotationAngle = newValue;
               }
           }

    This gets me *very* close to what I need, but it appears the rotation center is off by just a bit. Should I use Bounds instead of ActualBounds here? 
  3. UI for WPF is Visual Studio 2017 Ready
  4. Tim
    Tim avatar
    14 posts
    Member since:
    Apr 2013

    Posted 29 Oct 2013 Link to this post

    This seems to work perfectly...
    protected override void OnRotationAngleChanged(double newValue, double oldValue)
           {
               base.OnRotationAngleChanged(newValue, oldValue);
     
               List<RadDiagramShape> childrenList = Children.OfType<RadDiagramShape>().ToList();
     
               // get rotation center
               double centerX = ActualBounds.Center().X;
               double centerY = ActualBounds.Center().Y;
     
               double offset = -8;
     
               RotateTransform transform = new RotateTransform(newValue - oldValue, centerX + offset, centerY + offset);
     
               foreach (RadDiagramShape item in childrenList)
               {
                   item.Position = transform.Transform(item.Position);
                   item.RotationAngle = newValue;
               }
           }

    I have no idea why the -8 offset is needed... maybe BorderThickness or Margin/Padding values? 





  5. Answer
    Zarko
    Admin
    Zarko avatar
    755 posts

    Posted 01 Nov 2013 Link to this post

    Hi Tim,
    I'm really not sure why the 8 pixel offset works for you because when I tried it it didn't seem to work very well. By default there's a 10px margin in the container (DiagramConstants.ContainerMargin) and I guess it could interface with the rotation.
    This is the code that works for me and if you want you could try it out :
    protected override void OnRotationAngleChanged(double newValue, double oldValue)
    {
        base.OnRotationAngleChanged(newValue, oldValue);
     
        List<RadDiagramShape> childrenList = Children.OfType<RadDiagramShape>().ToList();
        // get rotation center
        var center = this.Bounds.Center();
     
        foreach (RadDiagramShape shape in childrenList)
        {
            var shapeBounds = shape.Bounds;
            shape.RotationAngle = newValue;
            var newCenter = shapeBounds.Center().Rotate(center, newValue - oldValue);
            shape.Position = new Point(newCenter.X - (shapeBounds.Width / 2), newCenter.Y - (shapeBounds.Height / 2));
        }
    }
    I hope I was able to help you and if you have further questions please feel free to ask.

    Regards,
    Zarko
    Telerik
    TRY TELERIK'S NEWEST PRODUCT - EQATEC APPLICATION ANALYTICS for WPF.
    Learn what features your users use (or don't use) in your application. Know your audience. Target it better. Develop wisely.
    Sign up for Free application insights >>
  6. Tim
    Tim avatar
    14 posts
    Member since:
    Apr 2013

    Posted 05 Nov 2013 Link to this post

    That works great. 

    Unfortunately, it leads to another issue. When loading a diagram and setting the saved rotation angle, the container is sized based on its children being rotated, but itself at a rotation angle of 0 degrees. I've tried various ways of delaying the call to set the rotation until after the container has been created, but nothing has worked so far. Is there a way to lock the container size down or to turn off the automatic sizing to children feature?

    Edit: Further investigation points to this piece of code being the culprit:

    protected virtual void OnChildBoundsChanged(IDiagramItem diagramItem)
           {
               if (((this.isLoaded && (base.ServiceLocator != null)) && (!base.ServiceLocator.DraggingService.IsDragging && !base.ServiceLocator.ResizingService.IsResizing)) && ((!base.ServiceLocator.RotationService.IsRotating && !base.ServiceLocator.ManipulationPointService.IsManipulating) && !base.ServiceLocator.UndoRedoService.IsActive))
               {
                   this.UpdateContainerLayout();
               }
           }

    Since the RotationService isn't being called to rotate the container during load (I'm setting the rotation angle manually), the container bounds are being recalculated when the rotation angle is set. I'm trying to replicate the exact code path that happens when the user rotates the container manually so this is a problem. 

    I can add RotationServce.StartRotate() and RotationService.CompleteRotate() calls to my OnRotationAngleChanged handler which will fix the loading issue, but this breaks when manually rotating the container. I can probably hack some state flags into the rotation angle handler but would really like a cleaner solution... 


    Edit #2: I guess the obvious solution of overriding the OnChildBoundsChanged method also works. Huzzah.
  7. Zarko
    Admin
    Zarko avatar
    755 posts

    Posted 08 Nov 2013 Link to this post

    Hi Tim,
    Yes you can override the OnChildBoundsChanged but I'm not sure if this will help you in your specific scenario because the UpdateContainerLayout is also called on loaded so it'll recalculate the size and mess up the layout. With Q3 we released a couple of new virtual methods (CalculateContentBounds, CalculateShapeBounds and etc.) that you can use to customize the container layout.
    For you scenario you can do something like this:
    private void OnLoaded(object sender, RoutedEventArgs e)
    {
        this.hasLoaded = true;
    }
     
    protected override Rect CalculateContentBounds(Rect newShapeBounds)
    {
        if (this.hasLoaded)
            return base.CalculateContentBounds(newShapeBounds);
     
        return this.ContentBounds;
    }
    And it should fix the problem.
    If you have more questions please feel free to ask.

    Regards,
    Zarko
    Telerik
    TRY TELERIK'S NEWEST PRODUCT - EQATEC APPLICATION ANALYTICS for WPF.
    Learn what features your users use (or don't use) in your application. Know your audience. Target it better. Develop wisely.
    Sign up for Free application insights >>
  8. Tim
    Tim avatar
    14 posts
    Member since:
    Apr 2013

    Posted 08 Nov 2013 Link to this post

    Hi Zarko,

    That helped a lot!

    However, I think you meant for the code snippet to look like this:

    private void OnLoaded(object sender, RoutedEventArgs e)
    {
        this.hasLoaded = true;
    }
      
    protected override Rect CalculateContentBounds(Rect newShapeBounds)
    {
        if (!this.hasLoaded)
            return base.CalculateContentBounds(newShapeBounds);
      
        return this.ContentBounds;
    }

    if(!hasLoaded) will get you the initial bounds calculation which will then be returned via ContentBounds in subsequent calls.

    This got me really close, but there are still a few gotcha's.

    My diagram data is loaded via an IGraphSource implementation. I set the position and rotation angle of an item before adding it to the graph. Using the above approach results in the initial content bounds calculation being done with the item at the give rotation angle.

    This is a problem because I need the initial calculation done when the item is not rotated, and then have the rotation applied after the bounds are calculated. 

    To get this working I needed to:

    1. Save of the item's rotation angle
    2. Set the item's rotation angle to zero
    3. Add the item to the graph
    4. Have the diagram draw the item
    5. Apply the rotation angle

    Steps 1-4 are straightforward, but Step 5 is a little tricky because of the timing of applying the rotation angle. I ended up using Dispatcher.BeginInvoke with a low priority to ensure the code inside the begin invoke was called after the diagram had drawn the item initially.

    I also had an issue where the item would "walk" on the graph because the position where it ended up after being rotated was not where it was initially saved. I solved this issue in essentially the same way as the rotation issue -- applying the saved position coordinates after the initial render.

    It looked something like this:

    DispatcherHelper.UIDispatcher.BeginInvoke((Action)(() =>
                                  {
                                      myContainer.Position = new Point(savedX, savedY);
                                      myContainer.RotationAngle = savedRotationAngle;
                                  }), DispatcherPriority.ApplicationIdle);

    The side effect of doing all this after the initial draw is detectable movement of items on load as everything gets re-positioned. Is there a better way of doing this that will eliminate the item jumpiness on load?
  9. Zarko
    Admin
    Zarko avatar
    755 posts

    Posted 13 Nov 2013 Link to this post

    Hello Tim,
    Sorry for the confusion:
    if (!this.hasLoaded)
    is indeed what I meant. If the ContentBounds are calculated before the loaded event you could try to remove this check:
    protected override Rect CalculateContentBounds(Rect newShapeBounds)
    {
        return this.ContentBounds;
    }
    I'm not sure if I understand this "I also had an issue where the item would "walk" on the graph because the position where it ended up after being rotated was not where it was initially saved" correctly because by default when you rotate a shape it doesn't change its position. The shape position might be changed in the OnRotationAngleChanged method but you could fix this with a simple check:
    protected override void OnRotationAngleChanged(double newValue, double oldValue)
    {
        base.OnRotationAngleChanged(newValue, oldValue);
     
        if (!this.hasLoaded) return;
        ...
    }
    I've attached a sample project that behaves as you want to (as far as I tested) so could you please take a look at it and if you have more questions feel free to ask.

    Regards,
    Zarko
    Telerik
    TRY TELERIK'S NEWEST PRODUCT - EQATEC APPLICATION ANALYTICS for WPF.
    Learn what features your users use (or don't use) in your application. Know your audience. Target it better. Develop wisely.
    Sign up for Free application insights >>
  10. Tim
    Tim avatar
    14 posts
    Member since:
    Apr 2013

    Posted 13 Nov 2013 Link to this post

    Hi Zarko,

    Your test application was really helpful and cleared up a lot of things on our side. Thanks!

    However, there's one minor thing that remains. When a container's height is set to less than 15 pixels (this may happen on width too, but I haven't tested), the ManipulationAdorner will initially be the correct size, but will expand to a height of 15 pixels when the container shape is moved.

    You can see this in your test solution by modifying the container style to this:

    <Style TargetType="telerik:RadDiagramContainerShape">
                       <Setter Property="RotationAngle" Value="{Binding RotationAngle, Mode=TwoWay}" />
                       <Setter Property="Position" Value="{Binding Position, Mode=TwoWay}" />
                       <Setter Property="MinHeight" Value="0" />
                       <Setter Property="MinWidth" Value="0" />
                       <Setter Property="Height" Value="10" />
                   </Style>

    I dug into the ManipulationAdorner a little bit, but didn't see any obvious coercing of the size. Any ideas on how to fix that?
  11. Zarko
    Admin
    Zarko avatar
    755 posts

    Posted 15 Nov 2013 Link to this post

    Hi Tim,
    There's a diagram constant - MinimumAdornerSize which is 15 by default and that's why the adorner is resized on move(the initial height of 10 is wrong in this case and we'll fix it). For your issue all you have to do is the minimum size to 10:
    public MainWindow()
    {
        DiagramConstants.MinimumAdornerSize = 10;
        InitializeComponent();
        ....
    }
    I hope I was able to help you and if you have more questions please feel free to ask.

    Regards,
    Zarko
    Telerik
    TRY TELERIK'S NEWEST PRODUCT - EQATEC APPLICATION ANALYTICS for WPF.
    Learn what features your users use (or don't use) in your application. Know your audience. Target it better. Develop wisely.
    Sign up for Free application insights >>
Back to Top
UI for WPF is Visual Studio 2017 Ready