// 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 { /// /// Particle System window allows to view and edit asset. /// Note: it uses ClonedAssetEditorWindowBase which is creating cloned asset to edit/preview. /// /// /// public sealed class ParticleSystemWindow : ClonedAssetEditorWindowBase { 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; } } /// /// The proxy object for editing particle system properties. /// 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; } } /// /// The proxy object for editing particle system track properties. /// [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 { /// 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(); }); } } } /// /// The proxy object for editing folder track properties. /// 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; /// /// Gets the particle system preview. /// public ParticleSystemPreview Preview => _preview; /// /// Gets the timeline editor. /// public ParticleSystemTimeline Timeline => _timeline; /// /// Gets the undo history context for this window. /// public Undo Undo => _undo; /// 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().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(); } /// /// Refreshes temporary asset to see changes live when editing the timeline. /// /// True if cannot refresh it, otherwise false. public bool RefreshTempAsset() { if (_asset == null || _isWaitingForTimelineLoad) return true; if (_timeline.IsModified) { _propertiesEditor.BuildLayoutOnUpdate(); _timeline.Save(_asset); } return false; } /// public override void Save() { if (!IsEdited) return; if (RefreshTempAsset()) { return; } if (SaveToOriginal()) { return; } ClearEditedFlag(); _item.RefreshThumbnail(); } /// protected override void UpdateToolstrip() { _saveButton.Enabled = IsEdited; _undoButton.Enabled = _undo.CanUndo; _redoButton.Enabled = _undo.CanRedo; base.UpdateToolstrip(); } /// protected override void UnlinkItem() { _propertiesEditor.Deselect(); _preview.System = null; _isWaitingForTimelineLoad = false; base.UnlinkItem(); } /// protected override void OnAssetLinked() { _preview.System = _asset; _isWaitingForTimelineLoad = true; base.OnAssetLinked(); } /// 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; } /// public override bool UseLayoutData => true; /// public override void OnLayoutSerialize(XmlWriter writer) { LayoutSerializeSplitter(writer, "Split1", _split1); LayoutSerializeSplitter(writer, "Split2", _split2); LayoutSerializeSplitter(writer, "Split3", _timeline.Splitter); } /// public override void OnLayoutDeserialize(XmlElement node) { LayoutDeserializeSplitter(node, "Split1", _split1); LayoutDeserializeSplitter(node, "Split2", _split2); LayoutDeserializeSplitter(node, "Split3", _timeline.Splitter); } /// public override void OnLayoutDeserialize() { _split1.SplitterValue = 0.6f; _split2.SplitterValue = 0.5f; _timeline.Splitter.SplitterValue = 0.2f; } /// 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(); } } }