The null-conditional operator checks for reference equality of the Object, but doesn't check the validity of the unmanaged pointer. This check is corrected in cases where the object was not immediately returned from the bindings layer and may have been destroyed earlier.
585 lines
22 KiB
C#
585 lines
22 KiB
C#
// Copyright (c) 2012-2023 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 : GeneralProxy
|
|
{
|
|
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(nameof(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(nameof(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)
|
|
: base(window)
|
|
{
|
|
_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 == null || 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 : GeneralProxy
|
|
{
|
|
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(ParticleSystemWindow window, FolderTrack track)
|
|
: base(window)
|
|
{
|
|
_track = track;
|
|
}
|
|
}
|
|
|
|
private readonly SplitPanel _split1;
|
|
private readonly SplitPanel _split2;
|
|
private ParticleSystemTimeline _timeline;
|
|
private readonly ParticleSystemPreview _preview;
|
|
private readonly CustomEditorPresenter _propertiesEditor;
|
|
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;
|
|
_timeline.SetNoTracksText("Loading...");
|
|
|
|
// Properties editor
|
|
var propertiesEditor = new CustomEditorPresenter(_undo, string.Empty);
|
|
propertiesEditor.Panel.Parent = _split2.Panel2;
|
|
propertiesEditor.Modified += OnParticleSystemPropertyEdited;
|
|
_propertiesEditor = propertiesEditor;
|
|
propertiesEditor.Select(new GeneralProxy(this));
|
|
|
|
// Toolstrip
|
|
_saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save");
|
|
_toolstrip.AddSeparator();
|
|
_undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)");
|
|
_redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)");
|
|
_toolstrip.AddSeparator();
|
|
_toolstrip.AddButton(editor.Icons.Docs64, () => 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();
|
|
|
|
if (!_isEditingInstancedParameterValue)
|
|
{
|
|
_propertiesEditor.BuildLayoutOnUpdate();
|
|
}
|
|
}
|
|
|
|
private void OnTimelineModified()
|
|
{
|
|
if (!_isEditingInstancedParameterValue)
|
|
{
|
|
_tmpParticleSystemIsDirty = true;
|
|
}
|
|
|
|
MarkAsEdited();
|
|
}
|
|
|
|
private void OnTimelineSelectionChanged()
|
|
{
|
|
if (_timeline.SelectedTracks.Count == 0)
|
|
{
|
|
_propertiesEditor.Select(new GeneralProxy(this));
|
|
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(this, folderTrack);
|
|
}
|
|
else
|
|
{
|
|
throw new NotImplementedException("Invalid track type.");
|
|
}
|
|
}
|
|
_propertiesEditor.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)
|
|
{
|
|
_propertiesEditor.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()
|
|
{
|
|
_propertiesEditor.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;
|
|
_propertiesEditor.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;
|
|
_timeline.SetNoTracksText(null);
|
|
_propertiesEditor.Select(new GeneralProxy(this));
|
|
ClearEditedFlag();
|
|
}
|
|
|
|
// Clear flag
|
|
_isEditingInstancedParameterValue = false;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override bool UseLayoutData => true;
|
|
|
|
/// <inheritdoc />
|
|
public override void OnLayoutSerialize(XmlWriter writer)
|
|
{
|
|
LayoutSerializeSplitter(writer, "Split1", _split1);
|
|
LayoutSerializeSplitter(writer, "Split2", _split2);
|
|
LayoutSerializeSplitter(writer, "Split3", _timeline.Splitter);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void OnLayoutDeserialize(XmlElement node)
|
|
{
|
|
LayoutDeserializeSplitter(node, "Split1", _split1);
|
|
LayoutDeserializeSplitter(node, "Split2", _split2);
|
|
LayoutDeserializeSplitter(node, "Split3", _timeline.Splitter);
|
|
}
|
|
|
|
/// <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;
|
|
_propertiesEditor?.Deselect();
|
|
_undo?.Clear();
|
|
_undo = null;
|
|
|
|
_timeline = null;
|
|
_saveButton = null;
|
|
_undoButton = null;
|
|
_redoButton = null;
|
|
|
|
base.OnDestroy();
|
|
}
|
|
}
|
|
}
|