You're breathtaking!

This commit is contained in:
Wojtek Figat
2020-12-07 23:40:54 +01:00
commit 6fb9eee74c
5143 changed files with 1153594 additions and 0 deletions

View 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);
}
}
}

View 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();
}
}
}

View 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();
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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();
}
}
}

View 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);
}
}
}

View 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();
}
}
}
}

View 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;
}
}
}

View 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();
}
}
}

View 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;
}
}
}

File diff suppressed because it is too large Load Diff

View 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>
/// 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();
}
}
}
}

View 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();
}
}
}

View 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();
}
}
}

View 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);
}
}
}

View 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);
}
}
}
}

View 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);
}
}
}

View 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();
}
}
}

View 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();
}
}
}

View 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();
}
}
}

View 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;
}
}
}

File diff suppressed because it is too large Load Diff

View 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;
}
}
}

View 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;
}
}
}

View 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();
}
}
}

File diff suppressed because it is too large Load Diff