refactored model viewer

This commit is contained in:
iAmAsval 2021-11-20 22:17:00 +01:00
parent 22d1a32651
commit 8a0a51867e
4 changed files with 240 additions and 154 deletions

View File

@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media.Media3D;
using CUE4Parse.UE4.Assets.Exports;
using CUE4Parse.UE4.Assets.Exports.Material;
@ -60,50 +62,48 @@ namespace FModel.ViewModels
set => SetProperty(ref _zAxis, value);
}
private bool _appendModeEnabled;
public bool AppendModeEnabled
private ModelAndCam _selectedModel; // selected mesh
public ModelAndCam SelectedModel
{
get => _appendModeEnabled;
set => SetProperty(ref _appendModeEnabled, value);
}
private MeshGeometryModel3D _selectedGeometry;
public MeshGeometryModel3D SelectedGeometry
{
get => _selectedGeometry;
get => _selectedModel;
set
{
SetProperty(ref _selectedGeometry, value);
if (_selectedGeometry == null || !_geometries.TryGetValue(_selectedGeometry, out var camAxis)) return;
SetProperty(ref _selectedModel, value);
if (_selectedModel == null) return;
XAxis = camAxis.XAxis;
YAxis = camAxis.YAxis;
ZAxis = camAxis.ZAxis;
XAxis = _selectedModel.XAxis;
YAxis = _selectedModel.YAxis;
ZAxis = _selectedModel.ZAxis;
Cam.UpDirection = new Vector3D(0, 1, 0);
Cam.Position = _selectedModel.Position;
Cam.LookDirection = _selectedModel.LookDirection;
}
}
private ObservableElement3DCollection _group3d;
public ObservableElement3DCollection Group3d
private readonly ObservableCollection<ModelAndCam> _loadedModels; // mesh list
public ICollectionView LoadedModelsView { get; }
private bool _appendMode;
public bool AppendMode
{
get => _group3d;
set => SetProperty(ref _group3d, value);
get => _appendMode;
set => SetProperty(ref _appendMode, value);
}
public bool CanAppend => SelectedModel != null;
public TextureModel HDRi { get; private set; }
private readonly FGame _game;
private readonly int[] _facesIndex = { 1, 0, 2 };
private readonly List<UObject> _meshes;
private readonly Dictionary<MeshGeometryModel3D, CamAxisHolder> _geometries;
public ModelViewerViewModel(FGame game)
{
_game = game;
_meshes = new List<UObject>();
_geometries = new Dictionary<MeshGeometryModel3D, CamAxisHolder>();
_loadedModels = new ObservableCollection<ModelAndCam>();
EffectManager = new DefaultEffectsManager();
Group3d = new ObservableElement3DCollection();
LoadedModelsView = new ListCollectionView(_loadedModels);
Cam = new PerspectiveCamera { NearPlaneDistance = 0.1, FarPlaneDistance = double.PositiveInfinity, FieldOfView = 90 };
LoadHDRi();
}
@ -119,46 +119,32 @@ namespace FModel.ViewModels
#if DEBUG
LoadHDRi();
#endif
if (!AppendModeEnabled) Clear();
_meshes.Add(export);
switch (export)
ModelAndCam p;
if (AppendMode && CanAppend)
p = SelectedModel;
else
{
case UStaticMesh st:
LoadStaticMesh(st);
break;
case USkeletalMesh sk:
LoadSkeletalMesh(sk);
break;
case UMaterialInstance mi:
LoadMaterialInstance(mi);
break;
default: // idiot
throw new ArgumentOutOfRangeException();
p = new ModelAndCam(export);
_loadedModels.Add(p);
}
if (_geometries.Count < 1) return;
foreach (var geometry in _geometries.Keys)
bool valid = export switch
{
// Tag is used as a flag to tell if the geometry is already in Group3d
if (geometry.Tag is not (bool and false)) continue;
UStaticMesh st => TryLoadStaticMesh(st, ref p),
USkeletalMesh sk => TryLoadSkeletalMesh(sk, ref p),
UMaterialInstance mi => TryLoadMaterialInstance(mi, ref p),
_ => throw new ArgumentOutOfRangeException(nameof(export))
};
geometry.Tag = true;
Group3d.Add(geometry);
}
if (!valid)
return;
if (AppendModeEnabled || Group3d[0] is not MeshGeometryModel3D selected ||
!_geometries.TryGetValue(selected, out var camAxis)) return;
SelectedGeometry = selected;
Cam.UpDirection = new Vector3D(0, 1, 0);
Cam.Position = camAxis.Position;
Cam.LookDirection = camAxis.LookDirection;
SelectedModel = p;
}
public void RenderingToggle()
{
foreach (var g in Group3d)
if (SelectedModel == null) return;
foreach (var g in SelectedModel.Group3d)
{
if (g is not MeshGeometryModel3D geometryModel)
continue;
@ -166,10 +152,10 @@ namespace FModel.ViewModels
geometryModel.IsRendering = !geometryModel.IsRendering;
}
}
public void WirefreameToggle()
{
foreach (var g in Group3d)
if (SelectedModel == null) return;
foreach (var g in SelectedModel.Group3d)
{
if (g is not MeshGeometryModel3D geometryModel)
continue;
@ -177,88 +163,90 @@ namespace FModel.ViewModels
geometryModel.RenderWireframe = !geometryModel.RenderWireframe;
}
}
public void DiffuseOnlyToggle()
{
foreach (var g in Group3d)
if (SelectedModel == null) return;
foreach (var g in SelectedModel.Group3d)
{
if (g is not MeshGeometryModel3D geometryModel)
if (g is not MeshGeometryModel3D { Material: PBRMaterial mat })
continue;
if (geometryModel.Material is PBRMaterial mat)
{
//mat.RenderAmbientOcclusionMap = !mat.RenderAmbientOcclusionMap;
mat.RenderDisplacementMap = !mat.RenderDisplacementMap;
//mat.RenderEmissiveMap = !mat.RenderEmissiveMap;
mat.RenderEnvironmentMap = !mat.RenderEnvironmentMap;
mat.RenderIrradianceMap = !mat.RenderIrradianceMap;
mat.RenderRoughnessMetallicMap = !mat.RenderRoughnessMetallicMap;
mat.RenderShadowMap = !mat.RenderShadowMap;
mat.RenderNormalMap = !mat.RenderNormalMap;
}
//mat.RenderAmbientOcclusionMap = !mat.RenderAmbientOcclusionMap;
mat.RenderDisplacementMap = !mat.RenderDisplacementMap;
//mat.RenderEmissiveMap = !mat.RenderEmissiveMap;
mat.RenderEnvironmentMap = !mat.RenderEnvironmentMap;
mat.RenderIrradianceMap = !mat.RenderIrradianceMap;
mat.RenderRoughnessMetallicMap = !mat.RenderRoughnessMetallicMap;
mat.RenderShadowMap = !mat.RenderShadowMap;
mat.RenderNormalMap = !mat.RenderNormalMap;
}
}
public void FocusOnSelectedGeometry()
public void FocusOnSelectedMesh()
{
if (!_geometries.TryGetValue(_selectedGeometry, out var camAxis)) return;
Cam.AnimateTo(camAxis.Position, camAxis.LookDirection, new Vector3D(0, 1, 0), 500);
Cam.AnimateTo(SelectedModel.Position, SelectedModel.LookDirection, new Vector3D(0, 1, 0), 500);
}
private void LoadMaterialInstance(UMaterialInstance materialInstance)
private bool TryLoadMaterialInstance(UMaterialInstance materialInstance, ref ModelAndCam cam)
{
var builder = new MeshBuilder();
builder.AddSphere(Vector3.Zero, 10);
var camAxis = SetupCameraAndAxis(new FBox(new FVector(-15), new FVector(15)));
var (m, isRendering, _) = LoadMaterial(materialInstance);
SetupCameraAndAxis(new FBox(new FVector(-15), new FVector(15)), ref cam);
var (m, isRendering, isTransparent) = LoadMaterial(materialInstance);
_geometries.Add(new MeshGeometryModel3D
cam.Group3d.Add(new MeshGeometryModel3D
{
Transform = new RotateTransform3D(new AxisAngleRotation3D(new Vector3D(1,0,0), -90)),
Name = FixName(materialInstance.Name), Geometry = builder.ToMeshGeometry3D(),
Material = m, IsRendering = isRendering, Tag = false // flag
}, camAxis);
Material = m, IsTransparent = isTransparent, IsRendering = isRendering
});
return true;
}
private void LoadStaticMesh(UStaticMesh mesh)
private bool TryLoadStaticMesh(UStaticMesh mesh, ref ModelAndCam cam)
{
if (!mesh.TryConvert(out var convertedMesh) || convertedMesh.LODs.Count <= 0)
{
return;
cam = null;
return false;
}
var camAxis = SetupCameraAndAxis(convertedMesh.BoundingBox);
SetupCameraAndAxis(convertedMesh.BoundingBox, ref cam);
foreach (var lod in convertedMesh.LODs)
{
if (lod.SkipLod) continue;
PushLod(lod.Sections.Value, lod.Verts, lod.Indices.Value, camAxis);
PushLod(lod.Sections.Value, lod.Verts, lod.Indices.Value, ref cam);
break;
}
return true;
}
private void LoadSkeletalMesh(USkeletalMesh mesh)
private bool TryLoadSkeletalMesh(USkeletalMesh mesh, ref ModelAndCam cam)
{
if (!mesh.TryConvert(out var convertedMesh) || convertedMesh.LODs.Count <= 0)
{
return;
cam = null;
return false;
}
var camAxis = SetupCameraAndAxis(convertedMesh.BoundingBox);
SetupCameraAndAxis(convertedMesh.BoundingBox, ref cam);
foreach (var lod in convertedMesh.LODs)
{
if (lod.SkipLod) continue;
PushLod(lod.Sections.Value, lod.Verts, lod.Indices.Value, camAxis);
PushLod(lod.Sections.Value, lod.Verts, lod.Indices.Value, ref cam);
break;
}
return true;
}
private void PushLod(CMeshSection[] sections, CMeshVertex[] verts, FRawStaticIndexBuffer indices, CamAxisHolder camAxis)
private void PushLod(CMeshSection[] sections, CMeshVertex[] verts, FRawStaticIndexBuffer indices, ref ModelAndCam cam)
{
foreach (var section in sections) // each section is a mesh part with its own material
{
var builder = new MeshBuilder();
// NumFaces * 3 (triangle) = next section FirstIndex
cam.TriangleCount += section.NumFaces; // NumFaces * 3 (triangle) = next section FirstIndex
for (var j = 0; j < section.NumFaces; j++) // draw a triangle for each face
{
foreach (var t in _facesIndex) // triangle face 1 then 0 then 2
@ -278,11 +266,11 @@ namespace FModel.ViewModels
continue;
var (m, isRendering, isTransparent) = LoadMaterial(unrealMaterial);
_geometries.Add(new MeshGeometryModel3D
cam.Group3d.Add(new MeshGeometryModel3D
{
Name = FixName(unrealMaterial.Name), Geometry = builder.ToMeshGeometry3D(),
Material = m, IsTransparent = isTransparent, IsRendering = isRendering, Tag = false // flag
}, camAxis);
Material = m, IsTransparent = isTransparent, IsRendering = isRendering
});
}
}
@ -330,9 +318,24 @@ namespace FModel.ViewModels
}
case FGame.ShooterGame:
{
// Valorant's Specular Texture Channels
// R Metallic
// G Specular
// B Roughness
unsafe
{
var offset = 0;
fixed (byte* d = data)
for (var i = 0; i < mip.SizeX * mip.SizeY; i++)
{
(d[offset], d[offset+2]) = (d[offset+2], d[offset]); // swap R and B
(d[offset], d[offset+1]) = (d[offset+1], d[offset]); // swap B and G
offset += 4;
}
}
parameters.RoughnessValue = 1;
parameters.MetallicValue = 1;
goto case FGame.Gameface;
break;
}
case FGame.Gameface:
{
@ -378,26 +381,25 @@ namespace FModel.ViewModels
return (m, isRendering, parameters.IsTransparent);
}
private CamAxisHolder SetupCameraAndAxis(FBox box)
private void SetupCameraAndAxis(FBox box, ref ModelAndCam cam)
{
var ret = new CamAxisHolder();
if (AppendMode && CanAppend) return;
var meanX = (box.Max.X + box.Min.X) / 2;
var meanY = (box.Max.Y + box.Min.Y) / 2;
var meanZ = (box.Max.Z + box.Min.Z) / 2;
var lineBuilder = new LineBuilder();
lineBuilder.AddLine(new Vector3(box.Min.X, meanZ, meanY), new Vector3(box.Max.X, meanZ, meanY));
ret.XAxis = lineBuilder.ToLineGeometry3D();
cam.XAxis = lineBuilder.ToLineGeometry3D();
lineBuilder = new LineBuilder();
lineBuilder.AddLine(new Vector3(meanX, box.Min.Z, meanY), new Vector3(meanX, box.Max.Z, meanY));
ret.YAxis = lineBuilder.ToLineGeometry3D();
cam.YAxis = lineBuilder.ToLineGeometry3D();
lineBuilder = new LineBuilder();
lineBuilder.AddLine(new Vector3(meanX, meanZ, box.Min.Y), new Vector3(meanX, meanZ, box.Max.Y));
ret.ZAxis = lineBuilder.ToLineGeometry3D();
cam.ZAxis = lineBuilder.ToLineGeometry3D();
ret.Position = new Point3D(box.Max.X + meanX * 2, meanZ, box.Min.Y + meanY * 2);
ret.LookDirection = new Vector3D(-ret.Position.X + meanX, 0, -ret.Position.Z + meanY);
return ret;
cam.Position = new Point3D(box.Max.X + meanX * 2, meanZ, box.Min.Y + meanY * 2);
cam.LookDirection = new Vector3D(-cam.Position.X + meanX, 0, -cam.Position.Z + meanY);
}
private string FixName(string input)
@ -411,10 +413,51 @@ namespace FModel.ViewModels
return input.Replace('-', '_');
}
private void Clear()
public void Clear()
{
_meshes.Clear();
_geometries.Clear();
foreach (var g in _loadedModels.ToList())
{
g.Dispose();
_loadedModels.Remove(g);
}
}
}
public class ModelAndCam : ViewModel
{
public UObject Export { get; }
public Point3D Position { get; set; }
public Vector3D LookDirection { get; set; }
public Geometry3D XAxis { get; set; }
public Geometry3D YAxis { get; set; }
public Geometry3D ZAxis { get; set; }
public int TriangleCount { get; set; }
private MeshGeometryModel3D _selectedGeometry; // selected material
public MeshGeometryModel3D SelectedGeometry
{
get => _selectedGeometry;
set => SetProperty(ref _selectedGeometry, value);
}
private ObservableElement3DCollection _group3d; // material list
public ObservableElement3DCollection Group3d
{
get => _group3d;
set => SetProperty(ref _group3d, value);
}
public ModelAndCam(UObject export)
{
Export = export;
TriangleCount = 0;
Group3d = new ObservableElement3DCollection();
}
public void Dispose()
{
TriangleCount = 0;
SelectedGeometry = null;
foreach (var g in Group3d.ToList())
{
g.Dispose();
@ -422,13 +465,4 @@ namespace FModel.ViewModels
}
}
}
public class CamAxisHolder
{
public Point3D Position;
public Vector3D LookDirection;
public Geometry3D XAxis;
public Geometry3D YAxis;
public Geometry3D ZAxis;
}
}

