This is a migrated thread and some comments may be shown as answers.

Rotating RadDiagramShape inside of RadDiagramContainer

9 Answers 138 Views
Diagram
This is a migrated thread and some comments may be shown as answers.
Tim
Top achievements
Rank 1
Tim asked on 29 Oct 2013, 03:59 PM
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? 



9 Answers, 1 is accepted

Sort by
0
Tim
Top achievements
Rank 1
answered on 29 Oct 2013, 05:11 PM
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? 
0
Tim
Top achievements
Rank 1
answered on 29 Oct 2013, 05:36 PM
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? 





0
Accepted
Zarko
Telerik team
answered on 01 Nov 2013, 03:55 PM
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 >>
0
Tim
Top achievements
Rank 1
answered on 05 Nov 2013, 04:35 PM
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.
0
Zarko
Telerik team
answered on 08 Nov 2013, 12:03 PM
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 >>
0
Tim
Top achievements
Rank 1
answered on 08 Nov 2013, 11:30 PM
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?
0
Zarko
Telerik team
answered on 13 Nov 2013, 01:25 PM
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 >>
0
Tim
Top achievements
Rank 1
answered on 13 Nov 2013, 08:21 PM
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?
0
Zarko
Telerik team
answered on 15 Nov 2013, 05:04 PM
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 >>
Tags
Diagram
Asked by
Tim
Top achievements
Rank 1
Answers by
Tim
Top achievements
Rank 1
Zarko
Telerik team
Share this question
or