Merge branch 'master' into master

This commit is contained in:
Ruan Lucas
2023-05-06 10:53:37 -04:00
committed by GitHub
36 changed files with 400 additions and 207 deletions
+2 -4
View File
@@ -72,11 +72,9 @@ namespace FlaxEditor.Content
/// <param name="materialItem">The material item to use as a base material.</param>
public static void CreateMaterialInstance(BinaryAssetItem materialItem)
{
if (materialItem == null)
throw new ArgumentNullException();
var materialInstanceName = materialItem.ShortName + " Instance";
var materialInstanceProxy = Editor.Instance.ContentDatabase.GetProxy<MaterialInstance>();
Editor.Instance.Windows.ContentWin.NewItem(materialInstanceProxy, null, item => OnMaterialInstanceCreated(item, materialItem));
Editor.Instance.Windows.ContentWin.NewItem(materialInstanceProxy, null, item => OnMaterialInstanceCreated(item, materialItem), materialInstanceName);
}
private static void OnMaterialInstanceCreated(ContentItem item, BinaryAssetItem materialItem)
@@ -145,7 +145,10 @@ namespace FlaxEditor.CustomEditors.Dedicated
else
{
// No localization so initialize with empty table
var path = Path.Combine(Path.Combine(Path.GetDirectoryName(GameSettings.Load().Localization.Path), "Localization", culture.Name + ".json"));
var folder = Path.Combine(Path.GetDirectoryName(GameSettings.Load().Localization.Path), "Localization");
if (!Directory.Exists(folder))
Directory.CreateDirectory(folder);
var path = Path.Combine(Path.Combine(folder, culture.Name + ".json"));
var table = FlaxEngine.Content.CreateVirtualAsset<LocalizedStringTable>();
table.Locale = culture.Name;
if (!table.Save(path))
@@ -145,7 +145,7 @@ namespace FlaxEditor.CustomEditors.Editors
_size.IntValue.MinValue = 0;
_size.IntValue.MaxValue = ushort.MaxValue;
_size.IntValue.Value = size;
_size.IntValue.ValueChanged += OnSizeChanged;
_size.IntValue.EditEnd += OnSizeChanged;
}
// Elements
@@ -190,7 +190,7 @@ namespace FlaxEditor.CustomEditors.Editors
_size.IntValue.MinValue = 0;
_size.IntValue.MaxValue = _notNullItems ? size : ushort.MaxValue;
_size.IntValue.Value = size;
_size.IntValue.ValueChanged += OnSizeChanged;
_size.IntValue.EditEnd += OnSizeChanged;
}
// Elements
@@ -6,7 +6,7 @@ using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Elements
{
/// <summary>
/// The combobx element.
/// The combobox element.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.LayoutElement" />
public class ComboBoxElement : LayoutElement
@@ -187,7 +187,7 @@ namespace FlaxEditor.CustomEditors
{
for (int i = 0; i < Count; i++)
{
if (this[i] == referenceSceneObject)
if ((SceneObject)this[i] == referenceSceneObject)
continue;
if (this[i] == null || (this[i] is SceneObject valueSceneObject && valueSceneObject && valueSceneObject.PrefabObjectID != referenceSceneObject.PrefabObjectID))
+73 -48
View File
@@ -9,7 +9,6 @@ using FlaxEditor.Content;
using FlaxEditor.Content.Import;
using FlaxEditor.Content.Settings;
using FlaxEditor.Content.Thumbnails;
using FlaxEditor.GUI;
using FlaxEditor.Modules;
using FlaxEditor.Modules.SourceCodeEditing;
using FlaxEditor.Options;
@@ -307,6 +306,7 @@ namespace FlaxEditor
_areModulesInited = true;
// Preload initial scene asset
try
{
var startupSceneMode = Options.Options.General.StartupSceneMode;
if (startupSceneMode == GeneralOptions.StartupSceneModes.LastOpened && !ProjectCache.HasCustomData(ProjectDataLastScene))
@@ -323,12 +323,23 @@ namespace FlaxEditor
}
case GeneralOptions.StartupSceneModes.LastOpened:
{
if (ProjectCache.TryGetCustomData(ProjectDataLastScene, out var lastSceneIdName) && Guid.TryParse(lastSceneIdName, out var lastSceneId))
Internal_LoadAsset(ref lastSceneId);
if (ProjectCache.TryGetCustomData(ProjectDataLastScene, out var lastSceneIdName))
{
var lastScenes = JsonSerializer.Deserialize<Guid[]>(lastSceneIdName);
foreach (var scene in lastScenes)
{
var lastScene = scene;
Internal_LoadAsset(ref lastScene);
}
}
break;
}
}
}
catch (Exception)
{
// Ignore errors
}
InitializationStart?.Invoke();
@@ -401,58 +412,69 @@ namespace FlaxEditor
}
// Load scene
// scene cmd line argument
var scene = ContentDatabase.Find(_startupSceneCmdLine);
if (scene is SceneItem)
try
{
Editor.Log("Loading scene specified in command line");
Scene.OpenScene(_startupSceneCmdLine);
return;
}
// if no scene cmd line argument is provided
var startupSceneMode = Options.Options.General.StartupSceneMode;
if (startupSceneMode == GeneralOptions.StartupSceneModes.LastOpened && !ProjectCache.HasCustomData(ProjectDataLastScene))
{
// Fallback to default project scene if nothing saved in the cache
startupSceneMode = GeneralOptions.StartupSceneModes.ProjectDefault;
}
switch (startupSceneMode)
{
case GeneralOptions.StartupSceneModes.ProjectDefault:
{
if (string.IsNullOrEmpty(GameProject.DefaultScene))
break;
JsonSerializer.ParseID(GameProject.DefaultScene, out var defaultSceneId);
var defaultScene = ContentDatabase.Find(defaultSceneId);
if (defaultScene is SceneItem)
// Scene cmd line argument
var scene = ContentDatabase.Find(_startupSceneCmdLine);
if (scene is SceneItem)
{
Editor.Log("Loading default project scene");
Scene.OpenScene(defaultSceneId);
// Use spawn point
Windows.EditWin.Viewport.ViewRay = GameProject.DefaultSceneSpawn;
Editor.Log("Loading scene specified in command line");
Scene.OpenScene(_startupSceneCmdLine);
return;
}
break;
}
case GeneralOptions.StartupSceneModes.LastOpened:
{
if (ProjectCache.TryGetCustomData(ProjectDataLastScene, out var lastSceneIdName) && Guid.TryParse(lastSceneIdName, out var lastSceneId))
var startupSceneMode = Options.Options.General.StartupSceneMode;
if (startupSceneMode == GeneralOptions.StartupSceneModes.LastOpened && !ProjectCache.HasCustomData(ProjectDataLastScene))
{
var lastScene = ContentDatabase.Find(lastSceneId);
if (lastScene is SceneItem)
// Fallback to default project scene if nothing saved in the cache
startupSceneMode = GeneralOptions.StartupSceneModes.ProjectDefault;
}
switch (startupSceneMode)
{
case GeneralOptions.StartupSceneModes.ProjectDefault:
{
if (string.IsNullOrEmpty(GameProject.DefaultScene))
break;
JsonSerializer.ParseID(GameProject.DefaultScene, out var defaultSceneId);
var defaultScene = ContentDatabase.Find(defaultSceneId);
if (defaultScene is SceneItem)
{
Editor.Log("Loading last opened scene");
Scene.OpenScene(lastSceneId);
Editor.Log("Loading default project scene");
Scene.OpenScene(defaultSceneId);
// Use spawn point
Windows.EditWin.Viewport.ViewRay = GameProject.DefaultSceneSpawn;
}
break;
}
case GeneralOptions.StartupSceneModes.LastOpened:
{
if (ProjectCache.TryGetCustomData(ProjectDataLastScene, out var lastSceneIdName))
{
var lastScenes = JsonSerializer.Deserialize<Guid[]>(lastSceneIdName);
foreach (var sceneId in lastScenes)
{
var lastScene = ContentDatabase.Find(sceneId);
if (!(lastScene is SceneItem))
continue;
Editor.Log($"Loading last opened scene: {lastScene.ShortName}");
if (sceneId == lastScenes[0])
Scene.OpenScene(sceneId);
else
Level.LoadSceneAsync(sceneId);
}
// Restore view
if (ProjectCache.TryGetCustomData(ProjectDataLastSceneSpawn, out var lastSceneSpawnName))
Windows.EditWin.Viewport.ViewRay = JsonSerializer.Deserialize<Ray>(lastSceneSpawnName);
}
break;
}
}
break;
}
catch (Exception)
{
// Ignore errors
}
}
@@ -581,8 +603,8 @@ namespace FlaxEditor
UI.UpdateStatusBar();
}
if (UI?.StatusBar?.Text != null && !UI.StatusBar.Text.Contains("Auto") &&
_saveNowButton != null && _cancelSaveButton != null &&
if (UI?.StatusBar?.Text != null && !UI.StatusBar.Text.Contains("Auto") &&
_saveNowButton != null && _cancelSaveButton != null &&
(_saveNowButton.Visible || _cancelSaveButton.Visible))
{
_saveNowButton.Visible = false;
@@ -662,11 +684,14 @@ namespace FlaxEditor
// Start exit
StateMachine.GoToState<ClosingState>();
// Cache last opened scene
// Cache last opened scenes
{
var lastSceneId = Level.ScenesCount > 0 ? Level.Scenes[0].ID : Guid.Empty;
var lastScenes = Level.Scenes;
var lastSceneIds = new Guid[lastScenes.Length];
for (int i = 0; i < lastScenes.Length; i++)
lastSceneIds[i] = lastScenes[i].ID;
var lastSceneSpawn = Windows.EditWin.Viewport.ViewRay;
ProjectCache.SetCustomData(ProjectDataLastScene, lastSceneId.ToString());
ProjectCache.SetCustomData(ProjectDataLastScene, JsonSerializer.Serialize(lastSceneIds));
ProjectCache.SetCustomData(ProjectDataLastSceneSpawn, JsonSerializer.Serialize(lastSceneSpawn));
}
@@ -174,7 +174,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks
continue;
// Prevent from adding the same track twice
if (SubTracks.Any(x => x is IObjectTrack y && y.Object == script))
if (SubTracks.Any(x => x is IObjectTrack y && y.Object as SceneObject == script))
continue;
var name = Utilities.Utils.GetPropertyNameUI(script.GetType().Name);
+4 -1
View File
@@ -661,17 +661,20 @@ namespace FlaxEditor.GUI.Tree
// Draw drag and drop effect
if (IsDragOver && _tree.DraggedOverNode == this)
{
Color dragOverColor = style.BackgroundSelected * 0.6f;
Color dragOverColor = style.BackgroundSelected;
Rectangle rect;
switch (_dragOverMode)
{
case DragItemPositioning.At:
dragOverColor *= 0.6f;
rect = textRect;
break;
case DragItemPositioning.Above:
dragOverColor *= 1.2f;
rect = new Rectangle(textRect.X, textRect.Top - DefaultDragInsertPositionMargin - DefaultNodeOffsetY - _margin.Top, textRect.Width, DefaultDragInsertPositionMargin * 2.0f);
break;
case DragItemPositioning.Below:
dragOverColor *= 1.2f;
rect = new Rectangle(textRect.X, textRect.Bottom + _margin.Bottom - DefaultDragInsertPositionMargin, textRect.Width, DefaultDragInsertPositionMargin * 2.0f);
break;
default:
@@ -655,7 +655,7 @@ namespace FlaxEditor.Modules
var children = folder.Children.ToArray();
for (int i = 0; i < children.Length; i++)
{
Delete(children[0]);
Delete(children[i]);
}
}
+36
View File
@@ -315,6 +315,42 @@ namespace FlaxEditor.Modules
Editor.StateMachine.ChangingScenesState.UnloadScene(Level.Scenes);
}
/// <summary>
/// Closes all of the scenes except for the specified scene (async).
/// </summary>
/// <param name="scene">The scene to not close.</param>
public void CloseAllScenesExcept(Scene scene)
{
// Check if cannot change scene now
if (!Editor.StateMachine.CurrentState.CanChangeScene)
return;
var scenes = new List<Scene>();
foreach (var s in Level.Scenes)
{
if (s == scene)
continue;
scenes.Add(s);
}
// In play-mode Editor mocks the level streaming script
if (Editor.IsPlayMode)
{
foreach (var s in scenes)
{
Level.UnloadSceneAsync(s);
}
return;
}
// Ensure to save all pending changes
if (CheckSaveBeforeClose())
return;
// Unload scenes
Editor.StateMachine.ChangingScenesState.UnloadScene(scenes);
}
/// <summary>
/// Show save before scene load/unload action.
/// </summary>
@@ -77,6 +77,8 @@ namespace FlaxEditor.SceneGraph.Actors
}
contextMenu.AddButton("Save scene", OnSave).LinkTooltip("Saves this scene.").Enabled = IsEdited && !Editor.IsPlayMode;
contextMenu.AddButton("Unload scene", OnUnload).LinkTooltip("Unloads this scene.").Enabled = Editor.Instance.StateMachine.CurrentState.CanChangeScene;
if (Level.ScenesCount > 1)
contextMenu.AddButton("Unload all but this scene", OnUnloadAllButSelectedScene).LinkTooltip("Unloads all of the active scenes except for the selected scene.").Enabled = Editor.Instance.StateMachine.CurrentState.CanChangeScene;
base.OnContextMenu(contextMenu);
}
@@ -95,5 +97,10 @@ namespace FlaxEditor.SceneGraph.Actors
{
Editor.Instance.Scene.CloseScene(Scene);
}
private void OnUnloadAllButSelectedScene()
{
Editor.Instance.Scene.CloseAllScenesExcept(Scene);
}
}
}
+1 -1
View File
@@ -47,7 +47,7 @@ namespace FlaxEditor.Surface.Archetypes
Description = desc,
Flags = NodeFlags.AllGraphs,
AlternativeTitles = altTitles,
Size = new Float2(140, 40),
Size = new Float2(150, 40),
DefaultType = new ScriptType(inputType),
ConnectionsHints = hints,
IndependentBoxes = new[] { 0, 1 },
+8 -36
View File
@@ -730,12 +730,8 @@ namespace FlaxEditor.Surface.Archetypes
Elements = new[]
{
NodeElementArchetype.Factory.Output(0, string.Empty, typeof(float), 0),
NodeElementArchetype.Factory.Text(0, 0, "Min", 30.0f, 18.0f),
NodeElementArchetype.Factory.Float(30, 0, 0),
NodeElementArchetype.Factory.Text(0, Surface.Constants.LayoutOffsetY, "Max", 30.0f, 18.0f),
NodeElementArchetype.Factory.Float(30, Surface.Constants.LayoutOffsetY, 1),
NodeElementArchetype.Factory.Input(0, "Min", true, typeof(float), 1, 0),
NodeElementArchetype.Factory.Input(1, "Max", true, typeof(float), 2, 1),
}
},
new NodeArchetype
@@ -753,14 +749,8 @@ namespace FlaxEditor.Surface.Archetypes
Elements = new[]
{
NodeElementArchetype.Factory.Output(0, string.Empty, typeof(Float2), 0),
NodeElementArchetype.Factory.Text(0, 0, "Min", 30.0f, 18.0f),
NodeElementArchetype.Factory.Vector_X(30, 0, 0),
NodeElementArchetype.Factory.Vector_Y(83, 0, 0),
NodeElementArchetype.Factory.Text(0, Surface.Constants.LayoutOffsetY, "Max", 30.0f, 18.0f),
NodeElementArchetype.Factory.Vector_X(30, Surface.Constants.LayoutOffsetY, 1),
NodeElementArchetype.Factory.Vector_Y(83, Surface.Constants.LayoutOffsetY, 1),
NodeElementArchetype.Factory.Input(0, "Min", true, typeof(Float2), 1, 0),
NodeElementArchetype.Factory.Input(1, "Max", true, typeof(Float2), 2, 1),
}
},
new NodeArchetype
@@ -778,16 +768,8 @@ namespace FlaxEditor.Surface.Archetypes
Elements = new[]
{
NodeElementArchetype.Factory.Output(0, string.Empty, typeof(Float3), 0),
NodeElementArchetype.Factory.Text(0, 0, "Min", 30.0f, 18.0f),
NodeElementArchetype.Factory.Vector_X(30, 0, 0),
NodeElementArchetype.Factory.Vector_Y(83, 0, 0),
NodeElementArchetype.Factory.Vector_Z(136, 0, 0),
NodeElementArchetype.Factory.Text(0, Surface.Constants.LayoutOffsetY, "Max", 30.0f, 18.0f),
NodeElementArchetype.Factory.Vector_X(30, Surface.Constants.LayoutOffsetY, 1),
NodeElementArchetype.Factory.Vector_Y(83, Surface.Constants.LayoutOffsetY, 1),
NodeElementArchetype.Factory.Vector_Z(136, Surface.Constants.LayoutOffsetY, 1),
NodeElementArchetype.Factory.Input(0, "Min", true, typeof(Float3), 1, 0),
NodeElementArchetype.Factory.Input(1, "Max", true, typeof(Float3), 2, 1),
}
},
new NodeArchetype
@@ -805,18 +787,8 @@ namespace FlaxEditor.Surface.Archetypes
Elements = new[]
{
NodeElementArchetype.Factory.Output(0, string.Empty, typeof(Float4), 0),
NodeElementArchetype.Factory.Text(0, 0, "Min", 30.0f, 18.0f),
NodeElementArchetype.Factory.Vector_X(30, 0, 0),
NodeElementArchetype.Factory.Vector_Y(83, 0, 0),
NodeElementArchetype.Factory.Vector_Z(136, 0, 0),
NodeElementArchetype.Factory.Vector_W(189, 0, 0),
NodeElementArchetype.Factory.Text(0, Surface.Constants.LayoutOffsetY, "Max", 30.0f, 18.0f),
NodeElementArchetype.Factory.Vector_X(30, Surface.Constants.LayoutOffsetY, 1),
NodeElementArchetype.Factory.Vector_Y(83, Surface.Constants.LayoutOffsetY, 1),
NodeElementArchetype.Factory.Vector_Z(136, Surface.Constants.LayoutOffsetY, 1),
NodeElementArchetype.Factory.Vector_W(189, Surface.Constants.LayoutOffsetY, 1),
NodeElementArchetype.Factory.Input(0, "Min", true, typeof(Float4), 1, 0),
NodeElementArchetype.Factory.Input(1, "Max", true, typeof(Float4), 2, 1),
}
},
@@ -47,6 +47,8 @@ AssetReference<MaterialInstance> CustomTextureMaterial;
ModelInstanceEntries InstanceBuffers[static_cast<int32>(IconTypes::MAX)];
Dictionary<ScriptingTypeHandle, IconTypes> ActorTypeToIconType;
Dictionary<ScriptingTypeHandle, AssetReference<Texture>> ActorTypeToTexture;
Dictionary<ScriptingObjectReference<Actor>, AssetReference<Texture>> ActorToTexture;
Dictionary<AssetReference<Texture>, AssetReference<MaterialBase>> TextureToMaterial;
class ViewportIconsRendererService : public EngineService
{
@@ -103,10 +105,18 @@ void ViewportIconsRenderer::AddActor(Actor* actor)
actor->GetSceneRendering()->AddViewportIcon(actor);
}
void ViewportIconsRenderer::AddActorWithTexture(Actor* actor, Texture* iconTexture)
{
CHECK(actor && actor->GetScene() && iconTexture);
ActorToTexture[actor] = iconTexture;
actor->GetSceneRendering()->AddViewportIcon(actor);
}
void ViewportIconsRenderer::RemoveActor(Actor* actor)
{
CHECK(actor && actor->GetScene());
actor->GetSceneRendering()->RemoveViewportIcon(actor);
ActorToTexture.Remove(actor);
}
void ViewportIconsRendererService::DrawIcons(RenderContext& renderContext, Scene* scene, Mesh::DrawInfo& draw)
@@ -128,7 +138,7 @@ void ViewportIconsRendererService::DrawIcons(RenderContext& renderContext, Scene
ScriptingTypeHandle typeHandle = icon->GetTypeHandle();
draw.Buffer = nullptr;
if (ActorTypeToTexture.TryGet(typeHandle, texture))
if (ActorToTexture.TryGet(icon, texture) || ActorTypeToTexture.TryGet(typeHandle, texture))
{
// Use custom texture
draw.Buffer = &InstanceBuffers[static_cast<int32>(IconTypes::CustomTexture)];
@@ -137,9 +147,20 @@ void ViewportIconsRendererService::DrawIcons(RenderContext& renderContext, Scene
// Lazy-init (use in-built icon material with custom texture)
draw.Buffer->Setup(1);
draw.Buffer->At(0).ReceiveDecals = false;
draw.Buffer->At(0).Material = InstanceBuffers[0][0].Material->CreateVirtualInstance();
draw.Buffer->At(0).ShadowsMode = ShadowsCastingMode::None;
}
draw.Buffer->At(0).Material->SetParameterValue(TEXT("Image"), Variant(texture));
AssetReference<MaterialBase> material;
if (!TextureToMaterial.TryGet(texture, material))
{
// Create custom material per custom texture
TextureToMaterial[texture] = InstanceBuffers[0][0].Material->CreateVirtualInstance();
TextureToMaterial[texture]->SetParameterValue(TEXT("Image"), Variant(texture));
material = TextureToMaterial[texture];
}
draw.Buffer->At(0).Material = material;
}
else if (ActorTypeToIconType.TryGet(typeHandle, iconType))
{
@@ -170,6 +191,8 @@ void ViewportIconsRendererService::DrawIcons(RenderContext& renderContext, Actor
Matrix m1, m2, world;
BoundingSphere sphere(actor->GetPosition() - renderContext.View.Origin, ICON_RADIUS);
IconTypes iconType;
AssetReference<Texture> texture;
if (frustum.Intersects(sphere) && ActorTypeToIconType.TryGet(actor->GetTypeHandle(), iconType))
{
// Create world matrix
@@ -182,7 +205,39 @@ void ViewportIconsRendererService::DrawIcons(RenderContext& renderContext, Actor
// Draw icon
GeometryDrawStateData drawState;
draw.DrawState = &drawState;
draw.Buffer = &InstanceBuffers[static_cast<int32>(iconType)];
// Support custom icons through types, but not onces that were added through actors,
// since they cant register while in prefab view anyway
if (ActorTypeToTexture.TryGet(actor->GetTypeHandle(), texture))
{
// Use custom texture
draw.Buffer = &InstanceBuffers[static_cast<int32>(IconTypes::CustomTexture)];
if (draw.Buffer->Count() == 0)
{
// Lazy-init (use in-built icon material with custom texture)
draw.Buffer->Setup(1);
draw.Buffer->At(0).ReceiveDecals = false;
draw.Buffer->At(0).ShadowsMode = ShadowsCastingMode::None;
}
AssetReference<MaterialBase> material;
if (!TextureToMaterial.TryGet(texture, material))
{
// Create custom material per custom texture
TextureToMaterial[texture] = InstanceBuffers[0][0].Material->CreateVirtualInstance();
TextureToMaterial[texture]->SetParameterValue(TEXT("Image"), Variant(texture));
material = TextureToMaterial[texture];
}
draw.Buffer->At(0).Material = material;
}
else
{
// Use predefined material
draw.Buffer = &InstanceBuffers[static_cast<int32>(iconType)];
}
draw.World = &world;
draw.Bounds = sphere;
QuadModel->Draw(renderContext, draw);
@@ -196,9 +251,10 @@ bool ViewportIconsRendererService::Init()
{
QuadModel = Content::LoadAsyncInternal<Model>(TEXT("Engine/Models/Quad"));
#define INIT(type, path) \
InstanceBuffers[static_cast<int32>(IconTypes::type)].Setup(1); \
InstanceBuffers[static_cast<int32>(IconTypes::type)][0].ReceiveDecals = false; \
InstanceBuffers[static_cast<int32>(IconTypes::type)][0].Material = Content::LoadAsyncInternal<MaterialInstance>(TEXT(path))
InstanceBuffers[static_cast<int32>(IconTypes::type)].Setup(1); \
InstanceBuffers[static_cast<int32>(IconTypes::type)][0].ReceiveDecals = false; \
InstanceBuffers[static_cast<int32>(IconTypes::type)][0].ShadowsMode = ShadowsCastingMode::None; \
InstanceBuffers[static_cast<int32>(IconTypes::type)][0].Material = Content::LoadAsyncInternal<MaterialInstance>(TEXT(path))
INIT(PointLight, "Editor/Icons/PointLight");
INIT(DirectionalLight, "Editor/Icons/DirectionalLight");
INIT(EnvironmentProbe, "Editor/Icons/EnvironmentProbe");
@@ -236,4 +292,6 @@ void ViewportIconsRendererService::Dispose()
for (int32 i = 0; i < ARRAY_COUNT(InstanceBuffers); i++)
InstanceBuffers[i].Release();
ActorTypeToIconType.Clear();
ActorToTexture.Clear();
TextureToMaterial.Clear();
}
@@ -37,6 +37,13 @@ public:
/// <param name="actor">The actor to register for icon drawing.</param>
API_FUNCTION() static void AddActor(Actor* actor);
/// <summary>
/// Adds actor to the viewport icon rendering.
/// </summary>
/// <param name="actor">The actor to register for icon drawing.</param>
/// <param name="iconTexture">The icon texture to draw.</param>
API_FUNCTION() static void AddActorWithTexture(Actor* actor, Texture* iconTexture);
/// <summary>
/// Removes actor from the viewport icon rendering.
/// </summary>
@@ -126,6 +126,10 @@ namespace FlaxEditor.Windows.Assets
_presenter.NoSelectionText = "Failed to load asset. See log for more. " + ex.Message.Replace('\n', ' ');
}
}
else if (string.IsNullOrEmpty(dataTypeName))
{
_presenter.NoSelectionText = "Empty data type.";
}
else
{
_presenter.NoSelectionText = string.Format("Missing type '{0}'.", dataTypeName);
@@ -156,14 +156,14 @@ namespace FlaxEditor.Windows.Assets
private bool HasEmitter => _track.Asset != null;
[EditorDisplay("Particle Emitter"), VisibleIf("HasEmitter"), EditorOrder(200), Tooltip("The start frame of the media event.")]
[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("HasEmitter"), EditorOrder(300), Tooltip("The total duration of the media event in the timeline sequence frames amount.")]
[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;
+5
View File
@@ -656,6 +656,11 @@ namespace FlaxEditor.Windows
throw new ArgumentNullException(nameof(proxy));
string name = initialName ?? proxy.NewItemName;
// If the proxy can not be created in the current folder, then navigate to the content folder
if (!proxy.CanCreate(CurrentViewFolder))
Navigate(Editor.Instance.ContentDatabase.Game.Content);
ContentFolder parentFolder = CurrentViewFolder;
string parentFolderPath = parentFolder.Path;
-5
View File
@@ -1002,17 +1002,12 @@ Asset* Content::load(const Guid& id, const ScriptingTypeHandle& type, AssetInfo&
}
#if ASSETS_LOADING_EXTRA_VERIFICATION
// Ensure we have valid asset info
ASSERT(assetInfo.TypeName.HasChars() && assetInfo.Path.HasChars());
// Check if file exists
if (!FileSystem::FileExists(assetInfo.Path))
{
LOG(Error, "Cannot find file '{0}'", assetInfo.Path);
return nullptr;
}
#endif
// Find asset factory based in its type
+5
View File
@@ -35,6 +35,11 @@ namespace FlaxEngine
return null;
var dataTypeName = DataTypeName;
if (string.IsNullOrEmpty(dataTypeName))
{
Debug.LogError(string.Format("Missing typename of data in Json asset '{0}'.", Path), this);
return null;
}
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
for (int i = 0; i < assemblies.Length; i++)
{
+1 -1
View File
@@ -379,7 +379,7 @@ public:
}
/// <summary>
/// Gets the actor static fags.
/// Gets the actor static flags.
/// </summary>
API_PROPERTY(Attributes="NoAnimate, EditorDisplay(\"General\"), EditorOrder(-80), CustomEditorAlias(\"FlaxEditor.CustomEditors.Editors.ActorStaticFlagsEditor\")")
FORCE_INLINE StaticFlags GetStaticFlags() const
+10 -2
View File
@@ -903,12 +903,11 @@ bool LevelImpl::unloadScene(Scene* scene)
bool LevelImpl::unloadScenes()
{
auto scenes = Level::Scenes;
for (int32 i = 0; i < scenes.Count(); i++)
for (int32 i = scenes.Count() - 1; i >= 0; i--)
{
if (unloadScene(scenes[i]))
return true;
}
return false;
}
@@ -1210,6 +1209,15 @@ bool LevelImpl::saveScene(Scene* scene, const String& path)
LOG(Info, "Scene saved! Time {0} ms", Math::CeilToInt((float)(DateTime::NowUTC() - startTime).GetTotalMilliseconds()));
#if USE_EDITOR
// Reload asset at the target location if is loaded
Asset* asset = Content::GetAsset(sceneId);
if (!asset)
asset = Content::GetAsset(path);
if (asset)
asset->Reload();
#endif
// Fire event
CallSceneEvent(SceneEventType::OnSceneSaved, scene, sceneId);
+2 -2
View File
@@ -73,7 +73,7 @@ Array<Tag> Tags::GetSubTags(Tag parentTag)
return subTags;
}
bool Tags::HasTag(const Array<Tag>& list, const Tag& tag)
bool Tags::HasTag(const Array<Tag>& list, const Tag tag)
{
if (tag.Index == 0)
return false;
@@ -87,7 +87,7 @@ bool Tags::HasTag(const Array<Tag>& list, const Tag& tag)
return false;
}
bool Tags::HasTagExact(const Array<Tag>& list, const Tag& tag)
bool Tags::HasTagExact(const Array<Tag>& list, const Tag tag)
{
if (tag.Index == 0)
return false;
+2 -2
View File
@@ -106,7 +106,7 @@ public:
/// <param name="list">The tags list to use.</param>
/// <param name="tag">The tag to check.</param>
/// <returns>True if given tag is contained by the list of tags. Returns false for empty list.</returns>
static bool HasTag(const Array<Tag>& list, const Tag& tag);
static bool HasTag(const Array<Tag>& list, const Tag tag);
/// <summary>
/// Checks if the list of tags contains a given tag (exact match). For example, HasTag({"A.B"}, "A") returns false, for parents check use HasTag.
@@ -114,7 +114,7 @@ public:
/// <param name="list">The tags list to use.</param>
/// <param name="tag">The tag to check.</param>
/// <returns>True if given tag is contained by the list of tags. Returns false for empty list.</returns>
static bool HasTagExact(const Array<Tag>& list, const Tag& tag);
static bool HasTagExact(const Array<Tag>& list, const Tag tag);
/// <summary>
/// Checks if the list of tags contains any of the given tags (including parent tags check). For example, HasAny({"A.B", "C"}, {"A"}) returns true, for exact check use HasAnyExact.
+1 -1
View File
@@ -100,7 +100,7 @@ API_ENUM(Attributes="Flags") enum class StaticFlags
Navigation = 1 << 3,
/// <summary>
/// Objects is fully static on the scene.
/// Object is fully static in the scene.
/// </summary>
FullyStatic = Transform | ReflectionProbe | Lightmap | Navigation,
@@ -15,6 +15,7 @@ REGISTER_JSON_ASSET(LocalizedStringTable, "FlaxEngine.LocalizedStringTable", tru
LocalizedStringTable::LocalizedStringTable(const SpawnParams& params, const AssetInfo* info)
: JsonAssetBase(params, info)
{
DataTypeName = TypeName;
}
void LocalizedStringTable::AddString(const StringView& id, const StringView& value)
@@ -388,33 +388,33 @@ void ParticleEmitterGPUGenerator::ProcessGroupParticles(Box* box, Node* node, Va
// Random Float Range
case 213:
{
auto& a = node->Values[0].AsFloat;
auto& b = node->Values[1].AsFloat;
value = writeLocal(VariantType::Float, String::Format(TEXT("lerp({0}, {1}, RAND)"), a, b), node);
auto a = tryGetValue(node->TryGetBox(1), node->Values[0]).AsFloat();
auto b = tryGetValue(node->TryGetBox(2), node->Values[1]).AsFloat();
value = writeLocal(VariantType::Float, String::Format(TEXT("lerp({0}, {1}, RAND)"), a.Value, b.Value), node);
break;
}
// Random Vector2 Range
case 214:
{
auto& a = node->Values[0].AsFloat2();
auto& b = node->Values[1].AsFloat2();
value = writeLocal(VariantType::Float2, String::Format(TEXT("float2(lerp({0}, {1}, RAND), lerp({2}, {3}, RAND))"), a.X, b.X, a.Y, b.Y), node);
auto a = tryGetValue(node->TryGetBox(1), node->Values[0]).AsFloat2();
auto b = tryGetValue(node->TryGetBox(2), node->Values[1]).AsFloat2();
value = writeLocal(VariantType::Float2, String::Format(TEXT("float2(lerp({0}.x, {1}.x, RAND), lerp({0}.y, {1}.y, RAND))"), a.Value, b.Value), node);
break;
}
// Random Vector3 Range
case 215:
{
auto& a = node->Values[0].AsFloat3();
auto& b = node->Values[1].AsFloat3();
value = writeLocal(VariantType::Float3, String::Format(TEXT("float3(lerp({0}, {1}, RAND), lerp({2}, {3}, RAND), lerp({4}, {5}, RAND))"), a.X, b.X, a.Y, b.Y, a.Z, b.Z), node);
auto a = tryGetValue(node->TryGetBox(1), node->Values[0]).AsFloat3();
auto b = tryGetValue(node->TryGetBox(2), node->Values[1]).AsFloat3();
value = writeLocal(VariantType::Float3, String::Format(TEXT("float3(lerp({0}.x, {1}.x, RAND), lerp({0}.y, {1}.y, RAND), lerp({0}.z, {1}.z, RAND))"), a.Value, b.Value), node);
break;
}
// Random Vector4 Range
case 216:
{
auto& a = node->Values[0].AsFloat4();
auto& b = node->Values[1].AsFloat4();
value = writeLocal(VariantType::Float4, String::Format(TEXT("float4(lerp({0}, {1}, RAND), lerp({2}, {3}, RAND), lerp({4}, {5}, RAND), lerp({6}, {7}, RAND))"), a.X, b.X, a.Y, b.Y, a.Z, b.Z, a.W, b.W), node);
auto a = tryGetValue(node->TryGetBox(1), node->Values[0]).AsFloat4();
auto b = tryGetValue(node->TryGetBox(2), node->Values[1]).AsFloat4();
value = writeLocal(VariantType::Float4, String::Format(TEXT("float4(lerp({0}.x, {1}.x, RAND), lerp({0}.y, {1}.y, RAND), lerp({0}.z, {1}.z, RAND), lerp({0}.w, {1}.w, RAND))"), a.Value, b.Value), node);
break;
}
// Particle Emitter Function
+10 -21
View File
@@ -212,6 +212,12 @@ Asset::LoadResult ParticleSystem::load()
int32 version;
stream.ReadInt32(&version);
#if USE_EDITOR
// Skip unused parameters
#define SKIP_UNUSED_PARAM_OVERRIDE() if (key.First < 0 || key.First >= Emitters.Count() || Emitters[key.First] == nullptr || Emitters[key.First]->Graph.GetParameter(key.Second) == nullptr) continue
#else
#define SKIP_UNUSED_PARAM_OVERRIDE()
#endif
switch (version)
{
case 1:
@@ -281,13 +287,7 @@ Asset::LoadResult ParticleSystem::load()
stream.ReadInt32(&key.First);
stream.Read(key.Second);
stream.ReadCommonValue(&value);
#if USE_EDITOR
// Skip unused parameters
if (key.First < 0 || key.First >= Emitters.Count() || Emitters[key.First]->Graph.GetParameter(key.Second) == nullptr)
continue;
#endif
SKIP_UNUSED_PARAM_OVERRIDE();
EmittersParametersOverrides.Add(key, Variant(value));
}
}
@@ -361,13 +361,7 @@ Asset::LoadResult ParticleSystem::load()
stream.ReadInt32(&key.First);
stream.Read(key.Second);
stream.ReadCommonValue(&value);
#if USE_EDITOR
// Skip unused parameters
if (key.First < 0 || key.First >= Emitters.Count() || Emitters[key.First]->Graph.GetParameter(key.Second) == nullptr)
continue;
#endif
SKIP_UNUSED_PARAM_OVERRIDE();
EmittersParametersOverrides[key] = Variant(value);
}
}
@@ -440,13 +434,7 @@ Asset::LoadResult ParticleSystem::load()
stream.ReadInt32(&key.First);
stream.Read(key.Second);
stream.ReadVariant(&value);
#if USE_EDITOR
// Skip unused parameters
if (key.First < 0 || key.First >= Emitters.Count() || Emitters[key.First]->Graph.GetParameter(key.Second) == nullptr)
continue;
#endif
SKIP_UNUSED_PARAM_OVERRIDE();
EmittersParametersOverrides[key] = value;
}
}
@@ -457,6 +445,7 @@ Asset::LoadResult ParticleSystem::load()
LOG(Warning, "Unknown timeline version {0}.", version);
return LoadResult::InvalidData;
}
#undef SKIP_UNUSED_PARAM_OVERRIDE
#if !BUILD_RELEASE
_debugName = StringUtils::GetFileNameWithoutExtension(GetPath());
+36
View File
@@ -205,6 +205,42 @@ namespace FlaxEngine
return obj != null && obj.__unmanagedPtr != IntPtr.Zero;
}
/// <summary>
/// Checks whether the two objects are equal.
/// </summary>
/// <param name="left"></param>
/// <param name="right"></param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator ==(Object left, Object right)
{
IntPtr leftPtr = (object)left != null ? left.__unmanagedPtr : IntPtr.Zero;
IntPtr rightPtr = (object)right != null ? right.__unmanagedPtr : IntPtr.Zero;
return leftPtr == rightPtr;
}
/// <summary>
/// Checks whether the two objects are not equal.
/// </summary>
/// <param name="left"></param>
/// <param name="right"></param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(Object left, Object right)
{
IntPtr leftPtr = (object)left != null ? left.__unmanagedPtr : IntPtr.Zero;
IntPtr rightPtr = (object)right != null ? right.__unmanagedPtr : IntPtr.Zero;
return leftPtr != rightPtr;
}
/// <inheritdoc />
public override bool Equals(object obj)
{
if (obj is FlaxEngine.Object o)
return o.__unmanagedPtr == __unmanagedPtr;
return false;
}
/// <summary>
/// Gets the pointer to the native object. Handles null object reference (returns zero).
/// </summary>
@@ -267,3 +267,5 @@ API_CLASS(InBuild) class FLAXENGINE_API PersistentScriptingObject : public Scrip
public:
PersistentScriptingObject(const SpawnParams& params);
};
extern FLAXENGINE_API class ScriptingObject* FindObject(const Guid& id, class MClass* type);
@@ -37,7 +37,7 @@ namespace FlaxEngine.Json
{
// Skip serialization as reference id for the root object serialization (eg. Script)
var cache = JsonSerializer.Current.Value;
if (cache != null && cache.IsDuringSerialization && cache.SerializerWriter.SerializeStackSize == 0)
if (cache != null && cache.IsWriting && cache.SerializerWriter.SerializeStackSize == 0)
{
return false;
}
+84 -43
View File
@@ -26,7 +26,8 @@ namespace FlaxEngine.Json
public JsonSerializerInternalWriter SerializerWriter;
public UnmanagedMemoryStream MemoryStream;
public StreamReader Reader;
public bool IsDuringSerialization;
public bool IsWriting;
public bool IsReading;
public unsafe SerializerCache(JsonSerializerSettings settings)
{
@@ -50,6 +51,49 @@ namespace FlaxEngine.Json
DateFormatString = JsonSerializer.DateFormatString,
};
}
public void ReadBegin()
{
if (IsReading)
{
// TODO: Reset reading state (eg if previous deserialization got exception)
}
IsWriting = false;
IsReading = true;
}
public void ReadEnd()
{
IsReading = false;
}
public void WriteBegin()
{
if (IsWriting)
{
// Reset writing state (eg if previous serialization got exception)
JsonWriter = new JsonTextWriter(StringWriter)
{
IndentChar = '\t',
Indentation = 1,
Formatting = JsonSerializer.Formatting,
DateFormatHandling = JsonSerializer.DateFormatHandling,
DateTimeZoneHandling = JsonSerializer.DateTimeZoneHandling,
FloatFormatHandling = JsonSerializer.FloatFormatHandling,
StringEscapeHandling = JsonSerializer.StringEscapeHandling,
Culture = JsonSerializer.Culture,
DateFormatString = JsonSerializer.DateFormatString,
};
}
StringBuilder.Clear();
IsWriting = true;
IsReading = false;
}
public void WriteEnd()
{
IsWriting = false;
}
}
internal static JsonSerializerSettings Settings = CreateDefaultSettings(false);
@@ -122,9 +166,9 @@ namespace FlaxEngine.Json
var cache = isManagedOnly ? CacheManagedOnly.Value : Cache.Value;
Current.Value = cache;
cache.StringBuilder.Clear();
cache.IsDuringSerialization = true;
cache.WriteBegin();
cache.SerializerWriter.Serialize(cache.JsonWriter, obj, type);
cache.WriteEnd();
return cache.StringBuilder.ToString();
}
@@ -141,9 +185,9 @@ namespace FlaxEngine.Json
var cache = isManagedOnly ? CacheManagedOnly.Value : Cache.Value;
Current.Value = cache;
cache.StringBuilder.Clear();
cache.IsDuringSerialization = true;
cache.WriteBegin();
cache.SerializerWriter.Serialize(cache.JsonWriter, obj, type);
cache.WriteEnd();
return cache.StringBuilder.ToString();
}
@@ -161,9 +205,9 @@ namespace FlaxEngine.Json
var cache = isManagedOnly ? CacheManagedOnly.Value : Cache.Value;
Current.Value = cache;
cache.StringBuilder.Clear();
cache.IsDuringSerialization = true;
cache.WriteBegin();
cache.SerializerWriter.SerializeDiff(cache.JsonWriter, obj, type, other);
cache.WriteEnd();
return cache.StringBuilder.ToString();
}
@@ -176,21 +220,20 @@ namespace FlaxEngine.Json
public static void Deserialize(object input, string json)
{
var cache = Cache.Value;
cache.IsDuringSerialization = false;
Current.Value = cache;
cache.ReadBegin();
using (JsonReader reader = new JsonTextReader(new StringReader(json)))
{
cache.JsonSerializer.Populate(reader, input);
if (!cache.JsonSerializer.CheckAdditionalContent)
return;
while (reader.Read())
if (cache.JsonSerializer.CheckAdditionalContent)
{
if (reader.TokenType != JsonToken.Comment)
throw new Exception("Additional text found in JSON string after finishing deserializing object.");
while (reader.Read())
{
if (reader.TokenType != JsonToken.Comment)
throw new Exception("Additional text found in JSON string after finishing deserializing object.");
}
}
}
cache.ReadEnd();
}
/// <summary>
@@ -214,22 +257,20 @@ namespace FlaxEngine.Json
{
object result;
var cache = Cache.Value;
cache.IsDuringSerialization = false;
Current.Value = cache;
cache.ReadBegin();
using (JsonReader reader = new JsonTextReader(new StringReader(json)))
{
result = cache.JsonSerializer.Deserialize(reader, objectType);
if (!cache.JsonSerializer.CheckAdditionalContent)
return result;
while (reader.Read())
if (cache.JsonSerializer.CheckAdditionalContent)
{
if (reader.TokenType != JsonToken.Comment)
throw new Exception("Additional text found in JSON string after finishing deserializing object.");
while (reader.Read())
{
if (reader.TokenType != JsonToken.Comment)
throw new Exception("Additional text found in JSON string after finishing deserializing object.");
}
}
}
cache.ReadEnd();
return result;
}
@@ -242,22 +283,21 @@ namespace FlaxEngine.Json
{
object result;
var cache = Cache.Value;
cache.IsDuringSerialization = false;
Current.Value = cache;
cache.ReadBegin();
using (JsonReader reader = new JsonTextReader(new StringReader(json)))
{
result = cache.JsonSerializer.Deserialize(reader);
if (!cache.JsonSerializer.CheckAdditionalContent)
return result;
while (reader.Read())
if (cache.JsonSerializer.CheckAdditionalContent)
{
if (reader.TokenType != JsonToken.Comment)
throw new Exception("Additional text found in JSON string after finishing deserializing object.");
while (reader.Read())
{
if (reader.TokenType != JsonToken.Comment)
throw new Exception("Additional text found in JSON string after finishing deserializing object.");
}
}
}
cache.ReadEnd();
return result;
}
@@ -270,8 +310,7 @@ namespace FlaxEngine.Json
public static unsafe void Deserialize(object input, byte* jsonBuffer, int jsonLength)
{
var cache = Cache.Value;
cache.IsDuringSerialization = false;
Current.Value = cache;
cache.ReadBegin();
/*// Debug json string reading
cache.MemoryStream.Initialize(jsonBuffer, jsonLength);
@@ -291,14 +330,16 @@ namespace FlaxEngine.Json
{
cache.JsonSerializer.Populate(jsonReader, input);
}
if (!cache.JsonSerializer.CheckAdditionalContent)
return;
while (jsonReader.Read())
if (cache.JsonSerializer.CheckAdditionalContent)
{
if (jsonReader.TokenType != JsonToken.Comment)
throw new Exception("Additional text found in JSON string after finishing deserializing object.");
while (jsonReader.Read())
{
if (jsonReader.TokenType != JsonToken.Comment)
throw new Exception("Additional text found in JSON string after finishing deserializing object.");
}
}
cache.ReadEnd();
}
/// <summary>
-2
View File
@@ -5,8 +5,6 @@
#include "Stream.h"
#include "Engine/Core/Templates.h"
extern FLAXENGINE_API class ScriptingObject* FindObject(const Guid& id, class MClass* type);
/// <summary>
/// Base class for all data read streams
/// </summary>
+1 -1
View File
@@ -456,7 +456,7 @@ namespace Serialization
Guid id;
Deserialize(stream, id, modifier);
modifier->IdsMapping.TryGet(id, id);
v = (T*)FindObject(id, T::GetStaticClass());
v = (T*)::FindObject(id, T::GetStaticClass());
}
// Scripting Object Reference
+8 -8
View File
@@ -1273,16 +1273,16 @@ void VisjectExecutor::ProcessGroupParticles(Box* box, Node* node, Value& value)
// Random Float Range
case 213:
{
auto& a = node->Values[0].AsFloat;
auto& b = node->Values[1].AsFloat;
auto a = tryGetValue(node->TryGetBox(1), node->Values[0]).AsFloat;
auto b = tryGetValue(node->TryGetBox(2), node->Values[1]).AsFloat;
value = Math::Lerp(a, b, RAND);
break;
}
// Random Vector2 Range
case 214:
{
auto a = (Float2)node->Values[0];
auto b = (Float2)node->Values[1];
auto a = tryGetValue(node->TryGetBox(1), node->Values[0]).AsFloat2();
auto b = tryGetValue(node->TryGetBox(2), node->Values[1]).AsFloat2();
value = Float2(
Math::Lerp(a.X, b.X, RAND),
Math::Lerp(a.Y, b.Y, RAND)
@@ -1292,8 +1292,8 @@ void VisjectExecutor::ProcessGroupParticles(Box* box, Node* node, Value& value)
// Random Vector3 Range
case 215:
{
auto a = (Float3)node->Values[0];
auto b = (Float3)node->Values[1];
auto a = tryGetValue(node->TryGetBox(1), node->Values[0]).AsFloat3();
auto b = tryGetValue(node->TryGetBox(2), node->Values[1]).AsFloat3();
value = Float3(
Math::Lerp(a.X, b.X, RAND),
Math::Lerp(a.Y, b.Y, RAND),
@@ -1304,8 +1304,8 @@ void VisjectExecutor::ProcessGroupParticles(Box* box, Node* node, Value& value)
// Random Vector4 Range
case 216:
{
auto a = (Float4)node->Values[0];
auto b = (Float4)node->Values[1];
auto a = tryGetValue(node->TryGetBox(1), node->Values[0]).AsFloat4();
auto b = tryGetValue(node->TryGetBox(2), node->Values[1]).AsFloat4();
value = Float4(
Math::Lerp(a.X, b.X, RAND),
Math::Lerp(a.Y, b.Y, RAND),