View File

@ -23,21 +23,89 @@
</adonisControls:AdonisWindow.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" MinWidth="250" />
<ColumnDefinition Width="Auto" MinWidth="350" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="4*" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<GroupBox Grid.Column="0" Padding="{adonisUi:Space 0}" Background="Transparent">
<DockPanel Margin="10">
<Grid DockPanel.Dock="Top">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ListBox Grid.Row="0" Style="{StaticResource MaterialsListBox}" MouseDoubleClick="OnMouseDoubleClick" />
<CheckBox Content="Append Mode" VerticalAlignment="Stretch" HorizontalAlignment="Center" Margin="0 0 0 5"
Grid.Row="1" IsChecked="{Binding ModelViewer.AppendModeEnabled}" Style="{DynamicResource {x:Static adonisUi:Styles.ToggleSwitch}}" />
</Grid>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Models" VerticalAlignment="Center" Margin="0 0 0 10" />
<ComboBox Grid.Row="0" Grid.Column="2" ItemsSource="{Binding ModelViewer.LoadedModelsView, IsAsync=True}"
SelectedItem="{Binding ModelViewer.SelectedModel, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Export.Name}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Grid.Row="1" Grid.Column="0" Text="Triangles" VerticalAlignment="Center" Margin="0 0 0 10" />
<TextBlock Grid.Row="1" Grid.Column="2" Text="{Binding ModelViewer.SelectedModel.TriangleCount, FallbackValue=0, StringFormat={}{0:### ### ### ###}}" VerticalAlignment="Center" HorizontalAlignment="Right" />
<TextBlock Grid.Row="2" Grid.Column="0" Text="Materials" VerticalAlignment="Center" Margin="0 0 0 10" />
<TextBlock Grid.Row="2" Grid.Column="2" Text="{Binding ModelViewer.SelectedModel.Group3d.Count, FallbackValue=0, StringFormat={}{0:### ###}}" VerticalAlignment="Center" HorizontalAlignment="Right" />
<Grid Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="3">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="10" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button Grid.Row="0" Grid.Column="0" Content="Focus" />
<ToggleButton Grid.Row="0" Grid.Column="2" IsChecked="{Binding ModelViewer.AppendMode}" Style="{DynamicResource {x:Static adonisUi:Styles.ToolbarToggleButton}}"
Content="{Binding ModelViewer.AppendMode, Converter={x:Static converters:BoolToToggleConverter.Instance}, StringFormat='{}Append {0}'}" />
<Button Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3" Style="{DynamicResource {x:Static adonisUi:Styles.AccentButton}}"
ContentStringFormat="{}Save All Loaded Models ({0})" Content="{Binding ModelViewer.LoadedModelsView.Count}" />
</Grid>
</Grid>
<Separator DockPanel.Dock="Top" Tag="MATERIALS" Style="{StaticResource CustomSeparator}" />
<ListBox DockPanel.Dock="Top" Style="{StaticResource MaterialsListBox}">
<ListBox.ContextMenu>
<ContextMenu DataContext="{Binding PlacementTarget, RelativeSource={RelativeSource Self}}">
<MenuItem Header="Copy Name">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource CopyIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Change Material">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource SwapIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
</ContextMenu>
</ListBox.ContextMenu>
</ListBox>
</DockPanel>
</GroupBox>
<GridSplitter Grid.Column="1" ResizeDirection="Columns" Width="4" VerticalAlignment="Stretch" ResizeBehavior="PreviousAndNext"
Background="{DynamicResource {x:Static adonisUi:Brushes.Layer3BackgroundBrush}}" />
@ -53,13 +121,14 @@
</helix:Viewport3DX.InputBindings>
<helix:EnvironmentMap3D Texture="{Binding ModelViewer.HDRi}" />
<helix:DirectionalLight3D Direction="{Binding ModelViewer.Cam.LookDirection}" Color="White" />
<helix:DirectionalLight3D Color="White" Direction="{Binding Camera.LookDirection,
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type helix:Viewport3DX}}}" />
<helix:LineGeometryModel3D Geometry="{Binding ModelViewer.XAxis}" Color="#FC3854" />
<helix:LineGeometryModel3D Geometry="{Binding ModelViewer.YAxis}" Color="#85CB22" />
<helix:LineGeometryModel3D Geometry="{Binding ModelViewer.ZAxis}" Color="#388EED" />
<helix:LineGeometryModel3D Geometry="{Binding ModelViewer.SelectedModel.XAxis}" Color="#FC3854" />
<helix:LineGeometryModel3D Geometry="{Binding ModelViewer.SelectedModel.YAxis}" Color="#85CB22" />
<helix:LineGeometryModel3D Geometry="{Binding ModelViewer.SelectedModel.ZAxis}" Color="#388EED" />
<helix:GroupModel3D x:Name="MyAntiCrashGroup" ItemsSource="{Binding ModelViewer.Group3d}" Mouse3DDown="OnMouse3DDown" />
<helix:GroupModel3D x:Name="MyAntiCrashGroup" ItemsSource="{Binding ModelViewer.SelectedModel.Group3d}" />
</helix:Viewport3DX>
</Grid>
</adonisControls:AdonisWindow>

