tabina
tabina

Reputation: 1155

HelixToolkit ZoomExtentsWhenLoaded and Binding

I am using HelixToolkit to display 3D Models in WPF. The loading works fine but the model is not zoomed properly though I am using ZoomExtentWhenLoaded="True" (I expect it to be zoomed to fit into my window). The Model is provided in the ViewModel and added to the viewport via Binding.

Here's my code:

The View

<h:HelixViewport3D ZoomExtentsWhenLoaded="True">
  <h:HelixViewport3D.Camera>
     <PerspectiveCamera/>
  </h:HelixViewport3D.Camera>
  <h:DefaultLights/>
  <ModelVisual3D Content="{Binding CurrentModel}"  />
</h:HelixViewport3D>

and parts of the ViewModel

Model3DGroup _currentModel;
public Model3DGroup CurrentModel
{
  get { return _currentModel; }
  set
  {
    _currentModel = value;
    OnPropertyChanged(nameof(CurrentModel));
  }
}

private void OnModelSelectionChanged(object sender, EventArgs args)
{
...
  if (SelectedModel == null)
    return;

  var model = LoadModelFromFile(SelectedModel.Path);
  CurrentModel = model;
}

private Model3DGroup LoadModelFromFile(string objPath, string texturePath = "")
{
   try
   {
      ObjReader objReader = new ObjReader();
      var model = objReader.Read(objPath);

      ApplyTexture(model, texturePath);
      return model;
   }
   catch (Exception e)
   {
     ...
   }
   return null;
}

private void ApplyTexture(Model3DGroup model, string texture)
{
...
   Material material;
   if (!string.IsNullOrEmpty(texture))
   {
     material = MaterialHelper.CreateImageMaterial(texture);
   }
   else
   {
     material = MaterialHelper.CreateMaterial(Colors.LightBlue);
   }

   foreach (var m in model.Children)
   {
     var mGeo = m as GeometryModel3D;
     mGeo.Material = material;
    }
}
...

I tried to use an attached property instead of ZoomExtentsWhenLoaded="True" and trigger ZoomExtents() from there, but cannot seem to find the right event that is actually triggered, when I change the model. How can I get ZoomExtentsWhenLoaded to work properly? Or is it the wrong property after all? How can I set zoom and transformation to fit the model into my window? Thank you for your help!

Upvotes: 1

Views: 1596

Answers (1)

Matt Breckon
Matt Breckon

Reputation: 3374

The ZoomToExtentsWhenLoaded property is a property of the ViewPort, which will be empty when "Loaded" in a WPF UserControl sense. The content of your ViewPort is a ModelVisual3D which is bound to the Model3DGroup you've exposed in your ViewModel and won't be updated until after the control has been Loaded.

You are wanting the ViewPort to zoom to the extents of the Model3DGroup when the property is changed. One way to achieve this is to listen in the View to an event from the ViewModel (see the suggestion at https://github.com/helix-toolkit/helix-toolkit/issues/1265) - I have provided a description of this in the next paragraph.

The ViewModel fires an event in the property set method and the View responds to the event by calling viewport.ZoomToExtents(). You could use a plain C# event but as this breaks the decoupled nature of databinding in MVVM some prefer to find an alternative route. If you are using an MVVM framework there is often support for attaching Views to ViewModels in ways that reduce the coupling (e.g. see IViewAware for Caliburn Micro).

An alternative way is to think of the ViewModel as exposing a "region of interest" to the View to which the View zooms when the value of that property changes. This has led me to create a small attached property that can be used as follows (using your example)...

<h:HelixViewport3D helixextensions:AutoFit.Bounds="{Binding Path=CurrentModel.Bounds}">
    <h:HelixViewport3D.Camera>
        <PerspectiveCamera />
    </h:HelixViewport3D.Camera>
    <h:DefaultLights/>
    <ModelVisual3D Content="{Binding CurrentModel}"/>
</h:HelixViewport3D>

You include this in your XAML file using xmlns:helixextensions="clr-namespace:HelixExtensions" in the Window or UserControl declaration.

This is the definition of the attached property...

namespace HelixExtensions
{
    public static class AutoFit
    {
        private static readonly Type OwnerType = typeof(AutoFit);

        public static readonly DependencyProperty BoundsProperty =
            DependencyProperty.RegisterAttached(
                "Bounds",
                typeof(Rect3D),
                OwnerType,
                new PropertyMetadata(Rect3D.Empty, OnDataContextChanged)
            );

        public static bool GetBounds(HelixViewport3D obj)
        {
            return (bool)obj.GetValue(BoundsProperty);
        }

        public static void SetBounds(HelixViewport3D obj, bool value)
        {
            obj.SetValue(BoundsProperty, value);
        }

        private static void OnDataContextChanged(
            DependencyObject d,
            DependencyPropertyChangedEventArgs e)
        {
            var viewport = d as HelixViewport3D;
            if (viewport.DataContext == null) return;
            viewport.ZoomExtents((Rect3D)e.NewValue);
        }
    }
}

It works by allowing you to bind to the Rect3D (a 3D bounding box) of your CurrentModel property. When the CurrentModel property changes, the OnDataContextChanged method will be called which zooms the ViewPort to the new extents.

If you wanted to zoom to the extents of more than one model or you had a particular bounds that you wanted to focus on then you could expose this as a Rect3D property on your ViewModel and simply bind to that property rather than CurrentModel.Bounds. In simple applications this extra level of control won't be necessary.

e.g.

<h:HelixViewport3D helixextensions:AutoFit.Bounds="{Binding Path=CurrentFocusBounds}">
    <h:HelixViewport3D.Camera>
        <PerspectiveCamera />
    </h:HelixViewport3D.Camera>
    <h:DefaultLights/>
    <ModelVisual3D Content="{Binding CurrentModel}"/>
</h:HelixViewport3D>

Upvotes: 2

Related Questions