Skip to content

Commit

Permalink
feat: load scenes and scene entities
Browse files Browse the repository at this point in the history
  • Loading branch information
abdes committed Oct 15, 2024
1 parent 9c1ae24 commit cc0f6a5
Show file tree
Hide file tree
Showing 11 changed files with 144 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public async Task ExpandItemAsync(ITreeItem itemAdapter)
return;
}

await this.RestoreExpandedChildrenAsync(itemAdapter).ConfigureAwait(false);
await this.RestoreExpandedChildrenAsync(itemAdapter).ConfigureAwait(true);
itemAdapter.IsExpanded = true;
}

Expand All @@ -35,7 +35,7 @@ public async Task CollapseItemAsync(ITreeItem itemAdapter)
return;
}

await this.HideChildrenAsync(itemAdapter).ConfigureAwait(false);
await this.HideChildrenAsync(itemAdapter).ConfigureAwait(true);
itemAdapter.IsExpanded = false;
}

Expand All @@ -45,15 +45,15 @@ protected async Task InitializeRootAsync(ITreeItem root)

// Do not add the root item, add its children instead and check if it they need to be expandedxx
root.IsExpanded = true;
await this.RestoreExpandedChildrenAsync(root).ConfigureAwait(false);
await this.RestoreExpandedChildrenAsync(root).ConfigureAwait(true);
}

protected async Task AddItem(ITreeItem parent, ITreeItem item)
{
// Always expand the item before adding to to force the children collection to be fully loaded.
if (!parent.IsExpanded)
{
await this.ExpandItemAsync(parent).ConfigureAwait(false);
await this.ExpandItemAsync(parent).ConfigureAwait(true);
}

// Fire the ItemBeingAdded event before any changes are made to the tree
Expand All @@ -71,7 +71,7 @@ protected async Task AddItem(ITreeItem parent, ITreeItem item)
// Clear selection first because after insertion, the selected indices will be invalid
this.SelectionModel?.ClearSelection();

await parent.InsertChildAsync(0, item).ConfigureAwait(false);
await parent.InsertChildAsync(0, item).ConfigureAwait(true);
var newItemIndex = this.ShownItems.IndexOf(parent) + 1;
this.ShownItems.Insert(newItemIndex, item);

Expand Down Expand Up @@ -104,11 +104,11 @@ protected async Task RemoveItem(ITreeItem item, bool updateSelection = true)
}

// Remove the item's children from ShownItems in a single pass
var children = (await item.Children.ConfigureAwait(false)).ToArray();
var children = (await item.Children.ConfigureAwait(true)).ToArray();
for (var childIndex = children.Length - 1; childIndex >= 0; childIndex--)
{
var child = children[childIndex];
await item.RemoveChildAsync(child).ConfigureAwait(false);
await item.RemoveChildAsync(child).ConfigureAwait(true);
if (item.IsExpanded)
{
this.ShownItems.RemoveAt(removeAtIndex + childIndex + 1);
Expand All @@ -126,7 +126,7 @@ protected async Task RemoveItem(ITreeItem item, bool updateSelection = true)
// Remove the item
var itemParent = item.Parent;
Debug.Assert(itemParent != null, "an item being removed from the tree always has a parent");
await itemParent.RemoveChildAsync(item).ConfigureAwait(false);
await itemParent.RemoveChildAsync(item).ConfigureAwait(true);
this.ShownItems.RemoveAt(removeAtIndex);

if (updateSelection)
Expand All @@ -152,12 +152,12 @@ protected virtual async Task RemoveSelectedItems()

if (this.SelectionModel is SingleSelectionModel)
{
await this.RemoveSingleSelectedItem().ConfigureAwait(false);
await this.RemoveSingleSelectedItem().ConfigureAwait(true);
}

if (this.SelectionModel is MultipleSelectionModel)
{
await this.RemoveMultipleSelectionItems().ConfigureAwait(false);
await this.RemoveMultipleSelectionItems().ConfigureAwait(true);
}
}

Expand All @@ -175,7 +175,7 @@ private async Task RemoveSingleSelectedItem()
var selectedIndex = this.SelectionModel.SelectedIndex;
this.SelectionModel.ClearSelection();

await this.RemoveItem(selectedItem).ConfigureAwait(false);
await this.RemoveItem(selectedItem).ConfigureAwait(true);
}
}