View File

@ -1,10 +1,8 @@
using System.ComponentModel;
using System.Windows.Controls;
using System.Windows.Input;
using CUE4Parse.UE4.Assets.Exports;
using FModel.Services;
using FModel.ViewModels;
using HelixToolkit.Wpf.SharpDX;
namespace FModel.Views
{
@ -21,7 +19,8 @@ namespace FModel.Views
public void Load(UObject export) => _applicationView.ModelViewer.LoadExport(export);
private void OnClosing(object sender, CancelEventArgs e)
{
_applicationView.ModelViewer.AppendModeEnabled = false;
_applicationView.ModelViewer.Clear();
_applicationView.ModelViewer.AppendMode = false;
MyAntiCrashGroup.ItemsSource = null; // <3
}
@ -40,20 +39,5 @@ namespace FModel.Views
break;
}
}
private void OnMouse3DDown(object sender, MouseDown3DEventArgs e)
{
if (!Keyboard.Modifiers.HasFlag(ModifierKeys.Shift) || e.HitTestResult.ModelHit is not MeshGeometryModel3D m) return;
_applicationView.ModelViewer.SelectedGeometry = m;
}
private void OnMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
if (e.OriginalSource is not TextBlock or Image)
return;
if (!_applicationView.IsReady || sender is not ListBox { SelectedItem: MeshGeometryModel3D }) return;
_applicationView.ModelViewer.FocusOnSelectedGeometry();
}
}
}

