diff --git a/AssetEditor/Themes/Controls.xaml.cs b/AssetEditor/Themes/Controls.xaml.cs index df10e3f51..5dac0df93 100644 --- a/AssetEditor/Themes/Controls.xaml.cs +++ b/AssetEditor/Themes/Controls.xaml.cs @@ -2,12 +2,15 @@ using System.Diagnostics; using System.IO; using System.Windows; +using Serilog; +using Shared.Core.ErrorHandling; using WindowHandling; namespace AssetEditor.Themes { public partial class Controls { + private static readonly ILogger _logger = Logging.Create(); private void CloseWindow_Event(object sender, RoutedEventArgs e) { if (e.Source != null) @@ -35,16 +38,46 @@ private void Help_Event(object sender, RoutedEventArgs e) if (window == null || string.IsNullOrWhiteSpace(window.HelpDocumentPath)) return; - var helpPath = Path.IsPathRooted(window.HelpDocumentPath) - ? window.HelpDocumentPath - : Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, window.HelpDocumentPath)); + var rawPath = window.HelpDocumentPath; + var queryString = ""; + var queryIndex = rawPath.IndexOf('?'); + if (queryIndex >= 0) + { + queryString = rawPath.Substring(queryIndex); + rawPath = rawPath.Substring(0, queryIndex); + } + + var helpPath = Path.IsPathRooted(rawPath) + ? rawPath + : Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, rawPath)); + + if (!File.Exists(helpPath) && Debugger.IsAttached) + { + _logger.Here().Information("Help file not found at '{HelpPath}', searching parent directories", helpPath); + var searchDir = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory); + while (searchDir?.Parent != null) + { + searchDir = searchDir.Parent; + var candidate = Path.Combine(searchDir.FullName, rawPath); + if (File.Exists(candidate)) + { + helpPath = candidate; + break; + } + } + } - if (File.Exists(helpPath) == false) + if (!File.Exists(helpPath)) + { + _logger.Here().Warning("Help file not found: '{HelpPath}'", helpPath); return; + } + var fileUri = new Uri(helpPath).AbsoluteUri + queryString; + _logger.Here().Information("Opening help document: {Uri}", fileUri); Process.Start(new ProcessStartInfo { - FileName = helpPath, + FileName = fileUri, UseShellExecute = true }); } diff --git a/Documentation/AssetEditorDocumentation.html b/Documentation/AssetEditorDocumentation.html index 7e78bca67..362b27b3e 100644 --- a/Documentation/AssetEditorDocumentation.html +++ b/Documentation/AssetEditorDocumentation.html @@ -110,6 +110,7 @@ { title: "Kitbashing", children: [ { title: "Basics", base: "kitbash_basics" }, { title: "Mesh Fitter", base: "Kitbash_MeshFitter" }, + { title: "Pin Tool", base: "Kitbash_PinTool" }, { title: "Photo Studio", base: "kitbash_photostudio" } ] } ]; diff --git a/Documentation/Documentation/Kitbash_PinTool.html b/Documentation/Documentation/Kitbash_PinTool.html new file mode 100644 index 000000000..1408257ee --- /dev/null +++ b/Documentation/Documentation/Kitbash_PinTool.html @@ -0,0 +1,264 @@ + + + + + Pin Tool + + + + + +

Pin Tool

+ +

+ The Pin Tool makes meshes move with an animated skeleton. + When you import a new piece of armor, a weapon, or any other 3D object into the Kitbash Editor, + that object does not know how to follow the character's animations yet. + The Pin Tool copies the animation information from an already-working mesh onto your new one. +

+ + + Pin Tool UI overview + +

Key concepts

+ +

+ In Total War games, every model that moves with an animation is connected to a skeleton — an invisible set of bones + (like arm_left, spine, head, and so on). Each point on the mesh surface is told which bone(s) it should follow and + by how much. This information is often called rigging or bone weights. +

+ +

+ Without correct rigging, your mesh will either stay frozen in place, or fly off in wrong directions when the character animates. + The Pin Tool gives you two ways to fix this: +

