Files
FlaxEngine/Source/Editor/Tools/Foliage/PaintTab.cs
Ari Vuollet 58445f04c4 Fix potential incorrect null checks in FlaxEngine.Objects
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.
2023-09-28 22:05:58 +03:00

339 lines
12 KiB
C#

// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
using FlaxEditor.CustomEditors;
using FlaxEditor.CustomEditors.Editors;
using FlaxEditor.GUI;
using FlaxEditor.GUI.Tabs;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.Tools.Foliage
{
/// <summary>
/// Foliage painting tab. Allows to add or remove foliage instances defined for the current foliage object.
/// </summary>
/// <seealso cref="GUI.Tabs.Tab" />
public class PaintTab : Tab
{
/// <summary>
/// The object for foliage painting settings adjusting via Custom Editor.
/// </summary>
[CustomEditor(typeof(ProxyObjectEditor))]
private sealed class ProxyObject
{
private readonly PaintFoliageGizmoMode _mode;
private readonly PaintTab _tab;
/// <summary>
/// The foliage actor.
/// </summary>
[HideInEditor]
public FlaxEngine.Foliage Foliage;
/// <summary>
/// The selected foliage instance type index.
/// </summary>
[HideInEditor]
public int SelectedFoliageTypeIndex;
/// <summary>
/// Initializes a new instance of the <see cref="ProxyObject"/> class.
/// </summary>
/// <param name="tab">The tab.</param>
/// <param name="mode">The mode.</param>
public ProxyObject(PaintTab tab, PaintFoliageGizmoMode mode)
{
_mode = mode;
_tab = tab;
SelectedFoliageTypeIndex = -1;
}
private FlaxEngine.FoliageType _type;
public void SyncOptions()
{
_type = Foliage.GetFoliageType(SelectedFoliageTypeIndex);
}
//
[EditorOrder(100), EditorDisplay("Brush", EditorDisplayAttribute.InlineStyle)]
public Brush Brush
{
get => _mode.CurrentBrush;
set { }
}
//
[EditorOrder(200), EditorDisplay("Painting"), Limit(0.0f), Tooltip("The foliage instances density defined in instances count per 1000x1000 units area.")]
public float Density
{
get => _type.PaintDensity;
set => _type.PaintDensity = value;
}
[EditorOrder(210), EditorDisplay("Painting"), Limit(0.0f), Tooltip("The minimum radius between foliage instances.")]
public float Radius
{
get => _type.PaintRadius;
set => _type.PaintRadius = value;
}
[EditorOrder(215), EditorDisplay("Painting"), Limit(0.0f, 360.0f), Tooltip("The minimum and maximum ground slope angle to paint foliage on it (in degrees).")]
public Float2 PaintGroundSlopeAngleRange
{
get => new Float2(_type.PaintGroundSlopeAngleMin, _type.PaintGroundSlopeAngleMax);
set
{
_type.PaintGroundSlopeAngleMin = value.X;
_type.PaintGroundSlopeAngleMax = value.Y;
}
}
[EditorOrder(220), EditorDisplay("Painting"), Tooltip("The scaling mode.")]
public FoliageScalingModes Scaling
{
get => _type.PaintScaling;
set => _type.PaintScaling = value;
}
[EditorOrder(230), EditorDisplay("Painting"), Limit(0.0f), CustomEditor(typeof(ActorTransformEditor.ScaleEditor)), Tooltip("The scale minimum values per axis.")]
public Float3 ScaleMin
{
get => _type.PaintScaleMin;
set => _type.PaintScaleMin = value;
}
[EditorOrder(240), EditorDisplay("Painting"), Limit(0.0f), CustomEditor(typeof(ActorTransformEditor.ScaleEditor)), Tooltip("The scale maximum values per axis.")]
public Float3 ScaleMax
{
get => _type.PaintScaleMax;
set => _type.PaintScaleMax = value;
}
//
[EditorOrder(300), EditorDisplay("Placement", "Offset Y"), Tooltip("The per-instance random offset range on axis Y (min-max).")]
public Float2 OffsetY
{
get => _type.PlacementOffsetY;
set => _type.PlacementOffsetY = value;
}
[EditorOrder(310), EditorDisplay("Placement"), Limit(0.0f), Tooltip("The random pitch angle range (uniform in both ways around normal vector).")]
public float RandomPitchAngle
{
get => _type.PlacementRandomPitchAngle;
set => _type.PlacementRandomPitchAngle = value;
}
[EditorOrder(320), EditorDisplay("Placement"), Limit(0.0f), Tooltip("The random roll angle range (uniform in both ways around normal vector).")]
public float RandomRollAngle
{
get => _type.PlacementRandomRollAngle;
set => _type.PlacementRandomRollAngle = value;
}
[EditorOrder(330), EditorDisplay("Placement", "Align To Normal"), Tooltip("If checked, instances will be aligned to normal of the placed surface.")]
public bool AlignToNormal
{
get => _type.PlacementAlignToNormal;
set => _type.PlacementAlignToNormal = value;
}
[EditorOrder(340), EditorDisplay("Placement"), Tooltip("If checked, instances will use randomized yaw when placed. Random yaw uses will rotation range over the Y axis.")]
public bool RandomYaw
{
get => _type.PlacementRandomYaw;
set => _type.PlacementRandomYaw = value;
}
}
/// <summary>
/// The custom editor for <see cref="ProxyObject"/>.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.Editors.GenericEditor" />
private sealed class ProxyObjectEditor : GenericEditor
{
/// <inheritdoc />
public override void Refresh()
{
// Sync selected foliage options once before update to prevent too many data copies when fetching data from UI properties accessors
var proxyObject = (ProxyObject)Values[0];
proxyObject.SyncOptions();
base.Refresh();
}
}
private readonly ProxyObject _proxy;
private readonly VerticalPanel _items;
private readonly CustomEditorPresenter _presenter;
/// <summary>
/// The parent foliage tab.
/// </summary>
public readonly FoliageTab Tab;
/// <summary>
/// Initializes a new instance of the <see cref="PaintTab"/> class.
/// </summary>
/// <param name="tab">The parent tab.</param>
/// <param name="mode">The gizmo mode.</param>
public PaintTab(FoliageTab tab, PaintFoliageGizmoMode mode)
: base("Paint")
{
Tab = tab;
Tab.SelectedFoliageChanged += OnSelectedFoliageChanged;
Tab.SelectedFoliageTypeIndexChanged += OnSelectedFoliageTypeIndexChanged;
Tab.SelectedFoliageTypesChanged += UpdateFoliageTypesList;
_proxy = new ProxyObject(this, mode);
// Main panel
var splitPanel = new SplitPanel(Orientation.Vertical, ScrollBars.Vertical, ScrollBars.Vertical)
{
SplitterValue = 0.2f,
AnchorPreset = AnchorPresets.StretchAll,
Offsets = Margin.Zero,
Parent = this
};
// Foliage types list
_items = new VerticalPanel
{
AnchorPreset = AnchorPresets.HorizontalStretchTop,
Offsets = new Margin(4, 4, 4, 0),
IsScrollable = true,
Parent = splitPanel.Panel1
};
// Options editor
// TODO: use editor undo for changing foliage type options
var editor = new CustomEditorPresenter(null, "No foliage type selected");
editor.Panel.Parent = splitPanel.Panel2;
editor.Modified += OnModified;
_presenter = editor;
}
private void OnModified()
{
Editor.Instance.Scene.MarkSceneEdited(_proxy.Foliage != null ? _proxy.Foliage.Scene : null);
}
private void OnSelectedFoliageChanged()
{
_proxy.SelectedFoliageTypeIndex = -1;
_proxy.Foliage = Tab.SelectedFoliage;
_presenter.Deselect();
UpdateFoliageTypesList();
}
private void OnSelectedFoliageTypeIndexChanged(int previousIndex, int currentIndex)
{
if (previousIndex != -1)
{
((ContainerControl)_items.Children[previousIndex]).Children[1].BackgroundColor = Color.Transparent;
}
_proxy.SelectedFoliageTypeIndex = currentIndex;
if (currentIndex != -1)
{
((ContainerControl)_items.Children[currentIndex]).Children[1].BackgroundColor = Style.Current.BackgroundSelected;
_proxy.SyncOptions();
_presenter.Select(_proxy);
_presenter.BuildLayoutOnUpdate();
}
else
{
_presenter.Deselect();
}
_presenter.Panel.Focus();
}
private void UpdateFoliageTypesList()
{
var foliage = Tab.SelectedFoliage;
// Cleanup previous items
_items.DisposeChildren();
// Add new ones
float y = 0;
if (foliage != null)
{
int typesCount = foliage.FoliageTypesCount;
for (int i = 0; i < typesCount; i++)
{
var model = foliage.GetFoliageType(i).Model;
var asset = Tab.Editor.ContentDatabase.FindAsset(model.ID);
var itemPanel = new ContainerControl();
var itemCheck = new CheckBox
{
AnchorPreset = AnchorPresets.VerticalStretchLeft,
Offsets = new Margin(0, 18, 0, 0),
TooltipText = "If checked, enables painting with this foliage type.",
Tag = i,
Parent = itemPanel,
};
// Try restore painting with the given model ID
if (!Tab.FoliageTypeModelIdsToPaint.TryGetValue(model.ID, out bool itemChecked))
{
// Enable by default
itemChecked = true;
Tab.FoliageTypeModelIdsToPaint[model.ID] = itemChecked;
}
itemCheck.Checked = itemChecked;
itemCheck.StateChanged += OnItemCheckStateChanged;
var itemView = new AssetSearchPopup.AssetItemView(asset)
{
AnchorPreset = AnchorPresets.StretchAll,
Offsets = new Margin(itemCheck.Width + 2, 0, 1, 1),
TooltipText = asset.NamePath,
Tag = i,
Parent = itemPanel,
};
itemView.Clicked += OnFoliageTypeListItemClicked;
itemPanel.Height = 34;
itemPanel.Parent = _items;
itemPanel.UnlockChildrenRecursive();
itemPanel.PerformLayout();
y += itemPanel.Height + _items.Spacing;
}
y += _items.Margin.Height;
}
_items.Height = y;
var selectedFoliageTypeIndex = Tab.SelectedFoliageTypeIndex;
if (selectedFoliageTypeIndex != -1)
{
((ContainerControl)_items.Children[selectedFoliageTypeIndex]).Children[1].BackgroundColor = Style.Current.BackgroundSelected;
}
}
private void OnItemCheckStateChanged(CheckBox item)
{
var index = (int)item.Tag;
var foliage = Tab.SelectedFoliage;
var model = foliage.GetFoliageType(index).Model;
Tab.FoliageTypeModelIdsToPaint[model.ID] = item.Checked;
}
private void OnFoliageTypeListItemClicked(ItemsListContextMenu.Item item)
{
Tab.SelectedFoliageTypeIndex = (int)item.Tag;
}
}
}