Expand Down Expand Up @@ -243,9 +243,9 @@ private async Task RemoveMultipleSelectionItems()
list.Add(item);
return list;
})
.Select(async item => await this.RemoveItem(item, updateSelection: false).ConfigureAwait(false));
.Select(async item => await this.RemoveItem(item, updateSelection: false).ConfigureAwait(true));

await Task.WhenAll(tasks).ConfigureAwait(false);
await Task.WhenAll(tasks).ConfigureAwait(true);

var removedItemsCount = originalShownItemsCount - this.ShownItems.Count;

Expand All @@ -268,17 +268,17 @@ private async Task ToggleExpanded(TreeItemAdapter itemAdapter)
private async Task RestoreExpandedChildrenAsync(ITreeItem itemAdapter)
{
var insertIndex = this.ShownItems.IndexOf(itemAdapter) + 1;
await this.RestoreExpandedChildrenRecursive(itemAdapter, insertIndex).ConfigureAwait(false);
await this.RestoreExpandedChildrenRecursive(itemAdapter, insertIndex).ConfigureAwait(true);
}

private async Task<int> RestoreExpandedChildrenRecursive(ITreeItem parent, int insertIndex)
{
foreach (var child in await parent.Children.ConfigureAwait(false))
foreach (var child in await parent.Children.ConfigureAwait(true))
{
this.ShownItems.Insert(insertIndex++, (TreeItemAdapter)child);
if (child.IsExpanded)
{
insertIndex = await this.RestoreExpandedChildrenRecursive(child, insertIndex).ConfigureAwait(false);
insertIndex = await this.RestoreExpandedChildrenRecursive(child, insertIndex).ConfigureAwait(true);
}
}

Expand All @@ -290,17 +290,17 @@ private async Task HideChildrenAsync(ITreeItem itemAdapter)
var removeIndex = this.ShownItems.IndexOf((TreeItemAdapter)itemAdapter) + 1;
Debug.Assert(removeIndex != -1, $"expecting item {itemAdapter.Label} to be in the shown list");

await this.HideChildrenRecursiveAsync(itemAdapter, removeIndex).ConfigureAwait(false);
await this.HideChildrenRecursiveAsync(itemAdapter, removeIndex).ConfigureAwait(true);
}

