You're breathtaking!
This commit is contained in:
87
Source/Editor/Windows/Assets/AnimationGraphFunctionWindow.cs
Normal file
87
Source/Editor/Windows/Assets/AnimationGraphFunctionWindow.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using FlaxEditor.Content;
|
||||
using FlaxEditor.Surface;
|
||||
using FlaxEditor.GUI;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
|
||||
namespace FlaxEditor.Windows.Assets
|
||||
{
|
||||
/// <summary>
|
||||
/// Animation Graph function window allows to view and edit <see cref="AnimationGraphFunction"/> asset.
|
||||
/// </summary>
|
||||
/// <seealso cref="AnimationGraphFunction" />
|
||||
/// <seealso cref="AnimationGraphFunctionSurface" />
|
||||
public sealed class AnimationGraphFunctionWindow : VisjectFunctionSurfaceWindow<AnimationGraphFunction, AnimationGraphFunctionSurface>
|
||||
{
|
||||
private readonly NavigationBar _navigationBar;
|
||||
|
||||
/// <inheritdoc />
|
||||
public AnimationGraphFunctionWindow(Editor editor, AssetItem item)
|
||||
: base(editor, item)
|
||||
{
|
||||
// Surface
|
||||
_surface = new AnimationGraphFunctionSurface(this, Save, _undo)
|
||||
{
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
Offsets = Margin.Zero,
|
||||
Parent = _panel,
|
||||
Enabled = false
|
||||
};
|
||||
_surface.ContextChanged += OnSurfaceContextChanged;
|
||||
|
||||
// Toolstrip
|
||||
_toolstrip.AddSeparator();
|
||||
_toolstrip.AddButton(editor.Icons.Docs32, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/animation/anim-graph/index.html")).LinkTooltip("See documentation to learn more");
|
||||
|
||||
// Navigation bar
|
||||
_navigationBar = new NavigationBar
|
||||
{
|
||||
Parent = this
|
||||
};
|
||||
}
|
||||
|
||||
private void OnSurfaceContextChanged(VisjectSurfaceContext context)
|
||||
{
|
||||
_surface.UpdateNavigationBar(_navigationBar, _toolstrip);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string SurfaceName => "Animation Graph Function";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override byte[] SurfaceData
|
||||
{
|
||||
get => _asset.LoadSurface();
|
||||
set
|
||||
{
|
||||
if (_asset.SaveSurface(value))
|
||||
{
|
||||
_surface.MarkAsEdited();
|
||||
Editor.LogError("Failed to save surface data");
|
||||
}
|
||||
_asset.Reload();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool LoadSurface()
|
||||
{
|
||||
var result = base.LoadSurface();
|
||||
|
||||
// Update navbar
|
||||
_surface.UpdateNavigationBar(_navigationBar, _toolstrip);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void PerformLayoutBeforeChildren()
|
||||
{
|
||||
base.PerformLayoutBeforeChildren();
|
||||
|
||||
_navigationBar?.UpdateBounds(_toolstrip);
|
||||
}
|
||||
}
|
||||
}
|
||||
467
Source/Editor/Windows/Assets/AnimationGraphWindow.cs
Normal file
467
Source/Editor/Windows/Assets/AnimationGraphWindow.cs
Normal file
@@ -0,0 +1,467 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FlaxEditor.Content;
|
||||
using FlaxEditor.CustomEditors;
|
||||
using FlaxEditor.CustomEditors.Editors;
|
||||
using FlaxEditor.GUI;
|
||||
using FlaxEditor.GUI.ContextMenu;
|
||||
using FlaxEditor.Scripting;
|
||||
using FlaxEditor.Surface;
|
||||
using FlaxEditor.Viewport.Cameras;
|
||||
using FlaxEditor.Viewport.Previews;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
|
||||
// ReSharper disable UnusedMember.Local
|
||||
// ReSharper disable UnusedMember.Global
|
||||
// ReSharper disable MemberCanBePrivate.Local
|
||||
|
||||
namespace FlaxEditor.Windows.Assets
|
||||
{
|
||||
/// <summary>
|
||||
/// Animation Graph window allows to view and edit <see cref="AnimationGraph"/> asset.
|
||||
/// </summary>
|
||||
/// <seealso cref="AnimationGraph" />
|
||||
/// <seealso cref="AnimGraphSurface" />
|
||||
/// <seealso cref="AnimatedModelPreview" />
|
||||
public sealed class AnimationGraphWindow : VisjectSurfaceWindow<AnimationGraph, AnimGraphSurface, AnimatedModelPreview>
|
||||
{
|
||||
internal static Guid BaseModelId = new Guid(1000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
|
||||
|
||||
private readonly ScriptType[] _newParameterTypes =
|
||||
{
|
||||
new ScriptType(typeof(float)),
|
||||
new ScriptType(typeof(bool)),
|
||||
new ScriptType(typeof(int)),
|
||||
new ScriptType(typeof(string)),
|
||||
new ScriptType(typeof(Vector2)),
|
||||
new ScriptType(typeof(Vector3)),
|
||||
new ScriptType(typeof(Vector4)),
|
||||
new ScriptType(typeof(Color)),
|
||||
new ScriptType(typeof(Quaternion)),
|
||||
new ScriptType(typeof(Transform)),
|
||||
new ScriptType(typeof(Matrix)),
|
||||
};
|
||||
|
||||
private sealed class AnimationGraphPreview : AnimatedModelPreview
|
||||
{
|
||||
private readonly AnimationGraphWindow _window;
|
||||
private ContextMenuButton _showFloorButton;
|
||||
private StaticModel _floorModel;
|
||||
|
||||
public AnimationGraphPreview(AnimationGraphWindow window)
|
||||
: base(true)
|
||||
{
|
||||
_window = window;
|
||||
|
||||
// Show floor widget
|
||||
_showFloorButton = ViewWidgetShowMenu.AddButton("Floor", OnShowFloorModelClicked);
|
||||
_showFloorButton.Icon = Style.Current.CheckBoxTick;
|
||||
_showFloorButton.IndexInParent = 1;
|
||||
|
||||
// Floor model
|
||||
_floorModel = new StaticModel();
|
||||
_floorModel.Position = new Vector3(0, -25, 0);
|
||||
_floorModel.Scale = new Vector3(5, 0.5f, 5);
|
||||
_floorModel.Model = FlaxEngine.Content.LoadAsync<Model>(StringUtils.CombinePaths(Globals.EngineContentFolder, "Editor/Primitives/Cube.flax"));
|
||||
Task.AddCustomActor(_floorModel);
|
||||
|
||||
// Enable shadows
|
||||
PreviewLight.ShadowsMode = ShadowsCastingMode.All;
|
||||
PreviewLight.CascadeCount = 2;
|
||||
PreviewLight.ShadowsDistance = 1000.0f;
|
||||
Task.ViewFlags |= ViewFlags.Shadows;
|
||||
}
|
||||
|
||||
private void OnShowFloorModelClicked(ContextMenuButton obj)
|
||||
{
|
||||
_floorModel.IsActive = !_floorModel.IsActive;
|
||||
_showFloorButton.Icon = _floorModel.IsActive ? Style.Current.CheckBoxTick : SpriteHandle.Invalid;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Draw()
|
||||
{
|
||||
base.Draw();
|
||||
|
||||
var style = Style.Current;
|
||||
if (_window.Asset == null || !_window.Asset.IsLoaded)
|
||||
{
|
||||
Render2D.DrawText(style.FontLarge, "Loading...", new Rectangle(Vector2.Zero, Size), style.ForegroundDisabled, TextAlignment.Center, TextAlignment.Center);
|
||||
}
|
||||
if (_window._properties.BaseModel == null)
|
||||
{
|
||||
Render2D.DrawText(style.FontLarge, "Missing Base Model", new Rectangle(Vector2.Zero, Size), Color.Red, TextAlignment.Center, TextAlignment.Center, TextWrapping.WrapWords);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnDestroy()
|
||||
{
|
||||
FlaxEngine.Object.Destroy(ref _floorModel);
|
||||
_showFloorButton = null;
|
||||
|
||||
base.OnDestroy();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The graph properties proxy object.
|
||||
/// </summary>
|
||||
private sealed class PropertiesProxy
|
||||
{
|
||||
private SkinnedModel _baseModel;
|
||||
|
||||
[EditorOrder(10), EditorDisplay("General"), Tooltip("The base model used to preview the animation and prepare the graph (skeleton bones structure must match in instanced AnimationModel actors)")]
|
||||
public SkinnedModel BaseModel
|
||||
{
|
||||
get => _baseModel;
|
||||
set
|
||||
{
|
||||
if (_baseModel != value)
|
||||
{
|
||||
_baseModel = value;
|
||||
if (Window != null)
|
||||
Window.PreviewActor.SkinnedModel = _baseModel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[EditorOrder(1000), EditorDisplay("Parameters"), CustomEditor(typeof(ParametersEditor)), NoSerialize]
|
||||
// ReSharper disable once UnusedAutoPropertyAccessor.Local
|
||||
public AnimationGraphWindow Window { get; set; }
|
||||
|
||||
[HideInEditor, Serialize]
|
||||
// ReSharper disable once UnusedMember.Local
|
||||
public List<SurfaceParameter> Parameters
|
||||
{
|
||||
get => Window.Surface.Parameters;
|
||||
set => throw new Exception("No setter.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gathers parameters from the graph.
|
||||
/// </summary>
|
||||
/// <param name="window">The graph window.</param>
|
||||
public void OnLoad(AnimationGraphWindow window)
|
||||
{
|
||||
Window = window;
|
||||
var surfaceParam = window.Surface.GetParameter(BaseModelId);
|
||||
if (surfaceParam != null)
|
||||
BaseModel = FlaxEngine.Content.LoadAsync<SkinnedModel>((Guid)surfaceParam.Value);
|
||||
else
|
||||
BaseModel = window.PreviewActor.GetParameterValue(BaseModelId) as SkinnedModel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the properties to the graph.
|
||||
/// </summary>
|
||||
/// <param name="window">The graph window.</param>
|
||||
public void OnSave(AnimationGraphWindow window)
|
||||
{
|
||||
var model = window.PreviewActor;
|
||||
model.SetParameterValue(BaseModelId, BaseModel);
|
||||
var surfaceParam = window.Surface.GetParameter(BaseModelId);
|
||||
if (surfaceParam != null)
|
||||
surfaceParam.Value = BaseModel?.ID ?? Guid.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears temporary data.
|
||||
/// </summary>
|
||||
public void OnClean()
|
||||
{
|
||||
// Unlink
|
||||
Window = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The graph parameters preview proxy object.
|
||||
/// </summary>
|
||||
private sealed class PreviewProxy
|
||||
{
|
||||
[EditorDisplay("Parameters"), CustomEditor(typeof(Editor)), NoSerialize]
|
||||
// ReSharper disable once UnusedAutoPropertyAccessor.Local
|
||||
public AnimationGraphWindow Window;
|
||||
|
||||
private class Editor : CustomEditor
|
||||
{
|
||||
public override DisplayStyle Style => DisplayStyle.InlineIntoParent;
|
||||
|
||||
public override void Initialize(LayoutElementsContainer layout)
|
||||
{
|
||||
var window = (AnimationGraphWindow)Values[0];
|
||||
var parameters = window.PreviewActor.Parameters;
|
||||
var data = SurfaceUtils.InitGraphParameters(parameters);
|
||||
SurfaceUtils.DisplayGraphParameters(layout, data,
|
||||
(instance, parameter, tag) => ((AnimationGraphWindow)instance).PreviewActor.GetParameterValue(parameter.Identifier),
|
||||
(instance, value, parameter, tag) => ((AnimationGraphWindow)instance).PreviewActor.SetParameterValue(parameter.Identifier, value),
|
||||
Values);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private FlaxObjectRefPickerControl _debugPicker;
|
||||
private NavigationBar _navigationBar;
|
||||
private PropertiesProxy _properties;
|
||||
private Tab _previewTab;
|
||||
private readonly List<Editor.AnimGraphDebugFlowInfo> _debugFlows = new List<Editor.AnimGraphDebugFlowInfo>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the animated model actor used for the animation preview.
|
||||
/// </summary>
|
||||
public AnimatedModel PreviewActor => _preview.PreviewActor;
|
||||
|
||||
/// <inheritdoc />
|
||||
public AnimationGraphWindow(Editor editor, AssetItem item)
|
||||
: base(editor, item, true)
|
||||
{
|
||||
// Asset preview
|
||||
_preview = new AnimationGraphPreview(this)
|
||||
{
|
||||
ViewportCamera = new FPSCamera(),
|
||||
ScaleToFit = false,
|
||||
PlayAnimation = true,
|
||||
Parent = _split2.Panel1
|
||||
};
|
||||
|
||||
// Asset properties proxy
|
||||
_properties = new PropertiesProxy();
|
||||
_propertiesEditor.Select(_properties);
|
||||
|
||||
// Preview properties editor
|
||||
_previewTab = new Tab("Preview");
|
||||
_previewTab.Presenter.Select(new PreviewProxy
|
||||
{
|
||||
Window = this,
|
||||
});
|
||||
_tabs.AddTab(_previewTab);
|
||||
|
||||
// Surface
|
||||
_surface = new AnimGraphSurface(this, Save, _undo)
|
||||
{
|
||||
Parent = _split1.Panel1,
|
||||
Enabled = false
|
||||
};
|
||||
_surface.ContextChanged += OnSurfaceContextChanged;
|
||||
|
||||
// Toolstrip
|
||||
_toolstrip.AddButton(editor.Icons.Bone32, () => _preview.ShowNodes = !_preview.ShowNodes).SetAutoCheck(true).LinkTooltip("Show animated model nodes debug view");
|
||||
_toolstrip.AddSeparator();
|
||||
_toolstrip.AddButton(editor.Icons.Docs32, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/animation/anim-graph/index.html")).LinkTooltip("See documentation to learn more");
|
||||
|
||||
// Debug picker
|
||||
var debugPickerContainer = new ContainerControl();
|
||||
var debugPickerLabel = new Label
|
||||
{
|
||||
AnchorPreset = AnchorPresets.VerticalStretchLeft,
|
||||
VerticalAlignment = TextAlignment.Center,
|
||||
HorizontalAlignment = TextAlignment.Far,
|
||||
Parent = debugPickerContainer,
|
||||
Size = new Vector2(50.0f, _toolstrip.Height),
|
||||
Text = "Debug:",
|
||||
TooltipText = "The current animated model actor to preview. Pick the player to debug it's playback. Leave empty to debug the model from the preview panel.",
|
||||
};
|
||||
_debugPicker = new FlaxObjectRefPickerControl
|
||||
{
|
||||
Location = new Vector2(debugPickerLabel.Right + 4.0f, 8.0f),
|
||||
Width = 120.0f,
|
||||
Type = new ScriptType(typeof(AnimatedModel)),
|
||||
Parent = debugPickerContainer,
|
||||
};
|
||||
debugPickerContainer.Width = _debugPicker.Right + 2.0f;
|
||||
debugPickerContainer.Size = new Vector2(_debugPicker.Right + 2.0f, _toolstrip.Height);
|
||||
debugPickerContainer.Parent = _toolstrip;
|
||||
_debugPicker.CheckValid = OnCheckValid;
|
||||
|
||||
// Navigation bar
|
||||
_navigationBar = new NavigationBar
|
||||
{
|
||||
Parent = this
|
||||
};
|
||||
|
||||
Editor.AnimGraphDebugFlow += OnDebugFlow;
|
||||
}
|
||||
|
||||
private void OnSurfaceContextChanged(VisjectSurfaceContext context)
|
||||
{
|
||||
_surface.UpdateNavigationBar(_navigationBar, _toolstrip);
|
||||
}
|
||||
|
||||
private bool OnCheckValid(FlaxEngine.Object obj, ScriptType type)
|
||||
{
|
||||
return obj is AnimatedModel player && player.AnimationGraph == OriginalAsset;
|
||||
}
|
||||
|
||||
private void OnDebugFlow(Editor.AnimGraphDebugFlowInfo flowInfo)
|
||||
{
|
||||
// Filter the flow
|
||||
if (_debugPicker.Value != null)
|
||||
{
|
||||
if (flowInfo.Asset != OriginalAsset || _debugPicker.Value != flowInfo.Object)
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (flowInfo.Asset != Asset || _preview.PreviewActor != flowInfo.Object)
|
||||
return;
|
||||
}
|
||||
|
||||
// Register flow to show it in UI on a surface
|
||||
lock (_debugFlows)
|
||||
{
|
||||
_debugFlows.Add(flowInfo);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IEnumerable<ScriptType> NewParameterTypes => _newParameterTypes;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void SetParameter(int index, object value)
|
||||
{
|
||||
try
|
||||
{
|
||||
var param = Surface.Parameters[index];
|
||||
PreviewActor.SetParameterValue(param.ID, value);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
|
||||
base.SetParameter(index, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void UnlinkItem()
|
||||
{
|
||||
_properties.OnClean();
|
||||
if (PreviewActor != null)
|
||||
PreviewActor.AnimationGraph = null;
|
||||
|
||||
base.UnlinkItem();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnAssetLinked()
|
||||
{
|
||||
PreviewActor.AnimationGraph = _asset;
|
||||
|
||||
base.OnAssetLinked();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string SurfaceName => "Anim Graph";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override byte[] SurfaceData
|
||||
{
|
||||
get => _asset.LoadSurface();
|
||||
set
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
// Error
|
||||
Editor.LogError("Failed to save animation graph surface");
|
||||
return;
|
||||
}
|
||||
|
||||
// Save data to the temporary asset
|
||||
if (_asset.SaveSurface(value))
|
||||
{
|
||||
// Error
|
||||
_surface.MarkAsEdited();
|
||||
Editor.LogError("Failed to save animation graph surface data");
|
||||
return;
|
||||
}
|
||||
_asset.Reload();
|
||||
|
||||
// Reset any root motion
|
||||
_preview.PreviewActor.ResetLocalTransform();
|
||||
_previewTab.Presenter.BuildLayoutOnUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool LoadSurface()
|
||||
{
|
||||
// Load surface graph
|
||||
if (_surface.Load())
|
||||
{
|
||||
// Error
|
||||
Editor.LogError("Failed to load animation graph surface.");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Init properties and parameters proxy
|
||||
_properties.OnLoad(this);
|
||||
_previewTab.Presenter.BuildLayoutOnUpdate();
|
||||
|
||||
// Update navbar
|
||||
_surface.UpdateNavigationBar(_navigationBar, _toolstrip);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool SaveSurface()
|
||||
{
|
||||
// Sync edited parameters
|
||||
_properties.OnSave(this);
|
||||
|
||||
// Save surface (will call SurfaceData setter)
|
||||
_surface.Save();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void PerformLayoutBeforeChildren()
|
||||
{
|
||||
base.PerformLayoutBeforeChildren();
|
||||
|
||||
if (_navigationBar != null && _debugPicker?.Parent != null && Parent != null)
|
||||
{
|
||||
_navigationBar.Bounds = new Rectangle
|
||||
(
|
||||
new Vector2(_debugPicker.Parent.Right + 8.0f, 0),
|
||||
new Vector2(Parent.Width - X - 8.0f, 32)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnUpdate()
|
||||
{
|
||||
base.OnUpdate();
|
||||
|
||||
// Update graph execution flow debugging visualization
|
||||
lock (_debugFlows)
|
||||
{
|
||||
foreach (var debugFlow in _debugFlows)
|
||||
{
|
||||
var node = Surface.Context.FindNode(debugFlow.NodeId);
|
||||
var box = node?.GetBox(debugFlow.BoxId);
|
||||
box?.HighlightConnections();
|
||||
}
|
||||
_debugFlows.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnDestroy()
|
||||
{
|
||||
Editor.AnimGraphDebugFlow -= OnDebugFlow;
|
||||
|
||||
_properties = null;
|
||||
_navigationBar = null;
|
||||
_debugPicker = null;
|
||||
|
||||
base.OnDestroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
302
Source/Editor/Windows/Assets/AnimationWindow.cs
Normal file
302
Source/Editor/Windows/Assets/AnimationWindow.cs
Normal file
@@ -0,0 +1,302 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Xml;
|
||||
using FlaxEditor.Content;
|
||||
using FlaxEditor.Content.Import;
|
||||
using FlaxEditor.CustomEditors;
|
||||
using FlaxEditor.CustomEditors.Editors;
|
||||
using FlaxEditor.GUI;
|
||||
using FlaxEditor.GUI.Timeline;
|
||||
using FlaxEditor.Scripting;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
|
||||
namespace FlaxEditor.Windows.Assets
|
||||
{
|
||||
/// <summary>
|
||||
/// Editor window to view/modify <see cref="Animation"/> asset.
|
||||
/// </summary>
|
||||
/// <seealso cref="Animation" />
|
||||
/// <seealso cref="FlaxEditor.Windows.Assets.AssetEditorWindow" />
|
||||
public sealed class AnimationWindow : AssetEditorWindowBase<Animation>
|
||||
{
|
||||
[CustomEditor(typeof(ProxyEditor))]
|
||||
private sealed class PropertiesProxy
|
||||
{
|
||||
private AnimationWindow Window;
|
||||
private Animation Asset;
|
||||
private ModelImportSettings ImportSettings = new ModelImportSettings();
|
||||
|
||||
public void OnLoad(AnimationWindow window)
|
||||
{
|
||||
// Link
|
||||
Window = window;
|
||||
Asset = window.Asset;
|
||||
|
||||
// Try to restore target asset import options (useful for fast reimport)
|
||||
ModelImportSettings.TryRestore(ref ImportSettings, window.Item.Path);
|
||||
}
|
||||
|
||||
public void OnClean()
|
||||
{
|
||||
// Unlink
|
||||
Window = null;
|
||||
Asset = null;
|
||||
}
|
||||
|
||||
public void Reimport()
|
||||
{
|
||||
Editor.Instance.ContentImporting.Reimport((BinaryAssetItem)Window.Item, ImportSettings, true);
|
||||
}
|
||||
|
||||
private class ProxyEditor : GenericEditor
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void Initialize(LayoutElementsContainer layout)
|
||||
{
|
||||
var proxy = (PropertiesProxy)Values[0];
|
||||
|
||||
if (proxy.Asset == null || !proxy.Asset.IsLoaded)
|
||||
{
|
||||
layout.Label("Loading...");
|
||||
return;
|
||||
}
|
||||
|
||||
base.Initialize(layout);
|
||||
|
||||
// General properties
|
||||
{
|
||||
var group = layout.Group("General");
|
||||
|
||||
var info = proxy.Asset.Info;
|
||||
group.Label("Length: " + info.Length + "s");
|
||||
group.Label("Frames: " + info.FramesCount);
|
||||
group.Label("Channels: " + info.ChannelsCount);
|
||||
group.Label("Keyframes: " + info.KeyframesCount);
|
||||
}
|
||||
|
||||
// Import Settings
|
||||
{
|
||||
var group = layout.Group("Import Settings");
|
||||
|
||||
var importSettingsField = typeof(PropertiesProxy).GetField("ImportSettings", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
var importSettingsValues = new ValueContainer(new ScriptMemberInfo(importSettingsField)) { proxy.ImportSettings };
|
||||
group.Object(importSettingsValues);
|
||||
|
||||
layout.Space(5);
|
||||
var reimportButton = group.Button("Reimport");
|
||||
reimportButton.Button.Clicked += () => ((PropertiesProxy)Values[0]).Reimport();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private CustomEditorPresenter _propertiesPresenter;
|
||||
private PropertiesProxy _properties;
|
||||
private SplitPanel _panel;
|
||||
private AnimationTimeline _timeline;
|
||||
private Undo _undo;
|
||||
private ToolStripButton _saveButton;
|
||||
private ToolStripButton _undoButton;
|
||||
private ToolStripButton _redoButton;
|
||||
private bool _isWaitingForTimelineLoad;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the animation timeline editor.
|
||||
/// </summary>
|
||||
public AnimationTimeline Timeline => _timeline;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the undo history context for this window.
|
||||
/// </summary>
|
||||
public Undo Undo => _undo;
|
||||
|
||||
/// <inheritdoc />
|
||||
public AnimationWindow(Editor editor, AssetItem item)
|
||||
: base(editor, item)
|
||||
{
|
||||
// Undo
|
||||
_undo = new Undo();
|
||||
_undo.UndoDone += OnUndoRedo;
|
||||
_undo.RedoDone += OnUndoRedo;
|
||||
_undo.ActionDone += OnUndoRedo;
|
||||
|
||||
// Main panel
|
||||
_panel = new SplitPanel(Orientation.Horizontal, ScrollBars.None, ScrollBars.Vertical)
|
||||
{
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
SplitterValue = 0.8f,
|
||||
Offsets = new Margin(0, 0, _toolstrip.Bottom, 0),
|
||||
Parent = this
|
||||
};
|
||||
|
||||
// Timeline
|
||||
_timeline = new AnimationTimeline(_undo)
|
||||
{
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
Offsets = Margin.Zero,
|
||||
Parent = _panel.Panel1,
|
||||
Enabled = false
|
||||
};
|
||||
_timeline.Modified += MarkAsEdited;
|
||||
|
||||
// Asset properties
|
||||
_propertiesPresenter = new CustomEditorPresenter(null);
|
||||
_propertiesPresenter.Panel.Parent = _panel.Panel2;
|
||||
_properties = new PropertiesProxy();
|
||||
_propertiesPresenter.Select(_properties);
|
||||
_propertiesPresenter.Modified += MarkAsEdited;
|
||||
|
||||
// Toolstrip
|
||||
_saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save32, Save).LinkTooltip("Save");
|
||||
_toolstrip.AddSeparator();
|
||||
_undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo32, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)");
|
||||
_redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo32, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)");
|
||||
_toolstrip.AddSeparator();
|
||||
_toolstrip.AddButton(editor.Icons.Docs32, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/animation/animation/index.html")).LinkTooltip("See documentation to learn more");
|
||||
|
||||
// Setup input actions
|
||||
InputActions.Add(options => options.Undo, _undo.PerformUndo);
|
||||
InputActions.Add(options => options.Redo, _undo.PerformRedo);
|
||||
}
|
||||
|
||||
private void OnUndoRedo(IUndoAction action)
|
||||
{
|
||||
MarkAsEdited();
|
||||
UpdateToolstrip();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnAssetLoaded()
|
||||
{
|
||||
_properties.OnLoad(this);
|
||||
_propertiesPresenter.BuildLayout();
|
||||
ClearEditedFlag();
|
||||
|
||||
base.OnAssetLoaded();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Save()
|
||||
{
|
||||
if (!IsEdited)
|
||||
return;
|
||||
|
||||
_timeline.Save(_asset);
|
||||
ClearEditedFlag();
|
||||
_item.RefreshThumbnail();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void UpdateToolstrip()
|
||||
{
|
||||
_saveButton.Enabled = IsEdited;
|
||||
_undoButton.Enabled = _undo.CanUndo;
|
||||
_redoButton.Enabled = _undo.CanRedo;
|
||||
|
||||
base.UpdateToolstrip();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void UnlinkItem()
|
||||
{
|
||||
_isWaitingForTimelineLoad = false;
|
||||
_properties.OnClean();
|
||||
|
||||
base.UnlinkItem();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnAssetLinked()
|
||||
{
|
||||
_isWaitingForTimelineLoad = true;
|
||||
|
||||
base.OnAssetLinked();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnItemReimported(ContentItem item)
|
||||
{
|
||||
// Refresh the properties (will get new data in OnAssetLoaded)
|
||||
_properties.OnClean();
|
||||
_propertiesPresenter.BuildLayout();
|
||||
ClearEditedFlag();
|
||||
|
||||
// Reload timeline
|
||||
_timeline.Enabled = false;
|
||||
_isWaitingForTimelineLoad = true;
|
||||
|
||||
base.OnItemReimported(item);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
base.Update(deltaTime);
|
||||
|
||||
if (_isWaitingForTimelineLoad && _asset.IsLoaded)
|
||||
{
|
||||
_isWaitingForTimelineLoad = false;
|
||||
_timeline.Load(_asset);
|
||||
_undo.Clear();
|
||||
_timeline.Enabled = true;
|
||||
ClearEditedFlag();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool UseLayoutData => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLayoutSerialize(XmlWriter writer)
|
||||
{
|
||||
writer.WriteAttributeString("TimelineSplitter", _timeline.Splitter.SplitterValue.ToString());
|
||||
writer.WriteAttributeString("TimeShowMode", _timeline.TimeShowMode.ToString());
|
||||
writer.WriteAttributeString("ShowPreviewValues", _timeline.ShowPreviewValues.ToString());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLayoutDeserialize(XmlElement node)
|
||||
{
|
||||
float value1;
|
||||
Timeline.TimeShowModes value2;
|
||||
bool value3;
|
||||
|
||||
if (float.TryParse(node.GetAttribute("TimelineSplitter"), out value1))
|
||||
_timeline.Splitter.SplitterValue = value1;
|
||||
if (Enum.TryParse(node.GetAttribute("TimeShowMode"), out value2))
|
||||
_timeline.TimeShowMode = value2;
|
||||
if (bool.TryParse(node.GetAttribute("ShowPreviewValues"), out value3))
|
||||
_timeline.ShowPreviewValues = value3;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLayoutDeserialize()
|
||||
{
|
||||
_timeline.Splitter.SplitterValue = 0.2f;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnDestroy()
|
||||
{
|
||||
if (_undo != null)
|
||||
{
|
||||
_undo.Enabled = false;
|
||||
_undo.Clear();
|
||||
_undo = null;
|
||||
}
|
||||
|
||||
_timeline = null;
|
||||
_propertiesPresenter = null;
|
||||
_properties = null;
|
||||
_panel = null;
|
||||
_saveButton = null;
|
||||
_undoButton = null;
|
||||
_redoButton = null;
|
||||
|
||||
base.OnDestroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
514
Source/Editor/Windows/Assets/AssetEditorWindow.cs
Normal file
514
Source/Editor/Windows/Assets/AssetEditorWindow.cs
Normal file
@@ -0,0 +1,514 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using FlaxEditor.Content;
|
||||
using FlaxEditor.GUI;
|
||||
using FlaxEditor.GUI.ContextMenu;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
|
||||
namespace FlaxEditor.Windows.Assets
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for assets editing/viewing windows.
|
||||
/// </summary>
|
||||
/// <seealso cref="FlaxEditor.Windows.EditorWindow" />
|
||||
public abstract class AssetEditorWindow : EditorWindow, IEditable, IContentItemOwner
|
||||
{
|
||||
/// <summary>
|
||||
/// The item.
|
||||
/// </summary>
|
||||
protected AssetItem _item;
|
||||
|
||||
/// <summary>
|
||||
/// The toolstrip.
|
||||
/// </summary>
|
||||
protected readonly ToolStrip _toolstrip;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item.
|
||||
/// </summary>
|
||||
public AssetItem Item => _item;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the toolstrip UI.
|
||||
/// </summary>
|
||||
public ToolStrip ToolStrip => _toolstrip;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string SerializationTypename => _item.ID.ToString();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AssetEditorWindow"/> class.
|
||||
/// </summary>
|
||||
/// <param name="editor">The editor.</param>
|
||||
/// <param name="item">The item.</param>
|
||||
protected AssetEditorWindow(Editor editor, AssetItem item)
|
||||
: base(editor, false, ScrollBars.None)
|
||||
{
|
||||
_item = item ?? throw new ArgumentNullException(nameof(item));
|
||||
_item.AddReference(this);
|
||||
|
||||
_toolstrip = new ToolStrip
|
||||
{
|
||||
Offsets = new Margin(0, 0, 0, 32),
|
||||
Parent = this
|
||||
};
|
||||
_toolstrip.AddButton(editor.Icons.Find32, () => Editor.Windows.ContentWin.Select(_item)).LinkTooltip("Show and select in Content Window");
|
||||
|
||||
InputActions.Add(options => options.Save, Save);
|
||||
|
||||
UpdateTitle();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unlinks the item. Removes reference to it and unbinds all events.
|
||||
/// </summary>
|
||||
protected virtual void UnlinkItem()
|
||||
{
|
||||
_item.RemoveReference(this);
|
||||
_item = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the toolstrip buttons and other controls. Called after some window events.
|
||||
/// </summary>
|
||||
protected virtual void UpdateToolstrip()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the window title format text ({0} to insert asset short name).
|
||||
/// </summary>
|
||||
protected virtual string WindowTitleName => "{0}";
|
||||
|
||||
/// <summary>
|
||||
/// Updates the window title text.
|
||||
/// </summary>
|
||||
protected void UpdateTitle()
|
||||
{
|
||||
string title = string.Format(WindowTitleName, _item?.ShortName ?? string.Empty);
|
||||
if (IsEdited)
|
||||
title += '*';
|
||||
Title = title;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to save asset changes if it has been edited.
|
||||
/// </summary>
|
||||
public virtual void Save()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsEditingItem(ContentItem item)
|
||||
{
|
||||
return item == _item;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool OnClosing(ClosingReason reason)
|
||||
{
|
||||
// Block closing only on user events
|
||||
if (reason == ClosingReason.User)
|
||||
{
|
||||
// Check if asset has been edited and not saved (and still has linked item)
|
||||
if (IsEdited && _item != null)
|
||||
{
|
||||
// Ask user for further action
|
||||
var result = MessageBox.Show(
|
||||
string.Format("Asset \'{0}\' has been edited. Save before closing?", _item.Path),
|
||||
"Save before closing?",
|
||||
MessageBoxButtons.YesNoCancel
|
||||
);
|
||||
if (result == DialogResult.OK || result == DialogResult.Yes)
|
||||
{
|
||||
// Save and close
|
||||
Save();
|
||||
}
|
||||
else if (result == DialogResult.Cancel || result == DialogResult.Abort)
|
||||
{
|
||||
// Cancel closing
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return base.OnClosing(reason);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnClose()
|
||||
{
|
||||
if (_item != null)
|
||||
{
|
||||
// Ensure to remove linkage to the item
|
||||
UnlinkItem();
|
||||
}
|
||||
|
||||
base.OnClose();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnDestroy()
|
||||
{
|
||||
if (_item != null)
|
||||
{
|
||||
// Ensure to remove linkage to the item
|
||||
UnlinkItem();
|
||||
}
|
||||
|
||||
base.OnDestroy();
|
||||
}
|
||||
|
||||
#region IEditable Implementation
|
||||
|
||||
private bool _isEdited;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when object gets edited.
|
||||
/// </summary>
|
||||
public event Action OnEdited;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsEdited
|
||||
{
|
||||
get => _isEdited;
|
||||
protected set
|
||||
{
|
||||
if (value)
|
||||
MarkAsEdited();
|
||||
else
|
||||
ClearEditedFlag();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void MarkAsEdited()
|
||||
{
|
||||
// Check if state will change
|
||||
if (_isEdited == false)
|
||||
{
|
||||
// Set flag
|
||||
_isEdited = true;
|
||||
|
||||
// Call events
|
||||
OnEditedState();
|
||||
OnEdited?.Invoke();
|
||||
OnEditedStateChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the edited flag.
|
||||
/// </summary>
|
||||
protected void ClearEditedFlag()
|
||||
{
|
||||
// Check if state will change
|
||||
if (_isEdited)
|
||||
{
|
||||
// Clear flag
|
||||
_isEdited = false;
|
||||
|
||||
// Call event
|
||||
OnEditedStateChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Action fired when object gets edited.
|
||||
/// </summary>
|
||||
protected virtual void OnEditedState()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Action fired when object edited state gets changed.
|
||||
/// </summary>
|
||||
protected virtual void OnEditedStateChanged()
|
||||
{
|
||||
UpdateTitle();
|
||||
UpdateToolstrip();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnShowContextMenu(ContextMenu menu)
|
||||
{
|
||||
base.OnShowContextMenu(menu);
|
||||
|
||||
menu.AddButton("Save", Save).Enabled = IsEdited;
|
||||
menu.AddButton("Copy name", () => Clipboard.Text = Item.NamePath);
|
||||
|
||||
menu.AddSeparator();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IContentItemOwner Implementation
|
||||
|
||||
/// <inheritdoc />
|
||||
public void OnItemDeleted(ContentItem item)
|
||||
{
|
||||
if (item == _item)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void OnItemRenamed(ContentItem item)
|
||||
{
|
||||
if (item == _item)
|
||||
{
|
||||
UpdateTitle();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void OnItemReimported(ContentItem item)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void OnItemDispose(ContentItem item)
|
||||
{
|
||||
if (item == _item)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generic base class for asset editors.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Asset type.</typeparam>
|
||||
/// <seealso cref="FlaxEditor.Windows.Assets.AssetEditorWindow" />
|
||||
public abstract class AssetEditorWindowBase<T> : AssetEditorWindow where T : Asset
|
||||
{
|
||||
/// <summary>
|
||||
/// Flag set to true if window is is waiting for asset to be loaded (to send <see cref="OnAssetLoaded"/> or <see cref="OnAssetLoadFailed"/> events).
|
||||
/// </summary>
|
||||
protected bool _isWaitingForLoaded;
|
||||
|
||||
/// <summary>
|
||||
/// The asset reference.
|
||||
/// </summary>
|
||||
protected T _asset;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the asset.
|
||||
/// </summary>
|
||||
public T Asset => _asset;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected AssetEditorWindowBase(Editor editor, AssetItem item)
|
||||
: base(editor, item)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reloads the asset (window will receive <see cref="OnAssetLoaded"/> or <see cref="OnAssetLoadFailed"/> events).
|
||||
/// </summary>
|
||||
public void ReloadAsset()
|
||||
{
|
||||
_asset.Reload();
|
||||
_isWaitingForLoaded = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the asset.
|
||||
/// </summary>
|
||||
/// <returns>Loaded asset or null if cannot do it.</returns>
|
||||
protected virtual T LoadAsset()
|
||||
{
|
||||
return FlaxEngine.Content.LoadAsync<T>(_item.Path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when asset gets linked and window can setup UI for it.
|
||||
/// </summary>
|
||||
protected virtual void OnAssetLinked()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when asset gets loaded and window can setup UI for it.
|
||||
/// </summary>
|
||||
protected virtual void OnAssetLoaded()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when asset fails to load and window can setup UI for it.
|
||||
/// </summary>
|
||||
protected virtual void OnAssetLoadFailed()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
if (_isWaitingForLoaded)
|
||||
{
|
||||
if (_asset == null)
|
||||
{
|
||||
_isWaitingForLoaded = false;
|
||||
}
|
||||
else if (_asset.IsLoaded)
|
||||
{
|
||||
_isWaitingForLoaded = false;
|
||||
OnAssetLoaded();
|
||||
}
|
||||
else if (_asset.LastLoadFailed)
|
||||
{
|
||||
_isWaitingForLoaded = false;
|
||||
OnAssetLoadFailed();
|
||||
}
|
||||
}
|
||||
|
||||
base.Update(deltaTime);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnShow()
|
||||
{
|
||||
// Check if has no asset (but has item linked)
|
||||
if (_asset == null && _item != null)
|
||||
{
|
||||
// Load asset
|
||||
_asset = LoadAsset();
|
||||
if (_asset == null)
|
||||
{
|
||||
Editor.LogError(string.Format("Cannot load asset \'{0}\' ({1})", _item.Path, typeof(T)));
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Fire event
|
||||
OnAssetLinked();
|
||||
_isWaitingForLoaded = true;
|
||||
}
|
||||
|
||||
// Base
|
||||
base.OnShow();
|
||||
|
||||
// Update
|
||||
UpdateTitle();
|
||||
UpdateToolstrip();
|
||||
PerformLayout();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void UnlinkItem()
|
||||
{
|
||||
_asset = null;
|
||||
|
||||
base.UnlinkItem();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnItemReimported(ContentItem item)
|
||||
{
|
||||
// Wait for loaded after reimport
|
||||
_isWaitingForLoaded = true;
|
||||
|
||||
base.OnItemReimported(item);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generic base class for asset editors that modify cloned asset and update original asset on save.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Asset type.</typeparam>
|
||||
/// <seealso cref="FlaxEditor.Windows.Assets.AssetEditorWindow" />
|
||||
public abstract class ClonedAssetEditorWindowBase<T> : AssetEditorWindowBase<T> where T : Asset
|
||||
{
|
||||
// TODO: delete cloned asset on usage end?
|
||||
|
||||
/// <summary>
|
||||
/// Gets the original asset. Note: <see cref="AssetEditorWindowBase{T}.Asset"/> is the cloned asset for local editing. Use <see cref="SaveToOriginal"/> to apply changes to the original asset.
|
||||
/// </summary>
|
||||
public T OriginalAsset => (T)FlaxEngine.Content.GetAsset(_item.ID);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected ClonedAssetEditorWindowBase(Editor editor, AssetItem item)
|
||||
: base(editor, item)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the copy of the asset to the original location. This action cannot be undone!
|
||||
/// </summary>
|
||||
/// <returns>True if failed, otherwise false.</returns>
|
||||
protected virtual bool SaveToOriginal()
|
||||
{
|
||||
// Wait until temporary asset file be fully loaded
|
||||
if (_asset.WaitForLoaded())
|
||||
{
|
||||
Editor.LogError(string.Format("Cannot save asset {0}. Wait for temporary asset loaded failed.", _item.Path));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Cache data
|
||||
var id = _item.ID;
|
||||
var sourcePath = _asset.Path;
|
||||
var destinationPath = _item.Path;
|
||||
|
||||
// Check if original asset is loaded
|
||||
var originalAsset = (T)FlaxEngine.Content.GetAsset(id);
|
||||
if (originalAsset)
|
||||
{
|
||||
// Wait for loaded to prevent any issues
|
||||
if (!originalAsset.IsLoaded && originalAsset.LastLoadFailed)
|
||||
{
|
||||
Editor.LogWarning(string.Format("Copying asset \'{0}\' to \'{1}\' (last load failed)", sourcePath, destinationPath));
|
||||
}
|
||||
else if (originalAsset.WaitForLoaded())
|
||||
{
|
||||
Editor.LogError(string.Format("Cannot save asset {0}. Wait for original asset loaded failed.", _item.Path));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Copy temporary material to the final destination (and restore ID)
|
||||
if (Editor.ContentEditing.CloneAssetFile(sourcePath, destinationPath, id))
|
||||
{
|
||||
Editor.LogError(string.Format("Cannot copy asset \'{0}\' to \'{1}\'", sourcePath, destinationPath));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Reload original asset
|
||||
if (originalAsset)
|
||||
{
|
||||
originalAsset.Reload();
|
||||
}
|
||||
|
||||
// Refresh thumbnail
|
||||
_item.RefreshThumbnail();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override T LoadAsset()
|
||||
{
|
||||
// Clone asset
|
||||
if (Editor.ContentEditing.FastTempAssetClone(_item.Path, out var clonePath))
|
||||
return null;
|
||||
|
||||
// Load cloned asset
|
||||
var asset = FlaxEngine.Content.LoadAsync<T>(clonePath);
|
||||
if (asset == null)
|
||||
return null;
|
||||
|
||||
// Validate data
|
||||
if (asset.ID == _item.ID)
|
||||
throw new InvalidOperationException("Cloned asset has the same IDs.");
|
||||
|
||||
return asset;
|
||||
}
|
||||
}
|
||||
}
|
||||
235
Source/Editor/Windows/Assets/AudioClipWindow.cs
Normal file
235
Source/Editor/Windows/Assets/AudioClipWindow.cs
Normal file
@@ -0,0 +1,235 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System.Xml;
|
||||
using FlaxEditor.Content;
|
||||
using FlaxEditor.Content.Import;
|
||||
using FlaxEditor.CustomEditors;
|
||||
using FlaxEditor.CustomEditors.Editors;
|
||||
using FlaxEditor.Viewport.Previews;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
|
||||
namespace FlaxEditor.Windows.Assets
|
||||
{
|
||||
/// <summary>
|
||||
/// Audio clip window allows to view and edit <see cref="AudioClip"/> asset.
|
||||
/// </summary>
|
||||
/// <seealso cref="AudioClip" />
|
||||
/// <seealso cref="FlaxEditor.Windows.Assets.AssetEditorWindow" />
|
||||
public sealed class AudioClipWindow : AssetEditorWindowBase<AudioClip>
|
||||
{
|
||||
/// <summary>
|
||||
/// The AudioClip properties proxy object.
|
||||
/// </summary>
|
||||
[CustomEditor(typeof(ProxyEditor))]
|
||||
private sealed class PropertiesProxy
|
||||
{
|
||||
private AudioClipWindow _window;
|
||||
|
||||
[EditorOrder(1000), EditorDisplay("Import Settings", EditorDisplayAttribute.InlineStyle)]
|
||||
public AudioImportSettings ImportSettings = new AudioImportSettings();
|
||||
|
||||
public sealed class ProxyEditor : GenericEditor
|
||||
{
|
||||
public override void Initialize(LayoutElementsContainer layout)
|
||||
{
|
||||
var window = ((PropertiesProxy)Values[0])._window;
|
||||
if (window == null)
|
||||
{
|
||||
layout.Label("Loading...", TextAlignment.Center);
|
||||
return;
|
||||
}
|
||||
|
||||
// Audio properties
|
||||
{
|
||||
var audio = window.Asset;
|
||||
AudioDataInfo info = audio.Info;
|
||||
int originalSize, importedSize;
|
||||
Editor.Internal_GetAudioClipMetadata(FlaxEngine.Object.GetUnmanagedPtr(audio), out originalSize, out importedSize);
|
||||
|
||||
var group = layout.Group("General");
|
||||
group.Label("Format: " + audio.Format);
|
||||
group.Label("Length: " + (Mathf.CeilToInt(audio.Length * 100.0f) / 100.0f) + "s");
|
||||
group.Label(string.Format("{0}kHz, channels: {1}, bit depth: {2}", info.SampleRate / 1000, info.NumChannels, info.BitDepth));
|
||||
group.Label("Original size: " + Utilities.Utils.FormatBytesCount((ulong)originalSize));
|
||||
group.Label("Imported size: " + Utilities.Utils.FormatBytesCount((ulong)importedSize));
|
||||
group.Label("Compression ratio: " + Mathf.CeilToInt((float)importedSize / originalSize * 100.0f) + "%");
|
||||
}
|
||||
|
||||
base.Initialize(layout);
|
||||
|
||||
layout.Space(10);
|
||||
var reimportButton = layout.Button("Reimport");
|
||||
reimportButton.Button.Clicked += () => ((PropertiesProxy)Values[0]).Reimport();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gathers parameters from the specified AudioClip.
|
||||
/// </summary>
|
||||
/// <param name="window">The asset window.</param>
|
||||
public void OnLoad(AudioClipWindow window)
|
||||
{
|
||||
// Link
|
||||
_window = window;
|
||||
|
||||
// Try to restore target asset AudioClip import options (useful for fast reimport)
|
||||
AudioImportSettings.TryRestore(ref ImportSettings, window.Item.Path);
|
||||
|
||||
// Prepare restore data
|
||||
PeekState();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records the current state to restore it on DiscardChanges.
|
||||
/// </summary>
|
||||
public void PeekState()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reimports asset.
|
||||
/// </summary>
|
||||
public void Reimport()
|
||||
{
|
||||
Editor.Instance.ContentImporting.Reimport((BinaryAssetItem)_window.Item, ImportSettings, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// On discard changes
|
||||
/// </summary>
|
||||
public void DiscardChanges()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears temporary data.
|
||||
/// </summary>
|
||||
public void OnClean()
|
||||
{
|
||||
// Unlink
|
||||
_window = null;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly SplitPanel _split;
|
||||
private readonly AudioClipPreview _preview;
|
||||
private readonly CustomEditorPresenter _propertiesEditor;
|
||||
|
||||
private readonly PropertiesProxy _properties;
|
||||
private bool _isWaitingForLoad;
|
||||
|
||||
/// <inheritdoc />
|
||||
public AudioClipWindow(Editor editor, AssetItem item)
|
||||
: base(editor, item)
|
||||
{
|
||||
// Split Panel
|
||||
_split = new SplitPanel(Orientation.Horizontal, ScrollBars.None, ScrollBars.Vertical)
|
||||
{
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
Offsets = new Margin(0, 0, _toolstrip.Bottom, 0),
|
||||
SplitterValue = 0.7f,
|
||||
Parent = this
|
||||
};
|
||||
|
||||
// AudioClip preview
|
||||
_preview = new AudioClipPreview
|
||||
{
|
||||
DrawMode = AudioClipPreview.DrawModes.Fill,
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
Offsets = Margin.Zero,
|
||||
Parent = _split.Panel1
|
||||
};
|
||||
|
||||
// AudioClip properties editor
|
||||
_propertiesEditor = new CustomEditorPresenter(null);
|
||||
_propertiesEditor.Panel.Parent = _split.Panel2;
|
||||
_properties = new PropertiesProxy();
|
||||
_propertiesEditor.Select(_properties);
|
||||
|
||||
// Toolstrip
|
||||
_toolstrip.AddButton(Editor.Icons.Import32, () => Editor.ContentImporting.Reimport((BinaryAssetItem)Item)).LinkTooltip("Reimport");
|
||||
_toolstrip.AddSeparator();
|
||||
_toolstrip.AddButton(editor.Icons.Docs32, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/audio/audio-clip.html")).LinkTooltip("See documentation to learn more");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void UnlinkItem()
|
||||
{
|
||||
_properties.OnClean();
|
||||
_preview.Asset = null;
|
||||
_isWaitingForLoad = false;
|
||||
|
||||
base.UnlinkItem();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnAssetLinked()
|
||||
{
|
||||
_preview.Asset = _asset;
|
||||
_isWaitingForLoad = true;
|
||||
|
||||
base.OnAssetLinked();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnItemReimported(ContentItem item)
|
||||
{
|
||||
// Invalidate data
|
||||
_isWaitingForLoad = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnClose()
|
||||
{
|
||||
// Discard unsaved changes
|
||||
_properties.DiscardChanges();
|
||||
|
||||
base.OnClose();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
base.Update(deltaTime);
|
||||
|
||||
// Check if need to load
|
||||
if (_isWaitingForLoad && _asset.IsLoaded)
|
||||
{
|
||||
// Clear flag
|
||||
_isWaitingForLoad = false;
|
||||
|
||||
// Init properties and parameters proxy
|
||||
_properties.OnLoad(this);
|
||||
_propertiesEditor.BuildLayout();
|
||||
|
||||
// Setup
|
||||
ClearEditedFlag();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool UseLayoutData => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLayoutSerialize(XmlWriter writer)
|
||||
{
|
||||
writer.WriteAttributeString("Split", _split.SplitterValue.ToString());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLayoutDeserialize(XmlElement node)
|
||||
{
|
||||
float value1;
|
||||
|
||||
if (float.TryParse(node.GetAttribute("Split"), out value1))
|
||||
_split.SplitterValue = value1;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLayoutDeserialize()
|
||||
{
|
||||
_split.SplitterValue = 0.7f;
|
||||
}
|
||||
}
|
||||
}
|
||||
320
Source/Editor/Windows/Assets/CollisionDataWindow.cs
Normal file
320
Source/Editor/Windows/Assets/CollisionDataWindow.cs
Normal file
@@ -0,0 +1,320 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System.Xml;
|
||||
using FlaxEditor.Content;
|
||||
using FlaxEditor.Content.Create;
|
||||
using FlaxEditor.CustomEditors;
|
||||
using FlaxEditor.CustomEditors.Editors;
|
||||
using FlaxEditor.CustomEditors.Elements;
|
||||
using FlaxEditor.Viewport.Cameras;
|
||||
using FlaxEditor.Viewport.Previews;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
|
||||
namespace FlaxEditor.Windows.Assets
|
||||
{
|
||||
/// <summary>
|
||||
/// Editor window to view/modify <see cref="CollisionData"/> asset.
|
||||
/// </summary>
|
||||
/// <seealso cref="CollisionData" />
|
||||
/// <seealso cref="FlaxEditor.Windows.Assets.AssetEditorWindow" />
|
||||
public sealed class CollisionDataWindow : AssetEditorWindowBase<CollisionData>
|
||||
{
|
||||
/// <summary>
|
||||
/// The asset properties proxy object.
|
||||
/// </summary>
|
||||
[CustomEditor(typeof(Editor))]
|
||||
private sealed class PropertiesProxy
|
||||
{
|
||||
private CollisionDataWindow Window;
|
||||
private CollisionData Asset;
|
||||
private bool _isCooking;
|
||||
|
||||
[EditorOrder(0), EditorDisplay("General"), Tooltip("Type of the collision data to use")]
|
||||
public CollisionDataType Type;
|
||||
|
||||
[EditorOrder(10), EditorDisplay("General"), Tooltip("Source model asset to use for collision data generation")]
|
||||
public Model Model;
|
||||
|
||||
[EditorOrder(20), Limit(0, 5), EditorDisplay("General", "Model LOD Index"), Tooltip("Source model LOD index to use for collision data generation (will be clamped to the actual model LODs collection size)")]
|
||||
public int ModelLodIndex;
|
||||
|
||||
[EditorOrder(100), EditorDisplay("Convex Mesh", "Convex Flags"), Tooltip("Convex mesh generation flags")]
|
||||
public ConvexMeshGenerationFlags ConvexFlags;
|
||||
|
||||
[EditorOrder(110), Limit(8, 255), EditorDisplay("Convex Mesh", "Vertex Limit"), Tooltip("Convex mesh vertex count limit")]
|
||||
public int ConvexVertexLimit;
|
||||
|
||||
public class Editor : GenericEditor
|
||||
{
|
||||
private ButtonElement _cookButton;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Initialize(LayoutElementsContainer layout)
|
||||
{
|
||||
base.Initialize(layout);
|
||||
|
||||
layout.Space(10);
|
||||
_cookButton = layout.Button("Cook");
|
||||
_cookButton.Button.Clicked += OnCookButtonClicked;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Refresh()
|
||||
{
|
||||
if (_cookButton != null && Values.Count == 1)
|
||||
{
|
||||
var p = (PropertiesProxy)Values[0];
|
||||
if (p._isCooking)
|
||||
{
|
||||
_cookButton.Button.Enabled = false;
|
||||
_cookButton.Button.Text = "Cooking...";
|
||||
}
|
||||
else
|
||||
{
|
||||
_cookButton.Button.Enabled = p.Type != CollisionDataType.None && p.Model != null;
|
||||
_cookButton.Button.Text = "Cook";
|
||||
}
|
||||
}
|
||||
|
||||
base.Refresh();
|
||||
}
|
||||
|
||||
private void OnCookButtonClicked()
|
||||
{
|
||||
((PropertiesProxy)Values[0]).Cook();
|
||||
}
|
||||
}
|
||||
|
||||
private class CookData : CreateFileEntry
|
||||
{
|
||||
private PropertiesProxy Proxy;
|
||||
private CollisionDataType Type;
|
||||
private Model Model;
|
||||
private int ModelLodIndex;
|
||||
private ConvexMeshGenerationFlags ConvexFlags;
|
||||
private int ConvexVertexLimit;
|
||||
|
||||
public CookData(PropertiesProxy proxy, string resultUrl, CollisionDataType type, Model model, int modelLodIndex, ConvexMeshGenerationFlags convexFlags, int convexVertexLimit)
|
||||
: base("Collision Data", resultUrl)
|
||||
{
|
||||
Proxy = proxy;
|
||||
Type = type;
|
||||
Model = model;
|
||||
ModelLodIndex = modelLodIndex;
|
||||
ConvexFlags = convexFlags;
|
||||
ConvexVertexLimit = convexVertexLimit;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Create()
|
||||
{
|
||||
bool failed = FlaxEditor.Editor.CookMeshCollision(ResultUrl, Type, Model, ModelLodIndex, ConvexFlags, ConvexVertexLimit);
|
||||
|
||||
Proxy._isCooking = false;
|
||||
Proxy.Window.UpdateWiresModel();
|
||||
|
||||
return failed;
|
||||
}
|
||||
}
|
||||
|
||||
public void Cook()
|
||||
{
|
||||
_isCooking = true;
|
||||
Window.Editor.ContentImporting.Create(new CookData(this, Asset.Path, Type, Model, ModelLodIndex, ConvexFlags, ConvexVertexLimit));
|
||||
}
|
||||
|
||||
public void OnLoad(CollisionDataWindow window)
|
||||
{
|
||||
// Link
|
||||
Window = window;
|
||||
Asset = window.Asset;
|
||||
|
||||
// Setup cooking parameters
|
||||
var options = Asset.Options;
|
||||
Type = options.Type;
|
||||
if (Type == CollisionDataType.None)
|
||||
Type = CollisionDataType.ConvexMesh;
|
||||
Model = FlaxEngine.Content.LoadAsync<Model>(options.Model);
|
||||
ModelLodIndex = options.ModelLodIndex;
|
||||
ConvexFlags = options.ConvexFlags;
|
||||
ConvexVertexLimit = options.ConvexVertexLimit;
|
||||
}
|
||||
|
||||
public void OnClean()
|
||||
{
|
||||
// Unlink
|
||||
Window = null;
|
||||
Asset = null;
|
||||
Model = null;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly SplitPanel _split;
|
||||
private readonly ModelPreview _preview;
|
||||
private readonly CustomEditorPresenter _propertiesPresenter;
|
||||
private readonly PropertiesProxy _properties;
|
||||
private Model _collisionWiresModel;
|
||||
private StaticModel _collisionWiresShowActor;
|
||||
private bool _updateWireMesh;
|
||||
|
||||
/// <inheritdoc />
|
||||
public CollisionDataWindow(Editor editor, AssetItem item)
|
||||
: base(editor, item)
|
||||
{
|
||||
// Toolstrip
|
||||
_toolstrip.AddSeparator();
|
||||
_toolstrip.AddButton(editor.Icons.Docs32, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/physics/colliders/collision-data.html")).LinkTooltip("See documentation to learn more");
|
||||
|
||||
// Split Panel
|
||||
_split = new SplitPanel(Orientation.Horizontal, ScrollBars.None, ScrollBars.Vertical)
|
||||
{
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
Offsets = new Margin(0, 0, _toolstrip.Bottom, 0),
|
||||
SplitterValue = 0.7f,
|
||||
Parent = this
|
||||
};
|
||||
|
||||
// Model preview
|
||||
_preview = new ModelPreview(true)
|
||||
{
|
||||
ViewportCamera = new FPSCamera(),
|
||||
Parent = _split.Panel1
|
||||
};
|
||||
|
||||
// Asset properties
|
||||
_propertiesPresenter = new CustomEditorPresenter(null);
|
||||
_propertiesPresenter.Panel.Parent = _split.Panel2;
|
||||
_properties = new PropertiesProxy();
|
||||
_propertiesPresenter.Select(_properties);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
// Sync helper actor size with actual preview model (preview scales model for better usage experience)
|
||||
if (_collisionWiresShowActor && _collisionWiresShowActor.IsActive)
|
||||
{
|
||||
_collisionWiresShowActor.Transform = _preview.PreviewActor.Transform;
|
||||
}
|
||||
|
||||
base.Update(deltaTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the collision data debug model.
|
||||
/// </summary>
|
||||
private void UpdateWiresModel()
|
||||
{
|
||||
// Don't update on a importer/worker thread
|
||||
if (Platform.CurrentThreadID != Globals.MainThreadID)
|
||||
{
|
||||
_updateWireMesh = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_collisionWiresModel == null)
|
||||
{
|
||||
_collisionWiresModel = FlaxEngine.Content.CreateVirtualAsset<Model>();
|
||||
_collisionWiresModel.SetupLODs(new[] { 1 });
|
||||
}
|
||||
Editor.Internal_GetCollisionWires(FlaxEngine.Object.GetUnmanagedPtr(Asset), out var triangles, out var indices);
|
||||
if (triangles != null && indices != null)
|
||||
_collisionWiresModel.LODs[0].Meshes[0].UpdateMesh(triangles, indices);
|
||||
else
|
||||
Editor.LogWarning("Failed to get collision wires for " + Asset);
|
||||
if (_collisionWiresShowActor == null)
|
||||
{
|
||||
_collisionWiresShowActor = new StaticModel();
|
||||
_preview.Task.AddCustomActor(_collisionWiresShowActor);
|
||||
}
|
||||
_collisionWiresShowActor.Model = _collisionWiresModel;
|
||||
_collisionWiresShowActor.SetMaterial(0, FlaxEngine.Content.LoadAsyncInternal<MaterialBase>(EditorAssets.WiresDebugMaterial));
|
||||
_preview.Model = FlaxEngine.Content.LoadAsync<Model>(_asset.Options.Model);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void UnlinkItem()
|
||||
{
|
||||
_properties.OnClean();
|
||||
_preview.Model = null;
|
||||
|
||||
base.UnlinkItem();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnAssetLinked()
|
||||
{
|
||||
_preview.Model = null;
|
||||
|
||||
base.OnAssetLinked();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnAssetLoaded()
|
||||
{
|
||||
_properties.OnLoad(this);
|
||||
_propertiesPresenter.BuildLayout();
|
||||
ClearEditedFlag();
|
||||
UpdateWiresModel();
|
||||
|
||||
base.OnAssetLoaded();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnItemReimported(ContentItem item)
|
||||
{
|
||||
// Refresh the properties (will get new data in OnAssetLoaded)
|
||||
_properties.OnClean();
|
||||
_propertiesPresenter.BuildLayout();
|
||||
ClearEditedFlag();
|
||||
|
||||
base.OnItemReimported(item);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnUpdate()
|
||||
{
|
||||
if (_updateWireMesh)
|
||||
{
|
||||
_updateWireMesh = false;
|
||||
UpdateWiresModel();
|
||||
}
|
||||
|
||||
base.OnUpdate();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnDestroy()
|
||||
{
|
||||
base.OnDestroy();
|
||||
|
||||
Object.Destroy(ref _collisionWiresShowActor);
|
||||
Object.Destroy(ref _collisionWiresModel);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool UseLayoutData => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLayoutSerialize(XmlWriter writer)
|
||||
{
|
||||
writer.WriteAttributeString("Split", _split.SplitterValue.ToString());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLayoutDeserialize(XmlElement node)
|
||||
{
|
||||
float value1;
|
||||
|
||||
if (float.TryParse(node.GetAttribute("Split"), out value1))
|
||||
_split.SplitterValue = value1;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLayoutDeserialize()
|
||||
{
|
||||
_split.SplitterValue = 0.7f;
|
||||
}
|
||||
}
|
||||
}
|
||||
227
Source/Editor/Windows/Assets/CubeTextureWindow.cs
Normal file
227
Source/Editor/Windows/Assets/CubeTextureWindow.cs
Normal file
@@ -0,0 +1,227 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System.Xml;
|
||||
using FlaxEditor.Content;
|
||||
using FlaxEditor.Content.Import;
|
||||
using FlaxEditor.CustomEditors;
|
||||
using FlaxEditor.CustomEditors.Editors;
|
||||
using FlaxEditor.Viewport.Previews;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
|
||||
namespace FlaxEditor.Windows.Assets
|
||||
{
|
||||
/// <summary>
|
||||
/// Editor window to view/modify <see cref="CubeTexture"/> asset.
|
||||
/// </summary>
|
||||
/// <seealso cref="CubeTexture" />
|
||||
/// <seealso cref="FlaxEditor.Windows.Assets.AssetEditorWindow" />
|
||||
public sealed class CubeTextureWindow : AssetEditorWindowBase<CubeTexture>
|
||||
{
|
||||
/// <summary>
|
||||
/// The texture properties proxy object.
|
||||
/// </summary>
|
||||
[CustomEditor(typeof(ProxyEditor))]
|
||||
private sealed class PropertiesProxy
|
||||
{
|
||||
private CubeTextureWindow _window;
|
||||
|
||||
[EditorOrder(1000), EditorDisplay("Import Settings", EditorDisplayAttribute.InlineStyle)]
|
||||
public TextureImportSettings ImportSettings = new TextureImportSettings();
|
||||
|
||||
public sealed class ProxyEditor : GenericEditor
|
||||
{
|
||||
public override void Initialize(LayoutElementsContainer layout)
|
||||
{
|
||||
var window = ((PropertiesProxy)Values[0])._window;
|
||||
if (window == null)
|
||||
{
|
||||
layout.Label("Loading...", TextAlignment.Center);
|
||||
return;
|
||||
}
|
||||
|
||||
// Texture properties
|
||||
{
|
||||
var texture = window.Asset;
|
||||
|
||||
var group = layout.Group("General");
|
||||
group.Label("Format: " + texture.Format);
|
||||
group.Label(string.Format("Size: {0}x{1}", texture.Width, texture.Height));
|
||||
group.Label("Mip levels: " + texture.MipLevels);
|
||||
group.Label("Memory usage: " + Utilities.Utils.FormatBytesCount(texture.TotalMemoryUsage));
|
||||
}
|
||||
|
||||
base.Initialize(layout);
|
||||
|
||||
layout.Space(10);
|
||||
var reimportButton = layout.Button("Reimport");
|
||||
reimportButton.Button.Clicked += () => ((PropertiesProxy)Values[0]).Reimport();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gathers parameters from the specified texture.
|
||||
/// </summary>
|
||||
/// <param name="window">The asset window.</param>
|
||||
public void OnLoad(CubeTextureWindow window)
|
||||
{
|
||||
// Link
|
||||
_window = window;
|
||||
|
||||
// Try to restore target asset texture import options (useful for fast reimport)
|
||||
TextureImportSettings.TryRestore(ref ImportSettings, window.Item.Path);
|
||||
|
||||
// Prepare restore data
|
||||
PeekState();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records the current state to restore it on DiscardChanges.
|
||||
/// </summary>
|
||||
public void PeekState()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reimports asset.
|
||||
/// </summary>
|
||||
public void Reimport()
|
||||
{
|
||||
Editor.Instance.ContentImporting.Reimport((BinaryAssetItem)_window.Item, ImportSettings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// On discard changes
|
||||
/// </summary>
|
||||
public void DiscardChanges()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears temporary data.
|
||||
/// </summary>
|
||||
public void OnClean()
|
||||
{
|
||||
// Unlink
|
||||
_window = null;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly SplitPanel _split;
|
||||
private readonly CubeTexturePreview _preview;
|
||||
private readonly CustomEditorPresenter _propertiesEditor;
|
||||
|
||||
private readonly PropertiesProxy _properties;
|
||||
private bool _isWaitingForLoad;
|
||||
|
||||
/// <inheritdoc />
|
||||
public CubeTextureWindow(Editor editor, AssetItem item)
|
||||
: base(editor, item)
|
||||
{
|
||||
// Split Panel
|
||||
_split = new SplitPanel(Orientation.Horizontal, ScrollBars.None, ScrollBars.Vertical)
|
||||
{
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
Offsets = new Margin(0, 0, _toolstrip.Bottom, 0),
|
||||
SplitterValue = 0.7f,
|
||||
Parent = this
|
||||
};
|
||||
|
||||
// Texture preview
|
||||
_preview = new CubeTexturePreview(true)
|
||||
{
|
||||
Parent = _split.Panel1
|
||||
};
|
||||
|
||||
// Texture properties editor
|
||||
_propertiesEditor = new CustomEditorPresenter(null);
|
||||
_propertiesEditor.Panel.Parent = _split.Panel2;
|
||||
_properties = new PropertiesProxy();
|
||||
_propertiesEditor.Select(_properties);
|
||||
|
||||
// Toolstrip
|
||||
_toolstrip.AddButton(Editor.Icons.Import32, () => Editor.ContentImporting.Reimport((BinaryAssetItem)Item)).LinkTooltip("Reimport");
|
||||
_toolstrip.AddSeparator();
|
||||
_toolstrip.AddButton(editor.Icons.Docs32, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/graphics/textures/cube-textures.html")).LinkTooltip("See documentation to learn more");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void UnlinkItem()
|
||||
{
|
||||
_properties.OnClean();
|
||||
_preview.CubeTexture = null;
|
||||
_isWaitingForLoad = false;
|
||||
|
||||
base.UnlinkItem();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnAssetLinked()
|
||||
{
|
||||
_preview.CubeTexture = _asset;
|
||||
_isWaitingForLoad = true;
|
||||
|
||||
base.OnAssetLinked();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnItemReimported(ContentItem item)
|
||||
{
|
||||
// Invalidate data
|
||||
_isWaitingForLoad = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnClose()
|
||||
{
|
||||
// Discard unsaved changes
|
||||
_properties.DiscardChanges();
|
||||
|
||||
base.OnClose();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
base.Update(deltaTime);
|
||||
|
||||
// Check if need to load
|
||||
if (_isWaitingForLoad && _asset.IsLoaded)
|
||||
{
|
||||
// Clear flag
|
||||
_isWaitingForLoad = false;
|
||||
|
||||
// Init properties and parameters proxy
|
||||
_properties.OnLoad(this);
|
||||
_propertiesEditor.BuildLayout();
|
||||
|
||||
// Setup
|
||||
ClearEditedFlag();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool UseLayoutData => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLayoutSerialize(XmlWriter writer)
|
||||
{
|
||||
writer.WriteAttributeString("Split", _split.SplitterValue.ToString());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLayoutDeserialize(XmlElement node)
|
||||
{
|
||||
float value1;
|
||||
|
||||
if (float.TryParse(node.GetAttribute("Split"), out value1))
|
||||
_split.SplitterValue = value1;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLayoutDeserialize()
|
||||
{
|
||||
_split.SplitterValue = 0.7f;
|
||||
}
|
||||
}
|
||||
}
|
||||
190
Source/Editor/Windows/Assets/FontWindow.cs
Normal file
190
Source/Editor/Windows/Assets/FontWindow.cs
Normal file
@@ -0,0 +1,190 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System.ComponentModel;
|
||||
using FlaxEditor.Content;
|
||||
using FlaxEditor.CustomEditors;
|
||||
using FlaxEditor.GUI;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
|
||||
namespace FlaxEditor.Windows.Assets
|
||||
{
|
||||
/// <summary>
|
||||
/// Editor window to view/modify <see cref="FontAsset"/> asset.
|
||||
/// </summary>
|
||||
/// <seealso cref="FontAsset" />
|
||||
/// <seealso cref="FlaxEditor.Windows.Assets.AssetEditorWindow" />
|
||||
public sealed class FontAssetWindow : AssetEditorWindowBase<FontAsset>
|
||||
{
|
||||
/// <summary>
|
||||
/// The font asset properties proxy object.
|
||||
/// </summary>
|
||||
private sealed class PropertiesProxy
|
||||
{
|
||||
[DefaultValue(FontHinting.Default)]
|
||||
[EditorOrder(10), EditorDisplay("Properties"), Tooltip("The font hinting used when rendering characters.")]
|
||||
public FontHinting Hinting;
|
||||
|
||||
[DefaultValue(true)]
|
||||
[EditorOrder(20), EditorDisplay("Properties"), Tooltip("Enables using anti-aliasing for font characters. Otherwise font will use monochrome data.")]
|
||||
public bool AntiAliasing;
|
||||
|
||||
[DefaultValue(false)]
|
||||
[EditorOrder(30), EditorDisplay("Properties"), Tooltip("Enables artificial embolden effect.")]
|
||||
public bool Bold;
|
||||
|
||||
[DefaultValue(false)]
|
||||
[EditorOrder(40), EditorDisplay("Properties"), Tooltip("Enables slant effect, emulating italic style.")]
|
||||
public bool Italic;
|
||||
|
||||
public void Get(out FontOptions options)
|
||||
{
|
||||
options = new FontOptions();
|
||||
options.Hinting = Hinting;
|
||||
if (AntiAliasing)
|
||||
options.Flags |= FontFlags.AntiAliasing;
|
||||
if (Bold)
|
||||
options.Flags |= FontFlags.Bold;
|
||||
if (Italic)
|
||||
options.Flags |= FontFlags.Italic;
|
||||
}
|
||||
|
||||
public void Set(ref FontOptions options)
|
||||
{
|
||||
Hinting = options.Hinting;
|
||||
AntiAliasing = (options.Flags & FontFlags.AntiAliasing) == FontFlags.AntiAliasing;
|
||||
Bold = (options.Flags & FontFlags.Bold) == FontFlags.Bold;
|
||||
Italic = (options.Flags & FontFlags.Italic) == FontFlags.Italic;
|
||||
}
|
||||
}
|
||||
|
||||
private TextBox _inputText;
|
||||
private Label _textPreview;
|
||||
private CustomEditorPresenter _propertiesEditor;
|
||||
private PropertiesProxy _proxy;
|
||||
private ToolStripButton _saveButton;
|
||||
|
||||
/// <inheritdoc />
|
||||
public FontAssetWindow(Editor editor, AssetItem item)
|
||||
: base(editor, item)
|
||||
{
|
||||
var panel1 = new SplitPanel(Orientation.Horizontal, ScrollBars.Vertical)
|
||||
{
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
Offsets = new Margin(0, 0, _toolstrip.Bottom, 0),
|
||||
SplitterValue = 0.7f,
|
||||
Parent = this
|
||||
};
|
||||
var panel2 = new SplitPanel(Orientation.Vertical, ScrollBars.Vertical)
|
||||
{
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
Offsets = Margin.Zero,
|
||||
SplitterValue = 0.2f,
|
||||
Parent = panel1.Panel1
|
||||
};
|
||||
|
||||
// Text preview
|
||||
_inputText = new TextBox(true, 0, 0)
|
||||
{
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
Parent = panel2.Panel1
|
||||
};
|
||||
_inputText.TextChanged += OnTextChanged;
|
||||
_textPreview = new Label
|
||||
{
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
Offsets = Margin.Zero,
|
||||
Margin = new Margin(4),
|
||||
Wrapping = TextWrapping.WrapWords,
|
||||
HorizontalAlignment = TextAlignment.Near,
|
||||
VerticalAlignment = TextAlignment.Near,
|
||||
Parent = panel2.Panel2
|
||||
};
|
||||
|
||||
// Font asset properties
|
||||
_propertiesEditor = new CustomEditorPresenter(null);
|
||||
_propertiesEditor.Panel.Parent = panel1.Panel2;
|
||||
_propertiesEditor.Modified += OnPropertyEdited;
|
||||
_proxy = new PropertiesProxy();
|
||||
_propertiesEditor.Select(_proxy);
|
||||
|
||||
// Toolstrip
|
||||
_saveButton = (ToolStripButton)_toolstrip.AddButton(editor.Icons.Save32, Save).LinkTooltip("Save");
|
||||
}
|
||||
|
||||
private void OnTextChanged()
|
||||
{
|
||||
_textPreview.Text = _inputText.Text;
|
||||
}
|
||||
|
||||
private void OnPropertyEdited()
|
||||
{
|
||||
MarkAsEdited();
|
||||
|
||||
_proxy.Get(out var options);
|
||||
var assetOptions = Asset.Options;
|
||||
if (assetOptions != options)
|
||||
{
|
||||
Asset.Options = options;
|
||||
Asset.Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void UnlinkItem()
|
||||
{
|
||||
_textPreview.Font = new FontReference();
|
||||
|
||||
base.UnlinkItem();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnAssetLinked()
|
||||
{
|
||||
Asset.WaitForLoaded();
|
||||
_textPreview.Font = new FontReference(Asset.CreateFont(30));
|
||||
_inputText.Text = string.Format("This is a sample text using font {0}.", Asset.FamilyName);
|
||||
var options = Asset.Options;
|
||||
_proxy.Set(ref options);
|
||||
|
||||
base.OnAssetLinked();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void UpdateToolstrip()
|
||||
{
|
||||
_saveButton.Enabled = IsEdited;
|
||||
|
||||
base.UpdateToolstrip();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Save()
|
||||
{
|
||||
// Check if don't need to push any new changes to the original asset
|
||||
if (!IsEdited)
|
||||
return;
|
||||
|
||||
// Save asset
|
||||
if (Asset.Save())
|
||||
{
|
||||
Editor.LogError("Cannot save asset.");
|
||||
return;
|
||||
}
|
||||
|
||||
ClearEditedFlag();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnDestroy()
|
||||
{
|
||||
base.OnDestroy();
|
||||
|
||||
_inputText = null;
|
||||
_textPreview = null;
|
||||
_propertiesEditor = null;
|
||||
_proxy = null;
|
||||
_saveButton = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
533
Source/Editor/Windows/Assets/GameplayGlobalsWindow.cs
Normal file
533
Source/Editor/Windows/Assets/GameplayGlobalsWindow.cs
Normal file
@@ -0,0 +1,533 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FlaxEditor.Content;
|
||||
using FlaxEditor.CustomEditors;
|
||||
using FlaxEditor.CustomEditors.GUI;
|
||||
using FlaxEditor.GUI;
|
||||
using FlaxEditor.GUI.ContextMenu;
|
||||
using FlaxEditor.Scripting;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
|
||||
namespace FlaxEditor.Windows.Assets
|
||||
{
|
||||
/// <summary>
|
||||
/// Editor window to view/modify <see cref="GameplayGlobals"/> asset.
|
||||
/// </summary>
|
||||
/// <seealso cref="GameplayGlobals" />
|
||||
/// <seealso cref="FlaxEditor.Windows.Assets.AssetEditorWindow" />
|
||||
public sealed class GameplayGlobalsWindow : AssetEditorWindowBase<GameplayGlobals>
|
||||
{
|
||||
private class AddRemoveParamAction : IUndoAction
|
||||
{
|
||||
public PropertiesProxy Proxy;
|
||||
public bool IsAdd;
|
||||
public string Name;
|
||||
public object DefaultValue;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string ActionString => IsAdd ? "Add parameter" : "Remove parameter";
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Do()
|
||||
{
|
||||
if (IsAdd)
|
||||
Add();
|
||||
else
|
||||
Remove();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Undo()
|
||||
{
|
||||
if (IsAdd)
|
||||
Remove();
|
||||
else
|
||||
Add();
|
||||
}
|
||||
|
||||
private void Add()
|
||||
{
|
||||
Proxy.DefaultValues[Name] = DefaultValue;
|
||||
|
||||
Proxy.Window._propertiesEditor.BuildLayoutOnUpdate();
|
||||
}
|
||||
|
||||
private void Remove()
|
||||
{
|
||||
DefaultValue = Proxy.DefaultValues[Name];
|
||||
|
||||
Proxy.DefaultValues.Remove(Name);
|
||||
|
||||
Proxy.Window._propertiesEditor.BuildLayoutOnUpdate();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
DefaultValue = null;
|
||||
Proxy = null;
|
||||
}
|
||||
}
|
||||
|
||||
private class RenameParamAction : IUndoAction
|
||||
{
|
||||
public PropertiesProxy Proxy;
|
||||
public string Before;
|
||||
public string After;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string ActionString => "Rename parameter";
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Do()
|
||||
{
|
||||
Rename(Before, After);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Undo()
|
||||
{
|
||||
Rename(After, Before);
|
||||
}
|
||||
|
||||
private void Rename(string from, string to)
|
||||
{
|
||||
var defaultValue = Proxy.DefaultValues[from];
|
||||
|
||||
Proxy.DefaultValues.Remove(from);
|
||||
Proxy.DefaultValues[to] = defaultValue;
|
||||
|
||||
Proxy.Window._propertiesEditor.BuildLayoutOnUpdate();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Before = null;
|
||||
After = null;
|
||||
}
|
||||
}
|
||||
|
||||
[CustomEditor(typeof(PropertiesProxyEditor))]
|
||||
private sealed class PropertiesProxy
|
||||
{
|
||||
[NoSerialize]
|
||||
public GameplayGlobalsWindow Window;
|
||||
|
||||
[NoSerialize]
|
||||
public GameplayGlobals Asset;
|
||||
|
||||
public Dictionary<string, object> DefaultValues;
|
||||
|
||||
public void Init(GameplayGlobalsWindow window)
|
||||
{
|
||||
Window = window;
|
||||
Asset = window.Asset;
|
||||
DefaultValues = Asset.DefaultValues;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class VariableValueContainer : ValueContainer
|
||||
{
|
||||
private readonly PropertiesProxy _proxy;
|
||||
private readonly string _name;
|
||||
private readonly bool _isDefault;
|
||||
|
||||
public VariableValueContainer(PropertiesProxy proxy, string name, object value, bool isDefault)
|
||||
: base(ScriptMemberInfo.Null, new ScriptType(value.GetType()))
|
||||
{
|
||||
_proxy = proxy;
|
||||
_name = name;
|
||||
_isDefault = isDefault;
|
||||
|
||||
Add(value);
|
||||
}
|
||||
|
||||
private object Getter(object instance, int index)
|
||||
{
|
||||
if (_isDefault)
|
||||
return _proxy.DefaultValues[_name];
|
||||
return _proxy.Asset.GetValue(_name);
|
||||
}
|
||||
|
||||
private void Setter(object instance, int index, object value)
|
||||
{
|
||||
if (_isDefault)
|
||||
_proxy.DefaultValues[_name] = value;
|
||||
else
|
||||
_proxy.Asset.SetValue(_name, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Refresh(ValueContainer instanceValues)
|
||||
{
|
||||
if (instanceValues == null || instanceValues.Count != Count)
|
||||
throw new ArgumentException();
|
||||
|
||||
for (int i = 0; i < Count; i++)
|
||||
{
|
||||
var v = instanceValues[i];
|
||||
this[i] = Getter(v, i);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Set(ValueContainer instanceValues, object value)
|
||||
{
|
||||
if (instanceValues == null || instanceValues.Count != Count)
|
||||
throw new ArgumentException();
|
||||
|
||||
for (int i = 0; i < Count; i++)
|
||||
{
|
||||
var v = instanceValues[i];
|
||||
Setter(v, i, value);
|
||||
this[i] = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Set(ValueContainer instanceValues, ValueContainer values)
|
||||
{
|
||||
/*if (instanceValues == null || instanceValues.Count != Count)
|
||||
throw new ArgumentException();
|
||||
if (values == null || values.Count != Count)
|
||||
throw new ArgumentException();
|
||||
|
||||
for (int i = 0; i < Count; i++)
|
||||
{
|
||||
var v = instanceValues[i];
|
||||
var value = ((CustomValueContainer)values)[i];
|
||||
Setter(v, i, value);
|
||||
this[i] = value;
|
||||
}*/
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Set(ValueContainer instanceValues)
|
||||
{
|
||||
if (instanceValues == null || instanceValues.Count != Count)
|
||||
throw new ArgumentException();
|
||||
|
||||
for (int i = 0; i < Count; i++)
|
||||
{
|
||||
var v = instanceValues[i];
|
||||
Setter(v, i, Getter(v, i));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void RefreshReferenceValue(object instanceValue)
|
||||
{
|
||||
// Not supported
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class PropertiesProxyEditor : CustomEditor
|
||||
{
|
||||
private PropertiesProxy _proxy;
|
||||
private ComboBox _addParamType;
|
||||
|
||||
private static readonly Type[] AllowedTypes =
|
||||
{
|
||||
typeof(float),
|
||||
typeof(bool),
|
||||
typeof(int),
|
||||
typeof(Vector2),
|
||||
typeof(Vector3),
|
||||
typeof(Vector4),
|
||||
typeof(Color),
|
||||
typeof(Quaternion),
|
||||
typeof(Transform),
|
||||
typeof(BoundingBox),
|
||||
typeof(BoundingSphere),
|
||||
typeof(Rectangle),
|
||||
typeof(Matrix),
|
||||
typeof(string),
|
||||
};
|
||||
|
||||
public override void Initialize(LayoutElementsContainer layout)
|
||||
{
|
||||
_proxy = (PropertiesProxy)Values[0];
|
||||
if (_proxy?.DefaultValues == null)
|
||||
{
|
||||
layout.Label("Loading...", TextAlignment.Center);
|
||||
return;
|
||||
}
|
||||
|
||||
var isPlayModeActive = _proxy.Window.Editor.StateMachine.IsPlayMode;
|
||||
if (isPlayModeActive)
|
||||
{
|
||||
layout.Label("Play mode is active. Editing runtime values.", TextAlignment.Center);
|
||||
layout.Space(10);
|
||||
|
||||
foreach (var e in _proxy.DefaultValues)
|
||||
{
|
||||
var name = e.Key;
|
||||
var value = _proxy.Asset.GetValue(name);
|
||||
var valueContainer = new VariableValueContainer(_proxy, name, value, false);
|
||||
var propertyLabel = new PropertyNameLabel(name)
|
||||
{
|
||||
Tag = name,
|
||||
};
|
||||
string tooltip = null;
|
||||
if (_proxy.DefaultValues.TryGetValue(name, out var defaultValue))
|
||||
tooltip = "Default value: " + defaultValue;
|
||||
layout.Object(propertyLabel, valueContainer, null, tooltip);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var e in _proxy.DefaultValues)
|
||||
{
|
||||
var name = e.Key;
|
||||
var value = e.Value;
|
||||
var valueContainer = new VariableValueContainer(_proxy, name, value, true);
|
||||
var propertyLabel = new ClickablePropertyNameLabel(name)
|
||||
{
|
||||
Tag = name,
|
||||
};
|
||||
propertyLabel.MouseLeftDoubleClick += (label, location) => StartParameterRenaming(name, label);
|
||||
propertyLabel.SetupContextMenu += OnPropertyLabelSetupContextMenu;
|
||||
layout.Object(propertyLabel, valueContainer, null, "Type: " + CustomEditorsUtil.GetTypeNameUI(value.GetType()));
|
||||
}
|
||||
|
||||
// TODO: improve the UI
|
||||
layout.Space(40);
|
||||
var addParamType = layout.ComboBox().ComboBox;
|
||||
addParamType.Items = AllowedTypes.Select(CustomEditorsUtil.GetTypeNameUI).ToList();
|
||||
addParamType.SelectedIndex = 0;
|
||||
_addParamType = addParamType;
|
||||
var addParamButton = layout.Button("Add").Button;
|
||||
addParamButton.Clicked += OnAddParamButtonClicked;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAddParamButtonClicked()
|
||||
{
|
||||
AddParameter(AllowedTypes[_addParamType.SelectedIndex]);
|
||||
}
|
||||
|
||||
private void OnPropertyLabelSetupContextMenu(PropertyNameLabel label, ContextMenu menu, CustomEditor linkedEditor)
|
||||
{
|
||||
var name = (string)label.Tag;
|
||||
menu.AddSeparator();
|
||||
menu.AddButton("Rename", () => StartParameterRenaming(name, label));
|
||||
menu.AddButton("Delete", () => DeleteParameter(name));
|
||||
}
|
||||
|
||||
private void AddParameter(Type type)
|
||||
{
|
||||
var asset = _proxy?.Asset;
|
||||
if (asset == null || asset.WaitForLoaded())
|
||||
return;
|
||||
var action = new AddRemoveParamAction
|
||||
{
|
||||
Proxy = _proxy,
|
||||
IsAdd = true,
|
||||
Name = StringUtils.IncrementNameNumber("New parameter", x => OnParameterRenameValidate(null, x)),
|
||||
DefaultValue = TypeUtils.GetDefaultValue(new ScriptType(type)),
|
||||
};
|
||||
_proxy.Window.Undo.AddAction(action);
|
||||
action.Do();
|
||||
}
|
||||
|
||||
private void StartParameterRenaming(string name, Control label)
|
||||
{
|
||||
var dialog = RenamePopup.Show(label, new Rectangle(0, 0, label.Width - 2, label.Height), name, false);
|
||||
dialog.Tag = name;
|
||||
dialog.Validate += OnParameterRenameValidate;
|
||||
dialog.Renamed += OnParameterRenamed;
|
||||
}
|
||||
|
||||
private bool OnParameterRenameValidate(RenamePopup popup, string value)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(value) && !_proxy.DefaultValues.ContainsKey(value);
|
||||
}
|
||||
|
||||
private void OnParameterRenamed(RenamePopup renamePopup)
|
||||
{
|
||||
var name = (string)renamePopup.Tag;
|
||||
var action = new RenameParamAction
|
||||
{
|
||||
Proxy = _proxy,
|
||||
Before = name,
|
||||
After = renamePopup.Text,
|
||||
};
|
||||
_proxy.Window.Undo.AddAction(action);
|
||||
action.Do();
|
||||
}
|
||||
|
||||
private void DeleteParameter(string name)
|
||||
{
|
||||
var action = new AddRemoveParamAction
|
||||
{
|
||||
Proxy = _proxy,
|
||||
IsAdd = false,
|
||||
Name = name,
|
||||
};
|
||||
_proxy.Window.Undo.AddAction(action);
|
||||
action.Do();
|
||||
}
|
||||
}
|
||||
|
||||
private CustomEditorPresenter _propertiesEditor;
|
||||
private PropertiesProxy _proxy;
|
||||
private ToolStripButton _saveButton;
|
||||
private ToolStripButton _undoButton;
|
||||
private ToolStripButton _redoButton;
|
||||
private ToolStripButton _resetButton;
|
||||
private Undo _undo;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the undo for asset editing actions.
|
||||
/// </summary>
|
||||
public Undo Undo => _undo;
|
||||
|
||||
/// <inheritdoc />
|
||||
public GameplayGlobalsWindow(Editor editor, AssetItem item)
|
||||
: base(editor, item)
|
||||
{
|
||||
_undo = new Undo();
|
||||
_undo.ActionDone += OnUndo;
|
||||
_undo.UndoDone += OnUndo;
|
||||
_undo.RedoDone += OnUndo;
|
||||
var panel = new Panel(ScrollBars.Vertical)
|
||||
{
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
Offsets = new Margin(0, 0, _toolstrip.Bottom, 0),
|
||||
Parent = this,
|
||||
};
|
||||
_propertiesEditor = new CustomEditorPresenter(_undo);
|
||||
_propertiesEditor.Panel.Parent = panel;
|
||||
_propertiesEditor.Modified += OnPropertiesEditorModified;
|
||||
_proxy = new PropertiesProxy();
|
||||
_propertiesEditor.Select(_proxy);
|
||||
|
||||
_saveButton = (ToolStripButton)_toolstrip.AddButton(editor.Icons.Save32, Save).LinkTooltip("Save asset");
|
||||
_toolstrip.AddSeparator();
|
||||
_undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo32, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)");
|
||||
_redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo32, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)");
|
||||
_toolstrip.AddSeparator();
|
||||
_resetButton = (ToolStripButton)_toolstrip.AddButton(editor.Icons.Rotate32, Reset).LinkTooltip("Resets the variables values to the default values");
|
||||
|
||||
InputActions.Add(options => options.Save, Save);
|
||||
InputActions.Add(options => options.Undo, _undo.PerformUndo);
|
||||
InputActions.Add(options => options.Redo, _undo.PerformRedo);
|
||||
}
|
||||
|
||||
private void OnPropertiesEditorModified()
|
||||
{
|
||||
if (_proxy.Window.Editor.StateMachine.IsPlayMode)
|
||||
return;
|
||||
|
||||
MarkAsEdited();
|
||||
}
|
||||
|
||||
private void OnUndo(IUndoAction action)
|
||||
{
|
||||
if (_proxy.Window.Editor.StateMachine.IsPlayMode)
|
||||
return;
|
||||
|
||||
UpdateToolstrip();
|
||||
MarkAsEdited();
|
||||
}
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
_asset.ResetValues();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnAssetLoaded()
|
||||
{
|
||||
_undo.Clear();
|
||||
_proxy.Init(this);
|
||||
_propertiesEditor.BuildLayoutOnUpdate();
|
||||
UpdateToolstrip();
|
||||
|
||||
base.OnAssetLoaded();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void UnlinkItem()
|
||||
{
|
||||
_undo.Dispose();
|
||||
|
||||
base.UnlinkItem();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void UpdateToolstrip()
|
||||
{
|
||||
_saveButton.Enabled = IsEdited;
|
||||
_undoButton.Enabled = _undo.CanUndo;
|
||||
_redoButton.Enabled = _undo.CanRedo;
|
||||
_resetButton.Enabled = _asset != null;
|
||||
|
||||
base.UpdateToolstrip();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnPlayBegin()
|
||||
{
|
||||
base.OnPlayBegin();
|
||||
|
||||
if (IsEdited)
|
||||
{
|
||||
if (MessageBox.Show("Gameplay Globals asset has been modified. Save it before entering the play mode?", "Save gameplay globals?", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
|
||||
{
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
ClearEditedFlag();
|
||||
_undo.Enabled = false;
|
||||
_undo.Clear();
|
||||
_propertiesEditor.BuildLayoutOnUpdate();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnPlayEnd()
|
||||
{
|
||||
base.OnPlayEnd();
|
||||
|
||||
_undo.Enabled = true;
|
||||
_undo.Clear();
|
||||
_propertiesEditor.BuildLayoutOnUpdate();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Save()
|
||||
{
|
||||
if (!IsEdited)
|
||||
return;
|
||||
|
||||
Asset.DefaultValues = _proxy.DefaultValues;
|
||||
if (Asset.Save())
|
||||
{
|
||||
Editor.LogError("Cannot save asset.");
|
||||
return;
|
||||
}
|
||||
|
||||
ClearEditedFlag();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnDestroy()
|
||||
{
|
||||
base.OnDestroy();
|
||||
|
||||
_undo = null;
|
||||
_propertiesEditor = null;
|
||||
_proxy = null;
|
||||
_saveButton = null;
|
||||
_undoButton = null;
|
||||
_redoButton = null;
|
||||
_resetButton = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
51
Source/Editor/Windows/Assets/IESProfileWindow.cs
Normal file
51
Source/Editor/Windows/Assets/IESProfileWindow.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using FlaxEditor.Content;
|
||||
using FlaxEditor.Viewport.Previews;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
|
||||
namespace FlaxEditor.Windows.Assets
|
||||
{
|
||||
/// <summary>
|
||||
/// Editor window to view/modify <see cref="IESProfile"/> asset.
|
||||
/// </summary>
|
||||
/// <seealso cref="FlaxEditor.Windows.Assets.AssetEditorWindow" />
|
||||
public sealed class IESProfileWindow : AssetEditorWindowBase<IESProfile>
|
||||
{
|
||||
private readonly IESProfilePreview _preview;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IESProfileWindow(Editor editor, AssetItem item)
|
||||
: base(editor, item)
|
||||
{
|
||||
// IES Profile preview
|
||||
_preview = new IESProfilePreview
|
||||
{
|
||||
Offsets = new Margin(0, 0, _toolstrip.Bottom, 0),
|
||||
Parent = this
|
||||
};
|
||||
|
||||
// Toolstrip
|
||||
_toolstrip.AddButton(editor.Icons.Import32, () => Editor.ContentImporting.Reimport((BinaryAssetItem)Item)).LinkTooltip("Reimport");
|
||||
_toolstrip.AddSeparator();
|
||||
_toolstrip.AddButton(editor.Icons.PageScale32, _preview.CenterView).LinkTooltip("Center view");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void UnlinkItem()
|
||||
{
|
||||
_preview.Asset = null;
|
||||
|
||||
base.UnlinkItem();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnAssetLoaded()
|
||||
{
|
||||
_preview.Asset = _asset;
|
||||
|
||||
base.OnAssetLoaded();
|
||||
}
|
||||
}
|
||||
}
|
||||
92
Source/Editor/Windows/Assets/JsonAssetWindow.cs
Normal file
92
Source/Editor/Windows/Assets/JsonAssetWindow.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using FlaxEditor.Content;
|
||||
using FlaxEditor.CustomEditors;
|
||||
using FlaxEditor.GUI;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
|
||||
namespace FlaxEditor.Windows.Assets
|
||||
{
|
||||
/// <summary>
|
||||
/// Editor window to view/modify <see cref="JsonAsset"/> asset.
|
||||
/// </summary>
|
||||
/// <seealso cref="JsonAsset" />
|
||||
/// <seealso cref="FlaxEditor.Windows.Assets.AssetEditorWindow" />
|
||||
public sealed class JsonAssetWindow : AssetEditorWindowBase<JsonAsset>
|
||||
{
|
||||
private readonly CustomEditorPresenter _presenter;
|
||||
private readonly ToolStripButton _saveButton;
|
||||
private object _object;
|
||||
|
||||
/// <inheritdoc />
|
||||
public JsonAssetWindow(Editor editor, AssetItem item)
|
||||
: base(editor, item)
|
||||
{
|
||||
// Toolstrip
|
||||
_saveButton = (ToolStripButton)_toolstrip.AddButton(editor.Icons.Save32, Save).LinkTooltip("Save");
|
||||
|
||||
// Panel
|
||||
var panel = new Panel(ScrollBars.Vertical)
|
||||
{
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
Offsets = new Margin(0, 0, _toolstrip.Bottom, 0),
|
||||
Parent = this
|
||||
};
|
||||
|
||||
// Properties
|
||||
_presenter = new CustomEditorPresenter(null, "Loading...");
|
||||
_presenter.Panel.Parent = panel;
|
||||
_presenter.Modified += MarkAsEdited;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Save()
|
||||
{
|
||||
if (!IsEdited)
|
||||
return;
|
||||
|
||||
if (_asset.WaitForLoaded())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Editor.SaveJsonAsset(_item.Path, _object))
|
||||
{
|
||||
Editor.LogError("Cannot save asset.");
|
||||
return;
|
||||
}
|
||||
|
||||
ClearEditedFlag();
|
||||
_item.RefreshThumbnail();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void UpdateToolstrip()
|
||||
{
|
||||
_saveButton.Enabled = IsEdited;
|
||||
|
||||
base.UpdateToolstrip();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnAssetLoaded()
|
||||
{
|
||||
_object = Asset.CreateInstance();
|
||||
_presenter.Select(_object);
|
||||
ClearEditedFlag();
|
||||
|
||||
base.OnAssetLoaded();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnItemReimported(ContentItem item)
|
||||
{
|
||||
// Refresh the properties (will get new data in OnAssetLoaded)
|
||||
_presenter.Deselect();
|
||||
ClearEditedFlag();
|
||||
|
||||
base.OnItemReimported(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
53
Source/Editor/Windows/Assets/MaterialFunctionWindow.cs
Normal file
53
Source/Editor/Windows/Assets/MaterialFunctionWindow.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using FlaxEditor.Content;
|
||||
using FlaxEditor.Surface;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
|
||||
namespace FlaxEditor.Windows.Assets
|
||||
{
|
||||
/// <summary>
|
||||
/// Material function window allows to view and edit <see cref="MaterialFunction"/> asset.
|
||||
/// </summary>
|
||||
/// <seealso cref="MaterialFunction" />
|
||||
/// <seealso cref="MaterialFunctionSurface" />
|
||||
public sealed class MaterialFunctionWindow : VisjectFunctionSurfaceWindow<MaterialFunction, MaterialFunctionSurface>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public MaterialFunctionWindow(Editor editor, AssetItem item)
|
||||
: base(editor, item)
|
||||
{
|
||||
// Surface
|
||||
_surface = new MaterialFunctionSurface(this, Save, _undo)
|
||||
{
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
Offsets = Margin.Zero,
|
||||
Parent = _panel,
|
||||
Enabled = false
|
||||
};
|
||||
|
||||
// Toolstrip
|
||||
_toolstrip.AddSeparator();
|
||||
_toolstrip.AddButton(editor.Icons.Docs32, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/graphics/materials/index.html")).LinkTooltip("See documentation to learn more");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string SurfaceName => "Material Function";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override byte[] SurfaceData
|
||||
{
|
||||
get => _asset.LoadSurface();
|
||||
set
|
||||
{
|
||||
if (_asset.SaveSurface(value))
|
||||
{
|
||||
_surface.MarkAsEdited();
|
||||
Editor.LogError("Failed to save surface data");
|
||||
}
|
||||
_asset.Reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
524
Source/Editor/Windows/Assets/MaterialInstanceWindow.cs
Normal file
524
Source/Editor/Windows/Assets/MaterialInstanceWindow.cs
Normal file
@@ -0,0 +1,524 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Xml;
|
||||
using FlaxEditor.Content;
|
||||
using FlaxEditor.CustomEditors;
|
||||
using FlaxEditor.CustomEditors.Editors;
|
||||
using FlaxEditor.CustomEditors.GUI;
|
||||
using FlaxEditor.GUI;
|
||||
using FlaxEditor.Surface;
|
||||
using FlaxEditor.Viewport.Previews;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
|
||||
namespace FlaxEditor.Windows.Assets
|
||||
{
|
||||
/// <summary>
|
||||
/// Material window allows to view and edit <see cref="MaterialInstance"/> asset.
|
||||
/// Note: it uses actual asset to modify so changes are visible live in the game/editor preview.
|
||||
/// </summary>
|
||||
/// <seealso cref="MaterialInstance" />
|
||||
/// <seealso cref="FlaxEditor.Windows.Assets.AssetEditorWindow" />
|
||||
public sealed class MaterialInstanceWindow : AssetEditorWindowBase<MaterialInstance>
|
||||
{
|
||||
private sealed class EditParamOverrideAction : IUndoAction
|
||||
{
|
||||
public MaterialInstanceWindow Window;
|
||||
public string Name;
|
||||
public bool Before;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string ActionString => "Edit Override";
|
||||
|
||||
private void Set(bool value)
|
||||
{
|
||||
Window.Asset.GetParameter(Name).IsOverride = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Do()
|
||||
{
|
||||
Set(!Before);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Undo()
|
||||
{
|
||||
Set(Before);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Window = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The material properties proxy object.
|
||||
/// </summary>
|
||||
[CustomEditor(typeof(ParametersEditor))]
|
||||
private sealed class PropertiesProxy
|
||||
{
|
||||
private MaterialBase _restoreBase;
|
||||
private Dictionary<string, object> _restoreParams;
|
||||
|
||||
[EditorDisplay("General"), Tooltip("The base material used to override it's properties")]
|
||||
public MaterialBase BaseMaterial
|
||||
{
|
||||
get => Window?.Asset?.BaseMaterial;
|
||||
set
|
||||
{
|
||||
var asset = Window?.Asset;
|
||||
if (asset)
|
||||
{
|
||||
if (value == asset)
|
||||
{
|
||||
MessageBox.Show("Cannot use material itself as instance base.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return;
|
||||
}
|
||||
asset.BaseMaterial = value;
|
||||
Window._editor.BuildLayoutOnUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The window reference. Used to handle some special logic.
|
||||
/// </summary>
|
||||
[NoSerialize, HideInEditor]
|
||||
public MaterialInstanceWindow Window;
|
||||
|
||||
/// <summary>
|
||||
/// The material parameter values collection. Used to record undo changes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Contains only items with raw values excluding Flax Objects.
|
||||
/// </remarks>
|
||||
[HideInEditor]
|
||||
public object[] Values
|
||||
{
|
||||
get => Window?.Asset?.Parameters.Select(x => x.Value).ToArray();
|
||||
set
|
||||
{
|
||||
var parameters = Window?.Asset?.Parameters;
|
||||
if (value != null && parameters != null)
|
||||
{
|
||||
if (value.Length != parameters.Length)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < value.Length; i++)
|
||||
{
|
||||
var p = parameters[i].Value;
|
||||
if (p is FlaxEngine.Object || p == null)
|
||||
continue;
|
||||
|
||||
parameters[i].Value = value[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The material parameter values collection. Used to record undo changes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Contains only items with references to Flax Objects identified by ID.
|
||||
/// </remarks>
|
||||
[HideInEditor]
|
||||
public FlaxEngine.Object[] ValuesRef
|
||||
{
|
||||
get => Window?.Asset?.Parameters.Select(x => x.Value as FlaxEngine.Object).ToArray();
|
||||
set
|
||||
{
|
||||
var parameters = Window?.Asset?.Parameters;
|
||||
if (value != null && parameters != null)
|
||||
{
|
||||
if (value.Length != parameters.Length)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < value.Length; i++)
|
||||
{
|
||||
var p = parameters[i].Value;
|
||||
if (!(p is FlaxEngine.Object || p == null))
|
||||
continue;
|
||||
|
||||
parameters[i].Value = value[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gathers parameters from the specified material.
|
||||
/// </summary>
|
||||
/// <param name="materialWin">The material window.</param>
|
||||
public void OnLoad(MaterialInstanceWindow materialWin)
|
||||
{
|
||||
// Link
|
||||
Window = materialWin;
|
||||
|
||||
// Prepare restore data
|
||||
PeekState();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records the current state to restore it on DiscardChanges.
|
||||
/// </summary>
|
||||
public void PeekState()
|
||||
{
|
||||
if (Window == null)
|
||||
return;
|
||||
|
||||
var material = Window.Asset;
|
||||
_restoreBase = material.BaseMaterial;
|
||||
var parameters = material.Parameters;
|
||||
_restoreParams = new Dictionary<string, object>();
|
||||
for (int i = 0; i < parameters.Length; i++)
|
||||
_restoreParams[parameters[i].Name] = parameters[i].Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// On discard changes
|
||||
/// </summary>
|
||||
public void DiscardChanges()
|
||||
{
|
||||
if (Window == null)
|
||||
return;
|
||||
|
||||
var material = Window.Asset;
|
||||
material.BaseMaterial = _restoreBase;
|
||||
var parameters = material.Parameters;
|
||||
for (int i = 0; i < parameters.Length; i++)
|
||||
{
|
||||
var p = parameters[i];
|
||||
if (p.IsPublic && _restoreParams.TryGetValue(p.Name, out var value))
|
||||
{
|
||||
p.Value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears temporary data.
|
||||
/// </summary>
|
||||
public void OnClean()
|
||||
{
|
||||
// Unlink
|
||||
Window = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Custom editor for editing material parameters collection.
|
||||
/// </summary>
|
||||
/// <seealso cref="FlaxEditor.CustomEditors.CustomEditor" />
|
||||
public class ParametersEditor : GenericEditor
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void Initialize(LayoutElementsContainer layout)
|
||||
{
|
||||
// Prepare
|
||||
var proxy = (PropertiesProxy)Values[0];
|
||||
var materialInstance = proxy.Window?.Asset;
|
||||
if (materialInstance == null)
|
||||
{
|
||||
layout.Label("No parameters");
|
||||
return;
|
||||
}
|
||||
if (!materialInstance.IsLoaded || materialInstance.BaseMaterial && !materialInstance.BaseMaterial.IsLoaded)
|
||||
{
|
||||
layout.Label("Loading...");
|
||||
return;
|
||||
}
|
||||
var parameters = materialInstance.Parameters;
|
||||
|
||||
base.Initialize(layout);
|
||||
|
||||
if (parameters.Length == 0)
|
||||
return;
|
||||
var parametersGroup = layout.Group("Parameters");
|
||||
var baseMaterial = materialInstance.BaseMaterial;
|
||||
var material = baseMaterial;
|
||||
if (material)
|
||||
{
|
||||
while (material is MaterialInstance instance)
|
||||
material = instance.BaseMaterial;
|
||||
}
|
||||
|
||||
var data = SurfaceUtils.InitGraphParameters(parameters, (Material)material);
|
||||
SurfaceUtils.DisplayGraphParameters(parametersGroup, data,
|
||||
(instance, parameter, tag) =>
|
||||
{
|
||||
// Get material parameter
|
||||
var p = (MaterialParameter)tag;
|
||||
var proxyEx = (PropertiesProxy)instance;
|
||||
var array = proxyEx.Window.Asset.Parameters;
|
||||
if (array == null || !array.Contains(p))
|
||||
throw new TargetException("Material parameters collection has been changed.");
|
||||
return p.Value;
|
||||
},
|
||||
(instance, value, parameter, tag) =>
|
||||
{
|
||||
// Set material parameter and surface parameter
|
||||
var p = (MaterialParameter)tag;
|
||||
var proxyEx = (PropertiesProxy)instance;
|
||||
p.Value = value;
|
||||
proxyEx.Window._paramValueChange = true;
|
||||
},
|
||||
Values,
|
||||
null,
|
||||
(LayoutElementsContainer itemLayout, ValueContainer valueContainer, ref SurfaceUtils.GraphParameterData e) =>
|
||||
{
|
||||
var p = (MaterialParameter)e.Tag;
|
||||
|
||||
// Try to get default value (from the base material)
|
||||
var pBase = baseMaterial?.GetParameter(p.Name);
|
||||
if (pBase != null && pBase.ParameterType == p.ParameterType)
|
||||
{
|
||||
valueContainer.SetDefaultValue(pBase.Value);
|
||||
}
|
||||
|
||||
// Add label with checkbox for parameter value override
|
||||
var label = new CheckablePropertyNameLabel(e.DisplayName);
|
||||
label.CheckBox.Checked = p.IsOverride;
|
||||
label.CheckBox.Tag = new KeyValuePair<PropertiesProxy, MaterialParameter>(proxy.Window._properties, p);
|
||||
label.CheckChanged += nameLabel =>
|
||||
{
|
||||
var pair = (KeyValuePair<PropertiesProxy, MaterialParameter>)nameLabel.CheckBox.Tag;
|
||||
var proxyEx = pair.Key;
|
||||
var pEx = pair.Value;
|
||||
pEx.IsOverride = nameLabel.CheckBox.Checked;
|
||||
proxyEx.Window._undo.AddAction(new EditParamOverrideAction
|
||||
{
|
||||
Window = proxyEx.Window,
|
||||
Name = pEx.Name,
|
||||
Before = !nameLabel.CheckBox.Checked,
|
||||
});
|
||||
};
|
||||
itemLayout.Property(label, valueContainer, null, e.Tooltip?.Text);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private readonly SplitPanel _split;
|
||||
private readonly MaterialPreview _preview;
|
||||
private readonly ToolStripButton _saveButton;
|
||||
private readonly ToolStripButton _undoButton;
|
||||
private readonly ToolStripButton _redoButton;
|
||||
|
||||
private readonly CustomEditorPresenter _editor;
|
||||
private readonly Undo _undo;
|
||||
private readonly PropertiesProxy _properties;
|
||||
private bool _isWaitingForLoad;
|
||||
internal bool _paramValueChange;
|
||||
|
||||
/// <inheritdoc />
|
||||
public MaterialInstanceWindow(Editor editor, AssetItem item)
|
||||
: base(editor, item)
|
||||
{
|
||||
// Undo
|
||||
_undo = new Undo();
|
||||
_undo.UndoDone += OnUndoRedo;
|
||||
_undo.RedoDone += OnUndoRedo;
|
||||
_undo.ActionDone += OnAction;
|
||||
|
||||
// Toolstrip
|
||||
_saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save32, Save).LinkTooltip("Save");
|
||||
_toolstrip.AddSeparator();
|
||||
_undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo32, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)");
|
||||
_redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo32, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)");
|
||||
_toolstrip.AddSeparator();
|
||||
_toolstrip.AddButton(Editor.Icons.Rotate32, OnRevertAllParameters).LinkTooltip("Revert all the parameters to the default values");
|
||||
_toolstrip.AddSeparator();
|
||||
_toolstrip.AddButton(editor.Icons.Docs32, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/graphics/materials/instanced-materials/index.html")).LinkTooltip("See documentation to learn more");
|
||||
|
||||
// Split Panel
|
||||
_split = new SplitPanel(Orientation.Horizontal, ScrollBars.None, ScrollBars.Vertical)
|
||||
{
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
Offsets = new Margin(0, 0, _toolstrip.Bottom, 0),
|
||||
SplitterValue = 0.5f,
|
||||
Parent = this
|
||||
};
|
||||
|
||||
// Material preview
|
||||
_preview = new MaterialPreview(true)
|
||||
{
|
||||
Parent = _split.Panel1
|
||||
};
|
||||
|
||||
// Material properties editor
|
||||
_editor = new CustomEditorPresenter(_undo);
|
||||
_editor.Panel.Parent = _split.Panel2;
|
||||
_properties = new PropertiesProxy();
|
||||
_editor.Select(_properties);
|
||||
_editor.Modified += OnMaterialPropertyEdited;
|
||||
|
||||
// Setup input actions
|
||||
InputActions.Add(options => options.Undo, _undo.PerformUndo);
|
||||
InputActions.Add(options => options.Redo, _undo.PerformRedo);
|
||||
}
|
||||
|
||||
private void OnRevertAllParameters()
|
||||
{
|
||||
var baseMaterial = Asset.BaseMaterial;
|
||||
if (!baseMaterial)
|
||||
return;
|
||||
var parameters = Asset.Parameters;
|
||||
var actions = new List<IUndoAction>();
|
||||
for (var i = 0; i < parameters.Length; i++)
|
||||
{
|
||||
var p = parameters[i];
|
||||
if (p.IsOverride)
|
||||
{
|
||||
p.IsOverride = false;
|
||||
actions.Add(new EditParamOverrideAction
|
||||
{
|
||||
Window = this,
|
||||
Name = p.Name,
|
||||
Before = true,
|
||||
});
|
||||
}
|
||||
}
|
||||
using (new UndoMultiBlock(_undo, _editor.Selection, "Revert all the parameters to the default values", new MultiUndoAction(actions)))
|
||||
{
|
||||
for (var i = 0; i < parameters.Length; i++)
|
||||
{
|
||||
var p = parameters[i];
|
||||
var pBase = baseMaterial.GetParameter(p.Name);
|
||||
if (pBase != null && pBase.ParameterType == p.ParameterType)
|
||||
p.Value = pBase.Value;
|
||||
}
|
||||
}
|
||||
_editor.BuildLayoutOnUpdate();
|
||||
}
|
||||
|
||||
private void OnAction(IUndoAction action)
|
||||
{
|
||||
_paramValueChange = false;
|
||||
MarkAsEdited();
|
||||
UpdateToolstrip();
|
||||
}
|
||||
|
||||
private void OnUndoRedo(IUndoAction action)
|
||||
{
|
||||
_paramValueChange = false;
|
||||
MarkAsEdited();
|
||||
UpdateToolstrip();
|
||||
_editor.BuildLayoutOnUpdate();
|
||||
}
|
||||
|
||||
private void OnMaterialPropertyEdited()
|
||||
{
|
||||
_paramValueChange = false;
|
||||
//MarkAsEdited();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Save()
|
||||
{
|
||||
if (!IsEdited)
|
||||
return;
|
||||
|
||||
if (Asset.Save())
|
||||
{
|
||||
Editor.LogError("Cannot save asset.");
|
||||
return;
|
||||
}
|
||||
|
||||
_properties.PeekState();
|
||||
ClearEditedFlag();
|
||||
_item.RefreshThumbnail();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void UpdateToolstrip()
|
||||
{
|
||||
_saveButton.Enabled = IsEdited;
|
||||
_undoButton.Enabled = _undo.CanUndo;
|
||||
_redoButton.Enabled = _undo.CanRedo;
|
||||
|
||||
base.UpdateToolstrip();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void UnlinkItem()
|
||||
{
|
||||
_properties.OnClean();
|
||||
_preview.Material = null;
|
||||
_isWaitingForLoad = false;
|
||||
|
||||
base.UnlinkItem();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnAssetLinked()
|
||||
{
|
||||
_preview.Material = _asset;
|
||||
_isWaitingForLoad = true;
|
||||
|
||||
base.OnAssetLinked();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnClose()
|
||||
{
|
||||
// Discard unsaved changes
|
||||
_properties.DiscardChanges();
|
||||
|
||||
// Cleanup
|
||||
_undo.Clear();
|
||||
|
||||
base.OnClose();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
// Check if need to load
|
||||
if (_isWaitingForLoad && _asset.IsLoaded && (_asset.BaseMaterial == null || _asset.BaseMaterial.IsLoaded))
|
||||
{
|
||||
// Clear flag
|
||||
_isWaitingForLoad = false;
|
||||
|
||||
// Init material properties and parameters proxy
|
||||
_properties.OnLoad(this);
|
||||
|
||||
// Setup
|
||||
ClearEditedFlag();
|
||||
_undo.Clear();
|
||||
_editor.BuildLayout();
|
||||
}
|
||||
|
||||
base.Update(deltaTime);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool UseLayoutData => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLayoutSerialize(XmlWriter writer)
|
||||
{
|
||||
writer.WriteAttributeString("Split", _split.SplitterValue.ToString());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLayoutDeserialize(XmlElement node)
|
||||
{
|
||||
float value1;
|
||||
|
||||
if (float.TryParse(node.GetAttribute("Split"), out value1))
|
||||
_split.SplitterValue = value1;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLayoutDeserialize()
|
||||
{
|
||||
_split.SplitterValue = 0.5f;
|
||||
}
|
||||
}
|
||||
}
|
||||
383
Source/Editor/Windows/Assets/MaterialWindow.cs
Normal file
383
Source/Editor/Windows/Assets/MaterialWindow.cs
Normal file
@@ -0,0 +1,383 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using FlaxEditor.Content;
|
||||
using FlaxEditor.Scripting;
|
||||
using FlaxEditor.Surface;
|
||||
using FlaxEditor.Viewport.Previews;
|
||||
using FlaxEngine;
|
||||
|
||||
// ReSharper disable UnusedMember.Local
|
||||
// ReSharper disable UnusedMember.Global
|
||||
// ReSharper disable MemberCanBePrivate.Local
|
||||
|
||||
namespace FlaxEditor.Windows.Assets
|
||||
{
|
||||
/// <summary>
|
||||
/// Material window allows to view and edit <see cref="Material"/> asset.
|
||||
/// </summary>
|
||||
/// <seealso cref="Material" />
|
||||
/// <seealso cref="MaterialSurface" />
|
||||
/// <seealso cref="MaterialPreview" />
|
||||
public sealed class MaterialWindow : VisjectSurfaceWindow<Material, MaterialSurface, MaterialPreview>
|
||||
{
|
||||
private readonly ScriptType[] _newParameterTypes =
|
||||
{
|
||||
new ScriptType(typeof(float)),
|
||||
new ScriptType(typeof(Texture)),
|
||||
new ScriptType(typeof(NormalMap)),
|
||||
new ScriptType(typeof(CubeTexture)),
|
||||
new ScriptType(typeof(GPUTexture)),
|
||||
new ScriptType(typeof(ChannelMask)),
|
||||
new ScriptType(typeof(bool)),
|
||||
new ScriptType(typeof(int)),
|
||||
new ScriptType(typeof(Vector2)),
|
||||
new ScriptType(typeof(Vector3)),
|
||||
new ScriptType(typeof(Vector4)),
|
||||
new ScriptType(typeof(Color)),
|
||||
new ScriptType(typeof(Quaternion)),
|
||||
new ScriptType(typeof(Transform)),
|
||||
new ScriptType(typeof(Matrix)),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The material properties proxy object.
|
||||
/// </summary>
|
||||
private sealed class PropertiesProxy
|
||||
{
|
||||
// General
|
||||
|
||||
[EditorOrder(10), EditorDisplay("General"), Tooltip("Material domain type.")]
|
||||
public MaterialDomain Domain;
|
||||
|
||||
[EditorOrder(20), EditorDisplay("General"), Tooltip("Defines how material inputs and properties are combined to result the final surface color.")]
|
||||
public MaterialShadingModel ShadingModel;
|
||||
|
||||
[EditorOrder(30), EditorDisplay("General"), Tooltip("Determinates how materials' color should be blended with the background colors.")]
|
||||
public MaterialBlendMode BlendMode;
|
||||
|
||||
// Rendering
|
||||
|
||||
[EditorOrder(100), DefaultValue(CullMode.Normal), EditorDisplay("Rendering"), Tooltip("Defines the primitives culling mode used during geometry rendering.")]
|
||||
public CullMode CullMode;
|
||||
|
||||
[EditorOrder(110), DefaultValue(false), EditorDisplay("Rendering"), Tooltip("If checked, geometry will be rendered in wireframe mode without solid triangles fill.")]
|
||||
public bool Wireframe;
|
||||
|
||||
[EditorOrder(120), DefaultValue(true), EditorDisplay("Rendering"), Tooltip("Enables performing depth test during material rendering.")]
|
||||
public bool DepthTest;
|
||||
|
||||
[EditorOrder(130), DefaultValue(true), EditorDisplay("Rendering"), Tooltip("Enable writing to the depth buffer during material rendering.")]
|
||||
public bool DepthWrite;
|
||||
|
||||
// Transparency
|
||||
|
||||
[EditorOrder(200), DefaultValue(true), EditorDisplay("Transparency"), Tooltip("Enables reflections when rendering material.")]
|
||||
public bool EnableReflections;
|
||||
|
||||
[EditorOrder(210), DefaultValue(true), EditorDisplay("Transparency"), Tooltip("Enables fog effects when rendering material.")]
|
||||
public bool EnableFog;
|
||||
|
||||
[EditorOrder(220), DefaultValue(true), EditorDisplay("Transparency"), Tooltip("Enables distortion effect when rendering.")]
|
||||
public bool EnableDistortion;
|
||||
|
||||
[EditorOrder(225), DefaultValue(false), EditorDisplay("Transparency"), Tooltip("Enables refraction offset based on the difference between the per-pixel normal and the per-vertex normal. Useful for large water-like surfaces.")]
|
||||
public bool PixelNormalOffsetRefraction;
|
||||
|
||||
[EditorOrder(230), DefaultValue(0.12f), EditorDisplay("Transparency"), Tooltip("Controls opacity values clipping point."), Limit(0.0f, 1.0f, 0.01f)]
|
||||
public float OpacityThreshold;
|
||||
|
||||
// Tessellation
|
||||
|
||||
[EditorOrder(300), DefaultValue(TessellationMethod.None), EditorDisplay("Tessellation"), Tooltip("Mesh tessellation method.")]
|
||||
public TessellationMethod TessellationMode;
|
||||
|
||||
[EditorOrder(310), DefaultValue(15), EditorDisplay("Tessellation"), Tooltip("Maximum triangle tessellation factor."), Limit(1, 60, 0.01f)]
|
||||
public int MaxTessellationFactor;
|
||||
|
||||
// Misc
|
||||
|
||||
[EditorOrder(400), DefaultValue(false), EditorDisplay("Misc"), Tooltip("If checked, material input normal will be assumed as world-space rather than tangent-space.")]
|
||||
public bool InputWorldSpaceNormal;
|
||||
|
||||
[EditorOrder(410), DefaultValue(false), EditorDisplay("Misc", "Dithered LOD Transition"), Tooltip("If checked, material uses dithered model LOD transition for smoother LODs switching.")]
|
||||
public bool DitheredLODTransition;
|
||||
|
||||
[EditorOrder(420), DefaultValue(0.3f), EditorDisplay("Misc"), Tooltip("Controls mask values clipping point."), Limit(0.0f, 1.0f, 0.01f)]
|
||||
public float MaskThreshold;
|
||||
|
||||
[EditorOrder(430), DefaultValue(MaterialDecalBlendingMode.Translucent), EditorDisplay("Misc"), Tooltip("The decal material blending mode.")]
|
||||
public MaterialDecalBlendingMode DecalBlendingMode;
|
||||
|
||||
[EditorOrder(440), DefaultValue(MaterialPostFxLocation.AfterPostProcessingPass), EditorDisplay("Misc"), Tooltip("The post fx material rendering location.")]
|
||||
public MaterialPostFxLocation PostFxLocation;
|
||||
|
||||
// Parameters
|
||||
|
||||
[EditorOrder(1000), EditorDisplay("Parameters"), CustomEditor(typeof(ParametersEditor)), NoSerialize]
|
||||
// ReSharper disable once UnusedAutoPropertyAccessor.Local
|
||||
public MaterialWindow Window { get; set; }
|
||||
|
||||
[HideInEditor, Serialize]
|
||||
// ReSharper disable once UnusedMember.Local
|
||||
public List<SurfaceParameter> Parameters
|
||||
{
|
||||
get => Window.Surface.Parameters;
|
||||
set => throw new Exception("No setter.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gathers parameters from the specified material.
|
||||
/// </summary>
|
||||
/// <param name="window">The window.</param>
|
||||
public void OnLoad(MaterialWindow window)
|
||||
{
|
||||
// Update cache
|
||||
var material = window.Asset;
|
||||
var info = material.Info;
|
||||
Wireframe = (info.FeaturesFlags & MaterialFeaturesFlags.Wireframe) != 0;
|
||||
CullMode = info.CullMode;
|
||||
DepthTest = (info.FeaturesFlags & MaterialFeaturesFlags.DisableDepthTest) == 0;
|
||||
DepthWrite = (info.FeaturesFlags & MaterialFeaturesFlags.DisableDepthWrite) == 0;
|
||||
EnableReflections = (info.FeaturesFlags & MaterialFeaturesFlags.DisableReflections) == 0;
|
||||
EnableFog = (info.FeaturesFlags & MaterialFeaturesFlags.DisableFog) == 0;
|
||||
EnableDistortion = (info.FeaturesFlags & MaterialFeaturesFlags.DisableDistortion) == 0;
|
||||
PixelNormalOffsetRefraction = (info.FeaturesFlags & MaterialFeaturesFlags.PixelNormalOffsetRefraction) != 0;
|
||||
InputWorldSpaceNormal = (info.FeaturesFlags & MaterialFeaturesFlags.InputWorldSpaceNormal) != 0;
|
||||
DitheredLODTransition = (info.FeaturesFlags & MaterialFeaturesFlags.DitheredLODTransition) != 0;
|
||||
OpacityThreshold = info.OpacityThreshold;
|
||||
TessellationMode = info.TessellationMode;
|
||||
MaxTessellationFactor = info.MaxTessellationFactor;
|
||||
MaskThreshold = info.MaskThreshold;
|
||||
DecalBlendingMode = info.DecalBlendingMode;
|
||||
PostFxLocation = info.PostFxLocation;
|
||||
BlendMode = info.BlendMode;
|
||||
ShadingModel = info.ShadingModel;
|
||||
Domain = info.Domain;
|
||||
|
||||
// Link
|
||||
Window = window;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the material properties to the material info structure.
|
||||
/// </summary>
|
||||
/// <param name="info">The material info.</param>
|
||||
public void OnSave(ref MaterialInfo info)
|
||||
{
|
||||
// Update flags
|
||||
info.CullMode = CullMode;
|
||||
if (Wireframe)
|
||||
info.FeaturesFlags |= MaterialFeaturesFlags.Wireframe;
|
||||
if (!DepthTest)
|
||||
info.FeaturesFlags |= MaterialFeaturesFlags.DisableDepthTest;
|
||||
if (!DepthWrite)
|
||||
info.FeaturesFlags |= MaterialFeaturesFlags.DisableDepthWrite;
|
||||
if (!EnableReflections)
|
||||
info.FeaturesFlags |= MaterialFeaturesFlags.DisableReflections;
|
||||
if (!EnableFog)
|
||||
info.FeaturesFlags |= MaterialFeaturesFlags.DisableFog;
|
||||
if (!EnableDistortion)
|
||||
info.FeaturesFlags |= MaterialFeaturesFlags.DisableDistortion;
|
||||
if (PixelNormalOffsetRefraction)
|
||||
info.FeaturesFlags |= MaterialFeaturesFlags.PixelNormalOffsetRefraction;
|
||||
if (InputWorldSpaceNormal)
|
||||
info.FeaturesFlags |= MaterialFeaturesFlags.InputWorldSpaceNormal;
|
||||
if (DitheredLODTransition)
|
||||
info.FeaturesFlags |= MaterialFeaturesFlags.DitheredLODTransition;
|
||||
info.OpacityThreshold = OpacityThreshold;
|
||||
info.TessellationMode = TessellationMode;
|
||||
info.MaxTessellationFactor = MaxTessellationFactor;
|
||||
info.MaskThreshold = MaskThreshold;
|
||||
info.DecalBlendingMode = DecalBlendingMode;
|
||||
info.PostFxLocation = PostFxLocation;
|
||||
info.BlendMode = BlendMode;
|
||||
info.ShadingModel = ShadingModel;
|
||||
info.Domain = Domain;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears temporary data.
|
||||
/// </summary>
|
||||
public void OnClean()
|
||||
{
|
||||
// Unlink
|
||||
Window = null;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly PropertiesProxy _properties;
|
||||
|
||||
/// <inheritdoc />
|
||||
public MaterialWindow(Editor editor, AssetItem item)
|
||||
: base(editor, item)
|
||||
{
|
||||
// Asset preview
|
||||
_preview = new MaterialPreview(true)
|
||||
{
|
||||
Parent = _split2.Panel1
|
||||
};
|
||||
|
||||
// Asset properties proxy
|
||||
_properties = new PropertiesProxy();
|
||||
_propertiesEditor.Select(_properties);
|
||||
|
||||
// Surface
|
||||
_surface = new MaterialSurface(this, Save, _undo)
|
||||
{
|
||||
Parent = _split1.Panel1,
|
||||
Enabled = false
|
||||
};
|
||||
|
||||
// Toolstrip
|
||||
_toolstrip.AddSeparator();
|
||||
_toolstrip.AddButton(editor.Icons.BracketsSlash32, () => ShowSourceCode(_asset)).LinkTooltip("Show generated shader source code");
|
||||
_toolstrip.AddButton(editor.Icons.Docs32, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/graphics/materials/index.html")).LinkTooltip("See documentation to learn more");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the material source code window.
|
||||
/// </summary>
|
||||
/// <param name="material">The material asset.</param>
|
||||
public static void ShowSourceCode(Material material)
|
||||
{
|
||||
var source = Editor.GetShaderSourceCode(material);
|
||||
Utilities.Utils.ShowSourceCodeWindow(source, "Material Source");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets material info from UI.
|
||||
/// </summary>
|
||||
/// <param name="info">Output info.</param>
|
||||
public void FillMaterialInfo(out MaterialInfo info)
|
||||
{
|
||||
info = MaterialInfo.CreateDefault();
|
||||
_properties.OnSave(ref info);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the main material node.
|
||||
/// </summary>
|
||||
public Surface.Archetypes.Material.SurfaceNodeMaterial MainNode
|
||||
{
|
||||
get
|
||||
{
|
||||
var mainNode = _surface.FindNode(1, 1) as Surface.Archetypes.Material.SurfaceNodeMaterial;
|
||||
if (mainNode == null)
|
||||
{
|
||||
// Error
|
||||
Editor.LogError("Failed to find main material node.");
|
||||
}
|
||||
return mainNode;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IEnumerable<ScriptType> NewParameterTypes => _newParameterTypes;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void SetParameter(int index, object value)
|
||||
{
|
||||
try
|
||||
{
|
||||
Asset.Parameters[index].Value = value;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
|
||||
base.SetParameter(index, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnPropertyEdited()
|
||||
{
|
||||
base.OnPropertyEdited();
|
||||
|
||||
// Refresh main node
|
||||
var mainNode = MainNode;
|
||||
mainNode?.UpdateBoxes();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void UnlinkItem()
|
||||
{
|
||||
_properties.OnClean();
|
||||
_preview.Material = null;
|
||||
|
||||
base.UnlinkItem();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnAssetLinked()
|
||||
{
|
||||
_preview.Material = _asset;
|
||||
|
||||
base.OnAssetLinked();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string SurfaceName => "Material";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override byte[] SurfaceData
|
||||
{
|
||||
get => _asset.LoadSurface(true);
|
||||
set
|
||||
{
|
||||
// Create material info
|
||||
FillMaterialInfo(out var info);
|
||||
|
||||
// Save data to the temporary material
|
||||
if (_asset.SaveSurface(value, info))
|
||||
{
|
||||
// Error
|
||||
_surface.MarkAsEdited();
|
||||
Editor.LogError("Failed to save material surface data");
|
||||
}
|
||||
_asset.Reload();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool LoadSurface()
|
||||
{
|
||||
// Init material properties and parameters proxy
|
||||
_properties.OnLoad(this);
|
||||
|
||||
// Load surface graph
|
||||
if (_surface.Load())
|
||||
{
|
||||
// Error
|
||||
Editor.LogError("Failed to load material surface.");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool SaveSurface()
|
||||
{
|
||||
_surface.Save();
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool CanEditSurfaceOnAssetLoadError => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool SaveToOriginal()
|
||||
{
|
||||
// Copy shader cache from the temporary Particle Emitter (will skip compilation on Reload - faster)
|
||||
Guid dstId = _item.ID;
|
||||
Guid srcId = _asset.ID;
|
||||
Editor.Internal_CopyCache(ref dstId, ref srcId);
|
||||
|
||||
return base.SaveToOriginal();
|
||||
}
|
||||
}
|
||||
}
|
||||
208
Source/Editor/Windows/Assets/ModelBaseWindow.cs
Normal file
208
Source/Editor/Windows/Assets/ModelBaseWindow.cs
Normal file
@@ -0,0 +1,208 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System.Xml;
|
||||
using FlaxEditor.Content;
|
||||
using FlaxEditor.CustomEditors;
|
||||
using FlaxEditor.CustomEditors.Editors;
|
||||
using FlaxEditor.GUI;
|
||||
using FlaxEditor.GUI.Tabs;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
|
||||
#pragma warning disable 1591
|
||||
|
||||
namespace FlaxEditor.Windows.Assets
|
||||
{
|
||||
/// <summary>
|
||||
/// Editor window to view/modify <see cref="ModelBase"/> asset.
|
||||
/// </summary>
|
||||
/// <seealso cref="Model" />
|
||||
/// <seealso cref="FlaxEditor.Windows.Assets.AssetEditorWindow" />
|
||||
public abstract class ModelBaseWindow<TAsset, TWindow> : AssetEditorWindowBase<TAsset>
|
||||
where TAsset : ModelBase
|
||||
where TWindow : ModelBaseWindow<TAsset, TWindow>
|
||||
{
|
||||
protected abstract class PropertiesProxyBase
|
||||
{
|
||||
[HideInEditor]
|
||||
public TWindow Window;
|
||||
|
||||
[HideInEditor]
|
||||
public TAsset Asset;
|
||||
|
||||
public virtual void OnLoad(TWindow window)
|
||||
{
|
||||
Window = window;
|
||||
Asset = window.Asset;
|
||||
}
|
||||
|
||||
public virtual void OnClean()
|
||||
{
|
||||
Window = null;
|
||||
Asset = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract class ProxyEditorBase : GenericEditor
|
||||
{
|
||||
internal override void RefreshInternal()
|
||||
{
|
||||
// Skip updates when model is not loaded
|
||||
var proxy = (PropertiesProxyBase)Values[0];
|
||||
if (proxy.Asset == null || !proxy.Asset.IsLoaded)
|
||||
return;
|
||||
|
||||
base.RefreshInternal();
|
||||
}
|
||||
}
|
||||
|
||||
protected class Tab : GUI.Tabs.Tab
|
||||
{
|
||||
public CustomEditorPresenter Presenter;
|
||||
public PropertiesProxyBase Proxy;
|
||||
|
||||
public Tab(string text, TWindow window, bool modifiesAsset = true)
|
||||
: base(text)
|
||||
{
|
||||
var scrollPanel = new Panel(ScrollBars.Vertical)
|
||||
{
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
Offsets = Margin.Zero,
|
||||
Parent = this
|
||||
};
|
||||
|
||||
Presenter = new CustomEditorPresenter(null);
|
||||
Presenter.Panel.Parent = scrollPanel;
|
||||
if (modifiesAsset)
|
||||
Presenter.Modified += window.MarkAsEdited;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnDestroy()
|
||||
{
|
||||
Presenter.Deselect();
|
||||
Presenter = null;
|
||||
Proxy = null;
|
||||
|
||||
base.OnDestroy();
|
||||
}
|
||||
}
|
||||
|
||||
protected readonly SplitPanel _split;
|
||||
protected readonly Tabs _tabs;
|
||||
protected readonly ToolStripButton _saveButton;
|
||||
|
||||
protected bool _refreshOnLODsLoaded;
|
||||
protected bool _skipEffectsGuiEvents;
|
||||
protected int _isolateIndex = -1;
|
||||
protected int _highlightIndex = -1;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected ModelBaseWindow(Editor editor, AssetItem item)
|
||||
: base(editor, item)
|
||||
{
|
||||
// Toolstrip
|
||||
_saveButton = (ToolStripButton)_toolstrip.AddButton(editor.Icons.Save32, Save).LinkTooltip("Save");
|
||||
|
||||
// Split Panel
|
||||
_split = new SplitPanel(Orientation.Horizontal, ScrollBars.None, ScrollBars.None)
|
||||
{
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
Offsets = new Margin(0, 0, _toolstrip.Bottom, 0),
|
||||
SplitterValue = 0.65f,
|
||||
Parent = this
|
||||
};
|
||||
|
||||
// Properties tabs
|
||||
_tabs = new Tabs
|
||||
{
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
Offsets = Margin.Zero,
|
||||
TabsSize = new Vector2(60, 20),
|
||||
TabsTextHorizontalAlignment = TextAlignment.Center,
|
||||
UseScroll = true,
|
||||
Parent = _split.Panel2
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void UpdateToolstrip()
|
||||
{
|
||||
_saveButton.Enabled = IsEdited;
|
||||
|
||||
base.UpdateToolstrip();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void UnlinkItem()
|
||||
{
|
||||
foreach (var child in _tabs.Children)
|
||||
{
|
||||
if (child is Tab tab && tab.Proxy.Window != null)
|
||||
{
|
||||
tab.Proxy.OnClean();
|
||||
}
|
||||
}
|
||||
|
||||
base.UnlinkItem();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnAssetLoaded()
|
||||
{
|
||||
foreach (var child in _tabs.Children)
|
||||
{
|
||||
if (child is Tab tab)
|
||||
{
|
||||
tab.Proxy.OnLoad((TWindow)this);
|
||||
tab.Presenter.BuildLayout();
|
||||
}
|
||||
}
|
||||
ClearEditedFlag();
|
||||
|
||||
base.OnAssetLoaded();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnItemReimported(ContentItem item)
|
||||
{
|
||||
// Refresh the properties (will get new data in OnAssetLoaded)
|
||||
foreach (var child in _tabs.Children)
|
||||
{
|
||||
if (child is Tab tab)
|
||||
{
|
||||
tab.Proxy.OnClean();
|
||||
tab.Presenter.BuildLayout();
|
||||
}
|
||||
}
|
||||
ClearEditedFlag();
|
||||
_refreshOnLODsLoaded = true;
|
||||
|
||||
base.OnItemReimported(item);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool UseLayoutData => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLayoutSerialize(XmlWriter writer)
|
||||
{
|
||||
writer.WriteAttributeString("Split", _split.SplitterValue.ToString());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLayoutDeserialize(XmlElement node)
|
||||
{
|
||||
float value1;
|
||||
|
||||
if (float.TryParse(node.GetAttribute("Split"), out value1))
|
||||
_split.SplitterValue = value1;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLayoutDeserialize()
|
||||
{
|
||||
_split.SplitterValue = 0.65f;
|
||||
}
|
||||
}
|
||||
}
|
||||
1036
Source/Editor/Windows/Assets/ModelWindow.cs
Normal file
1036
Source/Editor/Windows/Assets/ModelWindow.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using FlaxEditor.Content;
|
||||
using FlaxEditor.Surface;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
|
||||
namespace FlaxEditor.Windows.Assets
|
||||
{
|
||||
/// <summary>
|
||||
/// Particle function window allows to view and edit <see cref="ParticleEmitterFunction"/> asset.
|
||||
/// </summary>
|
||||
/// <seealso cref="ParticleEmitterFunction" />
|
||||
/// <seealso cref="ParticleEmitterFunctionSurface" />
|
||||
public sealed class ParticleEmitterFunctionWindow : VisjectFunctionSurfaceWindow<ParticleEmitterFunction, ParticleEmitterFunctionSurface>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public ParticleEmitterFunctionWindow(Editor editor, AssetItem item)
|
||||
: base(editor, item)
|
||||
{
|
||||
// Surface
|
||||
_surface = new ParticleEmitterFunctionSurface(this, Save, _undo)
|
||||
{
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
Offsets = Margin.Zero,
|
||||
Parent = _panel,
|
||||
Enabled = false
|
||||
};
|
||||
|
||||
// Toolstrip
|
||||
_toolstrip.AddSeparator();
|
||||
_toolstrip.AddButton(editor.Icons.Docs32, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/particles/index.html")).LinkTooltip("See documentation to learn more");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string SurfaceName => "Particle Emitter Function";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override byte[] SurfaceData
|
||||
{
|
||||
get => _asset.LoadSurface();
|
||||
set
|
||||
{
|
||||
if (_asset.SaveSurface(value))
|
||||
{
|
||||
_surface.MarkAsEdited();
|
||||
Editor.LogError("Failed to save surface data");
|
||||
}
|
||||
_asset.Reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
273
Source/Editor/Windows/Assets/ParticleEmitterWindow.cs
Normal file
273
Source/Editor/Windows/Assets/ParticleEmitterWindow.cs
Normal file
@@ -0,0 +1,273 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FlaxEditor.Content;
|
||||
using FlaxEditor.CustomEditors;
|
||||
using FlaxEditor.Scripting;
|
||||
using FlaxEditor.Surface;
|
||||
using FlaxEditor.Viewport.Previews;
|
||||
using FlaxEngine;
|
||||
|
||||
// ReSharper disable UnusedMember.Local
|
||||
// ReSharper disable UnusedMember.Global
|
||||
// ReSharper disable MemberCanBePrivate.Local
|
||||
|
||||
namespace FlaxEditor.Windows.Assets
|
||||
{
|
||||
/// <summary>
|
||||
/// Particle Emitter window allows to view and edit <see cref="ParticleEmitter"/> asset.
|
||||
/// </summary>
|
||||
/// <seealso cref="ParticleEmitter" />
|
||||
/// <seealso cref="ParticleEmitterSurface" />
|
||||
/// <seealso cref="ParticleEmitterPreview" />
|
||||
public sealed class ParticleEmitterWindow : VisjectSurfaceWindow<ParticleEmitter, ParticleEmitterSurface, ParticleEmitterPreview>
|
||||
{
|
||||
private readonly ScriptType[] _newParameterTypes =
|
||||
{
|
||||
new ScriptType(typeof(float)),
|
||||
new ScriptType(typeof(Texture)),
|
||||
new ScriptType(typeof(CubeTexture)),
|
||||
new ScriptType(typeof(GPUTexture)),
|
||||
new ScriptType(typeof(ChannelMask)),
|
||||
new ScriptType(typeof(bool)),
|
||||
new ScriptType(typeof(int)),
|
||||
new ScriptType(typeof(Vector2)),
|
||||
new ScriptType(typeof(Vector3)),
|
||||
new ScriptType(typeof(Vector4)),
|
||||
new ScriptType(typeof(Color)),
|
||||
new ScriptType(typeof(Quaternion)),
|
||||
new ScriptType(typeof(Transform)),
|
||||
new ScriptType(typeof(Matrix)),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The properties proxy object.
|
||||
/// </summary>
|
||||
private sealed class PropertiesProxy
|
||||
{
|
||||
[EditorOrder(1000), EditorDisplay("Parameters"), CustomEditor(typeof(ParametersEditor)), NoSerialize]
|
||||
// ReSharper disable once UnusedAutoPropertyAccessor.Local
|
||||
public ParticleEmitterWindow Window { get; set; }
|
||||
|
||||
[HideInEditor, Serialize]
|
||||
// ReSharper disable once UnusedMember.Local
|
||||
public List<SurfaceParameter> Parameters
|
||||
{
|
||||
get => Window.Surface.Parameters;
|
||||
set => throw new Exception("No setter.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gathers parameters from the specified ParticleEmitter.
|
||||
/// </summary>
|
||||
/// <param name="particleEmitterWin">The ParticleEmitter window.</param>
|
||||
public void OnLoad(ParticleEmitterWindow particleEmitterWin)
|
||||
{
|
||||
// Link
|
||||
Window = particleEmitterWin;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears temporary data.
|
||||
/// </summary>
|
||||
public void OnClean()
|
||||
{
|
||||
// Unlink
|
||||
Window = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The graph parameters preview proxy object.
|
||||
/// </summary>
|
||||
private sealed class PreviewProxy
|
||||
{
|
||||
[EditorDisplay("Parameters"), CustomEditor(typeof(Editor)), NoSerialize]
|
||||
// ReSharper disable once UnusedAutoPropertyAccessor.Local
|
||||
public ParticleEmitterWindow Window;
|
||||
|
||||
private class Editor : CustomEditor
|
||||
{
|
||||
public override DisplayStyle Style => DisplayStyle.InlineIntoParent;
|
||||
|
||||
public override void Initialize(LayoutElementsContainer layout)
|
||||
{
|
||||
var window = (ParticleEmitterWindow)Values[0];
|
||||
var parameters = window.Preview.PreviewActor.Parameters;
|
||||
var data = SurfaceUtils.InitGraphParameters(parameters);
|
||||
SurfaceUtils.DisplayGraphParameters(layout, data,
|
||||
(instance, parameter, tag) => ((ParticleEmitterWindow)instance).Preview.PreviewActor.GetParameterValue(string.Empty, parameter.Name),
|
||||
(instance, value, parameter, tag) => ((ParticleEmitterWindow)instance).Preview.PreviewActor.SetParameterValue(string.Empty, parameter.Name, value),
|
||||
Values);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly PropertiesProxy _properties;
|
||||
private Tab _previewTab;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ParticleEmitterWindow(Editor editor, AssetItem item)
|
||||
: base(editor, item, true)
|
||||
{
|
||||
// Asset preview
|
||||
_preview = new ParticleEmitterPreview(true)
|
||||
{
|
||||
PlaySimulation = true,
|
||||
Parent = _split2.Panel1
|
||||
};
|
||||
|
||||
// Asset properties proxy
|
||||
_properties = new PropertiesProxy();
|
||||
_propertiesEditor.Select(_properties);
|
||||
|
||||
// Preview properties editor
|
||||
_previewTab = new Tab("Preview");
|
||||
_previewTab.Presenter.Select(new PreviewProxy
|
||||
{
|
||||
Window = this,
|
||||
});
|
||||
_tabs.AddTab(_previewTab);
|
||||
|
||||
// Surface
|
||||
_surface = new ParticleEmitterSurface(this, Save, _undo)
|
||||
{
|
||||
Parent = _split1.Panel1,
|
||||
Enabled = false
|
||||
};
|
||||
|
||||
// Toolstrip
|
||||
_toolstrip.AddSeparator();
|
||||
_toolstrip.AddButton(editor.Icons.BracketsSlash32, () => ShowSourceCode(_asset)).LinkTooltip("Show generated shader source code");
|
||||
_toolstrip.AddButton(editor.Icons.Docs32, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/particles/index.html")).LinkTooltip("See documentation to learn more");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the ParticleEmitter source code window.
|
||||
/// </summary>
|
||||
/// <param name="particleEmitter">The ParticleEmitter asset.</param>
|
||||
public static void ShowSourceCode(ParticleEmitter particleEmitter)
|
||||
{
|
||||
var source = Editor.GetShaderSourceCode(particleEmitter);
|
||||
Utilities.Utils.ShowSourceCodeWindow(source, "Particle Emitter GPU Simulation Source");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnParamRenameUndo()
|
||||
{
|
||||
base.OnParamRenameUndo();
|
||||
|
||||
_refreshPropertiesOnLoad = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnParamAddUndo()
|
||||
{
|
||||
base.OnParamAddUndo();
|
||||
|
||||
_refreshPropertiesOnLoad = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnParamRemoveUndo()
|
||||
{
|
||||
base.OnParamRemoveUndo();
|
||||
|
||||
_refreshPropertiesOnLoad = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IEnumerable<ScriptType> NewParameterTypes => _newParameterTypes;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void SetParameter(int index, object value)
|
||||
{
|
||||
try
|
||||
{
|
||||
Preview.PreviewActor.Parameters[index].Value = value;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
|
||||
base.SetParameter(index, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void UnlinkItem()
|
||||
{
|
||||
_properties.OnClean();
|
||||
_preview.Emitter = null;
|
||||
|
||||
base.UnlinkItem();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnAssetLinked()
|
||||
{
|
||||
_preview.Emitter = _asset;
|
||||
|
||||
base.OnAssetLinked();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string SurfaceName => "Particle Emitter";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override byte[] SurfaceData
|
||||
{
|
||||
get => _asset.LoadSurface(true);
|
||||
set
|
||||
{
|
||||
// Save data to the temporary asset
|
||||
if (_asset.SaveSurface(value))
|
||||
{
|
||||
// Error
|
||||
_surface.MarkAsEdited();
|
||||
Editor.LogError("Failed to save Particle Emitter surface data");
|
||||
}
|
||||
_asset.Reload();
|
||||
_preview.PreviewActor.ResetSimulation();
|
||||
_previewTab.Presenter.BuildLayoutOnUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool LoadSurface()
|
||||
{
|
||||
// Load surface graph
|
||||
if (_surface.Load())
|
||||
{
|
||||
// Error
|
||||
Editor.LogError("Failed to load Particle Emitter surface.");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Init asset properties and parameters proxy
|
||||
_properties.OnLoad(this);
|
||||
_previewTab.Presenter.BuildLayoutOnUpdate();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool SaveSurface()
|
||||
{
|
||||
_surface.Save();
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool SaveToOriginal()
|
||||
{
|
||||
// Copy shader cache from the temporary Particle Emitter (will skip compilation on Reload - faster)
|
||||
Guid dstId = _item.ID;
|
||||
Guid srcId = _asset.ID;
|
||||
Editor.Internal_CopyCache(ref dstId, ref srcId);
|
||||
|
||||
return base.SaveToOriginal();
|
||||
}
|
||||
}
|
||||
}
|
||||
588
Source/Editor/Windows/Assets/ParticleSystemWindow.cs
Normal file
588
Source/Editor/Windows/Assets/ParticleSystemWindow.cs
Normal file
@@ -0,0 +1,588 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Xml;
|
||||
using FlaxEditor.Content;
|
||||
using FlaxEditor.CustomEditors;
|
||||
using FlaxEditor.CustomEditors.Editors;
|
||||
using FlaxEditor.CustomEditors.GUI;
|
||||
using FlaxEditor.GUI;
|
||||
using FlaxEditor.GUI.Timeline;
|
||||
using FlaxEditor.GUI.Timeline.Tracks;
|
||||
using FlaxEditor.Surface;
|
||||
using FlaxEditor.Viewport.Previews;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
|
||||
namespace FlaxEditor.Windows.Assets
|
||||
{
|
||||
/// <summary>
|
||||
/// Particle System window allows to view and edit <see cref="ParticleSystem"/> asset.
|
||||
/// Note: it uses ClonedAssetEditorWindowBase which is creating cloned asset to edit/preview.
|
||||
/// </summary>
|
||||
/// <seealso cref="ParticleSystem" />
|
||||
/// <seealso cref="FlaxEditor.Windows.Assets.AssetEditorWindow" />
|
||||
public sealed class ParticleSystemWindow : ClonedAssetEditorWindowBase<ParticleSystem>
|
||||
{
|
||||
sealed class EditParameterOverrideAction : IUndoAction
|
||||
{
|
||||
private ParticleSystemWindow _window;
|
||||
private string _trackName;
|
||||
private Guid _paramId;
|
||||
private bool _beforeOverride, _afterOverride;
|
||||
private object _beforeValue, _afterValue;
|
||||
|
||||
public string ActionString => "Edit Parameter Override";
|
||||
|
||||
public EditParameterOverrideAction(ParticleSystemWindow window, ParticleEmitterTrack track, GraphParameter parameter, bool newOverride)
|
||||
{
|
||||
_window = window;
|
||||
_trackName = track.Name;
|
||||
_paramId = parameter.Identifier;
|
||||
_beforeOverride = !newOverride;
|
||||
_afterOverride = newOverride;
|
||||
_beforeValue = _afterValue = parameter.Value;
|
||||
}
|
||||
|
||||
public EditParameterOverrideAction(ParticleSystemWindow window, ParticleEmitterTrack track, GraphParameter parameter, object newValue)
|
||||
{
|
||||
_window = window;
|
||||
_trackName = track.Name;
|
||||
_paramId = parameter.Identifier;
|
||||
_beforeOverride = true;
|
||||
_afterOverride = true;
|
||||
_beforeValue = parameter.Value;
|
||||
_afterValue = newValue;
|
||||
}
|
||||
|
||||
private void Set(bool isOverride, object value)
|
||||
{
|
||||
var track = (ParticleEmitterTrack)_window.Timeline.FindTrack(_trackName);
|
||||
if (track == null)
|
||||
throw new Exception($"Missing track of name {_trackName} in particle system {_window.Title}");
|
||||
if (_beforeOverride != _afterOverride)
|
||||
{
|
||||
if (isOverride)
|
||||
track.ParametersOverrides.Add(_paramId, value);
|
||||
else
|
||||
track.ParametersOverrides.Remove(_paramId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_window._isEditingInstancedParameterValue = true;
|
||||
var param = _window.Preview.PreviewActor.GetParameter(_trackName, _paramId);
|
||||
if (param != null)
|
||||
param.Value = value;
|
||||
|
||||
if (track.ParametersOverrides.ContainsKey(_paramId))
|
||||
track.ParametersOverrides[_paramId] = value;
|
||||
}
|
||||
_window.Timeline.OnEmittersParametersOverridesEdited();
|
||||
_window.Timeline.MarkAsEdited();
|
||||
}
|
||||
|
||||
public void Do()
|
||||
{
|
||||
Set(_afterOverride, _afterValue);
|
||||
}
|
||||
|
||||
public void Undo()
|
||||
{
|
||||
Set(_beforeOverride, _beforeValue);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_window = null;
|
||||
_trackName = null;
|
||||
_beforeValue = _afterValue = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The proxy object for editing particle system properties.
|
||||
/// </summary>
|
||||
private class GeneralProxy
|
||||
{
|
||||
private readonly ParticleSystemWindow _window;
|
||||
|
||||
[EditorDisplay("Particle System"), EditorOrder(100), Limit(1), Tooltip("The timeline animation duration in frames.")]
|
||||
public int TimelineDurationFrames
|
||||
{
|
||||
get => _window.Timeline.DurationFrames;
|
||||
set => _window.Timeline.DurationFrames = value;
|
||||
}
|
||||
|
||||
public GeneralProxy(ParticleSystemWindow window)
|
||||
{
|
||||
_window = window;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The proxy object for editing particle system track properties.
|
||||
/// </summary>
|
||||
[CustomEditor(typeof(EmitterTrackProxyEditor))]
|
||||
private class EmitterTrackProxy
|
||||
{
|
||||
private readonly ParticleSystemWindow _window;
|
||||
private readonly ParticleEffect _effect;
|
||||
private readonly int _emitterIndex;
|
||||
private readonly ParticleEmitterTrack _track;
|
||||
|
||||
[EditorDisplay("Particle Emitter"), EditorOrder(0), Tooltip("The name text.")]
|
||||
public string Name
|
||||
{
|
||||
get => _track.Name;
|
||||
set
|
||||
{
|
||||
if (!_track.Timeline.IsTrackNameValid(value))
|
||||
{
|
||||
MessageBox.Show("Invalid name. It must be unique.", "Invalid track name", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
_track.Name = value;
|
||||
}
|
||||
}
|
||||
|
||||
[EditorDisplay("Particle Emitter"), EditorOrder(100), Tooltip("The particle emitter to use for the track media event playback.")]
|
||||
public ParticleEmitter Emitter
|
||||
{
|
||||
get => _track.Asset;
|
||||
set => _track.Asset = value;
|
||||
}
|
||||
|
||||
private bool HasEmitter => _track.Asset != null;
|
||||
|
||||
[EditorDisplay("Particle Emitter"), VisibleIf("HasEmitter"), EditorOrder(200), Tooltip("The start frame of the media event.")]
|
||||
public int StartFrame
|
||||
{
|
||||
get => _track.Media.Count > 0 ? _track.TrackMedia.StartFrame : 0;
|
||||
set => _track.TrackMedia.StartFrame = value;
|
||||
}
|
||||
|
||||
[EditorDisplay("Particle Emitter"), Limit(1), VisibleIf("HasEmitter"), EditorOrder(300), Tooltip("The total duration of the media event in the timeline sequence frames amount.")]
|
||||
public int DurationFrames
|
||||
{
|
||||
get => _track.Media.Count > 0 ? _track.TrackMedia.DurationFrames : 0;
|
||||
set => _track.TrackMedia.DurationFrames = value;
|
||||
}
|
||||
|
||||
public EmitterTrackProxy(ParticleSystemWindow window, ParticleEffect effect, ParticleEmitterTrack track, int emitterIndex)
|
||||
{
|
||||
_window = window;
|
||||
_effect = effect;
|
||||
_emitterIndex = emitterIndex;
|
||||
_track = track;
|
||||
}
|
||||
|
||||
private sealed class EmitterTrackProxyEditor : GenericEditor
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void Initialize(LayoutElementsContainer layout)
|
||||
{
|
||||
base.Initialize(layout);
|
||||
|
||||
var emitterTrack = Values[0] as EmitterTrackProxy;
|
||||
if (emitterTrack?._effect?.Parameters == null)
|
||||
return;
|
||||
|
||||
var group = layout.Group("Parameters");
|
||||
var parameters = emitterTrack._effect.Parameters.Where(x => x.EmitterIndex == emitterTrack._emitterIndex && x.Emitter == emitterTrack.Emitter && x.IsPublic).ToArray();
|
||||
|
||||
if (!parameters.Any())
|
||||
{
|
||||
group.Label("No parameters", TextAlignment.Center);
|
||||
return;
|
||||
}
|
||||
|
||||
var data = SurfaceUtils.InitGraphParameters(parameters);
|
||||
SurfaceUtils.DisplayGraphParameters(group, data,
|
||||
(instance, parameter, tag) => ((ParticleEffectParameter)tag).Value,
|
||||
(instance, value, parameter, tag) =>
|
||||
{
|
||||
if (parameter.Value == value)
|
||||
return;
|
||||
|
||||
var proxy = (EmitterTrackProxy)instance;
|
||||
var action = new EditParameterOverrideAction(proxy._window, proxy._track, parameter, value);
|
||||
action.Do();
|
||||
proxy._window.Undo.AddAction(action);
|
||||
},
|
||||
Values,
|
||||
(instance, parameter, tag) => ((ParticleEffectParameter)tag).DefaultEmitterValue,
|
||||
(LayoutElementsContainer itemLayout, ValueContainer valueContainer, ref SurfaceUtils.GraphParameterData e) =>
|
||||
{
|
||||
// Use label with parameter value override checkbox
|
||||
var label = new CheckablePropertyNameLabel(e.Parameter.Name);
|
||||
label.CheckBox.Checked = emitterTrack._track.ParametersOverrides.ContainsKey(e.Parameter.Identifier);
|
||||
label.CheckBox.Tag = e.Parameter;
|
||||
label.CheckChanged += nameLabel =>
|
||||
{
|
||||
var parameter = (GraphParameter)nameLabel.CheckBox.Tag;
|
||||
var proxy = emitterTrack;
|
||||
var action = new EditParameterOverrideAction(proxy._window, proxy._track, parameter, nameLabel.CheckBox.Checked);
|
||||
action.Do();
|
||||
proxy._window.Undo.AddAction(action);
|
||||
};
|
||||
|
||||
itemLayout.Property(label, valueContainer, null, e.Tooltip?.Text);
|
||||
label.UpdateStyle();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The proxy object for editing folder track properties.
|
||||
/// </summary>
|
||||
private class FolderTrackProxy
|
||||
{
|
||||
private readonly FolderTrack _track;
|
||||
|
||||
[EditorDisplay("Folder"), EditorOrder(0), Tooltip("The name text.")]
|
||||
public string Name
|
||||
{
|
||||
get => _track.Name;
|
||||
set
|
||||
{
|
||||
if (!_track.Timeline.IsTrackNameValid(value))
|
||||
{
|
||||
MessageBox.Show("Invalid name. It must be unique.", "Invalid track name", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
_track.Name = value;
|
||||
}
|
||||
}
|
||||
|
||||
[EditorDisplay("Folder"), EditorOrder(200), Tooltip("The folder icon color.")]
|
||||
public Color Color
|
||||
{
|
||||
get => _track.IconColor;
|
||||
set => _track.IconColor = value;
|
||||
}
|
||||
|
||||
public FolderTrackProxy(FolderTrack track)
|
||||
{
|
||||
_track = track;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly SplitPanel _split1;
|
||||
private readonly SplitPanel _split2;
|
||||
private ParticleSystemTimeline _timeline;
|
||||
private readonly ParticleSystemPreview _preview;
|
||||
private readonly CustomEditorPresenter _propertiesEditor1;
|
||||
private readonly CustomEditorPresenter _propertiesEditor2;
|
||||
private ToolStripButton _saveButton;
|
||||
private ToolStripButton _undoButton;
|
||||
private ToolStripButton _redoButton;
|
||||
private Undo _undo;
|
||||
private bool _tmpParticleSystemIsDirty;
|
||||
private bool _isWaitingForTimelineLoad;
|
||||
private bool _isEditingInstancedParameterValue;
|
||||
private uint _parametersVersion;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the particle system preview.
|
||||
/// </summary>
|
||||
public ParticleSystemPreview Preview => _preview;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the timeline editor.
|
||||
/// </summary>
|
||||
public ParticleSystemTimeline Timeline => _timeline;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the undo history context for this window.
|
||||
/// </summary>
|
||||
public Undo Undo => _undo;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ParticleSystemWindow(Editor editor, AssetItem item)
|
||||
: base(editor, item)
|
||||
{
|
||||
// Undo
|
||||
_undo = new Undo();
|
||||
_undo.UndoDone += OnUndoRedo;
|
||||
_undo.RedoDone += OnUndoRedo;
|
||||
_undo.ActionDone += OnUndoRedo;
|
||||
|
||||
// Split Panel 1
|
||||
_split1 = new SplitPanel(Orientation.Vertical, ScrollBars.None, ScrollBars.None)
|
||||
{
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
Offsets = new Margin(0, 0, _toolstrip.Bottom, 0),
|
||||
SplitterValue = 0.6f,
|
||||
Parent = this
|
||||
};
|
||||
|
||||
// Split Panel 2
|
||||
_split2 = new SplitPanel(Orientation.Horizontal, ScrollBars.None, ScrollBars.Vertical)
|
||||
{
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
Offsets = Margin.Zero,
|
||||
SplitterValue = 0.5f,
|
||||
Parent = _split1.Panel1
|
||||
};
|
||||
|
||||
// Particles preview
|
||||
_preview = new ParticleSystemPreview(true)
|
||||
{
|
||||
PlaySimulation = true,
|
||||
Parent = _split2.Panel1
|
||||
};
|
||||
|
||||
// Timeline
|
||||
_timeline = new ParticleSystemTimeline(_preview, _undo)
|
||||
{
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
Offsets = Margin.Zero,
|
||||
Parent = _split1.Panel2,
|
||||
Enabled = false
|
||||
};
|
||||
_timeline.Modified += OnTimelineModified;
|
||||
_timeline.SelectionChanged += OnTimelineSelectionChanged;
|
||||
|
||||
// Properties editor (general)
|
||||
var propertiesEditor1 = new CustomEditorPresenter(null, string.Empty);
|
||||
propertiesEditor1.Panel.Parent = _split2.Panel2;
|
||||
propertiesEditor1.Modified += OnParticleSystemPropertyEdited;
|
||||
_propertiesEditor1 = propertiesEditor1;
|
||||
propertiesEditor1.Select(new GeneralProxy(this));
|
||||
|
||||
// Properties editor (selection)
|
||||
var propertiesEditor2 = new CustomEditorPresenter(null, string.Empty);
|
||||
propertiesEditor2.Panel.Parent = _split2.Panel2;
|
||||
propertiesEditor2.Modified += OnParticleSystemPropertyEdited;
|
||||
_propertiesEditor2 = propertiesEditor2;
|
||||
|
||||
// Toolstrip
|
||||
_saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save32, Save).LinkTooltip("Save");
|
||||
_toolstrip.AddSeparator();
|
||||
_undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo32, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)");
|
||||
_redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo32, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)");
|
||||
_toolstrip.AddSeparator();
|
||||
_toolstrip.AddButton(editor.Icons.Docs32, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/particles/index.html")).LinkTooltip("See documentation to learn more");
|
||||
|
||||
// Setup input actions
|
||||
InputActions.Add(options => options.Undo, _undo.PerformUndo);
|
||||
InputActions.Add(options => options.Redo, _undo.PerformRedo);
|
||||
}
|
||||
|
||||
private void OnUndoRedo(IUndoAction action)
|
||||
{
|
||||
MarkAsEdited();
|
||||
UpdateToolstrip();
|
||||
_propertiesEditor1.BuildLayoutOnUpdate();
|
||||
_propertiesEditor2.BuildLayoutOnUpdate();
|
||||
}
|
||||
|
||||
private void OnTimelineModified()
|
||||
{
|
||||
_tmpParticleSystemIsDirty = true;
|
||||
MarkAsEdited();
|
||||
}
|
||||
|
||||
private void OnTimelineSelectionChanged()
|
||||
{
|
||||
if (_timeline.SelectedTracks.Count == 0)
|
||||
{
|
||||
_propertiesEditor2.Deselect();
|
||||
return;
|
||||
}
|
||||
|
||||
var tracks = new object[_timeline.SelectedTracks.Count];
|
||||
var emitterTracks = _timeline.Tracks.Where(track => track is ParticleEmitterTrack).Cast<ParticleEmitterTrack>().ToList();
|
||||
for (var i = 0; i < _timeline.SelectedTracks.Count; i++)
|
||||
{
|
||||
var track = _timeline.SelectedTracks[i];
|
||||
if (track is ParticleEmitterTrack particleEmitterTrack)
|
||||
{
|
||||
tracks[i] = new EmitterTrackProxy(this, Preview.PreviewActor, particleEmitterTrack, emitterTracks.IndexOf(particleEmitterTrack));
|
||||
}
|
||||
else if (track is FolderTrack folderTrack)
|
||||
{
|
||||
tracks[i] = new FolderTrackProxy(folderTrack);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotImplementedException("Invalid track type.");
|
||||
}
|
||||
}
|
||||
_propertiesEditor2.Select(tracks);
|
||||
}
|
||||
|
||||
private void OnParticleSystemPropertyEdited()
|
||||
{
|
||||
if (_isEditingInstancedParameterValue)
|
||||
return;
|
||||
|
||||
_timeline.MarkAsEdited();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes temporary asset to see changes live when editing the timeline.
|
||||
/// </summary>
|
||||
/// <returns>True if cannot refresh it, otherwise false.</returns>
|
||||
public bool RefreshTempAsset()
|
||||
{
|
||||
if (_asset == null || _isWaitingForTimelineLoad)
|
||||
return true;
|
||||
|
||||
if (_timeline.IsModified)
|
||||
{
|
||||
_propertiesEditor1.BuildLayoutOnUpdate();
|
||||
_propertiesEditor2.BuildLayoutOnUpdate();
|
||||
_timeline.Save(_asset);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Save()
|
||||
{
|
||||
if (!IsEdited)
|
||||
return;
|
||||
|
||||
if (RefreshTempAsset())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (SaveToOriginal())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ClearEditedFlag();
|
||||
_item.RefreshThumbnail();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void UpdateToolstrip()
|
||||
{
|
||||
_saveButton.Enabled = IsEdited;
|
||||
_undoButton.Enabled = _undo.CanUndo;
|
||||
_redoButton.Enabled = _undo.CanRedo;
|
||||
|
||||
base.UpdateToolstrip();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void UnlinkItem()
|
||||
{
|
||||
_propertiesEditor2.Deselect();
|
||||
_preview.System = null;
|
||||
_isWaitingForTimelineLoad = false;
|
||||
|
||||
base.UnlinkItem();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnAssetLinked()
|
||||
{
|
||||
_preview.System = _asset;
|
||||
_isWaitingForTimelineLoad = true;
|
||||
|
||||
base.OnAssetLinked();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
// Check if need to refresh the parameters
|
||||
if (_parametersVersion != _preview.PreviewActor.ParametersVersion)
|
||||
{
|
||||
_parametersVersion = _preview.PreviewActor.ParametersVersion;
|
||||
_propertiesEditor2.BuildLayoutOnUpdate();
|
||||
}
|
||||
|
||||
base.Update(deltaTime);
|
||||
|
||||
// Check if temporary asset need to be updated
|
||||
if (_tmpParticleSystemIsDirty)
|
||||
{
|
||||
// Clear flag
|
||||
_tmpParticleSystemIsDirty = false;
|
||||
|
||||
// Update
|
||||
RefreshTempAsset();
|
||||
}
|
||||
|
||||
// Check if need to load timeline
|
||||
if (_isWaitingForTimelineLoad && _asset.IsLoaded)
|
||||
{
|
||||
// Clear flag
|
||||
_isWaitingForTimelineLoad = false;
|
||||
|
||||
// Load timeline data from the asset
|
||||
_timeline.Load(_asset);
|
||||
|
||||
// Setup
|
||||
_undo.Clear();
|
||||
_timeline.Enabled = true;
|
||||
_propertiesEditor1.BuildLayout();
|
||||
_propertiesEditor2.Deselect();
|
||||
ClearEditedFlag();
|
||||
}
|
||||
|
||||
// Clear flag
|
||||
_isEditingInstancedParameterValue = false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool UseLayoutData => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLayoutSerialize(XmlWriter writer)
|
||||
{
|
||||
writer.WriteAttributeString("Split1", _split1.SplitterValue.ToString());
|
||||
writer.WriteAttributeString("Split2", _split2.SplitterValue.ToString());
|
||||
writer.WriteAttributeString("Split3", _timeline.Splitter.SplitterValue.ToString());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLayoutDeserialize(XmlElement node)
|
||||
{
|
||||
float value1;
|
||||
|
||||
if (float.TryParse(node.GetAttribute("Split1"), out value1))
|
||||
_split1.SplitterValue = value1;
|
||||
if (float.TryParse(node.GetAttribute("Split2"), out value1))
|
||||
_split2.SplitterValue = value1;
|
||||
if (float.TryParse(node.GetAttribute("Split3"), out value1))
|
||||
_timeline.Splitter.SplitterValue = value1;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLayoutDeserialize()
|
||||
{
|
||||
_split1.SplitterValue = 0.6f;
|
||||
_split2.SplitterValue = 0.5f;
|
||||
_timeline.Splitter.SplitterValue = 0.2f;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnDestroy()
|
||||
{
|
||||
if (_undo != null)
|
||||
_undo.Enabled = false;
|
||||
_propertiesEditor1?.Deselect();
|
||||
_propertiesEditor2?.Deselect();
|
||||
_undo?.Clear();
|
||||
_undo = null;
|
||||
|
||||
_timeline = null;
|
||||
_saveButton = null;
|
||||
_undoButton = null;
|
||||
_redoButton = null;
|
||||
|
||||
base.OnDestroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
330
Source/Editor/Windows/Assets/PrefabWindow.Actions.cs
Normal file
330
Source/Editor/Windows/Assets/PrefabWindow.Actions.cs
Normal file
@@ -0,0 +1,330 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FlaxEditor.Actions;
|
||||
using FlaxEditor.SceneGraph;
|
||||
using FlaxEngine;
|
||||
using Object = FlaxEngine.Object;
|
||||
|
||||
namespace FlaxEditor.Windows.Assets
|
||||
{
|
||||
public sealed partial class PrefabWindow
|
||||
{
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="IUndoAction"/> used to change the root actor of the prefab.
|
||||
/// </summary>
|
||||
/// <seealso cref="FlaxEditor.IUndoAction" />
|
||||
public class SetRootAction : IUndoAction
|
||||
{
|
||||
private PrefabWindow _window;
|
||||
private readonly Guid _before;
|
||||
private readonly Guid _after;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SetRootAction"/> class.
|
||||
/// </summary>
|
||||
/// <param name="window">The window reference.</param>
|
||||
/// <param name="before">The root before.</param>
|
||||
/// <param name="after">The root after.</param>
|
||||
internal SetRootAction(PrefabWindow window, Actor before, Actor after)
|
||||
{
|
||||
_window = window;
|
||||
_before = before.ID;
|
||||
_after = after.ID;
|
||||
}
|
||||
|
||||
private void Set(Guid oldRootId, Guid newRootId)
|
||||
{
|
||||
var oldRoot = Object.Find<Actor>(ref oldRootId);
|
||||
var newRoot = Object.Find<Actor>(ref newRootId);
|
||||
|
||||
_window.Graph.MainActor = null;
|
||||
if (SceneGraphFactory.Nodes.TryGetValue(oldRootId, out var oldRootNode))
|
||||
oldRootNode.Dispose();
|
||||
if (SceneGraphFactory.Nodes.TryGetValue(newRootId, out var newRootNode))
|
||||
newRootNode.Dispose();
|
||||
|
||||
newRoot.Parent = null;
|
||||
oldRoot.Parent = newRoot;
|
||||
|
||||
_window.Graph.MainActor = newRoot;
|
||||
_window.Viewport.Instance = newRoot;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string ActionString => "Set root";
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Do()
|
||||
{
|
||||
Set(_before, _after);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Undo()
|
||||
{
|
||||
Set(_after, _before);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
_window = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Changes the root object of the prefab.
|
||||
/// </summary>
|
||||
private void SetRoot()
|
||||
{
|
||||
var oldRoot = Graph.MainActor;
|
||||
var newRoot = ((ActorNode)Selection[0]).Actor;
|
||||
|
||||
Deselect();
|
||||
|
||||
var action = new SetRootAction(this, oldRoot, newRoot);
|
||||
action.Do();
|
||||
Undo.AddAction(action);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cuts selected objects.
|
||||
/// </summary>
|
||||
public void Cut()
|
||||
{
|
||||
Copy();
|
||||
Delete();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies selected objects to system clipboard.
|
||||
/// </summary>
|
||||
public void Copy()
|
||||
{
|
||||
// Peek things that can be copied (copy all actors)
|
||||
var objects = Selection.Where(x => x.CanCopyPaste).ToList().BuildAllNodes().Where(x => x.CanCopyPaste && x is ActorNode).ToList();
|
||||
if (objects.Count == 0)
|
||||
return;
|
||||
|
||||
// Serialize actors
|
||||
var actors = objects.ConvertAll(x => ((ActorNode)x).Actor);
|
||||
var data = Actor.ToBytes(actors.ToArray());
|
||||
if (data == null)
|
||||
{
|
||||
Editor.LogError("Failed to copy actors data.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy data
|
||||
Clipboard.RawData = data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pastes objects from the system clipboard.
|
||||
/// </summary>
|
||||
public void Paste()
|
||||
{
|
||||
Paste(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pastes the copied objects. Supports undo/redo.
|
||||
/// </summary>
|
||||
/// <param name="pasteTargetActor">The target actor to paste copied data.</param>
|
||||
public void Paste(Actor pasteTargetActor)
|
||||
{
|
||||
// Get clipboard data
|
||||
var data = Clipboard.RawData;
|
||||
|
||||
// Set paste target if only one actor is selected and no target provided
|
||||
if (pasteTargetActor == null && Selection.Count == 1 && Selection[0] is ActorNode actorNode)
|
||||
{
|
||||
pasteTargetActor = actorNode.Actor;
|
||||
}
|
||||
|
||||
// Create paste action
|
||||
var pasteAction = CustomPasteActorsAction.CustomPaste(this, data, pasteTargetActor?.ID ?? Guid.Empty);
|
||||
if (pasteAction != null)
|
||||
{
|
||||
OnPasteAction(pasteAction);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Duplicates selected objects.
|
||||
/// </summary>
|
||||
public void Duplicate()
|
||||
{
|
||||
// Peek things that can be copied (copy all actors)
|
||||
var objects = Selection.Where(x => x.CanCopyPaste && x != Graph.Main).ToList().BuildAllNodes().Where(x => x.CanCopyPaste && x is ActorNode).ToList();
|
||||
if (objects.Count == 0)
|
||||
return;
|
||||
|
||||
// Serialize actors
|
||||
var actors = objects.ConvertAll(x => ((ActorNode)x).Actor);
|
||||
var data = Actor.ToBytes(actors.ToArray());
|
||||
if (data == null)
|
||||
{
|
||||
Editor.LogError("Failed to copy actors data.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create paste action (with selecting spawned objects)
|
||||
var pasteAction = CustomPasteActorsAction.CustomDuplicate(this, data, Guid.Empty);
|
||||
if (pasteAction != null)
|
||||
{
|
||||
OnPasteAction(pasteAction);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPasteAction(PasteActorsAction pasteAction)
|
||||
{
|
||||
pasteAction.Do(out _, out var nodeParents);
|
||||
|
||||
// Select spawned objects
|
||||
var selectAction = new SelectionChangeAction(Selection.ToArray(), nodeParents.Cast<SceneGraphNode>().ToArray(), OnSelectionUndo);
|
||||
selectAction.Do();
|
||||
|
||||
Undo.AddAction(new MultiUndoAction(pasteAction, selectAction));
|
||||
OnSelectionChanges();
|
||||
}
|
||||
|
||||
private class CustomDeleteActorsAction : DeleteActorsAction
|
||||
{
|
||||
public CustomDeleteActorsAction(List<SceneGraphNode> objects, bool isInverted = false)
|
||||
: base(objects, isInverted)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Delete()
|
||||
{
|
||||
var nodes = _nodeParents.ToArray();
|
||||
|
||||
// Unlink nodes from parents (actors spawned for prefab editing are not in a gameplay and may not send some important events)
|
||||
for (int i = 0; i < nodes.Length; i++)
|
||||
{
|
||||
nodes[i].Actor.Parent = null;
|
||||
}
|
||||
|
||||
base.Delete();
|
||||
|
||||
// Remove nodes (actors in prefab are not in a gameplay and some events from the engine may not be send eg. ActorDeleted)
|
||||
for (int i = 0; i < nodes.Length; i++)
|
||||
{
|
||||
nodes[i].Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override SceneGraphNode GetNode(Guid id)
|
||||
{
|
||||
return SceneGraphFactory.GetNode(id);
|
||||
}
|
||||
}
|
||||
|
||||
private class CustomPasteActorsAction : PasteActorsAction
|
||||
{
|
||||
private PrefabWindow _window;
|
||||
|
||||
private CustomPasteActorsAction(PrefabWindow window, byte[] data, Guid[] objectIds, ref Guid pasteParent, string name)
|
||||
: base(data, objectIds, ref pasteParent, name)
|
||||
{
|
||||
_window = window;
|
||||
}
|
||||
|
||||
internal static CustomPasteActorsAction CustomPaste(PrefabWindow window, byte[] data, Guid pasteParent)
|
||||
{
|
||||
var objectIds = Actor.TryGetSerializedObjectsIds(data);
|
||||
if (objectIds == null)
|
||||
return null;
|
||||
|
||||
return new CustomPasteActorsAction(window, data, objectIds, ref pasteParent, "Paste actors");
|
||||
}
|
||||
|
||||
internal static CustomPasteActorsAction CustomDuplicate(PrefabWindow window, byte[] data, Guid pasteParent)
|
||||
{
|
||||
var objectIds = Actor.TryGetSerializedObjectsIds(data);
|
||||
if (objectIds == null)
|
||||
return null;
|
||||
|
||||
return new CustomPasteActorsAction(window, data, objectIds, ref pasteParent, "Duplicate actors");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void LinkBrokenParentReference(Actor actor)
|
||||
{
|
||||
// Link to prefab root
|
||||
actor.SetParent(_window.Graph.MainActor, false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Undo()
|
||||
{
|
||||
var nodes = _nodeParents.ToArray();
|
||||
|
||||
for (int i = 0; i < nodes.Length; i++)
|
||||
{
|
||||
var node = SceneGraphFactory.FindNode(_nodeParents[i]);
|
||||
if (node != null)
|
||||
{
|
||||
// Unlink nodes from parents (actors spawned for prefab editing are not in a gameplay and may not send some important events)
|
||||
if (node is ActorNode actorNode)
|
||||
actorNode.Actor.Parent = null;
|
||||
|
||||
// Remove objects
|
||||
node.Delete();
|
||||
|
||||
// Remove nodes (actors in prefab are not in a gameplay and some events from the engine may not be send eg. ActorDeleted)
|
||||
node.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
_nodeParents.Clear();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override SceneGraphNode GetNode(Guid id)
|
||||
{
|
||||
return SceneGraphFactory.GetNode(id);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Dispose()
|
||||
{
|
||||
base.Dispose();
|
||||
|
||||
_window = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes selected objects.
|
||||
/// </summary>
|
||||
public void Delete()
|
||||
{
|
||||
// Peek things that can be removed
|
||||
var objects = Selection.Where(x => x != null && x.CanDelete && x != Graph.Main).ToList().BuildAllNodes().Where(x => x != null && x.CanDelete).ToList();
|
||||
if (objects.Count == 0)
|
||||
return;
|
||||
|
||||
// Change selection
|
||||
var action1 = new SelectionChangeAction(Selection.ToArray(), new SceneGraphNode[0], OnSelectionUndo);
|
||||
|
||||
// Delete objects
|
||||
var action2 = new CustomDeleteActorsAction(objects);
|
||||
|
||||
// Merge actions and perform them
|
||||
var action = new MultiUndoAction(new IUndoAction[]
|
||||
{
|
||||
action1,
|
||||
action2
|
||||
}, action2.ActionString);
|
||||
action.Do();
|
||||
Undo.AddAction(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
261
Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs
Normal file
261
Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs
Normal file
@@ -0,0 +1,261 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FlaxEditor.GUI.ContextMenu;
|
||||
using FlaxEditor.GUI.Tree;
|
||||
using FlaxEditor.SceneGraph;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
|
||||
namespace FlaxEditor.Windows.Assets
|
||||
{
|
||||
public sealed partial class PrefabWindow
|
||||
{
|
||||
/// <summary>
|
||||
/// The custom implementation of the root node for the scene graph.
|
||||
/// </summary>
|
||||
public class CustomRootNode : RootNode
|
||||
{
|
||||
private readonly PrefabWindow _window;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CustomRootNode"/> class.
|
||||
/// </summary>
|
||||
/// <param name="window">The window.</param>
|
||||
public CustomRootNode(PrefabWindow window)
|
||||
{
|
||||
_window = window;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Spawn(Actor actor, Actor parent)
|
||||
{
|
||||
_window.Spawn(actor, parent);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Undo Undo => _window.Undo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The prefab hierarchy tree control.
|
||||
/// </summary>
|
||||
/// <seealso cref="Tree" />
|
||||
public class PrefabTree : Tree
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PrefabTree"/> class.
|
||||
/// </summary>
|
||||
public PrefabTree()
|
||||
: base(true)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when prefab hierarchy panel wants to show the context menu. Allows to add custom options. Applies to all prefab windows.
|
||||
/// </summary>
|
||||
public static event Action<ContextMenu> ContextMenuShow;
|
||||
|
||||
/// <summary>
|
||||
/// Creates the context menu for the current objects selection.
|
||||
/// </summary>
|
||||
/// <returns>The context menu.</returns>
|
||||
private ContextMenu CreateContextMenu()
|
||||
{
|
||||
// Prepare
|
||||
|
||||
bool hasSthSelected = Selection.Count > 0;
|
||||
bool isSingleActorSelected = Selection.Count == 1 && Selection[0] is ActorNode;
|
||||
bool isRootSelected = isSingleActorSelected && Selection[0] == Graph.Main;
|
||||
|
||||
// Create popup
|
||||
|
||||
var contextMenu = new ContextMenu();
|
||||
contextMenu.MinimumWidth = 120;
|
||||
|
||||
// Basic editing options
|
||||
|
||||
var b = contextMenu.AddButton("Rename", Rename);
|
||||
b.Enabled = isSingleActorSelected;
|
||||
|
||||
b = contextMenu.AddButton("Duplicate", Duplicate);
|
||||
b.Enabled = hasSthSelected && !isRootSelected;
|
||||
|
||||
b = contextMenu.AddButton("Delete", Delete);
|
||||
b.Enabled = hasSthSelected && !isRootSelected;
|
||||
|
||||
contextMenu.AddSeparator();
|
||||
b = contextMenu.AddButton("Copy", Copy);
|
||||
b.Enabled = hasSthSelected;
|
||||
|
||||
b.Enabled = hasSthSelected;
|
||||
contextMenu.AddButton("Paste", Paste);
|
||||
|
||||
b = contextMenu.AddButton("Cut", Cut);
|
||||
b.Enabled = hasSthSelected && !isRootSelected;
|
||||
|
||||
b = contextMenu.AddButton("Set Root", SetRoot);
|
||||
b.Enabled = isSingleActorSelected && !isRootSelected;
|
||||
|
||||
// Prefab options
|
||||
|
||||
contextMenu.AddSeparator();
|
||||
|
||||
b = contextMenu.AddButton("Create Prefab", Editor.Prefabs.CreatePrefab);
|
||||
b.Enabled = isSingleActorSelected &&
|
||||
(Selection[0] as ActorNode).CanCreatePrefab &&
|
||||
Editor.Windows.ContentWin.CurrentViewFolder.CanHaveAssets;
|
||||
|
||||
bool hasPrefabLink = isSingleActorSelected && (Selection[0] as ActorNode).HasPrefabLink;
|
||||
|
||||
b = contextMenu.AddButton("Select Prefab", Editor.Prefabs.SelectPrefab);
|
||||
b.Enabled = hasPrefabLink;
|
||||
|
||||
// Spawning actors options
|
||||
|
||||
contextMenu.AddSeparator();
|
||||
var spawnMenu = contextMenu.AddChildMenu("New");
|
||||
var newActorCm = spawnMenu.ContextMenu;
|
||||
for (int i = 0; i < SceneTreeWindow.SpawnActorsGroups.Length; i++)
|
||||
{
|
||||
var group = SceneTreeWindow.SpawnActorsGroups[i];
|
||||
|
||||
if (group.Types.Length == 1)
|
||||
{
|
||||
var type = group.Types[0].Value;
|
||||
newActorCm.AddButton(group.Types[0].Key, () => Spawn(type));
|
||||
}
|
||||
else
|
||||
{
|
||||
var groupCm = newActorCm.AddChildMenu(group.Name).ContextMenu;
|
||||
for (int j = 0; j < group.Types.Length; j++)
|
||||
{
|
||||
var type = group.Types[j].Value;
|
||||
groupCm.AddButton(group.Types[j].Key, () => Spawn(type));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Custom options
|
||||
|
||||
ContextMenuShow?.Invoke(contextMenu);
|
||||
|
||||
return contextMenu;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the context menu on a given location (in the given control coordinates).
|
||||
/// </summary>
|
||||
/// <param name="parent">The parent control.</param>
|
||||
/// <param name="location">The location (within a given control).</param>
|
||||
private void ShowContextMenu(Control parent, ref Vector2 location)
|
||||
{
|
||||
var contextMenu = CreateContextMenu();
|
||||
|
||||
contextMenu.Show(parent, location);
|
||||
}
|
||||
|
||||
private void Rename()
|
||||
{
|
||||
((ActorNode)Selection[0]).TreeNode.StartRenaming();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns the specified actor to the prefab (adds actor to root).
|
||||
/// </summary>
|
||||
/// <param name="actor">The actor.</param>
|
||||
public void Spawn(Actor actor)
|
||||
{
|
||||
// Link to parent
|
||||
Actor parentActor = null;
|
||||
if (Selection.Count > 0 && Selection[0] is ActorNode actorNode)
|
||||
{
|
||||
parentActor = actorNode.Actor;
|
||||
actorNode.TreeNode.Expand();
|
||||
}
|
||||
if (parentActor == null)
|
||||
{
|
||||
parentActor = Graph.MainActor;
|
||||
}
|
||||
if (parentActor != null)
|
||||
{
|
||||
// Use the same location
|
||||
actor.Transform = parentActor.Transform;
|
||||
|
||||
// Rename actor to identify it easily
|
||||
actor.Name = StringUtils.IncrementNameNumber(actor.GetType().Name, x => parentActor.GetChild(x) == null);
|
||||
}
|
||||
|
||||
// Spawn it
|
||||
Spawn(actor, parentActor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns the actor of the specified type to the prefab (adds actor to root).
|
||||
/// </summary>
|
||||
/// <param name="type">The actor type.</param>
|
||||
public void Spawn(Type type)
|
||||
{
|
||||
// Create actor
|
||||
Actor actor = (Actor)FlaxEngine.Object.New(type);
|
||||
|
||||
// Spawn it
|
||||
Spawn(actor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns the specified actor.
|
||||
/// </summary>
|
||||
/// <param name="actor">The actor.</param>
|
||||
/// <param name="parent">The parent.</param>
|
||||
public void Spawn(Actor actor, Actor parent)
|
||||
{
|
||||
if (actor == null)
|
||||
throw new ArgumentNullException(nameof(actor));
|
||||
if (parent == null)
|
||||
throw new ArgumentNullException(nameof(parent));
|
||||
|
||||
// Link it
|
||||
actor.Parent = parent;
|
||||
|
||||
// Peek spawned node
|
||||
var actorNode = SceneGraphFactory.FindNode(actor.ID) as ActorNode ?? SceneGraphFactory.BuildActorNode(actor);
|
||||
if (actorNode == null)
|
||||
throw new InvalidOperationException("Failed to create scene node for the spawned actor.");
|
||||
var parentNode = SceneGraphFactory.FindNode(parent.ID) as ActorNode;
|
||||
if (parentNode == null)
|
||||
throw new InvalidOperationException("Missing scene graph node for the spawned parent actor.");
|
||||
actorNode.ParentNode = parentNode;
|
||||
|
||||
// Call post spawn action (can possibly setup custom default values)
|
||||
actorNode.PostSpawn();
|
||||
|
||||
// Create undo action
|
||||
var action = new CustomDeleteActorsAction(new List<SceneGraphNode>(1) { actorNode }, true);
|
||||
Undo.AddAction(action);
|
||||
}
|
||||
|
||||
private void OnTreeRightClick(TreeNode node, Vector2 location)
|
||||
{
|
||||
// Skip if it's loading data
|
||||
if (Graph.Main == null)
|
||||
return;
|
||||
|
||||
ShowContextMenu(node, ref location);
|
||||
}
|
||||
|
||||
private void Update(ActorNode actorNode)
|
||||
{
|
||||
actorNode.TreeNode.OnNameChanged();
|
||||
actorNode.TreeNode.OnOrderInParentChanged();
|
||||
|
||||
for (int i = 0; i < actorNode.ChildNodes.Count; i++)
|
||||
{
|
||||
if (actorNode.ChildNodes[i] is ActorNode child)
|
||||
Update(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
188
Source/Editor/Windows/Assets/PrefabWindow.Selection.cs
Normal file
188
Source/Editor/Windows/Assets/PrefabWindow.Selection.cs
Normal file
@@ -0,0 +1,188 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FlaxEditor.GUI.Tree;
|
||||
using FlaxEditor.SceneGraph;
|
||||
using FlaxEditor.SceneGraph.GUI;
|
||||
using FlaxEngine;
|
||||
|
||||
namespace FlaxEditor.Windows.Assets
|
||||
{
|
||||
public sealed partial class PrefabWindow
|
||||
{
|
||||
/// <summary>
|
||||
/// The current selection (readonly).
|
||||
/// </summary>
|
||||
public readonly List<SceneGraphNode> Selection = new List<SceneGraphNode>();
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when selection gets changed.
|
||||
/// </summary>
|
||||
public event Action SelectionChanged;
|
||||
|
||||
private void OnTreeSelectedChanged(List<TreeNode> before, List<TreeNode> after)
|
||||
{
|
||||
// Check if lock events
|
||||
if (_isUpdatingSelection)
|
||||
return;
|
||||
|
||||
if (after.Count > 0)
|
||||
{
|
||||
// Get actors from nodes
|
||||
var actors = new List<SceneGraphNode>(after.Count);
|
||||
for (int i = 0; i < after.Count; i++)
|
||||
{
|
||||
if (after[i] is ActorTreeNode node && node.Actor)
|
||||
actors.Add(node.ActorNode);
|
||||
}
|
||||
|
||||
// Select
|
||||
Select(actors);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Deselect
|
||||
Deselect();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when selection gets changed.
|
||||
/// </summary>
|
||||
/// <param name="before">The selection before the change.</param>
|
||||
public void OnSelectionChanged(SceneGraphNode[] before)
|
||||
{
|
||||
Undo.AddAction(new SelectionChangeAction(before, Selection.ToArray(), OnSelectionUndo));
|
||||
|
||||
OnSelectionChanges();
|
||||
}
|
||||
|
||||
private void OnSelectionUndo(SceneGraphNode[] toSelect)
|
||||
{
|
||||
Selection.Clear();
|
||||
Selection.AddRange(toSelect);
|
||||
|
||||
OnSelectionChanges();
|
||||
}
|
||||
|
||||
private void OnSelectionChanges()
|
||||
{
|
||||
_isUpdatingSelection = true;
|
||||
|
||||
// Update tree
|
||||
var selection = Selection;
|
||||
if (selection.Count == 0)
|
||||
{
|
||||
_tree.Deselect();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Find nodes to select
|
||||
var nodes = new List<TreeNode>(selection.Count);
|
||||
for (int i = 0; i < selection.Count; i++)
|
||||
{
|
||||
if (selection[i] is ActorNode node)
|
||||
{
|
||||
nodes.Add(node.TreeNode);
|
||||
}
|
||||
}
|
||||
|
||||
// Select nodes
|
||||
_tree.Select(nodes);
|
||||
|
||||
// For single node selected scroll view so user can see it
|
||||
if (nodes.Count == 1)
|
||||
{
|
||||
ScrollViewTo(nodes[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// Update properties editor
|
||||
var objects = Selection.ConvertAll(x => x.EditableObject).Distinct();
|
||||
_propertiesEditor.Select(objects);
|
||||
|
||||
_isUpdatingSelection = false;
|
||||
|
||||
// Send event
|
||||
SelectionChanged?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects the specified nodes collection.
|
||||
/// </summary>
|
||||
/// <param name="nodes">The nodes.</param>
|
||||
public void Select(List<SceneGraphNode> nodes)
|
||||
{
|
||||
if (nodes == null || nodes.Count == 0)
|
||||
{
|
||||
Deselect();
|
||||
return;
|
||||
}
|
||||
if (Utils.ArraysEqual(Selection, nodes))
|
||||
return;
|
||||
|
||||
var before = Selection.ToArray();
|
||||
Selection.Clear();
|
||||
Selection.AddRange(nodes);
|
||||
|
||||
OnSelectionChanged(before);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects the specified node.
|
||||
/// </summary>
|
||||
/// <param name="node">The node.</param>
|
||||
/// <param name="additive">if set to <c>true</c> will use additive mode, otherwise will clear previous selection.</param>
|
||||
public void Select(SceneGraphNode node, bool additive = false)
|
||||
{
|
||||
if (node == null)
|
||||
{
|
||||
Deselect();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if won't change
|
||||
if (!additive && Selection.Count == 1 && Selection[0] == node)
|
||||
return;
|
||||
if (additive && Selection.Contains(node))
|
||||
return;
|
||||
|
||||
var before = Selection.ToArray();
|
||||
if (!additive)
|
||||
Selection.Clear();
|
||||
Selection.Add(node);
|
||||
|
||||
OnSelectionChanged(before);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deselects the specified node.
|
||||
/// </summary>
|
||||
public void Deselect(SceneGraphNode node)
|
||||
{
|
||||
if (!Selection.Contains(node))
|
||||
return;
|
||||
|
||||
var before = Selection.ToArray();
|
||||
Selection.Remove(node);
|
||||
|
||||
OnSelectionChanged(before);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the selection.
|
||||
/// </summary>
|
||||
public void Deselect()
|
||||
{
|
||||
if (Selection.Count == 0)
|
||||
return;
|
||||
|
||||
var before = Selection.ToArray();
|
||||
Selection.Clear();
|
||||
|
||||
OnSelectionChanged(before);
|
||||
}
|
||||
}
|
||||
}
|
||||
466
Source/Editor/Windows/Assets/PrefabWindow.cs
Normal file
466
Source/Editor/Windows/Assets/PrefabWindow.cs
Normal file
@@ -0,0 +1,466 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Xml;
|
||||
using FlaxEditor.Content;
|
||||
using FlaxEditor.CustomEditors;
|
||||
using FlaxEditor.Gizmo;
|
||||
using FlaxEditor.GUI;
|
||||
using FlaxEditor.SceneGraph;
|
||||
using FlaxEditor.Viewport;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
|
||||
namespace FlaxEditor.Windows.Assets
|
||||
{
|
||||
/// <summary>
|
||||
/// Prefab window allows to view and edit <see cref="Prefab"/> asset.
|
||||
/// </summary>
|
||||
/// <seealso cref="Prefab" />
|
||||
/// <seealso cref="FlaxEditor.Windows.Assets.AssetEditorWindow" />
|
||||
public sealed partial class PrefabWindow : AssetEditorWindowBase<Prefab>
|
||||
{
|
||||
private readonly SplitPanel _split1;
|
||||
private readonly SplitPanel _split2;
|
||||
private TextBox _searchBox;
|
||||
private readonly PrefabTree _tree;
|
||||
private readonly PrefabWindowViewport _viewport;
|
||||
private readonly CustomEditorPresenter _propertiesEditor;
|
||||
|
||||
private readonly ToolStripButton _saveButton;
|
||||
private readonly ToolStripButton _toolStripUndo;
|
||||
private readonly ToolStripButton _toolStripRedo;
|
||||
private readonly ToolStripButton _toolStripTranslate;
|
||||
private readonly ToolStripButton _toolStripRotate;
|
||||
private readonly ToolStripButton _toolStripScale;
|
||||
private readonly ToolStripButton _toolStripLiveReload;
|
||||
|
||||
private Undo _undo;
|
||||
private bool _focusCamera;
|
||||
private bool _liveReload = false;
|
||||
private bool _isUpdatingSelection, _isScriptsReloading;
|
||||
private DateTime _modifiedTime = DateTime.MinValue;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the prefab hierarchy tree control.
|
||||
/// </summary>
|
||||
public PrefabTree Tree => _tree;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the viewport.
|
||||
/// </summary>
|
||||
public PrefabWindowViewport Viewport => _viewport;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the undo system used by this window for changes tracking.
|
||||
/// </summary>
|
||||
public Undo Undo => _undo;
|
||||
|
||||
/// <summary>
|
||||
/// The local scene nodes graph used by the prefab editor.
|
||||
/// </summary>
|
||||
public readonly LocalSceneGraph Graph;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether use live reloading for the prefab changes (applies prefab changes on modification by auto).
|
||||
/// </summary>
|
||||
public bool LiveReload
|
||||
{
|
||||
get => _liveReload;
|
||||
set
|
||||
{
|
||||
if (_liveReload != value)
|
||||
{
|
||||
_liveReload = value;
|
||||
|
||||
UpdateToolstrip();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the live reload timeout. It defines the time to apply prefab changes after modification.
|
||||
/// </summary>
|
||||
public TimeSpan LiveReloadTimeout { get; set; } = TimeSpan.FromMilliseconds(100);
|
||||
|
||||
/// <inheritdoc />
|
||||
public PrefabWindow(Editor editor, AssetItem item)
|
||||
: base(editor, item)
|
||||
{
|
||||
// Undo
|
||||
_undo = new Undo();
|
||||
_undo.UndoDone += OnUndoEvent;
|
||||
_undo.RedoDone += OnUndoEvent;
|
||||
_undo.ActionDone += OnUndoEvent;
|
||||
|
||||
// Split Panel 1
|
||||
_split1 = new SplitPanel(Orientation.Horizontal, ScrollBars.Both, ScrollBars.None)
|
||||
{
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
Offsets = new Margin(0, 0, _toolstrip.Bottom, 0),
|
||||
SplitterValue = 0.2f,
|
||||
Parent = this
|
||||
};
|
||||
|
||||
// Split Panel 2
|
||||
_split2 = new SplitPanel(Orientation.Horizontal, ScrollBars.None, ScrollBars.Vertical)
|
||||
{
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
Offsets = Margin.Zero,
|
||||
SplitterValue = 0.6f,
|
||||
Parent = _split1.Panel2
|
||||
};
|
||||
|
||||
// Prefab structure tree searching query input box
|
||||
var headerPanel = new ContainerControl
|
||||
{
|
||||
AnchorPreset = AnchorPresets.HorizontalStretchTop,
|
||||
IsScrollable = true,
|
||||
Offsets = new Margin(0, 0, 0, 18 + 6),
|
||||
Parent = _split1.Panel1,
|
||||
};
|
||||
_searchBox = new TextBox
|
||||
{
|
||||
AnchorPreset = AnchorPresets.HorizontalStretchMiddle,
|
||||
WatermarkText = "Search...",
|
||||
Parent = headerPanel,
|
||||
Bounds = new Rectangle(4, 4, headerPanel.Width - 8, 18),
|
||||
};
|
||||
_searchBox.TextChanged += OnSearchBoxTextChanged;
|
||||
|
||||
// Prefab structure tree
|
||||
Graph = new LocalSceneGraph(new CustomRootNode(this));
|
||||
_tree = new PrefabTree
|
||||
{
|
||||
Y = headerPanel.Bottom,
|
||||
Margin = new Margin(0.0f, 0.0f, -16.0f, 0.0f), // Hide root node
|
||||
};
|
||||
_tree.AddChild(Graph.Root.TreeNode);
|
||||
_tree.SelectedChanged += OnTreeSelectedChanged;
|
||||
_tree.RightClick += OnTreeRightClick;
|
||||
_tree.Parent = _split1.Panel1;
|
||||
|
||||
// Prefab viewport
|
||||
_viewport = new PrefabWindowViewport(this)
|
||||
{
|
||||
Parent = _split2.Panel1
|
||||
};
|
||||
_viewport.TransformGizmo.ModeChanged += UpdateToolstrip;
|
||||
|
||||
// Prefab properties editor
|
||||
_propertiesEditor = new CustomEditorPresenter(_undo);
|
||||
_propertiesEditor.Panel.Parent = _split2.Panel2;
|
||||
_propertiesEditor.Modified += MarkAsEdited;
|
||||
|
||||
// Toolstrip
|
||||
_saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save32, Save).LinkTooltip("Save");
|
||||
_toolstrip.AddSeparator();
|
||||
_toolStripUndo = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo32, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)");
|
||||
_toolStripRedo = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo32, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)");
|
||||
_toolstrip.AddSeparator();
|
||||
_toolStripTranslate = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Translate32, () => _viewport.TransformGizmo.ActiveMode = TransformGizmo.Mode.Translate).LinkTooltip("Change Gizmo tool mode to Translate (1)");
|
||||
_toolStripRotate = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Rotate32, () => _viewport.TransformGizmo.ActiveMode = TransformGizmo.Mode.Rotate).LinkTooltip("Change Gizmo tool mode to Rotate (2)");
|
||||
_toolStripScale = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Scale32, () => _viewport.TransformGizmo.ActiveMode = TransformGizmo.Mode.Scale).LinkTooltip("Change Gizmo tool mode to Scale (3)");
|
||||
_toolstrip.AddSeparator();
|
||||
_toolStripLiveReload = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Reload32, () => LiveReload = !LiveReload).SetChecked(true).SetAutoCheck(true).LinkTooltip("Live changes preview (applies prefab changes on modification by auto)");
|
||||
|
||||
Editor.Prefabs.PrefabApplied += OnPrefabApplied;
|
||||
ScriptsBuilder.ScriptsReloadBegin += OnScriptsReloadBegin;
|
||||
ScriptsBuilder.ScriptsReloadEnd += OnScriptsReloadEnd;
|
||||
|
||||
// Setup input actions
|
||||
InputActions.Add(options => options.Undo, () =>
|
||||
{
|
||||
_undo.PerformUndo();
|
||||
Focus();
|
||||
});
|
||||
InputActions.Add(options => options.Redo, () =>
|
||||
{
|
||||
_undo.PerformRedo();
|
||||
Focus();
|
||||
});
|
||||
InputActions.Add(options => options.Cut, Cut);
|
||||
InputActions.Add(options => options.Copy, Copy);
|
||||
InputActions.Add(options => options.Paste, Paste);
|
||||
InputActions.Add(options => options.Duplicate, Duplicate);
|
||||
InputActions.Add(options => options.Delete, Delete);
|
||||
}
|
||||
|
||||
private void OnSearchBoxTextChanged()
|
||||
{
|
||||
// Skip events during setup or init stuff
|
||||
if (IsLayoutLocked)
|
||||
return;
|
||||
|
||||
var root = Graph.Root;
|
||||
root.TreeNode.LockChildrenRecursive();
|
||||
|
||||
// Update tree
|
||||
var query = _searchBox.Text;
|
||||
root.TreeNode.UpdateFilter(query);
|
||||
|
||||
root.TreeNode.UnlockChildrenRecursive();
|
||||
PerformLayout();
|
||||
PerformLayout();
|
||||
}
|
||||
|
||||
private void OnScriptsReloadBegin()
|
||||
{
|
||||
_isScriptsReloading = true;
|
||||
|
||||
if (_asset == null || !_asset.IsLoaded)
|
||||
return;
|
||||
|
||||
Editor.Log("Reloading prefab editor data on scripts reload. Prefab: " + _asset.Path);
|
||||
|
||||
// Check if asset has been edited and not saved (and still has linked item)
|
||||
if (IsEdited)
|
||||
{
|
||||
// Ask user for further action
|
||||
var result = MessageBox.Show(
|
||||
string.Format("Asset \'{0}\' has been edited. Save before scripts reload?", _item.Path),
|
||||
"Save before reloading?",
|
||||
MessageBoxButtons.YesNo
|
||||
);
|
||||
if (result == DialogResult.OK || result == DialogResult.Yes)
|
||||
{
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
Deselect();
|
||||
Graph.MainActor = null;
|
||||
_viewport.Prefab = null;
|
||||
_undo?.Clear(); // TODO: maybe don't clear undo?
|
||||
}
|
||||
|
||||
private void OnScriptsReloadEnd()
|
||||
{
|
||||
_isScriptsReloading = false;
|
||||
|
||||
if (_asset == null || !_asset.IsLoaded)
|
||||
return;
|
||||
|
||||
// Restore
|
||||
_viewport.Prefab = _asset;
|
||||
Graph.MainActor = _viewport.Instance;
|
||||
Selection.Clear();
|
||||
Select(Graph.Main);
|
||||
Graph.Root.TreeNode.ExpandAll(true);
|
||||
_undo.Clear();
|
||||
ClearEditedFlag();
|
||||
}
|
||||
|
||||
private void OnUndoEvent(IUndoAction action)
|
||||
{
|
||||
// All undo actions modify the asset except selection change
|
||||
if (!(action is SelectionChangeAction))
|
||||
{
|
||||
MarkAsEdited();
|
||||
}
|
||||
|
||||
UpdateToolstrip();
|
||||
}
|
||||
|
||||
private void OnPrefabApplied(Prefab prefab, Actor instance)
|
||||
{
|
||||
if (prefab == Asset)
|
||||
{
|
||||
ClearEditedFlag();
|
||||
|
||||
_item.RefreshThumbnail();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Save()
|
||||
{
|
||||
// Check if don't need to push any new changes to the original asset
|
||||
if (!IsEdited)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
// Simply update changes
|
||||
Editor.Prefabs.ApplyAll(_viewport.Instance);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Disable live reload on error
|
||||
if (LiveReload)
|
||||
{
|
||||
LiveReload = false;
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void UpdateToolstrip()
|
||||
{
|
||||
var undoRedo = _undo;
|
||||
var gizmo = _viewport.TransformGizmo;
|
||||
|
||||
_saveButton.Enabled = IsEdited;
|
||||
_toolStripUndo.Enabled = undoRedo.CanUndo;
|
||||
_toolStripRedo.Enabled = undoRedo.CanRedo;
|
||||
//
|
||||
var gizmoMode = gizmo.ActiveMode;
|
||||
_toolStripTranslate.Checked = gizmoMode == TransformGizmo.Mode.Translate;
|
||||
_toolStripRotate.Checked = gizmoMode == TransformGizmo.Mode.Rotate;
|
||||
_toolStripScale.Checked = gizmoMode == TransformGizmo.Mode.Scale;
|
||||
//
|
||||
_toolStripLiveReload.Checked = _liveReload;
|
||||
|
||||
base.UpdateToolstrip();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnEditedState()
|
||||
{
|
||||
base.OnEditedState();
|
||||
|
||||
_modifiedTime = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnAssetLoaded()
|
||||
{
|
||||
// Skip during scripts reload to prevent issues
|
||||
if (_isScriptsReloading)
|
||||
{
|
||||
base.OnAssetLoaded();
|
||||
return;
|
||||
}
|
||||
|
||||
_viewport.Prefab = _asset;
|
||||
Graph.MainActor = _viewport.Instance;
|
||||
_focusCamera = true;
|
||||
Selection.Clear();
|
||||
Select(Graph.Main);
|
||||
Graph.Root.TreeNode.ExpandAll(true);
|
||||
|
||||
_undo.Clear();
|
||||
ClearEditedFlag();
|
||||
|
||||
base.OnAssetLoaded();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void UnlinkItem()
|
||||
{
|
||||
Deselect();
|
||||
Graph.MainActor = null;
|
||||
_viewport.Prefab = null;
|
||||
_undo?.Clear();
|
||||
|
||||
base.UnlinkItem();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Graph.Main != null)
|
||||
{
|
||||
// Due to fact that actors in prefab editor are only created but not added to gameplay
|
||||
// we have to manually update some data (Level events work only for actors in a gameplay)
|
||||
Update(Graph.Main);
|
||||
}
|
||||
|
||||
base.Update(deltaTime);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Info
|
||||
Editor.LogWarning("Error when updating prefab window for " + _asset);
|
||||
Editor.LogWarning(ex);
|
||||
|
||||
// Refresh
|
||||
Deselect();
|
||||
Graph.MainActor = null;
|
||||
_viewport.Prefab = null;
|
||||
if (_asset.IsLoaded)
|
||||
{
|
||||
_viewport.Prefab = _asset;
|
||||
Graph.MainActor = _viewport.Instance;
|
||||
Selection.Clear();
|
||||
Select(Graph.Main);
|
||||
Graph.Root.TreeNode.ExpandAll(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto fit
|
||||
if (_focusCamera && _viewport.Task.FrameCount > 1)
|
||||
{
|
||||
_focusCamera = false;
|
||||
|
||||
BoundingSphere bounds;
|
||||
Editor.GetActorEditorSphere(_viewport.Instance, out bounds);
|
||||
_viewport.ViewPosition = bounds.Center - _viewport.ViewDirection * (bounds.Radius * 1.2f);
|
||||
}
|
||||
|
||||
// Auto save
|
||||
if (IsEdited && LiveReload && DateTime.Now - _modifiedTime > LiveReloadTimeout)
|
||||
{
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool UseLayoutData => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLayoutSerialize(XmlWriter writer)
|
||||
{
|
||||
writer.WriteAttributeString("Split1", _split1.SplitterValue.ToString());
|
||||
writer.WriteAttributeString("Split2", _split2.SplitterValue.ToString());
|
||||
writer.WriteAttributeString("LiveReload", LiveReload.ToString());
|
||||
writer.WriteAttributeString("GizmoMode", Viewport.TransformGizmo.ActiveMode.ToString());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLayoutDeserialize(XmlElement node)
|
||||
{
|
||||
float value1;
|
||||
|
||||
if (float.TryParse(node.GetAttribute("Split1"), out value1))
|
||||
_split1.SplitterValue = value1;
|
||||
if (float.TryParse(node.GetAttribute("Split2"), out value1))
|
||||
_split2.SplitterValue = value1;
|
||||
|
||||
bool value2;
|
||||
|
||||
if (bool.TryParse(node.GetAttribute("LiveReload"), out value2))
|
||||
LiveReload = value2;
|
||||
|
||||
TransformGizmo.Mode value3;
|
||||
if (Enum.TryParse(node.GetAttribute("GizmoMode"), out value3))
|
||||
Viewport.TransformGizmo.ActiveMode = value3;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLayoutDeserialize()
|
||||
{
|
||||
_split1.SplitterValue = 0.2f;
|
||||
_split2.SplitterValue = 0.6f;
|
||||
LiveReload = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnDestroy()
|
||||
{
|
||||
Editor.Prefabs.PrefabApplied -= OnPrefabApplied;
|
||||
ScriptsBuilder.ScriptsReloadBegin -= OnScriptsReloadBegin;
|
||||
ScriptsBuilder.ScriptsReloadEnd -= OnScriptsReloadEnd;
|
||||
|
||||
_undo.Dispose();
|
||||
Graph.Dispose();
|
||||
|
||||
base.OnDestroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
50
Source/Editor/Windows/Assets/PreviewsCacheWindow.cs
Normal file
50
Source/Editor/Windows/Assets/PreviewsCacheWindow.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using FlaxEditor.Content;
|
||||
using FlaxEditor.Viewport.Previews;
|
||||
using FlaxEngine.GUI;
|
||||
|
||||
namespace FlaxEditor.Windows.Assets
|
||||
{
|
||||
/// <summary>
|
||||
/// Editor window that allows to view <see cref="PreviewsCache"/> asset.
|
||||
/// </summary>
|
||||
/// <seealso cref="PreviewsCache" />
|
||||
/// <seealso cref="FlaxEditor.Windows.Assets.AssetEditorWindow" />
|
||||
public sealed class PreviewsCacheWindow : AssetEditorWindowBase<PreviewsCache>
|
||||
{
|
||||
private readonly TexturePreview _preview;
|
||||
|
||||
/// <inheritdoc />
|
||||
public PreviewsCacheWindow(Editor editor, AssetItem item)
|
||||
: base(editor, item)
|
||||
{
|
||||
// Texture preview
|
||||
_preview = new TexturePreview(true)
|
||||
{
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
Offsets = new Margin(0, 0, _toolstrip.Bottom, 0),
|
||||
Parent = this
|
||||
};
|
||||
|
||||
// Toolstrip
|
||||
_toolstrip.AddButton(editor.Icons.PageScale32, _preview.CenterView).LinkTooltip("Center view");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void UnlinkItem()
|
||||
{
|
||||
_preview.Asset = null;
|
||||
|
||||
base.UnlinkItem();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnAssetLinked()
|
||||
{
|
||||
_preview.Asset = _asset;
|
||||
|
||||
base.OnAssetLinked();
|
||||
}
|
||||
}
|
||||
}
|
||||
926
Source/Editor/Windows/Assets/SceneAnimationWindow.cs
Normal file
926
Source/Editor/Windows/Assets/SceneAnimationWindow.cs
Normal file
@@ -0,0 +1,926 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Xml;
|
||||
using FlaxEditor.Content;
|
||||
using FlaxEditor.CustomEditors;
|
||||
using FlaxEditor.CustomEditors.Editors;
|
||||
using FlaxEditor.GUI;
|
||||
using FlaxEditor.GUI.Timeline;
|
||||
using FlaxEditor.Scripting;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
using Object = FlaxEngine.Object;
|
||||
|
||||
namespace FlaxEditor.Windows.Assets
|
||||
{
|
||||
/// <summary>
|
||||
/// Scene Animation window allows to view and edit <see cref="SceneAnimation"/> asset.
|
||||
/// Note: it uses ClonedAssetEditorWindowBase which is creating cloned asset to edit/preview.
|
||||
/// </summary>
|
||||
/// <seealso cref="SceneAnimation" />
|
||||
/// <seealso cref="FlaxEditor.Windows.Assets.AssetEditorWindow" />
|
||||
public sealed class SceneAnimationWindow : ClonedAssetEditorWindowBase<SceneAnimation>
|
||||
{
|
||||
struct StagingTexture
|
||||
{
|
||||
public GPUTexture Texture;
|
||||
public int AnimationFrame;
|
||||
public int TaskFrame;
|
||||
}
|
||||
|
||||
abstract class VideoOutput
|
||||
{
|
||||
public abstract void RenderFrame(GPUContext context, ref StagingTexture frame, RenderOptions options, GPUTexture output);
|
||||
|
||||
public abstract void CollectFrame(GPUContext context, ref StagingTexture frame, RenderOptions options);
|
||||
}
|
||||
|
||||
abstract class VideoOutputImage : VideoOutput
|
||||
{
|
||||
protected abstract string Extension { get; }
|
||||
|
||||
public override void RenderFrame(GPUContext context, ref StagingTexture frame, RenderOptions options, GPUTexture output)
|
||||
{
|
||||
// Copy texture back to the staging texture
|
||||
context.CopyTexture(frame.Texture, 0, 0, 0, 0, output, 0);
|
||||
}
|
||||
|
||||
public override void CollectFrame(GPUContext context, ref StagingTexture frame, RenderOptions options)
|
||||
{
|
||||
// Save the staging texture to the file
|
||||
var path = options.GetOutputPath(ref frame) + Extension;
|
||||
Screenshot.Capture(frame.Texture, path);
|
||||
}
|
||||
}
|
||||
|
||||
class VideoOutputPng : VideoOutputImage
|
||||
{
|
||||
protected override string Extension => ".png";
|
||||
}
|
||||
|
||||
class VideoOutputBmp : VideoOutputImage
|
||||
{
|
||||
protected override string Extension => ".bmp";
|
||||
}
|
||||
|
||||
class VideoOutputJpg : VideoOutputImage
|
||||
{
|
||||
protected override string Extension => ".jpg";
|
||||
}
|
||||
|
||||
sealed class VideoOutputEditor : ObjectSwitcherEditor
|
||||
{
|
||||
public override DisplayStyle Style => DisplayStyle.InlineIntoParent;
|
||||
|
||||
protected override OptionType[] Options => new[]
|
||||
{
|
||||
new OptionType("Image (.png)", typeof(VideoOutputPng)),
|
||||
new OptionType("Image (.bmp)", typeof(VideoOutputBmp)),
|
||||
new OptionType("Image (.jpg)", typeof(VideoOutputJpg)),
|
||||
};
|
||||
|
||||
protected override string TypeComboBoxName => "Video Output Type";
|
||||
}
|
||||
|
||||
enum ResolutionModes
|
||||
{
|
||||
[EditorDisplay(null, "640x480 (VGA)")]
|
||||
_640x480,
|
||||
|
||||
[EditorDisplay(null, "1280x720 (HD)")]
|
||||
_1280x720,
|
||||
|
||||
[EditorDisplay(null, "1920x1080 (Full HD)")]
|
||||
_1920x1080,
|
||||
|
||||
[EditorDisplay(null, "3840x2160 (4K)")]
|
||||
_3840x2160,
|
||||
|
||||
[EditorDisplay(null, "7680x4320 (8K)")]
|
||||
_7680x4320,
|
||||
|
||||
Custom,
|
||||
}
|
||||
|
||||
[CustomEditor(typeof(RenderOptionsEditor))]
|
||||
sealed class RenderOptions
|
||||
{
|
||||
[HideInEditor, NoSerialize]
|
||||
public SceneAnimation Animation;
|
||||
|
||||
[EditorDisplay("Options"), EditorOrder(0)]
|
||||
[Tooltip("The output video format type and options.")]
|
||||
[CustomEditor(typeof(VideoOutputEditor))]
|
||||
public VideoOutput VideoOutputFormat = new VideoOutputJpg();
|
||||
|
||||
[EditorDisplay("Options"), EditorOrder(10)]
|
||||
[Tooltip("The output video resolution (in pixels). Use Custom option to specify it manually.")]
|
||||
public ResolutionModes Resolution = ResolutionModes._1920x1080;
|
||||
|
||||
public bool IsCustomResolution => Resolution == ResolutionModes.Custom;
|
||||
|
||||
[EditorDisplay("Options"), EditorOrder(20)]
|
||||
[Tooltip("The custom output video width (in pixels).")]
|
||||
[VisibleIf(nameof(IsCustomResolution)), Limit(1, 8192)]
|
||||
public int Width = 1280;
|
||||
|
||||
[EditorDisplay("Options"), EditorOrder(21)]
|
||||
[Tooltip("The custom output video height (in pixels).")]
|
||||
[VisibleIf(nameof(IsCustomResolution)), Limit(1, 8192)]
|
||||
public int Height = 720;
|
||||
|
||||
[EditorDisplay("Options"), EditorOrder(40)]
|
||||
[Tooltip("The animation update rate (amount of frames per second).")]
|
||||
[Limit(10, 240)]
|
||||
public float FrameRate = 60.0f;
|
||||
|
||||
[EditorDisplay("Options"), EditorOrder(50)]
|
||||
[Tooltip("The animation playback start (in seconds). Can be used to trim rendered movie.")]
|
||||
[Limit(0)]
|
||||
public float StartTime = 0.0f;
|
||||
|
||||
[EditorDisplay("Options"), EditorOrder(60)]
|
||||
[Tooltip("The animation playback end time (in seconds). Can be used to trim rendered movie.")]
|
||||
[Limit(0)]
|
||||
public float EndTime;
|
||||
|
||||
[EditorDisplay("Options", "Warm Up Time"), EditorOrder(70)]
|
||||
[Tooltip("The rendering warmup time to wait before starting recording (in seconds). Can be used to pre-render few frames for the initial camera shot so all temporal visual effects are having enough history samples to give better-looking results.")]
|
||||
[Limit(0, 10)]
|
||||
public float WarmUpTime = 0.4f;
|
||||
|
||||
[EditorDisplay("Output"), EditorOrder(100)]
|
||||
[Tooltip("The output folder for the rendering process artifact files. Relative to the project folder or absolute path.")]
|
||||
public string OutputDirectory = "Output/Render";
|
||||
|
||||
[EditorDisplay("Output"), EditorOrder(110)]
|
||||
[Tooltip("The output file name format for the rendering process artifact files. Can be fixed or use following format arguments: {animation}, {fps}, {frame}, {width}, {height}.")]
|
||||
public string Filename = "{animation}_{frame}";
|
||||
|
||||
public Int2 GetResolution()
|
||||
{
|
||||
switch (Resolution)
|
||||
{
|
||||
case ResolutionModes._640x480: return new Int2(640, 480);
|
||||
case ResolutionModes._1280x720: return new Int2(1280, 720);
|
||||
case ResolutionModes._1920x1080: return new Int2(1920, 1080);
|
||||
case ResolutionModes._3840x2160: return new Int2(3840, 2160);
|
||||
case ResolutionModes._7680x4320: return new Int2(7680, 4320);
|
||||
case ResolutionModes.Custom: return new Int2(Width, Height);
|
||||
default: throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
public string GetOutputPath(ref StagingTexture texture)
|
||||
{
|
||||
var dir = Path.IsPathRooted(OutputDirectory) ? OutputDirectory : Path.Combine(Globals.ProjectFolder, OutputDirectory);
|
||||
if (!Directory.Exists(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
var resolution = GetResolution();
|
||||
var filename = Filename;
|
||||
filename = filename.Replace("{animation}", Path.GetFileNameWithoutExtension(Animation.Path).Replace(' ', '_'));
|
||||
filename = filename.Replace("{fps}", FrameRate.ToString(CultureInfo.InvariantCulture));
|
||||
filename = filename.Replace("{frame}", texture.AnimationFrame.ToString());
|
||||
filename = filename.Replace("{width}", resolution.X.ToString());
|
||||
filename = filename.Replace("{height}", resolution.Y.ToString());
|
||||
return Path.Combine(dir, filename);
|
||||
}
|
||||
}
|
||||
|
||||
sealed class RenderOptionsEditor : GenericEditor
|
||||
{
|
||||
public override void Initialize(LayoutElementsContainer layout)
|
||||
{
|
||||
layout.Label("Scene Animation Rendering Utility", TextAlignment.Center);
|
||||
layout.Space(10.0f);
|
||||
|
||||
base.Initialize(layout);
|
||||
}
|
||||
}
|
||||
|
||||
sealed class RenderProgress : Progress.ProgressHandler
|
||||
{
|
||||
public void Start()
|
||||
{
|
||||
OnStart();
|
||||
OnUpdate(0.0f, "Warming up...");
|
||||
}
|
||||
|
||||
public void Update(float progress, int frame)
|
||||
{
|
||||
OnUpdate(progress, string.Format("Rendering scene animation (frame {0})...", frame));
|
||||
}
|
||||
|
||||
public void End()
|
||||
{
|
||||
OnEnd();
|
||||
}
|
||||
}
|
||||
|
||||
sealed class RenderEditorState : States.EditorState
|
||||
{
|
||||
private readonly RenderPopup _popup;
|
||||
|
||||
public RenderEditorState(Editor editor, RenderPopup popup)
|
||||
: base(editor)
|
||||
{
|
||||
_popup = popup;
|
||||
}
|
||||
|
||||
public override bool CanChangeScene => false;
|
||||
|
||||
public override bool CanEditContent => false;
|
||||
|
||||
public override bool CanEditScene => false;
|
||||
|
||||
public override bool CanEnterPlayMode => false;
|
||||
|
||||
public override bool CanReloadScripts => false;
|
||||
|
||||
public override bool CanUseToolbox => false;
|
||||
|
||||
public override bool CanUseUndoRedo => false;
|
||||
|
||||
public override void UpdateFPS()
|
||||
{
|
||||
Time.UpdateFPS = Time.DrawFPS = _popup.Options.FrameRate;
|
||||
Time.PhysicsFPS = 0;
|
||||
}
|
||||
}
|
||||
|
||||
sealed class RenderPopup : Panel
|
||||
{
|
||||
private enum States
|
||||
{
|
||||
Warmup,
|
||||
Render,
|
||||
Update,
|
||||
Staging,
|
||||
}
|
||||
|
||||
private const int FrameLatency = 3;
|
||||
|
||||
private SceneAnimationWindow _window;
|
||||
private RenderOptions _options;
|
||||
private CustomEditorPresenter _presenter;
|
||||
private bool _isRendering;
|
||||
private float _warmUpTimeLeft;
|
||||
private float _dt;
|
||||
private int _animationFrame;
|
||||
private bool _wasGamePaused;
|
||||
private SceneAnimationPlayer _player;
|
||||
private States _state;
|
||||
private readonly StagingTexture[] _stagingTextures = new StagingTexture[FrameLatency + 1];
|
||||
private RenderProgress _progress;
|
||||
private RenderEditorState _editorState;
|
||||
|
||||
public RenderOptions Options => _options;
|
||||
|
||||
public RenderPopup(SceneAnimationWindow window)
|
||||
: base(ScrollBars.Vertical)
|
||||
{
|
||||
_window = window;
|
||||
_options = new RenderOptions
|
||||
{
|
||||
Animation = window.OriginalAsset,
|
||||
EndTime = window.Timeline.Duration,
|
||||
};
|
||||
_presenter = new CustomEditorPresenter(null);
|
||||
_presenter.Panel.AnchorPreset = AnchorPresets.HorizontalStretchTop;
|
||||
_presenter.Panel.IsScrollable = true;
|
||||
_presenter.Panel.Parent = this;
|
||||
_presenter.AfterLayout += OnAfterLayout;
|
||||
_presenter.Select(_options);
|
||||
var cancelButton = new Button
|
||||
{
|
||||
Bounds = new Rectangle(4, 4, 60, 24),
|
||||
Text = "Cancel",
|
||||
TooltipText = "Cancel the rendering",
|
||||
Parent = this
|
||||
};
|
||||
cancelButton.Clicked += Close;
|
||||
|
||||
_window.Timeline.Enabled = false;
|
||||
_window.Timeline.Visible = false;
|
||||
_window._toolstrip.Enabled = false;
|
||||
|
||||
BackgroundColor = Style.Current.Background;
|
||||
Offsets = Margin.Zero;
|
||||
AnchorPreset = AnchorPresets.StretchAll;
|
||||
Parent = _window;
|
||||
}
|
||||
|
||||
private void OnAfterLayout(LayoutElementsContainer layout)
|
||||
{
|
||||
layout.Space(10);
|
||||
var button = layout.Button("Render").Button;
|
||||
button.TooltipText = "Start the Scene Animation rendering using the specified options";
|
||||
button.Clicked += StartRendering;
|
||||
}
|
||||
|
||||
private void StartRendering()
|
||||
{
|
||||
if (_isRendering)
|
||||
return;
|
||||
|
||||
var editor = Editor.Instance;
|
||||
|
||||
// Check if save the asset before rendering
|
||||
if (_window.IsEdited)
|
||||
{
|
||||
var result = MessageBox.Show("Save scene animation asset to file before rendering?", "Save?", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question);
|
||||
if (result == DialogResult.Yes)
|
||||
_window.Save();
|
||||
else if (result != DialogResult.No)
|
||||
return;
|
||||
}
|
||||
|
||||
// Update UI
|
||||
_isRendering = true;
|
||||
_presenter.Panel.Enabled = false;
|
||||
_presenter.BuildLayoutOnUpdate();
|
||||
|
||||
// Start rendering
|
||||
Editor.Log("Starting scene animation rendering " + _options.Animation);
|
||||
_dt = 1.0f / _options.FrameRate;
|
||||
_player = new SceneAnimationPlayer
|
||||
{
|
||||
Animation = _options.Animation,
|
||||
HideFlags = HideFlags.FullyHidden,
|
||||
RestoreStateOnStop = true,
|
||||
PlayOnStart = false,
|
||||
RandomStartTime = false,
|
||||
StartTime = _options.StartTime,
|
||||
UpdateMode = SceneAnimationPlayer.UpdateModes.Manual,
|
||||
};
|
||||
FlaxEngine.Scripting.Update += Tick;
|
||||
_wasGamePaused = Time.GamePaused;
|
||||
Time.GamePaused = false;
|
||||
Time.SetFixedDeltaTime(true, _dt);
|
||||
Time.UpdateFPS = Time.DrawFPS = _options.FrameRate;
|
||||
if (!Editor.IsPlayMode)
|
||||
Time.PhysicsFPS = 0;
|
||||
Level.SpawnActor(_player);
|
||||
var gameWin = editor.Windows.GameWin;
|
||||
var resolution = _options.GetResolution();
|
||||
gameWin.Viewport.CustomResolution = resolution;
|
||||
gameWin.Viewport.BackgroundColor = Color.Black;
|
||||
gameWin.Viewport.KeepAspectRatio = true;
|
||||
gameWin.Viewport.Task.PostRender += OnPostRender;
|
||||
if (!gameWin.Visible)
|
||||
gameWin.Show();
|
||||
_warmUpTimeLeft = _options.WarmUpTime;
|
||||
_animationFrame = 0;
|
||||
var stagingTextureDesc = GPUTextureDescription.New2D(resolution.X, resolution.Y, gameWin.Viewport.Task.Output.Format, GPUTextureFlags.None);
|
||||
stagingTextureDesc.Usage = GPUResourceUsage.StagingReadback;
|
||||
for (int i = 0; i < _stagingTextures.Length; i++)
|
||||
{
|
||||
_stagingTextures[i].Texture = new GPUTexture();
|
||||
_stagingTextures[i].Texture.Init(ref stagingTextureDesc);
|
||||
_stagingTextures[i].AnimationFrame = -1;
|
||||
_stagingTextures[i].TaskFrame = -1;
|
||||
}
|
||||
_player.Play();
|
||||
if (_warmUpTimeLeft > 0.0f)
|
||||
{
|
||||
// Start warmup time
|
||||
_player.Tick(0.0f);
|
||||
_player.Pause();
|
||||
_state = States.Warmup;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Render first frame
|
||||
_state = States.Render;
|
||||
}
|
||||
if (!Editor.IsPlayMode)
|
||||
{
|
||||
_editorState = new RenderEditorState(editor, this);
|
||||
editor.StateMachine.AddState(_editorState);
|
||||
editor.StateMachine.GoToState(_editorState);
|
||||
}
|
||||
_progress = new RenderProgress();
|
||||
editor.ProgressReporting.RegisterHandler(_progress);
|
||||
_progress.Start();
|
||||
}
|
||||
|
||||
private void Close()
|
||||
{
|
||||
CancelRendering();
|
||||
Dispose();
|
||||
}
|
||||
|
||||
private void CancelRendering()
|
||||
{
|
||||
if (!_isRendering)
|
||||
return;
|
||||
|
||||
var editor = Editor.Instance;
|
||||
|
||||
// End rendering
|
||||
Editor.Log("Ending scene animation rendering");
|
||||
FlaxEngine.Scripting.Update -= Tick;
|
||||
Time.SetFixedDeltaTime(false, 0.0f);
|
||||
Time.GamePaused = _wasGamePaused;
|
||||
if (_player)
|
||||
{
|
||||
_player.Stop();
|
||||
_player.Parent = null;
|
||||
Object.Destroy(ref _player);
|
||||
}
|
||||
_window.Editor.StateMachine.CurrentState.UpdateFPS();
|
||||
for (int i = 0; i < _stagingTextures.Length; i++)
|
||||
{
|
||||
_stagingTextures[i].Texture.ReleaseGPU();
|
||||
Object.Destroy(ref _stagingTextures[i].Texture);
|
||||
}
|
||||
_progress.End();
|
||||
editor.ProgressReporting.UnregisterHandler(_progress);
|
||||
if (_editorState != null)
|
||||
{
|
||||
editor.StateMachine.GoToState(editor.StateMachine.EditingSceneState);
|
||||
editor.StateMachine.RemoveState(_editorState);
|
||||
}
|
||||
|
||||
// Update UI
|
||||
var gameWin = editor.Windows.GameWin;
|
||||
gameWin.Viewport.CustomResolution = null;
|
||||
gameWin.Viewport.BackgroundColor = Color.Transparent;
|
||||
gameWin.Viewport.KeepAspectRatio = false;
|
||||
gameWin.Viewport.Task.PostRender -= OnPostRender;
|
||||
_isRendering = false;
|
||||
_presenter.Panel.Enabled = true;
|
||||
_presenter.BuildLayoutOnUpdate();
|
||||
}
|
||||
|
||||
private void Tick()
|
||||
{
|
||||
switch (_state)
|
||||
{
|
||||
case States.Warmup:
|
||||
_warmUpTimeLeft -= _dt;
|
||||
if (_warmUpTimeLeft <= 0.0f)
|
||||
{
|
||||
// Render first frame
|
||||
_player.Play();
|
||||
_warmUpTimeLeft = -1;
|
||||
_state = States.Render;
|
||||
}
|
||||
break;
|
||||
case States.Update:
|
||||
// Update scene animation with a fixed delta-time
|
||||
if (!_player.IsStopped)
|
||||
{
|
||||
var speed = _player.Speed * (_window?._timeline.Player?.Speed ?? 1.0f);
|
||||
if (speed <= 0.001f)
|
||||
{
|
||||
Editor.LogError("Scene Animation Player speed was nearly zero. Cannot continue rendering.");
|
||||
CancelRendering();
|
||||
break;
|
||||
}
|
||||
var dt = _dt * speed;
|
||||
_player.Tick(dt);
|
||||
_animationFrame++;
|
||||
Editor.Log("Tick anim by " + dt + " to frame " + _animationFrame + " into time " + _player.Time);
|
||||
_progress.Update((_player.Time - _options.StartTime) / (_options.EndTime - _options.StartTime), _animationFrame);
|
||||
}
|
||||
if (_player.IsStopped || _player.Time >= _options.EndTime)
|
||||
{
|
||||
// End rendering but perform reaming copies of the staging textures so all data is captured (from GPU to CPU)
|
||||
_state = States.Staging;
|
||||
break;
|
||||
}
|
||||
|
||||
// Now wait for the next frame to be rendered
|
||||
_state = States.Render;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPostRender(GPUContext context, RenderContext renderContext)
|
||||
{
|
||||
var gameWin = Editor.Instance.Windows.GameWin;
|
||||
var task = gameWin.Viewport.Task;
|
||||
var taskFrame = task.FrameCount;
|
||||
|
||||
// Check all staging textures for finished GPU to CPU transfers
|
||||
Array.Sort(_stagingTextures, (x, y) => x.AnimationFrame.CompareTo(y.AnimationFrame));
|
||||
for (int i = 1; i < _stagingTextures.Length; i++)
|
||||
{
|
||||
ref var stagingTexture = ref _stagingTextures[i];
|
||||
if (stagingTexture.TaskFrame != -1 && taskFrame - stagingTexture.TaskFrame >= FrameLatency)
|
||||
{
|
||||
_options.VideoOutputFormat.CollectFrame(context, ref stagingTexture, _options);
|
||||
stagingTexture.AnimationFrame = -1;
|
||||
stagingTexture.TaskFrame = -1;
|
||||
}
|
||||
}
|
||||
|
||||
switch (_state)
|
||||
{
|
||||
case States.Render:
|
||||
// Find the first unused staging texture
|
||||
int textureIdx = -1;
|
||||
for (int i = 1; i < _stagingTextures.Length; i++)
|
||||
{
|
||||
if (_stagingTextures[i].AnimationFrame == -1)
|
||||
{
|
||||
textureIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (textureIdx == -1)
|
||||
throw new Exception("Failed to get unused staging texture buffer.");
|
||||
|
||||
// Copy the backbuffer (GPU) to the staging (CPU)
|
||||
ref var stagingTexture = ref _stagingTextures[textureIdx];
|
||||
stagingTexture.AnimationFrame = _animationFrame;
|
||||
stagingTexture.TaskFrame = taskFrame;
|
||||
_options.VideoOutputFormat.RenderFrame(context, ref stagingTexture, _options, gameWin.Viewport.Task.Output);
|
||||
|
||||
// Now wait for the next animation frame to be updated
|
||||
_state = States.Update;
|
||||
break;
|
||||
case States.Staging:
|
||||
// Check if all staging textures has been synchronized
|
||||
if (_stagingTextures.All(x => x.AnimationFrame == -1))
|
||||
{
|
||||
CancelRendering();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnDestroy()
|
||||
{
|
||||
CancelRendering();
|
||||
_window.Timeline.Enabled = true;
|
||||
_window.Timeline.Visible = true;
|
||||
_window._toolstrip.Enabled = true;
|
||||
_window._popup = null;
|
||||
_window = null;
|
||||
_presenter = null;
|
||||
_options = null;
|
||||
|
||||
base.OnDestroy();
|
||||
}
|
||||
}
|
||||
|
||||
private SceneAnimationTimeline _timeline;
|
||||
private ToolStripButton _saveButton;
|
||||
private ToolStripButton _undoButton;
|
||||
private ToolStripButton _redoButton;
|
||||
private ToolStripButton _renderButton;
|
||||
private FlaxObjectRefPickerControl _previewPlayerPicker;
|
||||
private Undo _undo;
|
||||
private bool _tmpSceneAnimationIsDirty;
|
||||
private bool _isWaitingForTimelineLoad;
|
||||
private Guid _cachedPlayerId;
|
||||
private RenderPopup _popup;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the timeline editor.
|
||||
/// </summary>
|
||||
public SceneAnimationTimeline Timeline => _timeline;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the undo history context for this window.
|
||||
/// </summary>
|
||||
public Undo Undo => _undo;
|
||||
|
||||
/// <inheritdoc />
|
||||
public SceneAnimationWindow(Editor editor, AssetItem item)
|
||||
: base(editor, item)
|
||||
{
|
||||
// Undo
|
||||
_undo = new Undo();
|
||||
_undo.UndoDone += OnUndoRedo;
|
||||
_undo.RedoDone += OnUndoRedo;
|
||||
_undo.ActionDone += OnUndoRedo;
|
||||
|
||||
Level.ActorDeleted += OnActorDeleted;
|
||||
|
||||
// Timeline
|
||||
_timeline = new SceneAnimationTimeline(_undo)
|
||||
{
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
Offsets = new Margin(0, 0, _toolstrip.Bottom, 0),
|
||||
Parent = this,
|
||||
Enabled = false
|
||||
};
|
||||
_timeline.Modified += OnTimelineModified;
|
||||
_timeline.PlayerChanged += OnTimelinePlayerChanged;
|
||||
|
||||
// Toolstrip
|
||||
_saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save32, Save).LinkTooltip("Save");
|
||||
_toolstrip.AddSeparator();
|
||||
_undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo32, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)");
|
||||
_redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo32, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)");
|
||||
_toolstrip.AddSeparator();
|
||||
_renderButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Build32, OnRenderButtonClicked).LinkTooltip("Open the scene animation rendering utility...");
|
||||
_toolstrip.AddSeparator();
|
||||
_toolstrip.AddButton(editor.Icons.Docs32, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/animation/scene-animations/index.html")).LinkTooltip("See documentation to learn more");
|
||||
|
||||
// Preview player picker
|
||||
var previewPlayerPickerContainer = new ContainerControl();
|
||||
var previewPlayerPickerLabel = new Label
|
||||
{
|
||||
AnchorPreset = AnchorPresets.VerticalStretchLeft,
|
||||
VerticalAlignment = TextAlignment.Center,
|
||||
HorizontalAlignment = TextAlignment.Far,
|
||||
Parent = previewPlayerPickerContainer,
|
||||
Size = new Vector2(60.0f, _toolstrip.Height),
|
||||
Text = "Player:",
|
||||
TooltipText = "The current scene animation player actor to preview. Pick the player to debug it's playback.",
|
||||
};
|
||||
_previewPlayerPicker = new FlaxObjectRefPickerControl
|
||||
{
|
||||
Location = new Vector2(previewPlayerPickerLabel.Right + 4.0f, 8.0f),
|
||||
Width = 140.0f,
|
||||
Type = new ScriptType(typeof(SceneAnimationPlayer)),
|
||||
Parent = previewPlayerPickerContainer,
|
||||
};
|
||||
previewPlayerPickerContainer.Width = _previewPlayerPicker.Right + 2.0f;
|
||||
previewPlayerPickerContainer.Size = new Vector2(_previewPlayerPicker.Right + 2.0f, _toolstrip.Height);
|
||||
previewPlayerPickerContainer.Parent = _toolstrip;
|
||||
_previewPlayerPicker.CheckValid = OnCheckValid;
|
||||
_previewPlayerPicker.ValueChanged += OnPreviewPlayerPickerChanged;
|
||||
|
||||
// Setup input actions
|
||||
InputActions.Add(options => options.Undo, _undo.PerformUndo);
|
||||
InputActions.Add(options => options.Redo, _undo.PerformRedo);
|
||||
}
|
||||
|
||||
private void OnUndoRedo(IUndoAction action)
|
||||
{
|
||||
MarkAsEdited();
|
||||
UpdateToolstrip();
|
||||
}
|
||||
|
||||
private void OnRenderButtonClicked()
|
||||
{
|
||||
if (_popup != null)
|
||||
return;
|
||||
_popup = new RenderPopup(this);
|
||||
}
|
||||
|
||||
private bool OnCheckValid(Object obj, ScriptType type)
|
||||
{
|
||||
return obj is SceneAnimationPlayer player && player.Animation == OriginalAsset;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnSceneUnloading(Scene scene, Guid sceneId)
|
||||
{
|
||||
base.OnSceneUnloading(scene, sceneId);
|
||||
|
||||
if (scene == _timeline.Player?.Scene)
|
||||
{
|
||||
var id = _timeline.Player.ID;
|
||||
_timeline.Player = null;
|
||||
_cachedPlayerId = id;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnActorDeleted(Actor actor)
|
||||
{
|
||||
if (actor == _timeline.Player)
|
||||
{
|
||||
var id = actor.ID;
|
||||
_timeline.Player = null;
|
||||
_cachedPlayerId = id;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTimelinePlayerChanged()
|
||||
{
|
||||
_previewPlayerPicker.Value = _timeline.Player;
|
||||
_cachedPlayerId = _timeline.Player?.ID ?? Guid.Empty;
|
||||
}
|
||||
|
||||
private void OnPreviewPlayerPickerChanged()
|
||||
{
|
||||
_timeline.Player = _previewPlayerPicker.Value as SceneAnimationPlayer;
|
||||
}
|
||||
|
||||
private void OnTimelineModified()
|
||||
{
|
||||
_tmpSceneAnimationIsDirty = true;
|
||||
MarkAsEdited();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes temporary asset to see changes live when editing the timeline.
|
||||
/// </summary>
|
||||
/// <returns>True if cannot refresh it, otherwise false.</returns>
|
||||
public bool RefreshTempAsset()
|
||||
{
|
||||
if (_asset == null || _isWaitingForTimelineLoad)
|
||||
return true;
|
||||
|
||||
if (_timeline.IsModified)
|
||||
{
|
||||
_timeline.Save(_asset);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Save()
|
||||
{
|
||||
if (!IsEdited)
|
||||
return;
|
||||
|
||||
if (RefreshTempAsset())
|
||||
return;
|
||||
if (SaveToOriginal())
|
||||
return;
|
||||
|
||||
ClearEditedFlag();
|
||||
_item.RefreshThumbnail();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void UpdateToolstrip()
|
||||
{
|
||||
_saveButton.Enabled = IsEdited;
|
||||
_undoButton.Enabled = _undo.CanUndo;
|
||||
_redoButton.Enabled = _undo.CanRedo;
|
||||
_renderButton.Enabled = Level.IsAnySceneLoaded && (Editor.IsPlayMode || Editor.StateMachine.IsEditMode);
|
||||
|
||||
base.UpdateToolstrip();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void UnlinkItem()
|
||||
{
|
||||
_isWaitingForTimelineLoad = false;
|
||||
|
||||
base.UnlinkItem();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnAssetLinked()
|
||||
{
|
||||
_isWaitingForTimelineLoad = true;
|
||||
|
||||
base.OnAssetLinked();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnSceneLoaded(Scene scene, Guid sceneId)
|
||||
{
|
||||
base.OnSceneLoaded(scene, sceneId);
|
||||
|
||||
UpdateToolstrip();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnSceneUnloaded(Scene scene, Guid sceneId)
|
||||
{
|
||||
base.OnSceneUnloaded(scene, sceneId);
|
||||
|
||||
UpdateToolstrip();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnPlayBegin()
|
||||
{
|
||||
base.OnPlayBegin();
|
||||
|
||||
UpdateToolstrip();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnPlayEnd()
|
||||
{
|
||||
base.OnPlayEnd();
|
||||
|
||||
UpdateToolstrip();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnEditorStateChanged()
|
||||
{
|
||||
base.OnEditorStateChanged();
|
||||
|
||||
UpdateToolstrip();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
base.Update(deltaTime);
|
||||
|
||||
// Check if temporary asset need to be updated
|
||||
if (_tmpSceneAnimationIsDirty)
|
||||
{
|
||||
// Clear flag
|
||||
_tmpSceneAnimationIsDirty = false;
|
||||
|
||||
// Update
|
||||
RefreshTempAsset();
|
||||
}
|
||||
|
||||
// Check if need to load timeline
|
||||
if (_isWaitingForTimelineLoad && _asset.IsLoaded)
|
||||
{
|
||||
// Clear flag
|
||||
_isWaitingForTimelineLoad = false;
|
||||
|
||||
// Load timeline data from the asset
|
||||
_timeline.Load(_asset);
|
||||
|
||||
// Setup
|
||||
_undo.Clear();
|
||||
_timeline.Enabled = true;
|
||||
ClearEditedFlag();
|
||||
}
|
||||
|
||||
// Try to reassign the player
|
||||
if (_timeline.Player == null && _cachedPlayerId != Guid.Empty)
|
||||
{
|
||||
var obj = Object.TryFind<SceneAnimationPlayer>(ref _cachedPlayerId);
|
||||
if (obj)
|
||||
{
|
||||
_cachedPlayerId = Guid.Empty;
|
||||
if (obj.Animation == OriginalAsset)
|
||||
_timeline.Player = obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool UseLayoutData => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLayoutSerialize(XmlWriter writer)
|
||||
{
|
||||
writer.WriteAttributeString("TimelineSplitter", _timeline.Splitter.SplitterValue.ToString());
|
||||
writer.WriteAttributeString("TimeShowMode", _timeline.TimeShowMode.ToString());
|
||||
var id = _timeline.Player?.ID ?? _cachedPlayerId;
|
||||
writer.WriteAttributeString("SelectedPlayer", id.ToString());
|
||||
writer.WriteAttributeString("ShowPreviewValues", _timeline.ShowPreviewValues.ToString());
|
||||
writer.WriteAttributeString("ShowSelected3dTrack", _timeline.ShowSelected3dTrack.ToString());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLayoutDeserialize(XmlElement node)
|
||||
{
|
||||
float value1;
|
||||
Guid value2;
|
||||
Timeline.TimeShowModes value3;
|
||||
bool value4;
|
||||
|
||||
if (float.TryParse(node.GetAttribute("TimelineSplitter"), out value1))
|
||||
_timeline.Splitter.SplitterValue = value1;
|
||||
if (Guid.TryParse(node.GetAttribute("SelectedPlayer"), out value2))
|
||||
_cachedPlayerId = value2;
|
||||
if (Enum.TryParse(node.GetAttribute("TimeShowMode"), out value3))
|
||||
_timeline.TimeShowMode = value3;
|
||||
if (bool.TryParse(node.GetAttribute("ShowPreviewValues"), out value4))
|
||||
_timeline.ShowPreviewValues = value4;
|
||||
if (bool.TryParse(node.GetAttribute("ShowSelected3dTrack"), out value4))
|
||||
_timeline.ShowSelected3dTrack = value4;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLayoutDeserialize()
|
||||
{
|
||||
_timeline.Splitter.SplitterValue = 0.2f;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnDestroy()
|
||||
{
|
||||
Level.ActorDeleted -= OnActorDeleted;
|
||||
|
||||
if (_popup != null)
|
||||
{
|
||||
_popup.Dispose();
|
||||
_popup = null;
|
||||
}
|
||||
|
||||
if (_undo != null)
|
||||
{
|
||||
_undo.Enabled = false;
|
||||
_undo.Clear();
|
||||
_undo = null;
|
||||
}
|
||||
|
||||
_timeline = null;
|
||||
_saveButton = null;
|
||||
_undoButton = null;
|
||||
_redoButton = null;
|
||||
_renderButton = null;
|
||||
_previewPlayerPicker = null;
|
||||
|
||||
base.OnDestroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
311
Source/Editor/Windows/Assets/SkeletonMaskWindow.cs
Normal file
311
Source/Editor/Windows/Assets/SkeletonMaskWindow.cs
Normal file
@@ -0,0 +1,311 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System.Collections;
|
||||
using System.Xml;
|
||||
using FlaxEditor.Content;
|
||||
using FlaxEditor.CustomEditors;
|
||||
using FlaxEditor.CustomEditors.Editors;
|
||||
using FlaxEditor.CustomEditors.Elements;
|
||||
using FlaxEditor.GUI;
|
||||
using FlaxEditor.Viewport.Cameras;
|
||||
using FlaxEditor.Viewport.Previews;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
|
||||
namespace FlaxEditor.Windows.Assets
|
||||
{
|
||||
/// <summary>
|
||||
/// Editor window to view/modify <see cref="SkeletonMask"/> asset.
|
||||
/// </summary>
|
||||
/// <seealso cref="SkeletonMask" />
|
||||
/// <seealso cref="FlaxEditor.Windows.Assets.AssetEditorWindow" />
|
||||
public sealed class SkeletonMaskWindow : AssetEditorWindowBase<SkeletonMask>
|
||||
{
|
||||
/// <summary>
|
||||
/// The asset properties proxy object.
|
||||
/// </summary>
|
||||
[CustomEditor(typeof(ProxyEditor))]
|
||||
private sealed class PropertiesProxy
|
||||
{
|
||||
private SkeletonMaskWindow Window;
|
||||
private SkeletonMask Asset;
|
||||
|
||||
[EditorDisplay("Skeleton"), Tooltip("The skinned model asset used for the skeleton mask reference.")]
|
||||
public SkinnedModel Skeleton
|
||||
{
|
||||
get => Window._preview.SkinnedModel;
|
||||
set
|
||||
{
|
||||
if (value != Window._preview.SkinnedModel)
|
||||
{
|
||||
// Change skeleton, invalidate mask and request UI update
|
||||
Window._preview.SkinnedModel = value;
|
||||
Window._preview.NodesMask = null;
|
||||
Window._propertiesPresenter.BuildLayoutOnUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[HideInEditor]
|
||||
public bool[] NodesMask
|
||||
{
|
||||
get => Window._preview.NodesMask;
|
||||
set => Window._preview.NodesMask = value;
|
||||
}
|
||||
|
||||
public void OnLoad(SkeletonMaskWindow window)
|
||||
{
|
||||
// Link
|
||||
Window = window;
|
||||
Asset = window.Asset;
|
||||
|
||||
// Get data from the asset
|
||||
Skeleton = Asset.Skeleton;
|
||||
NodesMask = Asset.NodesMask;
|
||||
}
|
||||
|
||||
public void OnClean()
|
||||
{
|
||||
// Unlink
|
||||
Window = null;
|
||||
Asset = null;
|
||||
}
|
||||
|
||||
private class ProxyEditor : GenericEditor
|
||||
{
|
||||
private bool _waitForSkeletonLoaded;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Initialize(LayoutElementsContainer layout)
|
||||
{
|
||||
var proxy = (PropertiesProxy)Values[0];
|
||||
if (proxy.Asset == null || !proxy.Asset.IsLoaded)
|
||||
{
|
||||
layout.Label("Loading...");
|
||||
return;
|
||||
}
|
||||
|
||||
base.Initialize(layout);
|
||||
|
||||
// Check reference skeleton
|
||||
var skeleton = proxy.Skeleton;
|
||||
if (skeleton == null)
|
||||
return;
|
||||
if (!skeleton.IsLoaded)
|
||||
{
|
||||
// We need to have skeleton loaded for a nodes references
|
||||
_waitForSkeletonLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Init mask if missing or validate it
|
||||
var nodes = skeleton.Nodes;
|
||||
if (nodes == null || nodes.Length == 0)
|
||||
return;
|
||||
var mask = proxy.NodesMask;
|
||||
if (mask == null || mask.Length != nodes.Length)
|
||||
{
|
||||
if (mask != null)
|
||||
Debug.Write(LogType.Error, $"Invalid size nodes mask (got {mask.Length} but there are {nodes.Length} nodes)");
|
||||
mask = proxy.NodesMask = new bool[nodes.Length];
|
||||
for (int i = 0; i < nodes.Length; i++)
|
||||
mask[i] = true;
|
||||
}
|
||||
|
||||
// Skeleton Mask
|
||||
var group = layout.Group("Mask");
|
||||
var tree = group.Tree();
|
||||
for (int nodeIndex = 0; nodeIndex < nodes.Length; nodeIndex++)
|
||||
{
|
||||
if (nodes[nodeIndex].ParentIndex == -1)
|
||||
{
|
||||
BuildSkeletonNodeTree(mask, nodes, nodeIndex, tree);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Refresh()
|
||||
{
|
||||
if (_waitForSkeletonLoaded)
|
||||
{
|
||||
_waitForSkeletonLoaded = false;
|
||||
RebuildLayout();
|
||||
return;
|
||||
}
|
||||
|
||||
base.Refresh();
|
||||
}
|
||||
|
||||
private void BuildSkeletonNodeTree(bool[] mask, SkeletonNode[] nodes, int nodeIndex, ITreeElement layout)
|
||||
{
|
||||
var node = layout.Node(nodes[nodeIndex].Name);
|
||||
node.TreeNode.ClipChildren = false;
|
||||
node.TreeNode.TextMargin = new Margin(20.0f, 2.0f, 2.0f, 2.0f);
|
||||
node.TreeNode.Expand(true);
|
||||
var checkbox = new CheckBox(0, 0, mask[nodeIndex])
|
||||
{
|
||||
Height = 16.0f,
|
||||
IsScrollable = false,
|
||||
Tag = nodeIndex,
|
||||
Parent = node.TreeNode
|
||||
};
|
||||
checkbox.StateChanged += OnCheckChanged;
|
||||
|
||||
for (int i = 0; i < nodes.Length; i++)
|
||||
{
|
||||
if (nodes[i].ParentIndex == nodeIndex)
|
||||
{
|
||||
BuildSkeletonNodeTree(mask, nodes, i, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCheckChanged(CheckBox checkBox)
|
||||
{
|
||||
var proxy = (PropertiesProxy)Values[0];
|
||||
int nodeIndex = (int)checkBox.Tag;
|
||||
proxy.NodesMask[nodeIndex] = checkBox.Checked;
|
||||
proxy.Window.MarkAsEdited();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly SplitPanel _split;
|
||||
private readonly AnimatedModelPreview _preview;
|
||||
private readonly CustomEditorPresenter _propertiesPresenter;
|
||||
private readonly PropertiesProxy _properties;
|
||||
private readonly ToolStripButton _saveButton;
|
||||
|
||||
/// <inheritdoc />
|
||||
public SkeletonMaskWindow(Editor editor, AssetItem item)
|
||||
: base(editor, item)
|
||||
{
|
||||
// Toolstrip
|
||||
_saveButton = (ToolStripButton)_toolstrip.AddButton(editor.Icons.Save32, Save).LinkTooltip("Save asset to the file");
|
||||
_toolstrip.AddSeparator();
|
||||
_toolstrip.AddButton(editor.Icons.Docs32, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/animation/skeleton-mask.html")).LinkTooltip("See documentation to learn more");
|
||||
|
||||
// Split Panel
|
||||
_split = new SplitPanel(Orientation.Horizontal, ScrollBars.None, ScrollBars.Vertical)
|
||||
{
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
Offsets = new Margin(0, 0, _toolstrip.Bottom, 0),
|
||||
SplitterValue = 0.7f,
|
||||
Parent = this
|
||||
};
|
||||
|
||||
// Model preview
|
||||
_preview = new AnimatedModelPreview(true)
|
||||
{
|
||||
ViewportCamera = new FPSCamera(),
|
||||
ShowNodes = true,
|
||||
Parent = _split.Panel1
|
||||
};
|
||||
|
||||
// Model properties
|
||||
_propertiesPresenter = new CustomEditorPresenter(null);
|
||||
_propertiesPresenter.Panel.Parent = _split.Panel2;
|
||||
_properties = new PropertiesProxy();
|
||||
_propertiesPresenter.Select(_properties);
|
||||
_propertiesPresenter.Modified += MarkAsEdited;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Save()
|
||||
{
|
||||
if (!IsEdited)
|
||||
return;
|
||||
|
||||
_asset.Skeleton = _properties.Skeleton;
|
||||
int count = 0;
|
||||
var nodesMask = _preview.NodesMask;
|
||||
if (nodesMask != null)
|
||||
{
|
||||
for (int nodeIndex = 0; nodeIndex < nodesMask.Length; nodeIndex++)
|
||||
{
|
||||
if (nodesMask[nodeIndex])
|
||||
count++;
|
||||
}
|
||||
}
|
||||
var nodes = new string[count];
|
||||
if (nodesMask != null)
|
||||
{
|
||||
var i = 0;
|
||||
for (int nodeIndex = 0; nodeIndex < nodesMask.Length; nodeIndex++)
|
||||
{
|
||||
if (nodesMask[nodeIndex])
|
||||
nodes[i++] = _properties.Skeleton.Nodes[nodeIndex].Name;
|
||||
}
|
||||
}
|
||||
_asset.MaskedNodes = nodes;
|
||||
if (_asset.Save())
|
||||
{
|
||||
Editor.LogError("Cannot save asset.");
|
||||
return;
|
||||
}
|
||||
|
||||
ClearEditedFlag();
|
||||
_item.RefreshThumbnail();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void UpdateToolstrip()
|
||||
{
|
||||
_saveButton.Enabled = IsEdited;
|
||||
|
||||
base.UpdateToolstrip();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void UnlinkItem()
|
||||
{
|
||||
_properties.OnClean();
|
||||
_preview.SkinnedModel = null;
|
||||
|
||||
base.UnlinkItem();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnAssetLinked()
|
||||
{
|
||||
_preview.SkinnedModel = null;
|
||||
|
||||
base.OnAssetLinked();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnAssetLoaded()
|
||||
{
|
||||
_properties.OnLoad(this);
|
||||
_propertiesPresenter.BuildLayout();
|
||||
ClearEditedFlag();
|
||||
|
||||
base.OnAssetLoaded();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool UseLayoutData => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLayoutSerialize(XmlWriter writer)
|
||||
{
|
||||
writer.WriteAttributeString("Split", _split.SplitterValue.ToString());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLayoutDeserialize(XmlElement node)
|
||||
{
|
||||
float value1;
|
||||
|
||||
if (float.TryParse(node.GetAttribute("Split"), out value1))
|
||||
_split.SplitterValue = value1;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLayoutDeserialize()
|
||||
{
|
||||
_split.SplitterValue = 0.7f;
|
||||
}
|
||||
}
|
||||
}
|
||||
1132
Source/Editor/Windows/Assets/SkinnedModelWindow.cs
Normal file
1132
Source/Editor/Windows/Assets/SkinnedModelWindow.cs
Normal file
File diff suppressed because it is too large
Load Diff
367
Source/Editor/Windows/Assets/SpriteAtlasWindow.cs
Normal file
367
Source/Editor/Windows/Assets/SpriteAtlasWindow.cs
Normal file
@@ -0,0 +1,367 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System.Xml;
|
||||
using FlaxEditor.Content;
|
||||
using FlaxEditor.Content.Import;
|
||||
using FlaxEditor.CustomEditors;
|
||||
using FlaxEditor.CustomEditors.Editors;
|
||||
using FlaxEditor.GUI;
|
||||
using FlaxEditor.Scripting;
|
||||
using FlaxEditor.Viewport.Previews;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
|
||||
namespace FlaxEditor.Windows.Assets
|
||||
{
|
||||
/// <summary>
|
||||
/// Sprite Atlas window allows to view and edit <see cref="SpriteAtlas"/> asset.
|
||||
/// </summary>
|
||||
/// <seealso cref="SpriteAtlas" />
|
||||
/// <seealso cref="FlaxEditor.Windows.Assets.AssetEditorWindow" />
|
||||
public sealed class SpriteAtlasWindow : AssetEditorWindowBase<SpriteAtlas>
|
||||
{
|
||||
// TODO: allow to select and move sprites
|
||||
// TODO: restore changes on win close without a changes
|
||||
|
||||
/// <summary>
|
||||
/// Atlas view control. Shows sprites.
|
||||
/// </summary>
|
||||
/// <seealso cref="FlaxEditor.Viewport.Previews.SpriteAtlasPreview" />
|
||||
private sealed class AtlasView : SpriteAtlasPreview
|
||||
{
|
||||
public AtlasView(bool useWidgets)
|
||||
: base(useWidgets)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void DrawTexture(ref Rectangle rect)
|
||||
{
|
||||
base.DrawTexture(ref rect);
|
||||
|
||||
if (Asset && Asset.IsLoaded)
|
||||
{
|
||||
var count = Asset.SpritesCount;
|
||||
var style = Style.Current;
|
||||
|
||||
// Draw all splits
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var sprite = Asset.GetSprite(i);
|
||||
var area = sprite.Area;
|
||||
|
||||
Vector2 position = area.Location * rect.Size + rect.Location;
|
||||
Vector2 size = area.Size * rect.Size;
|
||||
|
||||
Render2D.DrawRectangle(new Rectangle(position, size), style.BackgroundSelected);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The texture properties proxy object.
|
||||
/// </summary>
|
||||
[CustomEditor(typeof(ProxyEditor))]
|
||||
private sealed class PropertiesProxy
|
||||
{
|
||||
private SpriteAtlasWindow _window;
|
||||
|
||||
public class SpriteEntry
|
||||
{
|
||||
[HideInEditor]
|
||||
public SpriteHandle Sprite;
|
||||
|
||||
public SpriteEntry(SpriteAtlas atlas, int index)
|
||||
{
|
||||
Sprite = new SpriteHandle(atlas, index);
|
||||
}
|
||||
|
||||
[EditorOrder(0)]
|
||||
public string Name
|
||||
{
|
||||
get => Sprite.Name;
|
||||
set => Sprite.Name = value;
|
||||
}
|
||||
|
||||
[EditorOrder(1), Limit(-4096, 4096)]
|
||||
public Vector2 Location
|
||||
{
|
||||
get => Sprite.Location;
|
||||
set => Sprite.Location = value;
|
||||
}
|
||||
|
||||
[EditorOrder(3), Limit(0, 4096)]
|
||||
public Vector2 Size
|
||||
{
|
||||
get => Sprite.Size;
|
||||
set => Sprite.Size = value;
|
||||
}
|
||||
}
|
||||
|
||||
[EditorOrder(0), EditorDisplay("Sprites")]
|
||||
[CustomEditor(typeof(SpritesColelctionEditor))]
|
||||
public SpriteEntry[] Sprites;
|
||||
|
||||
[EditorOrder(1000), EditorDisplay("Import Settings", EditorDisplayAttribute.InlineStyle)]
|
||||
public TextureImportSettings ImportSettings { get; set; } = new TextureImportSettings();
|
||||
|
||||
public sealed class ProxyEditor : GenericEditor
|
||||
{
|
||||
public override void Initialize(LayoutElementsContainer layout)
|
||||
{
|
||||
var window = ((PropertiesProxy)Values[0])._window;
|
||||
|
||||
base.Initialize(layout);
|
||||
|
||||
layout.Space(10);
|
||||
var reimportButton = layout.Button("Reimport");
|
||||
reimportButton.Button.Clicked += () => ((PropertiesProxy)Values[0]).Reimport();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SpritesColelctionEditor : CustomEditor
|
||||
{
|
||||
public override DisplayStyle Style => DisplayStyle.InlineIntoParent;
|
||||
|
||||
public override void Initialize(LayoutElementsContainer layout)
|
||||
{
|
||||
var sprites = (SpriteEntry[])Values[0];
|
||||
|
||||
if (sprites != null)
|
||||
{
|
||||
var elementType = new ScriptType(typeof(SpriteEntry));
|
||||
for (int i = 0; i < sprites.Length; i++)
|
||||
{
|
||||
var group = layout.Group(sprites[i].Name);
|
||||
group.Object(new ListValueContainer(elementType, i, Values));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the sprites collection.
|
||||
/// </summary>
|
||||
public void UpdateSprites()
|
||||
{
|
||||
var atlas = _window.Asset;
|
||||
Sprites = new SpriteEntry[atlas.SpritesCount];
|
||||
for (int i = 0; i < Sprites.Length; i++)
|
||||
{
|
||||
Sprites[i] = new SpriteEntry(atlas, i);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gathers parameters from the specified texture.
|
||||
/// </summary>
|
||||
/// <param name="win">The texture window.</param>
|
||||
public void OnLoad(SpriteAtlasWindow win)
|
||||
{
|
||||
// Link
|
||||
_window = win;
|
||||
UpdateSprites();
|
||||
|
||||
// Try to restore target asset texture import options (useful for fast reimport)
|
||||
TextureImportSettings.InternalOptions options;
|
||||
if (TextureImportEntry.Internal_GetTextureImportOptions(win.Item.Path, out options))
|
||||
{
|
||||
// Restore settings
|
||||
ImportSettings.FromInternal(ref options);
|
||||
}
|
||||
|
||||
// Prepare restore data
|
||||
PeekState();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records the current state to restore it on DiscardChanges.
|
||||
/// </summary>
|
||||
public void PeekState()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reimports asset.
|
||||
/// </summary>
|
||||
public void Reimport()
|
||||
{
|
||||
Editor.Instance.ContentImporting.Reimport((BinaryAssetItem)_window.Item, ImportSettings, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// On discard changes
|
||||
/// </summary>
|
||||
public void DiscardChanges()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears temporary data.
|
||||
/// </summary>
|
||||
public void OnClean()
|
||||
{
|
||||
// Unlink
|
||||
_window = null;
|
||||
Sprites = null;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly SplitPanel _split;
|
||||
private readonly AtlasView _preview;
|
||||
private readonly CustomEditorPresenter _propertiesEditor;
|
||||
private readonly ToolStripButton _saveButton;
|
||||
|
||||
private readonly PropertiesProxy _properties;
|
||||
private bool _isWaitingForLoad;
|
||||
|
||||
/// <inheritdoc />
|
||||
public SpriteAtlasWindow(Editor editor, AssetItem item)
|
||||
: base(editor, item)
|
||||
{
|
||||
// Split Panel
|
||||
_split = new SplitPanel(Orientation.Horizontal, ScrollBars.None, ScrollBars.Vertical)
|
||||
{
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
Offsets = new Margin(0, 0, _toolstrip.Bottom, 0),
|
||||
SplitterValue = 0.7f,
|
||||
Parent = this
|
||||
};
|
||||
|
||||
// Sprite atlas preview
|
||||
_preview = new AtlasView(true)
|
||||
{
|
||||
Parent = _split.Panel1
|
||||
};
|
||||
|
||||
// Sprite atlas properties editor
|
||||
_propertiesEditor = new CustomEditorPresenter(null);
|
||||
_propertiesEditor.Panel.Parent = _split.Panel2;
|
||||
_properties = new PropertiesProxy();
|
||||
_propertiesEditor.Select(_properties);
|
||||
_propertiesEditor.Modified += MarkAsEdited;
|
||||
|
||||
// Toolstrip
|
||||
_saveButton = (ToolStripButton)_toolstrip.AddButton(editor.Icons.Save32, Save).LinkTooltip("Save");
|
||||
_toolstrip.AddButton(editor.Icons.Import32, () => Editor.ContentImporting.Reimport((BinaryAssetItem)Item)).LinkTooltip("Reimport");
|
||||
_toolstrip.AddSeparator();
|
||||
_toolstrip.AddButton(editor.Icons.AddDoc32, () =>
|
||||
{
|
||||
var sprite = new Sprite
|
||||
{
|
||||
Name = "New Sprite",
|
||||
Area = new Rectangle(Vector2.Zero, Vector2.One),
|
||||
};
|
||||
Asset.AddSprite(sprite);
|
||||
MarkAsEdited();
|
||||
_properties.UpdateSprites();
|
||||
_propertiesEditor.BuildLayout();
|
||||
}).LinkTooltip("Add a new sprite");
|
||||
_toolstrip.AddSeparator();
|
||||
_toolstrip.AddButton(editor.Icons.PageScale32, _preview.CenterView).LinkTooltip("Center view");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Save()
|
||||
{
|
||||
if (!IsEdited)
|
||||
return;
|
||||
|
||||
if (Asset.SaveSprites())
|
||||
{
|
||||
Editor.LogError("Cannot save asset.");
|
||||
return;
|
||||
}
|
||||
|
||||
ClearEditedFlag();
|
||||
_item.RefreshThumbnail();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void UpdateToolstrip()
|
||||
{
|
||||
_saveButton.Enabled = IsEdited;
|
||||
|
||||
base.UpdateToolstrip();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void UnlinkItem()
|
||||
{
|
||||
_properties.OnClean();
|
||||
_preview.Asset = null;
|
||||
_isWaitingForLoad = false;
|
||||
|
||||
base.UnlinkItem();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnAssetLinked()
|
||||
{
|
||||
_preview.Asset = _asset;
|
||||
_isWaitingForLoad = true;
|
||||
|
||||
base.OnAssetLinked();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnItemReimported(ContentItem item)
|
||||
{
|
||||
// Invalidate data
|
||||
_isWaitingForLoad = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnClose()
|
||||
{
|
||||
// Discard unsaved changes
|
||||
_properties.DiscardChanges();
|
||||
|
||||
base.OnClose();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
base.Update(deltaTime);
|
||||
|
||||
// Check if need to load
|
||||
if (_isWaitingForLoad && _asset.IsLoaded)
|
||||
{
|
||||
// Clear flag
|
||||
_isWaitingForLoad = false;
|
||||
|
||||
// Init properties and parameters proxy
|
||||
_properties.OnLoad(this);
|
||||
_propertiesEditor.BuildLayout();
|
||||
|
||||
// Setup
|
||||
ClearEditedFlag();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool UseLayoutData => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLayoutSerialize(XmlWriter writer)
|
||||
{
|
||||
writer.WriteAttributeString("Split", _split.SplitterValue.ToString());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLayoutDeserialize(XmlElement node)
|
||||
{
|
||||
float value1;
|
||||
|
||||
if (float.TryParse(node.GetAttribute("Split"), out value1))
|
||||
_split.SplitterValue = value1;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLayoutDeserialize()
|
||||
{
|
||||
_split.SplitterValue = 0.7f;
|
||||
}
|
||||
}
|
||||
}
|
||||
229
Source/Editor/Windows/Assets/TextureWindow.cs
Normal file
229
Source/Editor/Windows/Assets/TextureWindow.cs
Normal file
@@ -0,0 +1,229 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System.Xml;
|
||||
using FlaxEditor.Content;
|
||||
using FlaxEditor.Content.Import;
|
||||
using FlaxEditor.CustomEditors;
|
||||
using FlaxEditor.CustomEditors.Editors;
|
||||
using FlaxEditor.Viewport.Previews;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
|
||||
namespace FlaxEditor.Windows.Assets
|
||||
{
|
||||
/// <summary>
|
||||
/// Texture window allows to view and edit <see cref="Texture"/> asset.
|
||||
/// </summary>
|
||||
/// <seealso cref="Texture" />
|
||||
/// <seealso cref="FlaxEditor.Windows.Assets.AssetEditorWindow" />
|
||||
public sealed class TextureWindow : AssetEditorWindowBase<Texture>
|
||||
{
|
||||
/// <summary>
|
||||
/// The texture properties proxy object.
|
||||
/// </summary>
|
||||
[CustomEditor(typeof(ProxyEditor))]
|
||||
private sealed class PropertiesProxy
|
||||
{
|
||||
private TextureWindow _window;
|
||||
|
||||
[EditorOrder(1000), EditorDisplay("Import Settings", EditorDisplayAttribute.InlineStyle)]
|
||||
public TextureImportSettings ImportSettings = new TextureImportSettings();
|
||||
|
||||
public sealed class ProxyEditor : GenericEditor
|
||||
{
|
||||
public override void Initialize(LayoutElementsContainer layout)
|
||||
{
|
||||
var window = ((PropertiesProxy)Values[0])._window;
|
||||
if (window == null)
|
||||
{
|
||||
layout.Label("Loading...", TextAlignment.Center);
|
||||
return;
|
||||
}
|
||||
|
||||
// Texture properties
|
||||
{
|
||||
var texture = window.Asset;
|
||||
|
||||
var group = layout.Group("General");
|
||||
group.Label("Format: " + texture.Format);
|
||||
group.Label(string.Format("Size: {0}x{1}", texture.Width, texture.Height));
|
||||
group.Label("Mip levels: " + texture.MipLevels);
|
||||
group.Label("Memory usage: " + Utilities.Utils.FormatBytesCount(texture.TotalMemoryUsage));
|
||||
}
|
||||
|
||||
base.Initialize(layout);
|
||||
|
||||
layout.Space(10);
|
||||
var reimportButton = layout.Button("Reimport");
|
||||
reimportButton.Button.Clicked += () => ((PropertiesProxy)Values[0]).Reimport();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gathers parameters from the specified texture.
|
||||
/// </summary>
|
||||
/// <param name="window">The asset window.</param>
|
||||
public void OnLoad(TextureWindow window)
|
||||
{
|
||||
// Link
|
||||
_window = window;
|
||||
|
||||
// Try to restore target asset texture import options (useful for fast reimport)
|
||||
TextureImportSettings.TryRestore(ref ImportSettings, window.Item.Path);
|
||||
|
||||
// Prepare restore data
|
||||
PeekState();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records the current state to restore it on DiscardChanges.
|
||||
/// </summary>
|
||||
public void PeekState()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reimports asset.
|
||||
/// </summary>
|
||||
public void Reimport()
|
||||
{
|
||||
Editor.Instance.ContentImporting.Reimport((BinaryAssetItem)_window.Item, ImportSettings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// On discard changes
|
||||
/// </summary>
|
||||
public void DiscardChanges()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears temporary data.
|
||||
/// </summary>
|
||||
public void OnClean()
|
||||
{
|
||||
// Unlink
|
||||
_window = null;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly SplitPanel _split;
|
||||
private readonly TexturePreview _preview;
|
||||
private readonly CustomEditorPresenter _propertiesEditor;
|
||||
|
||||
private readonly PropertiesProxy _properties;
|
||||
private bool _isWaitingForLoad;
|
||||
|
||||
/// <inheritdoc />
|
||||
public TextureWindow(Editor editor, AssetItem item)
|
||||
: base(editor, item)
|
||||
{
|
||||
// Split Panel
|
||||
_split = new SplitPanel(Orientation.Horizontal, ScrollBars.None, ScrollBars.Vertical)
|
||||
{
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
Offsets = new Margin(0, 0, _toolstrip.Bottom, 0),
|
||||
SplitterValue = 0.7f,
|
||||
Parent = this
|
||||
};
|
||||
|
||||
// Texture preview
|
||||
_preview = new TexturePreview(true)
|
||||
{
|
||||
Parent = _split.Panel1
|
||||
};
|
||||
|
||||
// Texture properties editor
|
||||
_propertiesEditor = new CustomEditorPresenter(null);
|
||||
_propertiesEditor.Panel.Parent = _split.Panel2;
|
||||
_properties = new PropertiesProxy();
|
||||
_propertiesEditor.Select(_properties);
|
||||
|
||||
// Toolstrip
|
||||
_toolstrip.AddButton(Editor.Icons.Import32, () => Editor.ContentImporting.Reimport((BinaryAssetItem)Item)).LinkTooltip("Reimport");
|
||||
_toolstrip.AddSeparator();
|
||||
_toolstrip.AddButton(Editor.Icons.PageScale32, _preview.CenterView).LinkTooltip("Center view");
|
||||
_toolstrip.AddSeparator();
|
||||
_toolstrip.AddButton(editor.Icons.Docs32, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/graphics/textures/index.html")).LinkTooltip("See documentation to learn more");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void UnlinkItem()
|
||||
{
|
||||
_properties.OnClean();
|
||||
_preview.Asset = null;
|
||||
_isWaitingForLoad = false;
|
||||
|
||||
base.UnlinkItem();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnAssetLinked()
|
||||
{
|
||||
_preview.Asset = _asset;
|
||||
_isWaitingForLoad = true;
|
||||
|
||||
base.OnAssetLinked();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnItemReimported(ContentItem item)
|
||||
{
|
||||
// Invalidate data
|
||||
_isWaitingForLoad = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnClose()
|
||||
{
|
||||
// Discard unsaved changes
|
||||
_properties.DiscardChanges();
|
||||
|
||||
base.OnClose();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
base.Update(deltaTime);
|
||||
|
||||
// Check if need to load
|
||||
if (_isWaitingForLoad && _asset.IsLoaded)
|
||||
{
|
||||
// Clear flag
|
||||
_isWaitingForLoad = false;
|
||||
|
||||
// Init properties and parameters proxy
|
||||
_properties.OnLoad(this);
|
||||
_propertiesEditor.BuildLayout();
|
||||
|
||||
// Setup
|
||||
ClearEditedFlag();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool UseLayoutData => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLayoutSerialize(XmlWriter writer)
|
||||
{
|
||||
writer.WriteAttributeString("Split", _split.SplitterValue.ToString());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLayoutDeserialize(XmlElement node)
|
||||
{
|
||||
float value1;
|
||||
|
||||
if (float.TryParse(node.GetAttribute("Split"), out value1))
|
||||
_split.SplitterValue = value1;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLayoutDeserialize()
|
||||
{
|
||||
_split.SplitterValue = 0.7f;
|
||||
}
|
||||
}
|
||||
}
|
||||
269
Source/Editor/Windows/Assets/VisjectFunctionSurfaceWindow.cs
Normal file
269
Source/Editor/Windows/Assets/VisjectFunctionSurfaceWindow.cs
Normal file
@@ -0,0 +1,269 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using FlaxEditor.Content;
|
||||
using FlaxEditor.GUI;
|
||||
using FlaxEditor.Surface;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
|
||||
namespace FlaxEditor.Windows.Assets
|
||||
{
|
||||
/// <summary>
|
||||
/// The base class for editor windows that use <see cref="FlaxEditor.Surface.VisjectSurface"/> for content editing by graph functions (eg. material functions, particle emitter functions).
|
||||
/// </summary>
|
||||
/// <seealso cref="FlaxEditor.Windows.Assets.AssetEditorWindow" />
|
||||
/// <seealso cref="FlaxEditor.Surface.IVisjectSurfaceOwner" />
|
||||
public abstract class VisjectFunctionSurfaceWindow<TAsset, TSurface> : ClonedAssetEditorWindowBase<TAsset>, IVisjectSurfaceOwner
|
||||
where TAsset : Asset
|
||||
where TSurface : VisjectSurface
|
||||
{
|
||||
/// <summary>
|
||||
/// The surface.
|
||||
/// </summary>
|
||||
protected TSurface _surface;
|
||||
|
||||
/// <summary>
|
||||
/// The panel for the surface.
|
||||
/// </summary>
|
||||
protected readonly Panel _panel;
|
||||
|
||||
private readonly ToolStripButton _saveButton;
|
||||
private readonly ToolStripButton _undoButton;
|
||||
private readonly ToolStripButton _redoButton;
|
||||
private bool _showWholeGraphOnLoad = true;
|
||||
|
||||
/// <summary>
|
||||
/// True if temporary asset is dirty, otherwise false.
|
||||
/// </summary>
|
||||
protected bool _tmpAssetIsDirty;
|
||||
|
||||
/// <summary>
|
||||
/// True if window is waiting for asset load to load surface.
|
||||
/// </summary>
|
||||
protected bool _isWaitingForSurfaceLoad;
|
||||
|
||||
/// <summary>
|
||||
/// The undo.
|
||||
/// </summary>
|
||||
protected Undo _undo;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Visject Surface.
|
||||
/// </summary>
|
||||
public TSurface Surface => _surface;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the undo history context for this window.
|
||||
/// </summary>
|
||||
public Undo Undo => _undo;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected VisjectFunctionSurfaceWindow(Editor editor, AssetItem item)
|
||||
: base(editor, item)
|
||||
{
|
||||
// Undo
|
||||
_undo = new Undo();
|
||||
_undo.UndoDone += OnUndoRedo;
|
||||
_undo.RedoDone += OnUndoRedo;
|
||||
_undo.ActionDone += OnUndoRedo;
|
||||
|
||||
// Toolstrip
|
||||
_saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save32, Save).LinkTooltip("Save");
|
||||
_toolstrip.AddSeparator();
|
||||
_undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo32, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)");
|
||||
_redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo32, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)");
|
||||
_toolstrip.AddSeparator();
|
||||
_toolstrip.AddButton(editor.Icons.PageScale32, ShowWholeGraph).LinkTooltip("Show whole graph");
|
||||
|
||||
// Panel
|
||||
_panel = new Panel(ScrollBars.None)
|
||||
{
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
Offsets = new Margin(0, 0, _toolstrip.Bottom, 0),
|
||||
Parent = this
|
||||
};
|
||||
|
||||
// Setup input actions
|
||||
InputActions.Add(options => options.Undo, _undo.PerformUndo);
|
||||
InputActions.Add(options => options.Redo, _undo.PerformRedo);
|
||||
}
|
||||
|
||||
private void OnUndoRedo(IUndoAction action)
|
||||
{
|
||||
MarkAsEdited();
|
||||
UpdateToolstrip();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the whole surface graph.
|
||||
/// </summary>
|
||||
public void ShowWholeGraph()
|
||||
{
|
||||
_surface.ShowWholeGraph();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes temporary asset to see changes live when editing the surface.
|
||||
/// </summary>
|
||||
/// <returns>True if cannot refresh it, otherwise false.</returns>
|
||||
public bool RefreshTempAsset()
|
||||
{
|
||||
// Early check
|
||||
if (_asset == null || _isWaitingForSurfaceLoad)
|
||||
return true;
|
||||
|
||||
// Check if surface has been edited
|
||||
if (_surface.IsEdited)
|
||||
{
|
||||
return SaveSurface();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Save()
|
||||
{
|
||||
if (!IsEdited)
|
||||
return;
|
||||
|
||||
if (RefreshTempAsset())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (SaveToOriginal())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ClearEditedFlag();
|
||||
OnSurfaceEditedChanged();
|
||||
_item.RefreshThumbnail();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void UpdateToolstrip()
|
||||
{
|
||||
_saveButton.Enabled = IsEdited;
|
||||
_undoButton.Enabled = _undo.CanUndo;
|
||||
_redoButton.Enabled = _undo.CanRedo;
|
||||
|
||||
base.UpdateToolstrip();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void UnlinkItem()
|
||||
{
|
||||
_isWaitingForSurfaceLoad = false;
|
||||
|
||||
base.UnlinkItem();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnAssetLinked()
|
||||
{
|
||||
_isWaitingForSurfaceLoad = true;
|
||||
|
||||
base.OnAssetLinked();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract string SurfaceName { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract byte[] SurfaceData { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void OnContextCreated(VisjectSurfaceContext context)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void OnSurfaceEditedChanged()
|
||||
{
|
||||
if (_surface.IsEdited)
|
||||
MarkAsEdited();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void OnSurfaceGraphEdited()
|
||||
{
|
||||
// Mark as dirty
|
||||
_tmpAssetIsDirty = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void OnSurfaceClose()
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the surface from the asset. Called during <see cref="Update"/> when asset is loaded and surface is missing.
|
||||
/// </summary>
|
||||
/// <returns>True if failed, otherwise false.</returns>
|
||||
protected virtual bool LoadSurface()
|
||||
{
|
||||
if (_surface.Load())
|
||||
{
|
||||
Editor.LogError("Failed to load surface.");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the surface to the asset. Called during <see cref="Update"/> when asset is loaded and surface is missing.
|
||||
/// </summary>
|
||||
/// <returns>True if failed, otherwise false.</returns>
|
||||
protected virtual bool SaveSurface()
|
||||
{
|
||||
_surface.Save();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
base.Update(deltaTime);
|
||||
|
||||
if (_tmpAssetIsDirty)
|
||||
{
|
||||
_tmpAssetIsDirty = false;
|
||||
|
||||
RefreshTempAsset();
|
||||
}
|
||||
|
||||
if (_isWaitingForSurfaceLoad && _asset.IsLoaded)
|
||||
{
|
||||
_isWaitingForSurfaceLoad = false;
|
||||
|
||||
if (LoadSurface())
|
||||
{
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup
|
||||
_undo.Clear();
|
||||
_surface.Enabled = true;
|
||||
ClearEditedFlag();
|
||||
if (_showWholeGraphOnLoad)
|
||||
{
|
||||
_showWholeGraphOnLoad = false;
|
||||
_surface.ShowWholeGraph();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnDestroy()
|
||||
{
|
||||
_undo.Clear();
|
||||
|
||||
base.OnDestroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
1182
Source/Editor/Windows/Assets/VisualScriptWindow.cs
Normal file
1182
Source/Editor/Windows/Assets/VisualScriptWindow.cs
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user