Files
FlaxEngine/Source/Editor/CustomEditors/Dedicated/RagdollEditor.cs

316 lines
11 KiB
C#

// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using FlaxEditor.GUI;
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.SceneGraph;
using FlaxEditor.SceneGraph.Actors;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Dedicated
{
/// <summary>
/// Custom editor for <see cref="Ragdoll"/>.
/// </summary>
/// <seealso cref="ActorEditor" />
[CustomEditor(typeof(Ragdoll)), DefaultEditor]
public class RagdollEditor : ActorEditor
{
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
base.Initialize(layout);
var ragdoll = Values.Count == 1 ? Values[0] as Ragdoll : null;
if (ragdoll == null)
return;
var editorGroup = layout.Group("Ragdoll Editor");
editorGroup.Panel.Open(false);
// Info
var text = $"Total mass: {Utils.RoundTo1DecimalPlace(ragdoll.TotalMass)}kg";
var label = editorGroup.Label(text);
label.Label.AutoHeight = true;
if (ragdoll.Parent is AnimatedModel animatedModel && animatedModel.SkinnedModel)
{
// Builder
var grid = editorGroup.CustomContainer<UniformGridPanel>();
var gridControl = grid.CustomControl;
gridControl.ClipChildren = false;
gridControl.Height = Button.DefaultHeight;
gridControl.SlotsHorizontally = 3;
gridControl.SlotsVertically = 1;
grid.Button("Rebuild").Button.ButtonClicked += OnRebuild;
grid.Button("Rebuild/Add bone").Button.ButtonClicked += OnRebuildBone;
grid.Button("Remove bone").Button.ButtonClicked += OnRemoveBone;
}
if (Presenter.Owner != null)
{
// Selection
var grid = editorGroup.CustomContainer<UniformGridPanel>();
var gridControl = grid.CustomControl;
gridControl.ClipChildren = false;
gridControl.Height = Button.DefaultHeight;
gridControl.SlotsHorizontally = 3;
gridControl.SlotsVertically = 1;
grid.Button("Select all joints").Button.Clicked += OnSelectAllJoints;
grid.Button("Select all colliders").Button.Clicked += OnSelectAllColliders;
grid.Button("Select all bodies").Button.Clicked += OnSelectAllBodies;
}
}
class RebuildContextMenu : ContextMenuBase
{
private Action<AnimatedModelNode.RebuildOptions> _rebuild;
private AnimatedModelNode.RebuildOptions _options = new AnimatedModelNode.RebuildOptions();
public RebuildContextMenu(Action<AnimatedModelNode.RebuildOptions> rebuild)
{
_rebuild = rebuild;
const float width = 280.0f;
const float height = 220.0f;
Size = new Float2(width, height);
// Title
var title = new Label(2, 2, width - 4, 23.0f)
{
Font = new FontReference(FlaxEngine.GUI.Style.Current.FontLarge),
Text = "Ragdoll Options",
Parent = this
};
// Buttons
var rebuildButton = new Button(2.0f, title.Bottom + 2.0f, width - 4.0f, 20.0f)
{
Text = "Rebuild",
Parent = this
};
rebuildButton.Clicked += OnRebuild;
// Actual panel
var panel1 = new Panel(ScrollBars.Vertical)
{
Bounds = new Rectangle(0, rebuildButton.Bottom + 2.0f, width, height - rebuildButton.Bottom - 2.0f),
Parent = this
};
var editor = new CustomEditorPresenter(null);
editor.Panel.AnchorPreset = AnchorPresets.HorizontalStretchTop;
editor.Panel.IsScrollable = true;
editor.Panel.Parent = panel1;
editor.Select(_options);
}
private void OnRebuild()
{
Hide();
_rebuild(_options);
}
protected override void OnShow()
{
Focus();
base.OnShow();
}
public override void Hide()
{
if (!Visible)
return;
Focus(null);
base.Hide();
}
/// <inheritdoc />
public override bool OnKeyDown(KeyboardKeys key)
{
if (key == KeyboardKeys.Escape)
{
Hide();
return true;
}
return base.OnKeyDown(key);
}
}
private void OnRebuild(Button button)
{
var cm = new RebuildContextMenu(Rebuild);
cm.Show(button.Parent, button.BottomLeft);
}
private void Rebuild(AnimatedModelNode.RebuildOptions options)
{
var ragdoll = (Ragdoll)Values[0];
var animatedModel = (AnimatedModel)ragdoll.Parent;
// Remove existing bodies
var bodies = ragdoll.Children.Where(x => x is RigidBody && x.IsActive);
var actions = new List<IUndoAction>();
foreach (var body in bodies)
{
var action = new Actions.DeleteActorsAction(body);
action.Do();
actions.Add(action);
}
Presenter.Undo?.AddAction(new MultiUndoAction(actions));
// Build ragdoll
AnimatedModelNode.BuildRagdoll(animatedModel, options, ragdoll);
}
private void OnRebuildBone(Button button)
{
PickBone(button, RebuildBone, false);
}
private void RebuildBone(string name)
{
var ragdoll = (Ragdoll)Values[0];
var animatedModel = (AnimatedModel)ragdoll.Parent;
// Remove existing body
var bodies = ragdoll.Children.Where(x => x is RigidBody && x.IsActive);
var body = bodies.FirstOrDefault(x => x.Name == name);
if (body != null)
{
var action = new Actions.DeleteActorsAction(body);
action.Do();
Presenter.Undo?.AddAction(action);
}
// Build ragdoll
AnimatedModelNode.BuildRagdoll(animatedModel, new AnimatedModelNode.RebuildOptions(), ragdoll, name);
}
private void OnRemoveBone(Button button)
{
PickBone(button, RemoveBone, true);
}
private void RemoveBone(string name)
{
var ragdoll = (Ragdoll)Values[0];
var bodies = ragdoll.Children.Where(x => x is RigidBody && x.IsActive);
var joints = bodies.SelectMany(y => y.Children).Where(z => z is Joint && z.IsActive).Cast<Joint>();
var body = bodies.First(x => x.Name == name);
var replacementJoint = joints.FirstOrDefault(x => x.Parent == body && x.Target != null);
// Fix joints using this bone
foreach (var joint in joints)
{
if (joint.Target == body)
{
if (replacementJoint != null)
{
// Swap the joint target to the parent of the removed body to keep ragdoll connected
using (new UndoBlock(Presenter.Undo, joint, "Fix joint"))
{
joint.Target = replacementJoint.Target;
joint.EnableAutoAnchor = true;
}
}
else
{
// Remove joint that will no longer be valid
var action = new Actions.DeleteActorsAction(joint);
action.Do();
Presenter.Undo?.AddAction(action);
}
}
}
// Remove body
{
var action = new Actions.DeleteActorsAction(body);
action.Do();
Presenter.Undo?.AddAction(action);
}
}
private void PickBone(Button button, Action<string> action, bool showOnlyExisting)
{
// Show context menu with list of bones to pick
var cm = new ItemsListContextMenu(280);
var ragdoll = (Ragdoll)Values[0];
var bodies = ragdoll.Children.Where(x => x is RigidBody && x.IsActive);
var animatedModel = (AnimatedModel)ragdoll.Parent;
animatedModel.SkinnedModel.WaitForLoaded();
var nodes = animatedModel.SkinnedModel.Nodes;
var bones = animatedModel.SkinnedModel.Bones;
foreach (var bone in bones)
{
var node = nodes[bone.NodeIndex];
if (showOnlyExisting && !bodies.Any(x => x.Name == node.Name))
continue;
string prefix = string.Empty, tooltip = node.Name;
var boneParentIndex = bone.ParentIndex;
while (boneParentIndex != -1)
{
prefix += " ";
tooltip = nodes[bones[boneParentIndex].NodeIndex].Name + " > " + tooltip;
boneParentIndex = bones[boneParentIndex].ParentIndex;
}
var item = new ItemsListContextMenu.Item
{
Name = prefix + node.Name,
TooltipText = tooltip,
Tag = node.Name,
};
if (!showOnlyExisting && !bodies.Any(x => x.Name == node.Name))
item.TintColor = new Color(1, 0.8f, 0.8f, 0.6f);
cm.AddItem(item);
}
cm.ItemClicked += item => action((string)item.Tag);
cm.SortItems();
cm.Show(button.Parent, button.BottomLeft);
}
private void OnSelectAllJoints()
{
var ragdoll = (Ragdoll)Values[0];
var bodies = ragdoll.Children.Where(x => x is RigidBody && x.IsActive);
var joints = bodies.SelectMany(y => y.Children).Where(z => z is Joint && z.IsActive);
Select(joints);
}
private void OnSelectAllColliders()
{
var ragdoll = (Ragdoll)Values[0];
var bodies = ragdoll.Children.Where(x => x is RigidBody && x.IsActive);
var colliders = bodies.SelectMany(y => y.Children).Where(z => z is Collider && z.IsActive);
Select(colliders);
}
private void OnSelectAllBodies()
{
var ragdoll = (Ragdoll)Values[0];
var bodies = ragdoll.Children.Where(x => x is RigidBody && x.IsActive);
Select(bodies);
}
private void Select(IEnumerable<Actor> list)
{
var selection = new List<SceneGraphNode>();
foreach (var e in list)
{
var node = SceneGraphFactory.FindNode(e.ID);
if (node != null)
selection.Add(node);
}
Presenter.Owner.Select(selection);
}
}
}