private async Task HideChildrenRecursiveAsync(ITreeItem parent, int removeIndex)
{
foreach (var child in await parent.Children.ConfigureAwait(false))
foreach (var child in await parent.Children.ConfigureAwait(true))
{
this.ShownItems.RemoveAt(removeIndex);
if (child.IsExpanded)
{
await this.HideChildrenRecursiveAsync(child, removeIndex).ConfigureAwait(false);
await this.HideChildrenRecursiveAsync(child, removeIndex).ConfigureAwait(true);
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions projects/Oxygen.Editor.Projects/src/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ public static class Constants
public const string ProjectFileName = "Project.oxy";

public const string ScenesFolderName = "Scenes";

public const string SceneFileExtension = ".scene";
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public interface IProjectManagerService

Task<bool> LoadProjectAsync(IProjectInfo projectInfo);

Task<bool> LoadProjectScenesAsync(IProject project);
Task<bool> LoadProjectScenesAsync(Project project);

Task<bool> LoadSceneEntitiesAsync(Scene scene);
}
3 changes: 3 additions & 0 deletions projects/Oxygen.Editor.Projects/src/NamedItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@

namespace Oxygen.Editor.Projects;

using System.Text.Json.Serialization;

public class NamedItem
{
private string name;

[JsonConstructor]
protected NamedItem(string name)
{
ValidateName(name);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Options" />
<PackageReference Include="System.Linq.Async" />
</ItemGroup>

<ItemGroup>
Expand Down
32 changes: 23 additions & 9 deletions projects/Oxygen.Editor.Projects/src/ProjectManagerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,11 @@ public async Task<bool> LoadProjectAsync(IProjectInfo projectInfo)
}
}

public async Task<bool> LoadProjectScenesAsync(IProject project)
public async Task<bool> LoadProjectScenesAsync(Project project)
{
try
{
// TODO: Populate the project's list of scenes
await Task.CompletedTask.ConfigureAwait(false);

await this.projectSource.LoadProjectScenesAsync(project).ConfigureAwait(false);
return true;
}
catch (Exception ex)
Expand All @@ -70,17 +68,33 @@ public async Task<bool> LoadProjectScenesAsync(IProject project)

public async Task<bool> LoadSceneEntitiesAsync(Scene scene)
{
if (scene.Project is not { ProjectInfo.Location: not null })
{
this.CouldNotLoadSceneEntities(scene.Name, "null project or project location");
return false;
}

try
{
// TODO: Load the scene from its corresponding file
await Task.CompletedTask.ConfigureAwait(false);
var loadedScene = await this.projectSource.LoadSceneAsync(
scene.Name,
scene.Project.ProjectInfo.Location)
.ConfigureAwait(false);

if (loadedScene is null)
{
return false;
}

scene.Entities.Clear();
scene.Entities.AddRange(loadedScene.Entities);

return true;
}
catch (Exception ex)
{
this.CouldNotLoadScene(scene.Project.ProjectInfo.Location ?? string.Empty, ex.Message);
return await Task.FromResult(true).ConfigureAwait(false);
this.CouldNotLoadSceneEntities(scene.Project.ProjectInfo.Location, ex.Message);
return false;
}
}

Expand All @@ -98,5 +112,5 @@ public async Task<bool> SaveProjectInfoAsync(IProjectInfo projectInfo) =>
[LoggerMessage(
Level = LogLevel.Error,
Message = "Could not load scene from `{location}`; {error}")]
partial void CouldNotLoadScene(string location, string error);
partial void CouldNotLoadSceneEntities(string location, string error);
}
15 changes: 12 additions & 3 deletions projects/Oxygen.Editor.Projects/src/Scene.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,18 @@

namespace Oxygen.Editor.Projects;

public class Scene(string name, Project project) : NamedItem(name)
using System.Text.Json.Serialization;

[method: JsonConstructor]
public class Scene(string name) : NamedItem(name)
{
public Project Project => project;
[JsonIgnore]
public Project? Project { get; init; }

public IList<Entity> Entities { get; set; } = [];
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Design",
"MA0016:Prefer using collection abstraction instead of implementation",
Justification = "need AddRange from List")]
[JsonInclude]
public List<Entity> Entities { get; private set; } = [];

Check failure on line 20 in projects/Oxygen.Editor.Projects/src/Scene.cs

View workflow job for this annotation

GitHub Actions / build (Release, x64, true)

Use read-only auto-implemented property (https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1170)
}
4 changes: 4 additions & 0 deletions projects/Oxygen.Editor.Projects/src/Storage/IProjectSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,8 @@ public interface IProjectSource
Task<IProjectInfo?> LoadProjectInfoAsync(string projectFolderPath);

Task<bool> SaveProjectInfoAsync(IProjectInfo projectInfo);

Task LoadProjectScenesAsync(Project project);

Task<Scene?> LoadSceneAsync(string sceneName, string projectLocation);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Oxygen.Editor.Projects.Storage;