View File

@ -70,6 +70,7 @@
<Geometry x:Key="NotVisibleIcon">M12 6.5c2.76 0 5 2.24 5 5 0 .51-.1 1-.24 1.46l3.06 3.06c1.39-1.23 2.49-2.77 3.18-4.53C21.27 7.11 17 4 12 4c-1.27 0-2.49.2-3.64.57l2.17 2.17c.47-.14.96-.24 1.47-.24zM2.71 3.16c-.39.39-.39 1.02 0 1.41l1.97 1.97C3.06 7.83 1.77 9.53 1 11.5 2.73 15.89 7 19 12 19c1.52 0 2.97-.3 4.31-.82l2.72 2.72c.39.39 1.02.39 1.41 0 .39-.39.39-1.02 0-1.41L4.13 3.16c-.39-.39-1.03-.39-1.42 0zM12 16.5c-2.76 0-5-2.24-5-5 0-.77.18-1.5.49-2.14l1.57 1.57c-.03.18-.06.37-.06.57 0 1.66 1.34 3 3 3 .2 0 .38-.03.57-.07L14.14 16c-.65.32-1.37.5-2.14.5zm2.97-5.33c-.15-1.4-1.25-2.49-2.64-2.64l2.64 2.64z</Geometry>
<Geometry x:Key="WireframeIcon">M3 5h2V3c-1.1 0-2 .9-2 2zm0 8h2v-2H3v2zm4 8h2v-2H7v2zM3 9h2V7H3v2zm10-6h-2v2h2V3zm6 0v2h2c0-1.1-.9-2-2-2zM5 21v-2H3c0 1.1.9 2 2 2zm-2-4h2v-2H3v2zM9 3H7v2h2V3zm2 18h2v-2h-2v2zm8-8h2v-2h-2v2zm0 8c1.1 0 2-.9 2-2h-2v2zm0-12h2V7h-2v2zm0 8h2v-2h-2v2zm-4 4h2v-2h-2v2zm0-16h2V3h-2v2zM8 17h8c.55 0 1-.45 1-1V8c0-.55-.45-1-1-1H8c-.55 0-1 .45-1 1v8c0 .55.45 1 1 1zm1-8h6v6H9V9z</Geometry>
<Geometry x:Key="NotWireframeIcon">M3,13h2v-2H3V13z M7,21h2v-2H7V21z M13,3h-2v2h2V3z M19,3v2h2C21,3.9,20.1,3,19,3z M5,21v-2H3C3,20.1,3.9,21,5,21z M3,17h2 v-2H3V17z M11,21h2v-2h-2V21z M19,13h2v-2h-2V13z M19,9h2V7h-2V9z M15,5h2V3h-2V5z M7.83,5L7,4.17V3h2v2H7.83z M19.83,17L19,16.17 V15h2v2H19.83z M9,15v-3.17L12.17,15H9z M2.1,3.51c-0.39,0.39-0.39,1.02,0,1.41L4.17,7H3v2h2V7.83l2,2V16c0,0.55,0.45,1,1,1h6.17 l2,2H15v2h2v-1.17l2.07,2.07c0.39,0.39,1.02,0.39,1.41,0c0.39-0.39,0.39-1.02,0-1.41L3.51,3.51C3.12,3.12,2.49,3.12,2.1,3.51z M17,8c0-0.55-0.45-1-1-1H9.83l2,2H15v3.17l2,2V8z</Geometry>
<Geometry x:Key="SwapIcon">M16 17.01V11c0-.55-.45-1-1-1s-1 .45-1 1v6.01h-1.79c-.45 0-.67.54-.35.85l2.79 2.78c.2.19.51.19.71 0l2.79-2.78c.32-.31.09-.85-.35-.85H16zM8.65 3.35L5.86 6.14c-.32.31-.1.85.35.85H8V13c0 .55.45 1 1 1s1-.45 1-1V6.99h1.79c.45 0 .67-.54.35-.85L9.35 3.35c-.19-.19-.51-.19-.7 0z</Geometry>
<Style x:Key="TabItemFillSpace" TargetType="TabItem" BasedOn="{StaticResource {x:Type TabItem}}">
<Setter Property="Width">
@ -618,13 +619,11 @@
</Style>
<Style x:Key="MaterialsListBox" TargetType="ListBox" BasedOn="{StaticResource {x:Type ListBox}}">
<Setter Property="ItemsSource" Value="{Binding ModelViewer.Group3d, IsAsync=True}" />
<Setter Property="SelectedItem" Value="{Binding ModelViewer.SelectedGeometry}" />
<Setter Property="ItemsSource" Value="{Binding ModelViewer.SelectedModel.Group3d, IsAsync=True}" />
<Setter Property="SelectedItem" Value="{Binding ModelViewer.SelectedModel.SelectedGeometry}" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled" />
<Setter Property="adonisExtensions:ScrollViewerExtension.VerticalScrollBarExpansionMode" Value="NeverExpand"/>
<Setter Property="adonisExtensions:ScrollViewerExtension.VerticalScrollBarPlacement" Value="Docked"/>
<Setter Property="adonisExtensions:ScrollViewerExtension.HorizontalScrollBarExpansionMode" Value="NeverExpand"/>
<Setter Property="adonisExtensions:ScrollViewerExtension.HorizontalScrollBarPlacement" Value="Docked"/>
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate>
@ -684,7 +683,7 @@
</Setter.Value>
</Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding ModelViewer.Group3d.Count, FallbackValue=0}" Value="0">
<DataTrigger Binding="{Binding ModelViewer.SelectedModel.Group3d.Count, FallbackValue=0}" Value="0">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>