+ + + +
+ Quick rule of thumb: If the object should stay rigid (a sword, a helmet ornament, a belt buckle), use Pin. + If the object should bend and deform with the body (a shirt, a cape, flexible armor plates, skin), use Skin Wrap. +
+ +

When to use this tool

+ + + +

Examples

+ + + +

Before you start

+ + + +

How to use — Pin mode

+ +

Use this when the object should stay rigid and follow one spot on the skeleton.

+ + + Pin mode workflow + +
    +
  1. Open the Pin Tool.
  2. +
  3. Make sure the mode is set to Pin.
  4. +
  5. In the 3D view, click on the mesh(es) you want to give animation to.
  6. +
  7. Press Add selected meshes to add them to the "Apply animation to" list.
  8. +
  9. Switch to vertex selection mode in the 3D view (this lets you click on individual points instead of whole meshes).
  10. +
  11. Click on a point on the mesh that already animates correctly — pick a point at the spot where you want your object to attach.
  12. +
  13. Press Set from selected Vertex.
  14. +
  15. Press Apply.
  16. +
+ +

+ After applying, every point on your target mesh will follow the same bone as the vertex you picked. + The mesh will move as one solid piece. +

+ +
+ Tip: Try to pick a vertex that follows only one bone. + Points near joints (like elbows or knees) often follow two bones at once, which can make a "pinned" object wobble slightly. + Pick a point further from any joint for the cleanest result. +
+ +

How to use — Skin Wrap mode

+ +

Use this when the object should bend and move like the body underneath it.

+ + + Skin Wrap mode workflow + +
    +
  1. Open the Pin Tool.
  2. +
  3. Set the mode to Skin Wrap.
  4. +
  5. In the 3D view, select the mesh(es) that need animation.
  6. +
  7. Press Add selected meshes to add them to the "Apply animation to" list.
  8. +
  9. Now select the mesh(es) that already animate correctly — these are the ones you want to copy from.
  10. +
  11. Press Add selected meshes in the source area.
  12. +
  13. Press Apply.
  14. +
+ + + Skin Wrap before and after + +

+ The tool goes through every point on your target mesh, finds the closest spot on the source mesh surface, + and copies the animation data from there. This means areas near the arm will follow the arm, areas near the + torso will follow the torso, and so on — all done automatically. +

+ +
+ Multiple sources: You can add more than one source mesh. + This is useful when the body is split into separate pieces (torso, arms, legs, head). + The tool will look across all of them and copy from whichever is closest for each point. +
+ +

Controls reference

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ControlWhat it does
Animation transfer modeSwitches between Pin and Skin Wrap.
Apply animation to — Add selected meshesAdds the meshes you have selected in the 3D view to the list of meshes that will receive animation.
Apply animation to — Remove allClears the list.
Set from selected Vertex (Pin mode)Saves the vertex you have selected in the 3D view as the animation source.
Add selected meshes (Skin Wrap source)Adds meshes to copy animation from.
Remove all (Skin Wrap source)Clears the source mesh list.
ApplyRuns the tool and closes the window. The result can be undone with Ctrl+Z.
+ +

Things to watch out for

+ + + +
+ Important: Skin Wrap works best when your new mesh sits right on top of the source mesh. + If they are far apart or in very different poses, the results will be poor. + Position and align your meshes before running the tool. +
+ +

Pin vs Skin Wrap — quick comparison

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
PinSkin Wrap
Object stays rigid — no bendingObject bends and deforms with the body
Good for: weapons, shields, buckles, ornamentsGood for: clothing, capes, skin, flexible armor
You pick one point to attach toEvery point is matched automatically
Very fast — one clickTakes a moment to process, but fully automatic
+ +

When results are not good enough

+ +

The Pin Tool is a quick automated solution. It may not produce perfect results in every situation:

+ + + + + diff --git a/Editors/AnimationMeta/Test.AnimationMeta/MetaDataEditorViewModelTests.cs b/Editors/AnimationMeta/Test.AnimationMeta/MetaDataEditorViewModelTests.cs index a56dbbcd7..2c6f0d7fa 100644 --- a/Editors/AnimationMeta/Test.AnimationMeta/MetaDataEditorViewModelTests.cs +++ b/Editors/AnimationMeta/Test.AnimationMeta/MetaDataEditorViewModelTests.cs @@ -192,5 +192,55 @@ public void MetaDataEditor_CopyPaste_AddsNewTag() var pasted = editor.ParsedFile.Attributes.Last(); Assert.That(pasted, Is.InstanceOf(originalType)); } + + [Test] + public void MetaDataEditor_CopyPaste_EditPastedTagAndSave_UsesEditedValues() + { + var packFile = PathHelper.GetDataFile("Throt.pack"); + + var runner = new AssetEditorTestRunner(); + runner.CreateCaContainer(); + var outputPackFile = runner.LoadPackFile(packFile, true); + + var filePath = @"animations/battle/humanoid17/throt_whip_catcher/attacks/hu17_whip_catcher_attack_05.anm.meta"; + var metaPackFile = runner.PackFileService.FindFile(filePath); + var editor = runner.CommandFactory + .Create() + .Execute(metaPackFile!, Shared.Core.ToolCreation.EditorEnums.Meta_Editor); + + Assert.That(editor.ParsedFile, Is.Not.Null); + + var sourceIndex = 4; + editor.Tags[sourceIndex].IsSelected = true; + editor.CopyActionCommand.Execute(null); + editor.PasteActionCommand.Execute(null); + + var pastedIndex = editor.Tags.Count - 1; + Assert.That(editor.ParsedFile.Attributes[pastedIndex], Is.InstanceOf()); + + // Edit the pasted tag values, not the original source tag. + var editedFilter = "edited_after_paste"; + var editedAoeShape = 13; + var editedEndPositionX = 777; + + editor.Tags[pastedIndex].Variables[3].ValueAsString = editedFilter; + editor.Tags[pastedIndex].Variables[5].ValueAsString = editedAoeShape.ToString(); + (editor.Tags[pastedIndex].Variables[7] as VectorAttributeViewModel)!.Value.X.Value = editedEndPositionX; + + editor.SaveActionCommand.Execute(null); + + var savedFile = runner.PackFileService.FindFile(filePath, outputPackFile); + Assert.That(savedFile, Is.Not.Null); + + var parser = runner.GetRequiredServiceInCurrentEditorScope(); + var parsedFile = parser.ParseFile(savedFile); + Assert.That(parsedFile, Is.Not.Null); + + var pastedSplashAttack = parsedFile.Attributes[pastedIndex] as SplashAttack_v10; + Assert.That(pastedSplashAttack, Is.Not.Null); + Assert.That(pastedSplashAttack.Filter, Is.EqualTo(editedFilter)); + Assert.That(pastedSplashAttack.AoeShape, Is.EqualTo(editedAoeShape)); + Assert.That(pastedSplashAttack.EndPosition.X, Is.EqualTo(editedEndPositionX)); + } } } diff --git a/Editors/Kitbashing/KitbasherEditor/ChildEditors/MeshFitter/MeshFitterWindow.xaml b/Editors/Kitbashing/KitbasherEditor/ChildEditors/MeshFitter/MeshFitterWindow.xaml index 4d87cf7ea..5d2bd16c6 100644 --- a/Editors/Kitbashing/KitbasherEditor/ChildEditors/MeshFitter/MeshFitterWindow.xaml +++ b/Editors/Kitbashing/KitbasherEditor/ChildEditors/MeshFitter/MeshFitterWindow.xaml @@ -10,7 +10,7 @@ xmlns:loc="clr-namespace:Shared.Ui.Common;assembly=Shared.Ui" mc:Ignorable="d" Style="{StaticResource CustomWindowStyle}" - HelpDocumentPath="Documentation\AssetEditorDocumentation.html" + HelpDocumentPath="Documentation\AssetEditorDocumentation.html?doc=Kitbash_MeshFitter" Title="{loc:Loc KitbashTool.MeshFitterTool.Title}" Height="500" Width="1000"> diff --git a/Editors/Kitbashing/KitbasherEditor/ChildEditors/PinTool/Commands/PinMeshToVertexCommand.cs b/Editors/Kitbashing/KitbasherEditor/ChildEditors/PinTool/Commands/PinMeshToVertexCommand.cs index 2922a710e..d596236d7 100644 --- a/Editors/Kitbashing/KitbasherEditor/ChildEditors/PinTool/Commands/PinMeshToVertexCommand.cs +++ b/Editors/Kitbashing/KitbasherEditor/ChildEditors/PinTool/Commands/PinMeshToVertexCommand.cs @@ -3,19 +3,21 @@ using GameWorld.Core.Rendering.Geometry; using GameWorld.Core.SceneNodes; using Microsoft.Xna.Framework; +using Serilog; +using Shared.Core.ErrorHandling; namespace Editors.KitbasherEditor.ChildEditors.PinTool.Commands { public class PinMeshToVertexCommand : ICommand { - ISelectionState _selectionOldState; - SelectionManager _selectionManager; + private readonly ILogger _logger = Logging.Create(); + private readonly SelectionManager _selectionManager; - List _originalGeos; - - List _meshesToPin; - Rmv2MeshNode _source; - int _vertexId; + private ISelectionState? _selectionOldState; + private List? _originalGeos; + private List _meshesToPin = []; + private Rmv2MeshNode? _source; + private int _vertexId; public void Configure(IEnumerable meshesToPin, Rmv2MeshNode source, int vertexId) { @@ -34,6 +36,14 @@ public PinMeshToVertexCommand(SelectionManager selectionManager) public void Execute() { + if (_source == null || _meshesToPin.Count == 0) + throw new InvalidOperationException("PinMeshToVertexCommand not configured before Execute"); + + _logger.Here().Information("Executing pin: {Count} meshes to vertex {VertexId} on '{Source}'", _meshesToPin.Count, _vertexId, _source.Name); + + if (_vertexId < 0 || _vertexId >= _source.Geometry.VertexCount()) + throw new InvalidOperationException($"Vertex index {_vertexId} is out of range for mesh '{_source.Name}' (vertex count: {_source.Geometry.VertexCount()})"); + // Create undo state _originalGeos = _meshesToPin.Select(x => x.Geometry.Clone()).ToList(); _selectionOldState = _selectionManager.GetStateCopy(); @@ -45,9 +55,9 @@ public void Execute() currentMesh.Geometry.ChangeVertexType(_source.Geometry.VertexFormat, false); currentMesh.Geometry.UpdateSkeletonName(_source.Geometry.SkeletonName); + currentMesh.PivotPoint = Vector3.Zero; for (var i = 0; i < currentMesh.Geometry.VertexCount(); i++) { - currentMesh.PivotPoint = Vector3.Zero; currentMesh.Geometry.SetVertexBlendIndex(i, sourceVert.BlendIndices); currentMesh.Geometry.SetVertexWeights(i, sourceVert.BlendWeights); } @@ -58,6 +68,9 @@ public void Execute() public void Undo() { + if (_originalGeos == null || _selectionOldState == null) + return; + for (var i = 0; i < _meshesToPin.Count; i++) { _meshesToPin[i].Geometry = _originalGeos[i]; diff --git a/Editors/Kitbashing/KitbasherEditor/ChildEditors/PinTool/Commands/SkinWrapRiggingCommand.cs b/Editors/Kitbashing/KitbasherEditor/ChildEditors/PinTool/Commands/SkinWrapRiggingCommand.cs index 4ebea77f9..507ab57e8 100644 --- a/Editors/Kitbashing/KitbasherEditor/ChildEditors/PinTool/Commands/SkinWrapRiggingCommand.cs +++ b/Editors/Kitbashing/KitbasherEditor/ChildEditors/PinTool/Commands/SkinWrapRiggingCommand.cs @@ -2,55 +2,63 @@ using GameWorld.Core.Components.Selection; using GameWorld.Core.Rendering.Geometry; using GameWorld.Core.SceneNodes; +using Serilog; +using Shared.Core.ErrorHandling; namespace Editors.KitbasherEditor.ChildEditors.PinTool.Commands { public class SkinWrapRiggingCommand : ICommand { - ISelectionState _selectionOldState; + private readonly ILogger _logger = Logging.Create(); private readonly SelectionManager _selectionManager; - List _originalGeometries; - - List _giveAnimationToList; - Rmv2MeshNode _takeAnimationFrom; + private ISelectionState? _selectionOldState; + private List? _originalGeometries; + private List _giveAnimationToList = []; + private List _takeAnimationFromList = []; public string HintText { get => "Skin wrap re-rigging"; } public bool IsMutation { get => true; } - - public SkinWrapRiggingCommand(SelectionManager selectionManager) { - _selectionManager = selectionManager; ; + _selectionManager = selectionManager; } - public void Configure(IEnumerable giveAnimationTo, Rmv2MeshNode takeAnimationFrom) + public void Configure(IEnumerable giveAnimationTo, List takeAnimationFrom) { _giveAnimationToList = giveAnimationTo.ToList(); - _takeAnimationFrom = takeAnimationFrom; + _takeAnimationFromList = takeAnimationFrom; } public void Execute() { - // Create undo state + if (_takeAnimationFromList.Count == 0 || _giveAnimationToList.Count == 0) + throw new InvalidOperationException("SkinWrapRiggingCommand not configured before Execute"); + _originalGeometries = _giveAnimationToList.Select(x => x.Geometry.Clone()).ToList(); _selectionOldState = _selectionManager.GetStateCopy(); - // Update the meshes + var firstSource = _takeAnimationFromList[0]; + _logger.Here().Information("Executing skin wrap: {TargetCount} target meshes, {SourceCount} source meshes, vertex format '{Format}'", + _giveAnimationToList.Count, _takeAnimationFromList.Count, firstSource.Geometry.VertexFormat); + foreach (var giveAnimationTo in _giveAnimationToList) { - // Set skeleton and vertex type from first source object - giveAnimationTo.Geometry.ChangeVertexType(_takeAnimationFrom.Geometry.VertexFormat, false); - giveAnimationTo.Geometry.UpdateSkeletonName(_takeAnimationFrom.Geometry.SkeletonName); + giveAnimationTo.Geometry.ChangeVertexType(firstSource.Geometry.VertexFormat, false); + giveAnimationTo.Geometry.UpdateSkeletonName(firstSource.Geometry.SkeletonName); + + var maxBoneInfluences = giveAnimationTo.Geometry.WeightCount; for (var i = 0; i < giveAnimationTo.Geometry.VertexCount(); i++) { - var inputVertexPos = giveAnimationTo.Geometry.VertexArray[i].Position3(); - var res = RegiggingHelper.FindClosestUV(inputVertexPos, _takeAnimationFrom.Geometry, _takeAnimationFrom.Position); + var localVertexPos = giveAnimationTo.Geometry.VertexArray[i].Position3(); + var worldVertexPos = localVertexPos + giveAnimationTo.Position; - giveAnimationTo.Geometry.VertexArray[i].BlendIndices = res.Bones; - giveAnimationTo.Geometry.VertexArray[i].BlendWeights = res.BlendWeights; + var result = RegiggingHelper.FindClosestBoneWeightsMultiMesh(worldVertexPos, _takeAnimationFromList, maxBoneInfluences); + + giveAnimationTo.Geometry.SetVertexBlendIndex(i, result.BoneIndices); + giveAnimationTo.Geometry.SetVertexWeights(i, result.BlendWeights); } giveAnimationTo.Geometry.RebuildVertexBuffer(); @@ -59,6 +67,9 @@ public void Execute() public void Undo() { + if (_originalGeometries == null || _selectionOldState == null) + return; + for (var i = 0; i < _giveAnimationToList.Count; i++) _giveAnimationToList[i].Geometry = _originalGeometries[i]; diff --git a/Editors/Kitbashing/KitbasherEditor/ChildEditors/PinTool/PinRiggingAlgorithm.cs b/Editors/Kitbashing/KitbasherEditor/ChildEditors/PinTool/PinRiggingAlgorithm.cs index 071a06bae..c6cbe53ab 100644 --- a/Editors/Kitbashing/KitbasherEditor/ChildEditors/PinTool/PinRiggingAlgorithm.cs +++ b/Editors/Kitbashing/KitbasherEditor/ChildEditors/PinTool/PinRiggingAlgorithm.cs @@ -5,18 +5,22 @@ using GameWorld.Core.Components.Selection; using GameWorld.Core.SceneNodes; using Microsoft.Xna.Framework; +using Serilog; +using Shared.Core.ErrorHandling; using Shared.Core.Services; +using Shared.GameFormats.RigidModel; namespace Editors.KitbasherEditor.ChildEditors.PinTool { public partial class PinRiggingAlgorithm : ObservableObject { + private readonly ILogger _logger = Logging.Create(); private readonly IStandardDialogs _standardDialogs; private readonly SelectionManager _selectionManager; private readonly CommandFactory _commandFactory; - [ObservableProperty] List _selectedVertex =[]; - [ObservableProperty] Rmv2MeshNode _selectedMesh; + [ObservableProperty] List _selectedVertex = []; + [ObservableProperty] Rmv2MeshNode? _selectedMesh; [ObservableProperty] string _description = ""; public PinRiggingAlgorithm(CommandFactory commandFactory, IStandardDialogs standardDialogs, SelectionManager selectionManager) @@ -40,6 +44,13 @@ public bool Execute(List meshesToAffect) return false; } + if (SelectedMesh.Geometry.VertexFormat == UiVertexFormat.Static) + { + _standardDialogs.ShowDialogBox("Source mesh has no bone weights (static format). Use an animated mesh as source.", "Error"); + return false; + } + + _logger.Here().Information("Pinning {Count} meshes to vertex {VertexId} on '{MeshName}'", meshesToAffect.Count, SelectedVertex.First(), SelectedMesh.Name); _commandFactory.Create().Configure(x => x.Configure(meshesToAffect, SelectedMesh, SelectedVertex.First())).BuildAndExecute(); return true; } @@ -49,7 +60,6 @@ [RelayCommand] void SetSelection() SelectedVertex.Clear(); SelectedMesh = null; - var description = "No Mesh selected"; var selectionState = _selectionManager.GetState(); if (selectionState == null || selectionState.SelectionCount() == 0) { @@ -59,19 +69,21 @@ [RelayCommand] void SetSelection() var selectionAsMeshNode = selectionState.GetSingleSelectedObject() as Rmv2MeshNode; if (selectionAsMeshNode == null) - throw new Exception($"Unexpected result for selection. State = {selectionState}"); + { + _logger.Here().Error("Unexpected selection type: {State}", selectionState); + _standardDialogs.ShowDialogBox("Unexpected selection type - expected a mesh node", "Error"); + return; + } if (selectionAsMeshNode.PivotPoint != Vector3.Zero) { - _standardDialogs.ShowDialogBox("Selected mesh has a pivot point, the tool will not work correctly", "error"); + _standardDialogs.ShowDialogBox("Selected mesh has a pivot point, the tool will not work correctly", "Error"); return; } SelectedMesh = selectionAsMeshNode; SelectedVertex = selectionState.SelectedVertices.ToList(); - - description = $"Mesh:{SelectedMesh.Name}', Num Verts: {SelectedVertex.Count}"; - Description = description; + Description = $"Mesh: '{SelectedMesh.Name}', Num Verts: {SelectedVertex.Count}"; } } } diff --git a/Editors/Kitbashing/KitbasherEditor/ChildEditors/PinTool/PinToolViewModel.cs b/Editors/Kitbashing/KitbasherEditor/ChildEditors/PinTool/PinToolViewModel.cs index 3219e1133..bdb315155 100644 --- a/Editors/Kitbashing/KitbasherEditor/ChildEditors/PinTool/PinToolViewModel.cs +++ b/Editors/Kitbashing/KitbasherEditor/ChildEditors/PinTool/PinToolViewModel.cs @@ -5,6 +5,8 @@ using GameWorld.Core.Components.Selection; using GameWorld.Core.SceneNodes; using Microsoft.Xna.Framework; +using Serilog; +using Shared.Core.ErrorHandling; using Shared.Core.Services; namespace Editors.KitbasherEditor.ChildEditors.PinTool @@ -17,6 +19,7 @@ public enum RiggingMode public partial class PinToolViewModel : ObservableObject { + private readonly ILogger _logger = Logging.Create(); private readonly SelectionManager _selectionManager; private readonly CommandFactory _commandFactory; private readonly IStandardDialogs _standardDialogs; @@ -26,7 +29,6 @@ public partial class PinToolViewModel : ObservableObject [ObservableProperty] RiggingMode[] _possibleRiggingModes = Enum.GetValues(); [ObservableProperty] RiggingMode _selectedRiggingMode = RiggingMode.Pin; [ObservableProperty] ObservableCollection _affectedMeshCollection = []; - [ObservableProperty] ObservableCollection _sourceMeshCollection = []; public PinToolViewModel(SelectionManager selectionManager, CommandFactory commandFactory, IStandardDialogs standardDialogs) { @@ -51,8 +53,7 @@ void AddSelectionToList(ObservableCollection itemList) } var selectedObjects = selectionState.SelectedObjects() - .Select(x => x as Rmv2MeshNode) - .Where(x => x != null) + .OfType() .ToList(); if (selectedObjects.Any(x => x.PivotPoint != Vector3.Zero)) @@ -81,17 +82,18 @@ public bool Apply() return false; } + _logger.Here().Information("Applying {Mode} to {Count} meshes", SelectedRiggingMode, AffectedMeshCollection.Count); + switch (SelectedRiggingMode) { case RiggingMode.Pin: return PinMode.Execute(AffectedMeshCollection.ToList()); case RiggingMode.SkinWrap: - return SkinWrapMode.Excute(AffectedMeshCollection.ToList()); + return SkinWrapMode.Execute(AffectedMeshCollection.ToList()); default: - throw new NotImplementedException($"unable to find an algorithm for selected mode '{SelectedRiggingMode}'"); + throw new NotImplementedException($"Unable to find an algorithm for selected mode '{SelectedRiggingMode}'"); } - } } } diff --git a/Editors/Kitbashing/KitbasherEditor/ChildEditors/PinTool/Presentation/PinToolWindow.xaml b/Editors/Kitbashing/KitbasherEditor/ChildEditors/PinTool/Presentation/PinToolWindow.xaml index 7ad2527a6..c4202701a 100644 --- a/Editors/Kitbashing/KitbasherEditor/ChildEditors/PinTool/Presentation/PinToolWindow.xaml +++ b/Editors/Kitbashing/KitbasherEditor/ChildEditors/PinTool/Presentation/PinToolWindow.xaml @@ -10,29 +10,10 @@ mc:Ignorable="d" Style="{StaticResource CustomWindowStyle}" + HelpDocumentPath="Documentation\AssetEditorDocumentation.html?doc=Kitbash_PinTool" Title="{loc:Loc KitbashTool.PinTool.Title}" Height="600" Width="400"> - - - 1. Select mode: - - 'Pin': for things which should not bend - - 'Skin wrap': for things which should deform - - 2. Select meshes which should get new animation - - 3. Select the meshes to take animation from - - 'Pin': go into vertex mode and press button with a vertex selected - - 'Skin wrap': select mesh - - 4. Press Apply - - -