using System.Diagnostics;
using System.Linq;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
Expand Down Expand Up @@ -58,7 +59,13 @@ public LocalProjectsSource(
var projectFile = await projectFolder.GetDocumentAsync(Constants.ProjectFileName)
.ConfigureAwait(false);
var json = await projectFile.ReadAllTextAsync().ConfigureAwait(false);
return JsonSerializer.Deserialize<ProjectInfo>(json, this.jsonOptions);
var projectInfo = JsonSerializer.Deserialize<ProjectInfo>(json, this.jsonOptions);
if (projectInfo != null)
{
projectInfo.Location = projectFolderPath;
}

return projectInfo;
}
catch (Exception ex)
{
Expand All @@ -68,6 +75,40 @@ public LocalProjectsSource(
return null;
}

public async Task<Scene?> LoadSceneAsync(string sceneName, string projectLocation)
{
try
{
var projectFolder = await this.storage.GetFolderFromPathAsync(projectLocation)
.ConfigureAwait(false);
var scenesFolder = await projectFolder.GetFolderAsync(Constants.ScenesFolderName).ConfigureAwait(false);
var sceneFile = await scenesFolder.GetDocumentAsync(sceneName + Constants.SceneFileExtension)
.ConfigureAwait(false);
if (!await sceneFile.ExistsAsync().ConfigureAwait(false))
{
this.CouldNotLoadScene(sceneFile.Location, "file does not exist");
return null;
}

var json = await sceneFile.ReadAllTextAsync().ConfigureAwait(false);
var loadedScene = JsonSerializer.Deserialize<Scene>(json, this.jsonOptions);
if (loadedScene is null)
{
this.CouldNotLoadScene(sceneFile.Location, "JSON deserialization failed");
}

return loadedScene;
}
catch (Exception ex)
{
var sceneLocation = this.storage.NormalizeRelativeTo(
projectLocation,
$"{Constants.ScenesFolderName}/{sceneName}{Constants.SceneFileExtension}");
this.CouldNotLoadScene(sceneLocation, ex.Message);
return null;
}
}

public async Task<bool> SaveProjectInfoAsync(IProjectInfo projectInfo)
{
Debug.Assert(projectInfo.Location != null, "The project location must be valid!");
Expand All @@ -91,6 +132,28 @@ public async Task<bool> SaveProjectInfoAsync(IProjectInfo projectInfo)
return false;
}

public async Task LoadProjectScenesAsync(Project project)
{
if (project.ProjectInfo.Location == null)
{
throw new InvalidOperationException("cannot load project scenes for a project with null location");
}

var projectFolder
= await this.storage.GetFolderFromPathAsync(project.ProjectInfo.Location).ConfigureAwait(false);
var scenesFolder = await projectFolder.GetFolderAsync(Constants.ScenesFolderName).ConfigureAwait(false);
project.Scenes.Clear();

var scenes = scenesFolder.GetDocumentsAsync()
.Where(d => d.Name.EndsWith(Constants.SceneFileExtension, StringComparison.OrdinalIgnoreCase));
await foreach (var item in scenes.ConfigureAwait(false))
{
var sceneName = item.Name[..item.Name.LastIndexOf('.')];
var scene = new Scene(sceneName) { Project = project };
project.Scenes.Add(scene);
}
}

[LoggerMessage(
Level = LogLevel.Error,
Message = "Could not load project info from `{location}`; {error}")]
Expand All @@ -100,4 +163,9 @@ public async Task<bool> SaveProjectInfoAsync(IProjectInfo projectInfo)
Level = LogLevel.Error,
Message = "Could not load project info from `{location}`; {error}")]
partial void CouldNotLoadProjectInfo(string location, string error);

[LoggerMessage(
Level = LogLevel.Error,
Message = "Could not load scene from `{location}`; {error}")]
partial void CouldNotLoadScene(string location, string error);
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,10 @@ public class UniversalProjectSource(LocalProjectsSource localSource) : IProjectS

public Task<bool> SaveProjectInfoAsync(IProjectInfo projectInfo)
=> localSource.SaveProjectInfoAsync(projectInfo);

public Task LoadProjectScenesAsync(Project project)
=> localSource.LoadProjectScenesAsync(project);

public Task<Scene?> LoadSceneAsync(string sceneName, string projectLocation)
=> localSource.LoadSceneAsync(sceneName, projectLocation);
}
Loading

0 comments on commit cc0f6a5

Please sign in to comment.