using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Newtonsoft.Json;
using SpaceEngineersBlueprintEditor.Implements.Services;
using SpaceEngineersBlueprintEditor.Interface.Services;
using SpaceEngineersBlueprintEditor.Model;
using SpaceEngineersBlueprintEditor.SpaceEngineersCore;
using SpaceEngineersBlueprintEditor.SpaceEngineersCore.BlueprintEditing;
using SpaceEngineersBlueprintEditor.SpaceEngineersCore.GridConverter;
using SpaceEngineersBlueprintEditor.Utilities;
using SpaceEngineersBlueprintEditor.Views;
using System.Collections.ObjectModel;
using System.Diagnostics;
using VRage;
using VRage.Game;
using VRage.ObjectBuilders;
using VRageMath;
using Windows.Storage;
using Windows.Storage.Pickers;
using WinRT.Interop;
using XFEExtension.NetCore.WinUIHelper.Interface.Services;
using XFEExtension.NetCore.WinUIHelper.Utilities;
using XFEExtension.NetCore.WinUIHelper.Utilities.Helper;
namespace SpaceEngineersBlueprintEditor.ViewModels;
public partial class BlueprintEditSubPageViewModel : ViewModelBase
{
[ObservableProperty]
private bool isCubeGridListVisible;
[ObservableProperty]
private bool isShipBlueprintPropertyVisible;
[ObservableProperty]
private bool isThreeDimensionalEditorVisible;
[ObservableProperty]
private bool isSheetMetalEditorVisible;
[ObservableProperty]
private bool isContentGridVisible = false;
[ObservableProperty]
private bool isInitialGridVisible = true;
[ObservableProperty]
private string searchText = string.Empty;
[ObservableProperty]
private string propertiesSearchText = string.Empty;
[ObservableProperty]
private BlueprintPropertyViewData? selectedCubeBlock;
[ObservableProperty]
private object? selectedCubeBlockObject;
[ObservableProperty]
private object? selectedCubeGrid;
[ObservableProperty]
private TreeViewNode? selectedTreeViewNode;
[ObservableProperty]
private string selectedThreeDimensionalBlockText = "No block selected";
[ObservableProperty]
private bool hasUnsavedChanges;
private bool isLoaded;
private int sceneGeneration;
private BlueprintModel? currentBlueprintModel;
private MyObjectBuilder_Definitions? currentDefinitions;
private MyObjectBuilder_ShipBlueprintDefinition? currentShipBlueprint;
private IReadOnlyDictionary<int, Blueprint3DBlockReference> sceneBlockReferences =
new Dictionary<int, Blueprint3DBlockReference>();
private Blueprint3DBlockReference? selectedSceneBlock;
private readonly Stack<BlueprintEditAction> undoActions = new();
private readonly Stack<BlueprintEditAction> redoActions = new();
private readonly List<BlueprintPropertyViewData> blueprintPropertyRoots = [];
private readonly List<BlueprintPropertyViewData> cubePropertyRoots = [];
private readonly ILoadingService? loadingService = ServiceManager.GetGlobalService<ILoadingService>();
private readonly IMessageService? messageService = ServiceManager.GetGlobalService<IMessageService>();
private readonly INavigationViewService? navigationViewService = ServiceManager.GetGlobalService<INavigationViewService>();
private readonly ITabViewTitleService? tabViewTitleService = ServiceManager.GetGlobalService<ITabViewTitleService>();
public ObservableCollection<BlueprintGroupList> BlueprintCubeGridList { get; } = [];
public IBackgroundImageService? BackgroundImageService { get; set; } = ServiceManager.GetGlobalService<IBackgroundImageService>();
public IListViewDisplayService<BlueprintPropertyViewData> ListViewDisplayService { get; set; } = new ListViewDisplayService<BlueprintPropertyViewData>();
public IAutoNavigationParameterService<BlueprintModel> AutoNavigationParameterService { get; set; } = ServiceManager.GetService<IAutoNavigationParameterService<BlueprintModel>>();
public IFileDropService FileDropService { get; set; } = new BlueprintDropService();
public ITreeViewService BlueprintTreeViewService { get; set; } = new TreeViewService();
public ITreeViewService CubeBlockTreeViewService { get; set; } = new TreeViewService();
public ISelectorBarService SelectorBarService { get; set; } = ServiceManager.GetService<ISelectorBarService>();
public event EventHandler<string>? ThreeDimensionalSceneChanged;
public BlueprintEditSubPageViewModel()
{
AutoNavigationParameterService.ParameterChange += AutoNavigationParameterService_ParameterChange;
ListViewDisplayService.SelectionChanged += ListViewDisplayService_SelectionChanged;
SelectorBarService.SelectionChanged += SelectorBarService_SelectionChanged;
FileDropService.Drop += FileDropService_Drop;
}
private void ListViewDisplayService_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.FirstOrDefault() is BlueprintPropertyViewData blueprintPropertyViewData)
{
SelectedCubeBlock = blueprintPropertyViewData;
}
}
partial void OnSelectedCubeBlockChanged(
BlueprintPropertyViewData? oldValue,
BlueprintPropertyViewData? newValue)
{
var selectedBlock = newValue?.Value as MyObjectBuilder_CubeBlock;
SelectedCubeBlockObject = selectedBlock;
SelectedCubeGrid = selectedBlock is null
? null
: currentShipBlueprint?.CubeGrids.FirstOrDefault(grid =>
grid.CubeBlocks.Any(block => ReferenceEquals(block, selectedBlock)));
LoadCubeProperties();
}
private async void FileDropService_Drop(object? sender, (string, DragEventArgs) e)
{
loadingService?.StartLoading<BlueprintEditSubPage>($"{"LoadingDefinitions".GetLocalized()}...");
if (File.Exists(e.Item1) && await SpaceEngineersHelper.LoadBlueprintModel(e.Item1) is BlueprintModel blueprintModel)
{
AutoNavigationParameterService.Parameter = blueprintModel;
if (blueprintModel.ViewData is not null)
tabViewTitleService?.SetTabViewTitle(blueprintModel.ViewData.Name, blueprintModel);
await SetDefinitions(blueprintModel.BlueprintDefinitions);
}
loadingService?.StopLoading<BlueprintEditSubPage>();
}
partial void OnSearchTextChanged(string value) => SearchCubeGrids(value);
partial void OnPropertiesSearchTextChanged(string value)
{
PopulatePropertyTree(BlueprintTreeViewService, blueprintPropertyRoots, value);
PopulatePropertyTree(CubeBlockTreeViewService, cubePropertyRoots, value);
}
private async void SelectorBarService_SelectionChanged(SelectorBar sender, SelectorBarSelectionChangedEventArgs args) => await LoadByName(sender.SelectedItem.Tag as string ?? "Grids");
private async Task SetDefinitions(MyObjectBuilder_Definitions? definitions)
{
if (definitions is not null)
{
currentDefinitions = definitions;
selectedSceneBlock = null;
sceneBlockReferences = new Dictionary<int, Blueprint3DBlockReference>();
undoActions.Clear();
redoActions.Clear();
HasUnsavedChanges = false;
if (currentDefinitions.ShipBlueprints is not null && currentDefinitions.ShipBlueprints.Length > 0)
{
currentShipBlueprint = currentDefinitions.ShipBlueprints[0];
IsContentGridVisible = true;
IsInitialGridVisible = false;
await Task.Delay(100);
if (!isLoaded)
await LoadByName("Grids");
}
else
{
messageService?.ShowMessage("Error_CantLoadingShipDefinitions".GetLocalized(), "Error".GetLocalized(), InfoBarSeverity.Error);
}
}
else
{
messageService?.ShowMessage("Error_CantFindShipDefinitions".GetLocalized(), "Error".GetLocalized(), InfoBarSeverity.Error);
}
}
private async void AutoNavigationParameterService_ParameterChange(object? sender, BlueprintModel? e)
{
if (e is null || AutoNavigationParameterService.SameAsLast)
return;
currentBlueprintModel = e;
if (currentBlueprintModel.ViewData is not null)
BackgroundImageService?.SetBackgroundImage(currentBlueprintModel.ViewData.BlueprintImage);
if (navigationViewService is not null) navigationViewService.Header = null;
if (e.ViewData is not null)
tabViewTitleService?.SetTabViewTitle(e.ViewData.Name, e);
if (e.BlueprintDefinitions is not null)
{
await SetDefinitions(e.BlueprintDefinitions);
}
else if (e.ViewData is not null)
{
await SetDefinitions(await SpaceEngineersHelper.LoadBlueprintAsync(e.ViewData.FilePath));
}
}
private async Task LoadByName(string caseName)
{
if (currentShipBlueprint is null) return;
isLoaded = true;
loadingService?.StartLoading<BlueprintEditSubPage>($"{"LoadingDefinitions".GetLocalized()}...");
await Task.Delay(50);
switch (caseName)
{
case "Grids":
IsCubeGridListVisible = true;
IsShipBlueprintPropertyVisible = false;
IsThreeDimensionalEditorVisible = false;
IsSheetMetalEditorVisible = false;
LoadCubeGrids();
break;
case "Groups":
IsCubeGridListVisible = true;
IsShipBlueprintPropertyVisible = false;
IsThreeDimensionalEditorVisible = false;
IsSheetMetalEditorVisible = false;
LoadCubeGroups();
break;
case "Properties":
IsCubeGridListVisible = false;
IsShipBlueprintPropertyVisible = true;
IsThreeDimensionalEditorVisible = false;
IsSheetMetalEditorVisible = false;
LoadBlueprintPropertyDefinitions();
break;
case "ThreeDimensional":
IsCubeGridListVisible = false;
IsShipBlueprintPropertyVisible = false;
IsThreeDimensionalEditorVisible = true;
IsSheetMetalEditorVisible = false;
await RefreshThreeDimensionalSceneAsync();
break;
case "SheetMetal":
IsCubeGridListVisible = false;
IsShipBlueprintPropertyVisible = false;
IsThreeDimensionalEditorVisible = false;
IsSheetMetalEditorVisible = true;
LoadCubeGrids();
break;
default:
break;
}
loadingService?.StopLoading<BlueprintEditSubPage>();
}
private void LoadBlueprintPropertyDefinitions()
{
var parent = new BlueprintPropertyViewData
{
Value = currentShipBlueprint,
Name = "Ship blueprint",
Type = currentShipBlueprint!.GetType(),
ValueChanged = () => HasUnsavedChanges = true
};
SpaceEngineersHelper.AnalyzeBlueprint(parent);
blueprintPropertyRoots.Clear();
blueprintPropertyRoots.AddRange(parent.Children);
PopulatePropertyTree(BlueprintTreeViewService, blueprintPropertyRoots, PropertiesSearchText);
}
private void SearchCubeGrids(string name)
{
BlueprintCubeGridList.Clear();
currentShipBlueprint!.CubeGrids.Select(grid =>
{
var type = grid.CubeBlocks.GetType();
var gridViewData = new BlueprintPropertyViewData
{
Name = type.Name,
Type = type,
Value = grid.CubeBlocks
};
SpaceEngineersHelper.AnalyzeBlueprint(gridViewData);
var targetChild = gridViewData.Children.Where(child => child.MatchesSearch(name));
if (!targetChild.Any())
return null;
return new BlueprintGroupList(targetChild)
{
GroupName = grid.DisplayName
};
}).Where(grid => grid is not null).ForEach(BlueprintCubeGridList.Add!);
}
private async void LoadCubeGrids()
{
BlueprintCubeGridList.Clear();
currentShipBlueprint!.CubeGrids.Select(grid =>
{
var type = grid.CubeBlocks.GetType();
var gridViewData = new BlueprintPropertyViewData
{
Name = type.Name,
Type = type,
Value = grid.CubeBlocks
};
SpaceEngineersHelper.AnalyzeBlueprint(gridViewData);
return new BlueprintGroupList(gridViewData.Children)
{
GroupName = grid.DisplayName
};
}).ForEach(BlueprintCubeGridList.Add);
await Helper.Wait(() => ListViewDisplayService.IsPageLoaded);
ListViewDisplayService.Select(BlueprintCubeGridList.FirstOrDefault()?.FirstOrDefault());
}
private async void LoadCubeGroups()
{
BlueprintCubeGridList.Clear();
var blueprintCubeGridList = new List<BlueprintGroupList>();
foreach (var grid in currentShipBlueprint!.CubeGrids ?? [])
{
if (grid?.CubeBlocks is null)
{
continue;
}
var type = grid.CubeBlocks.GetType();
var resolvedGroups = BlockGroupResolver.ResolveGroups(grid);
foreach (var group in grid.BlockGroups ?? [])
{
if (group is null)
{
continue;
}
var targetCubeBlocks = resolvedGroups.TryGetValue(group, out var blocks)
? blocks
: Array.Empty<MyObjectBuilder_CubeBlock>();
var groupData = new BlueprintPropertyViewData
{
Name = type.Name,
Type = type,
Value = targetCubeBlocks
};
SpaceEngineersHelper.AnalyzeBlueprint(groupData);
if (blueprintCubeGridList.FirstOrDefault(item => item.GroupName == group.Name) is BlueprintGroupList blueprintGroupList)
{
blueprintGroupList.AddRange(groupData.Children);
}
else
{
blueprintCubeGridList.Add(new BlueprintGroupList(groupData.Children)
{
GroupName = group.Name ?? string.Empty
});
}
}
}
blueprintCubeGridList.ForEach(BlueprintCubeGridList.Add);
await Helper.Wait(() => ListViewDisplayService.IsPageLoaded);
ListViewDisplayService.Select(BlueprintCubeGridList.FirstOrDefault()?.FirstOrDefault());
}
private void LoadCubeProperties()
{
cubePropertyRoots.Clear();
if (SelectedCubeBlock is not null)
{
SelectedCubeBlock.ValueChanged = () => HasUnsavedChanges = true;
SpaceEngineersHelper.AnalyzeBlueprint(SelectedCubeBlock);
SelectedCubeBlock.Children.ForEach(child => child.ValueChanged = SelectedCubeBlock.ValueChanged);
cubePropertyRoots.AddRange(SelectedCubeBlock.Children);
}
PopulatePropertyTree(CubeBlockTreeViewService, cubePropertyRoots, PropertiesSearchText);
}
private static void PopulatePropertyTree(
ITreeViewService treeViewService,
IEnumerable<BlueprintPropertyViewData> source,
string searchText)
{
treeViewService.Clear();
foreach (var property in source.Where(property => property.MatchesSearch(searchText)))
{
treeViewService.Add(new TreeViewNode
{
Content = property,
HasUnrealizedChildren = property.CanExpand
});
}
}
[RelayCommand]
async Task OpenBlueprint()
{
loadingService?.StartLoading<BlueprintEditSubPage>($"{"LoadingDefinitions".GetLocalized()}...");
var openPicker = new FileOpenPicker();
InitializeWithWindow.Initialize(openPicker, WindowNative.GetWindowHandle(App.MainWindow));
openPicker.ViewMode = PickerViewMode.List;
openPicker.FileTypeFilter.Add(".sbc");
if (await openPicker.PickSingleFileAsync() is StorageFile file && File.Exists(file.Path))
{
if (await SpaceEngineersHelper.LoadBlueprintModel(file.Path) is BlueprintModel blueprintModel)
{
AutoNavigationParameterService.Parameter = blueprintModel;
if (blueprintModel.ViewData is not null)
tabViewTitleService?.SetTabViewTitle(blueprintModel.ViewData.Name, blueprintModel);
await SetDefinitions(blueprintModel.BlueprintDefinitions);
}
}
loadingService?.StopLoading<BlueprintEditSubPage>();
}
[RelayCommand]
void OpenInFolder()
{
if (currentBlueprintModel is not null && currentBlueprintModel.ViewData is not null)
Process.Start("explorer.exe", Path.GetDirectoryName(currentBlueprintModel.ViewData.FilePath) ?? string.Empty);
else
messageService?.ShowMessage("Warning_CantFindFile".GetLocalized(), "Warning".GetLocalized(), InfoBarSeverity.Warning);
}
[RelayCommand]
void ViewBlueprintsList() => navigationViewService?.NavigateTo<BlueprintsViewPage>("Local");
[RelayCommand]
async Task Save()
{
var path = currentBlueprintModel?.ViewData?.FilePath;
if (string.IsNullOrWhiteSpace(path))
{
await SaveAs();
return;
}
await SaveToPath(path);
}
[RelayCommand]
async Task SaveAs()
{
if (currentDefinitions is null)
{
return;
}
var savePicker = new FileSavePicker();
InitializeWithWindow.Initialize(savePicker, WindowNative.GetWindowHandle(App.MainWindow));
savePicker.FileTypeChoices.Add(new("BlueprintFile".GetLocalized(), [".sbc"]));
if (currentBlueprintModel?.ViewData?.FilePath is string currentPath && File.Exists(currentPath))
{
savePicker.SuggestedSaveFile = await StorageFile.GetFileFromPathAsync(currentPath);
}
savePicker.SuggestedFileName = "bp.sbc";
savePicker.DefaultFileExtension = ".sbc";
if (await savePicker.PickSaveFileAsync() is StorageFile file)
{
await SaveToPath(file.Path);
if (currentBlueprintModel is not null)
{
currentBlueprintModel.ViewData = SpaceEngineersHelper.LoadBlueprintInfo(file.Path)?.ToBlueprintInfoViewData();
}
}
}
private async Task SaveToPath(string path)
{
if (currentDefinitions is null)
{
return;
}
loadingService?.StartLoading<BlueprintEditSubPage>($"{"SavingBlueprint".GetLocalized()}...");
var cachePath = Path.Combine(AppPathHelper.AppCache, $"blueprint-{Guid.NewGuid():N}.json");
try
{
Directory.CreateDirectory(AppPathHelper.AppCache);
await File.WriteAllTextAsync(cachePath, JsonConvert.SerializeObject(
currentDefinitions,
new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto }));
var converterPath = GetBlueprintConverterPath();
using var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = converterPath,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
}
};
process.StartInfo.ArgumentList.Add("--input");
process.StartInfo.ArgumentList.Add(cachePath);
process.StartInfo.ArgumentList.Add("--output");
process.StartInfo.ArgumentList.Add(path);
process.Start();
var standardOutput = process.StandardOutput.ReadToEndAsync();
var standardError = process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
var result = await standardOutput;
var error = await standardError;
if (process.ExitCode != 0 || !string.Equals(result, "Successful", StringComparison.Ordinal))
{
throw new InvalidOperationException(
string.IsNullOrWhiteSpace(result) ? error : result.Replace("Error:", string.Empty));
}
HasUnsavedChanges = false;
messageService?.ShowMessage("SavingSuccessful".GetLocalized(), "Complete".GetLocalized(), InfoBarSeverity.Success);
}
catch (Exception ex)
{
messageService?.ShowMessage($"{"Failed_SaveFailed".GetLocalized()}: {ex.Message}", "Failed".GetLocalized(), InfoBarSeverity.Error);
}
finally
{
if (File.Exists(cachePath))
{
File.Delete(cachePath);
}
loadingService?.StopLoading<BlueprintEditSubPage>();
}
}
private static string GetBlueprintConverterPath()
{
var deployedPath = Path.Combine(
AppContext.BaseDirectory,
"Converter",
"SpaceEngineersBlueprintEditor.BlueprintConverter.exe");
if (File.Exists(deployedPath))
{
return deployedPath;
}
var parent = new DirectoryInfo(AppContext.BaseDirectory);
while (parent is not null)
{
foreach (var configuration in new[] { "Debug", "Release" })
{
var developmentPath = Path.Combine(
parent.FullName,
"SpaceEngineersBlueprintEditor.BlueprintConverter",
"bin", configuration, "net48",
"SpaceEngineersBlueprintEditor.BlueprintConverter.exe");
if (File.Exists(developmentPath))
{
return developmentPath;
}
}
parent = parent.Parent;
}
throw new FileNotFoundException("Blueprint converter was not deployed with the application.", deployedPath);
}
[RelayCommand]
async Task ConvertBlueprint(string command)
{
if (currentShipBlueprint is null)
{
return;
}
BlueprintTransformResult? transformResult = null;
var changed = false;
if (currentShipBlueprint is not null)
switch (command)
{
case "Destructible":
foreach (var grid in currentShipBlueprint.CubeGrids)
grid.DestructibleBlocks = true;
changed = true;
break;
case "Indestructible":
foreach (var grid in currentShipBlueprint.CubeGrids)
grid.DestructibleBlocks = false;
changed = true;
break;
case "Editable":
foreach (var grid in currentShipBlueprint.CubeGrids)
grid.Editable = true;
changed = true;
break;
case "Non-Editable":
foreach (var grid in currentShipBlueprint.CubeGrids)
grid.Editable = false;
changed = true;
break;
case "SmallGrid":
transformResult = BlueprintTransformService.ConvertGridSize(currentShipBlueprint, MyCubeSize.Small);
break;
case "LargeGrid":
transformResult = BlueprintTransformService.ConvertGridSize(currentShipBlueprint, MyCubeSize.Large);
break;
case "LightBlock":
transformResult = BlueprintTransformService.ConvertArmor(currentShipBlueprint, BlueprintArmorType.Light);
break;
case "HeavyBlock":
transformResult = BlueprintTransformService.ConvertArmor(currentShipBlueprint, BlueprintArmorType.Heavy);
break;
case "Static":
foreach (var grid in currentShipBlueprint.CubeGrids)
grid.IsStatic = true;
changed = true;
break;
case "Active":
foreach (var grid in currentShipBlueprint.CubeGrids)
grid.IsStatic = false;
changed = true;
break;
default:
break;
}
if (transformResult is not null)
{
changed = transformResult.ConvertedBlocks > 0 || transformResult.ChangedGrids > 0;
var summary = string.Format(
"BlueprintTransformSummary".GetLocalized(),
transformResult.ConvertedBlocks,
transformResult.AlreadyTargetBlocks,
transformResult.UnsupportedBlocks);
messageService?.ShowMessage(summary, "Complete".GetLocalized(), InfoBarSeverity.Success);
}
else
{
messageService?.ShowMessage("ConvertComplete".GetLocalized(), "Complete".GetLocalized(), InfoBarSeverity.Success);
}
if (changed)
{
HasUnsavedChanges = true;
await RefreshEditorViewsAsync();
}
}
public void SelectThreeDimensionalBlock(int id)
{
if (!sceneBlockReferences.TryGetValue(id, out var reference))
{
selectedSceneBlock = null;
SelectedThreeDimensionalBlockText = "Blueprint3D_NoSelection".GetLocalized();
return;
}
selectedSceneBlock = reference;
var block = reference.Block;
var gridName = string.IsNullOrWhiteSpace(reference.Grid.DisplayName)
? $"Grid {reference.GridIndex + 1}"
: reference.Grid.DisplayName;
SelectedThreeDimensionalBlockText =
$"{gridName} · {block.SubtypeName} · ({block.Min.X}, {block.Min.Y}, {block.Min.Z})";
}
public async Task HandleThreeDimensionalEditorActionAsync(string action)
{
switch (action)
{
case "moveXPositive":
await MoveSelectedBlockAsync(1, 0, 0);
break;
case "moveXNegative":
await MoveSelectedBlockAsync(-1, 0, 0);
break;
case "moveYPositive":
await MoveSelectedBlockAsync(0, 1, 0);
break;
case "moveYNegative":
await MoveSelectedBlockAsync(0, -1, 0);
break;
case "moveZPositive":
await MoveSelectedBlockAsync(0, 0, 1);
break;
case "moveZNegative":
await MoveSelectedBlockAsync(0, 0, -1);
break;
case "rotateX":
await RotateSelectedBlockAsync('X');
break;
case "rotateY":
await RotateSelectedBlockAsync('Y');
break;
case "rotateZ":
await RotateSelectedBlockAsync('Z');
break;
case "delete":
await DeleteSelectedBlockAsync();
break;
case "duplicate":
await DuplicateSelectedBlockAsync();
break;
case "undo":
await UndoThreeDimensionalEditAsync();
break;
case "redo":
await RedoThreeDimensionalEditAsync();
break;
case "save":
await Save();
break;
case "saveAs":
await SaveAs();
break;
case "refresh":
await RefreshThreeDimensionalSceneAsync();
break;
}
}
public async Task RefreshThreeDimensionalSceneAsync()
{
if (currentShipBlueprint is null)
{
return;
}
var generation = ++sceneGeneration;
var selectedBlock = selectedSceneBlock?.Block;
// Build from the live object-builder graph on the UI thread. WebView messages can
// otherwise mutate CubeBlocks while a background enumeration is still in progress.
var buildResult = Blueprint3DSceneBuilder.Build(currentShipBlueprint);
if (generation != sceneGeneration)
{
return;
}
int? selectedId = null;
if (selectedBlock is not null)
{
foreach (var pair in buildResult.BlockReferences)
{
if (ReferenceEquals(pair.Value.Block, selectedBlock))
{
selectedId = pair.Key;
selectedSceneBlock = pair.Value;
break;
}
}
}
buildResult.Scene.SelectedBlockId = selectedId;
sceneBlockReferences = buildResult.BlockReferences;
var sceneJson = await Task.Run(() => JsonConvert.SerializeObject(buildResult.Scene));
if (generation == sceneGeneration)
{
ThreeDimensionalSceneChanged?.Invoke(this, sceneJson);
}
}
private async Task RefreshEditorViewsAsync()
{
if (IsThreeDimensionalEditorVisible)
{
await RefreshThreeDimensionalSceneAsync();
}
else if (IsCubeGridListVisible)
{
LoadCubeGrids();
}
else if (IsShipBlueprintPropertyVisible)
{
LoadBlueprintPropertyDefinitions();
}
}
private async Task MoveSelectedBlockAsync(int x, int y, int z)
{
if (selectedSceneBlock is not { } reference)
{
return;
}
var oldMinimum = reference.Block.Min;
var newMinimum = new SerializableVector3I(
oldMinimum.X + x,
oldMinimum.Y + y,
oldMinimum.Z + z);
if (reference.Grid.CubeBlocks.Any(block =>
!ReferenceEquals(block, reference.Block) && SameMinimum(block.Min, newMinimum)))
{
messageService?.ShowMessage(
"Blueprint3D_PositionOccupied".GetLocalized(),
"Warning".GetLocalized(),
InfoBarSeverity.Warning);
return;
}
await ExecuteThreeDimensionalEditAsync(new BlueprintEditAction(
() => SetBlockMinimum(reference, newMinimum),
() => SetBlockMinimum(reference, oldMinimum)));
}
private async Task RotateSelectedBlockAsync(char axis)
{
if (selectedSceneBlock is not { } reference)
{
return;
}
var oldOrientation = reference.Block.BlockOrientation;
var newOrientation = new SerializableBlockOrientation(
RotateDirection(oldOrientation.Forward, axis),
RotateDirection(oldOrientation.Up, axis));
await ExecuteThreeDimensionalEditAsync(new BlueprintEditAction(
() => reference.Block.BlockOrientation = newOrientation,
() => reference.Block.BlockOrientation = oldOrientation));
}
private async Task DeleteSelectedBlockAsync()
{
if (selectedSceneBlock is not { } reference)
{
return;
}
var blocks = reference.Grid.CubeBlocks;
var originalIndex = blocks.IndexOf(reference.Block);
if (originalIndex < 0)
{
return;
}
var oldMinimum = new Vector3I(reference.Block.Min.X, reference.Block.Min.Y, reference.Block.Min.Z);
var groups = (reference.Grid.BlockGroups ?? [])
.Where(group => group.Blocks?.Any(position => position == oldMinimum) == true)
.ToArray();
await ExecuteThreeDimensionalEditAsync(new BlueprintEditAction(
() =>
{
blocks.Remove(reference.Block);
foreach (var group in groups)
{
group.Blocks.RemoveAll(position => position == oldMinimum);
}
selectedSceneBlock = null;
SelectedThreeDimensionalBlockText = "Blueprint3D_NoSelection".GetLocalized();
},
() =>
{
blocks.Insert(Math.Min(originalIndex, blocks.Count), reference.Block);
foreach (var group in groups)
{
if (!group.Blocks.Contains(oldMinimum))
{
group.Blocks.Add(oldMinimum);
}
}
selectedSceneBlock = reference;
}));
}
private async Task DuplicateSelectedBlockAsync()
{
if (selectedSceneBlock is not { } reference)
{
return;
}
var clone = (MyObjectBuilder_CubeBlock)MyObjectBuilderSerializer.Clone(reference.Block);
clone.EntityId = 0;
var offset = 1;
while (offset < 1024 && reference.Grid.CubeBlocks.Any(block =>
block.Min.X == reference.Block.Min.X + offset &&
block.Min.Y == reference.Block.Min.Y &&
block.Min.Z == reference.Block.Min.Z))
{
offset++;
}
clone.Min = new SerializableVector3I(
reference.Block.Min.X + offset,
reference.Block.Min.Y,
reference.Block.Min.Z);
var cloneReference = new Blueprint3DBlockReference
{
Grid = reference.Grid,
Block = clone,
GridIndex = reference.GridIndex
};
await ExecuteThreeDimensionalEditAsync(new BlueprintEditAction(
() =>
{
reference.Grid.CubeBlocks.Add(clone);
selectedSceneBlock = cloneReference;
},
() =>
{
reference.Grid.CubeBlocks.Remove(clone);
selectedSceneBlock = reference;
}));
}
private async Task ExecuteThreeDimensionalEditAsync(BlueprintEditAction editAction)
{
editAction.Apply();
undoActions.Push(editAction);
redoActions.Clear();
HasUnsavedChanges = true;
await RefreshThreeDimensionalSceneAsync();
}
private async Task UndoThreeDimensionalEditAsync()
{
if (!undoActions.TryPop(out var action))
{
return;
}
action.Undo();
redoActions.Push(action);
HasUnsavedChanges = true;
await RefreshThreeDimensionalSceneAsync();
}
private async Task RedoThreeDimensionalEditAsync()
{
if (!redoActions.TryPop(out var action))
{
return;
}
action.Apply();
undoActions.Push(action);
HasUnsavedChanges = true;
await RefreshThreeDimensionalSceneAsync();
}
private static void SetBlockMinimum(Blueprint3DBlockReference reference, SerializableVector3I minimum)
{
var oldMinimum = new Vector3I(reference.Block.Min.X, reference.Block.Min.Y, reference.Block.Min.Z);
reference.Block.Min = minimum;
var newMinimum = new Vector3I(minimum.X, minimum.Y, minimum.Z);
foreach (var group in reference.Grid.BlockGroups ?? [])
{
for (var index = 0; index < group.Blocks.Count; index++)
{
if (group.Blocks[index] == oldMinimum)
{
group.Blocks[index] = newMinimum;
}
}
}
}
private static bool SameMinimum(SerializableVector3I left, SerializableVector3I right)
{
return left.X == right.X && left.Y == right.Y && left.Z == right.Z;
}
private static Base6Directions.Direction RotateDirection(Base6Directions.Direction direction, char axis)
{
var vector = direction switch
{
Base6Directions.Direction.Forward => new Vector3I(0, 0, -1),
Base6Directions.Direction.Backward => new Vector3I(0, 0, 1),
Base6Directions.Direction.Left => new Vector3I(-1, 0, 0),
Base6Directions.Direction.Right => new Vector3I(1, 0, 0),
Base6Directions.Direction.Up => new Vector3I(0, 1, 0),
Base6Directions.Direction.Down => new Vector3I(0, -1, 0),
_ => new Vector3I(0, 0, -1)
};
var rotated = axis switch
{
'X' => new Vector3I(vector.X, -vector.Z, vector.Y),
'Y' => new Vector3I(vector.Z, vector.Y, -vector.X),
'Z' => new Vector3I(-vector.Y, vector.X, vector.Z),
_ => vector
};
return Base6Directions.GetDirection(rotated);
}
private sealed class BlueprintEditAction(Action apply, Action undo)
{
public Action Apply { get; } = apply;
public Action Undo { get; } = undo;
}
[RelayCommand]
void ConvertToSecBlueprint()
{
var exportHelper = new GridExportHelper();
//exportHelper.ExportGrid(currentShipBlueprint?.CubeGrids.First());
}
}
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Newtonsoft.Json;
using SpaceEngineersBlueprintEditor.Implements.Services;
using SpaceEngineersBlueprintEditor.Interface.Services;
using SpaceEngineersBlueprintEditor.Model;
using SpaceEngineersBlueprintEditor.SpaceEngineersCore;
using SpaceEngineersBlueprintEditor.SpaceEngineersCore.BlueprintEditing;
using SpaceEngineersBlueprintEditor.SpaceEngineersCore.GridConverter;
using SpaceEngineersBlueprintEditor.Utilities;
using SpaceEngineersBlueprintEditor.Views;
using System.Collections.ObjectModel;
using System.Diagnostics;
using VRage;
using VRage.Game;
using VRage.ObjectBuilders;
using VRageMath;
using Windows.Storage;
using Windows.Storage.Pickers;
using WinRT.Interop;
using XFEExtension.NetCore.WinUIHelper.Interface.Services;
using XFEExtension.NetCore.WinUIHelper.Utilities;
using XFEExtension.NetCore.WinUIHelper.Utilities.Helper;
namespace SpaceEngineersBlueprintEditor.ViewModels;
public partial class BlueprintEditSubPageViewModel : ViewModelBase
{
[ObservableProperty]
private bool isCubeGridListVisible;
[ObservableProperty]
private bool isShipBlueprintPropertyVisible;
[ObservableProperty]
private bool isThreeDimensionalEditorVisible;
[ObservableProperty]
private bool isSheetMetalEditorVisible;
[ObservableProperty]
private bool isContentGridVisible = false;
[ObservableProperty]
private bool isInitialGridVisible = true;
[ObservableProperty]
private string searchText = string.Empty;
[ObservableProperty]
private string propertiesSearchText = string.Empty;
[ObservableProperty]
private BlueprintPropertyViewData? selectedCubeBlock;
[ObservableProperty]
private object? selectedCubeBlockObject;
[ObservableProperty]
private object? selectedCubeGrid;
[ObservableProperty]
private TreeViewNode? selectedTreeViewNode;
[ObservableProperty]
private string selectedThreeDimensionalBlockText = "No block selected";
[ObservableProperty]
private bool hasUnsavedChanges;
private bool isLoaded;
private int sceneGeneration;
private BlueprintModel? currentBlueprintModel;
private MyObjectBuilder_Definitions? currentDefinitions;
private MyObjectBuilder_ShipBlueprintDefinition? currentShipBlueprint;
private IReadOnlyDictionary<int, Blueprint3DBlockReference> sceneBlockReferences =
new Dictionary<int, Blueprint3DBlockReference>();
private Blueprint3DBlockReference? selectedSceneBlock;
private readonly Stack<BlueprintEditAction> undoActions = new();
private readonly Stack<BlueprintEditAction> redoActions = new();
private readonly List<BlueprintPropertyViewData> blueprintPropertyRoots = [];
private readonly List<BlueprintPropertyViewData> cubePropertyRoots = [];
private readonly ILoadingService? loadingService = ServiceManager.GetGlobalService<ILoadingService>();
private readonly IMessageService? messageService = ServiceManager.GetGlobalService<IMessageService>();
private readonly INavigationViewService? navigationViewService = ServiceManager.GetGlobalService<INavigationViewService>();
private readonly ITabViewTitleService? tabViewTitleService = ServiceManager.GetGlobalService<ITabViewTitleService>();
public ObservableCollection<BlueprintGroupList> BlueprintCubeGridList { get; } = [];
public IBackgroundImageService? BackgroundImageService { get; set; } = ServiceManager.GetGlobalService<IBackgroundImageService>();
public IListViewDisplayService<BlueprintPropertyViewData> ListViewDisplayService { get; set; } = new ListViewDisplayService<BlueprintPropertyViewData>();
public IAutoNavigationParameterService<BlueprintModel> AutoNavigationParameterService { get; set; } = ServiceManager.GetService<IAutoNavigationParameterService<BlueprintModel>>();
public IFileDropService FileDropService { get; set; } = new BlueprintDropService();
public ITreeViewService BlueprintTreeViewService { get; set; } = new TreeViewService();
public ITreeViewService CubeBlockTreeViewService { get; set; } = new TreeViewService();
public ISelectorBarService SelectorBarService { get; set; } = ServiceManager.GetService<ISelectorBarService>();
public event EventHandler<string>? ThreeDimensionalSceneChanged;
public BlueprintEditSubPageViewModel()
{
AutoNavigationParameterService.ParameterChange += AutoNavigationParameterService_ParameterChange;
ListViewDisplayService.SelectionChanged += ListViewDisplayService_SelectionChanged;
SelectorBarService.SelectionChanged += SelectorBarService_SelectionChanged;
FileDropService.Drop += FileDropService_Drop;
}
private void ListViewDisplayService_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.FirstOrDefault() is BlueprintPropertyViewData blueprintPropertyViewData)
{
SelectedCubeBlock = blueprintPropertyViewData;
}
}
partial void OnSelectedCubeBlockChanged(
BlueprintPropertyViewData? oldValue,
BlueprintPropertyViewData? newValue)
{
var selectedBlock = newValue?.Value as MyObjectBuilder_CubeBlock;
SelectedCubeBlockObject = selectedBlock;
SelectedCubeGrid = selectedBlock is null
? null
: currentShipBlueprint?.CubeGrids.FirstOrDefault(grid =>
grid.CubeBlocks.Any(block => ReferenceEquals(block, selectedBlock)));
LoadCubeProperties();
}
private async void FileDropService_Drop(object? sender, (string, DragEventArgs) e)
{
loadingService?.StartLoading<BlueprintEditSubPage>($"{"LoadingDefinitions".GetLocalized()}...");
if (File.Exists(e.Item1) && await SpaceEngineersHelper.LoadBlueprintModel(e.Item1) is BlueprintModel blueprintModel)
{
AutoNavigationParameterService.Parameter = blueprintModel;
if (blueprintModel.ViewData is not null)
tabViewTitleService?.SetTabViewTitle(blueprintModel.ViewData.Name, blueprintModel);
await SetDefinitions(blueprintModel.BlueprintDefinitions);
}
loadingService?.StopLoading<BlueprintEditSubPage>();
}
partial void OnSearchTextChanged(string value) => SearchCubeGrids(value);
partial void OnPropertiesSearchTextChanged(string value)
{
PopulatePropertyTree(BlueprintTreeViewService, blueprintPropertyRoots, value);
PopulatePropertyTree(CubeBlockTreeViewService, cubePropertyRoots, value);
}
private async void SelectorBarService_SelectionChanged(SelectorBar sender, SelectorBarSelectionChangedEventArgs args) => await LoadByName(sender.SelectedItem.Tag as string ?? "Grids");
private async Task SetDefinitions(MyObjectBuilder_Definitions? definitions)
{
if (definitions is not null)
{
currentDefinitions = definitions;
selectedSceneBlock = null;
sceneBlockReferences = new Dictionary<int, Blueprint3DBlockReference>();
undoActions.Clear();
redoActions.Clear();
HasUnsavedChanges = false;
if (currentDefinitions.ShipBlueprints is not null && currentDefinitions.ShipBlueprints.Length > 0)
{
currentShipBlueprint = currentDefinitions.ShipBlueprints[0];
IsContentGridVisible = true;
IsInitialGridVisible = false;
await Task.Delay(100);
if (!isLoaded)
await LoadByName("Grids");
}
else
{
messageService?.ShowMessage("Error_CantLoadingShipDefinitions".GetLocalized(), "Error".GetLocalized(), InfoBarSeverity.Error);
}
}
else
{
messageService?.ShowMessage("Error_CantFindShipDefinitions".GetLocalized(), "Error".GetLocalized(), InfoBarSeverity.Error);
}
}
private async void AutoNavigationParameterService_ParameterChange(object? sender, BlueprintModel? e)
{
if (e is null || AutoNavigationParameterService.SameAsLast)
return;
currentBlueprintModel = e;
if (currentBlueprintModel.ViewData is not null)
BackgroundImageService?.SetBackgroundImage(currentBlueprintModel.ViewData.BlueprintImage);
if (navigationViewService is not null) navigationViewService.Header = null;
if (e.ViewData is not null)
tabViewTitleService?.SetTabViewTitle(e.ViewData.Name, e);
if (e.BlueprintDefinitions is not null)
{
await SetDefinitions(e.BlueprintDefinitions);
}
else if (e.ViewData is not null)
{
await SetDefinitions(await SpaceEngineersHelper.LoadBlueprintAsync(e.ViewData.FilePath));
}
}
private async Task LoadByName(string caseName)
{
if (currentShipBlueprint is null) return;
isLoaded = true;
loadingService?.StartLoading<BlueprintEditSubPage>($"{"LoadingDefinitions".GetLocalized()}...");
await Task.Delay(50);
switch (caseName)
{
case "Grids":
IsCubeGridListVisible = true;
IsShipBlueprintPropertyVisible = false;
IsThreeDimensionalEditorVisible = false;
IsSheetMetalEditorVisible = false;
LoadCubeGrids();
break;
case "Groups":
IsCubeGridListVisible = true;
IsShipBlueprintPropertyVisible = false;
IsThreeDimensionalEditorVisible = false;
IsSheetMetalEditorVisible = false;
LoadCubeGroups();
break;
case "Properties":
IsCubeGridListVisible = false;
IsShipBlueprintPropertyVisible = true;
IsThreeDimensionalEditorVisible = false;
IsSheetMetalEditorVisible = false;
LoadBlueprintPropertyDefinitions();
break;
case "ThreeDimensional":
IsCubeGridListVisible = false;
IsShipBlueprintPropertyVisible = false;
IsThreeDimensionalEditorVisible = true;
IsSheetMetalEditorVisible = false;
await RefreshThreeDimensionalSceneAsync();
break;
case "SheetMetal":
IsCubeGridListVisible = false;
IsShipBlueprintPropertyVisible = false;
IsThreeDimensionalEditorVisible = false;
IsSheetMetalEditorVisible = true;
LoadCubeGrids();
break;
default:
break;
}
loadingService?.StopLoading<BlueprintEditSubPage>();
}
private void LoadBlueprintPropertyDefinitions()
{
var parent = new BlueprintPropertyViewData
{
Value = currentShipBlueprint,
Name = "Ship blueprint",
Type = currentShipBlueprint!.GetType(),
ValueChanged = () => HasUnsavedChanges = true
};
SpaceEngineersHelper.AnalyzeBlueprint(parent);
blueprintPropertyRoots.Clear();
blueprintPropertyRoots.AddRange(parent.Children);
PopulatePropertyTree(BlueprintTreeViewService, blueprintPropertyRoots, PropertiesSearchText);
}
private void SearchCubeGrids(string name)
{
BlueprintCubeGridList.Clear();
currentShipBlueprint!.CubeGrids.Select(grid =>
{
var type = grid.CubeBlocks.GetType();
var gridViewData = new BlueprintPropertyViewData
{
Name = type.Name,
Type = type,
Value = grid.CubeBlocks
};
SpaceEngineersHelper.AnalyzeBlueprint(gridViewData);
var targetChild = gridViewData.Children.Where(child => child.MatchesSearch(name));
if (!targetChild.Any())
return null;
return new BlueprintGroupList(targetChild)
{
GroupName = grid.DisplayName
};
}).Where(grid => grid is not null).ForEach(BlueprintCubeGridList.Add!);
}
private async void LoadCubeGrids()
{
BlueprintCubeGridList.Clear();
currentShipBlueprint!.CubeGrids.Select(grid =>
{
var type = grid.CubeBlocks.GetType();
var gridViewData = new BlueprintPropertyViewData
{
Name = type.Name,
Type = type,
Value = grid.CubeBlocks
};
SpaceEngineersHelper.AnalyzeBlueprint(gridViewData);
return new BlueprintGroupList(gridViewData.Children)
{
GroupName = grid.DisplayName
};
}).ForEach(BlueprintCubeGridList.Add);
await Helper.Wait(() => ListViewDisplayService.IsPageLoaded);
ListViewDisplayService.Select(BlueprintCubeGridList.FirstOrDefault()?.FirstOrDefault());
}
private async void LoadCubeGroups()
{
BlueprintCubeGridList.Clear();
var blueprintCubeGridList = new List<BlueprintGroupList>();
foreach (var grid in currentShipBlueprint!.CubeGrids ?? [])
{
if (grid?.CubeBlocks is null)
{
continue;
}
var type = grid.CubeBlocks.GetType();
var resolvedGroups = BlockGroupResolver.ResolveGroups(grid);
foreach (var group in grid.BlockGroups ?? [])
{
if (group is null)
{
continue;
}
var targetCubeBlocks = resolvedGroups.TryGetValue(group, out var blocks)
? blocks
: Array.Empty<MyObjectBuilder_CubeBlock>();
var groupData = new BlueprintPropertyViewData
{
Name = type.Name,
Type = type,
Value = targetCubeBlocks
};
SpaceEngineersHelper.AnalyzeBlueprint(groupData);
if (blueprintCubeGridList.FirstOrDefault(item => item.GroupName == group.Name) is BlueprintGroupList blueprintGroupList)
{
blueprintGroupList.AddRange(groupData.Children);
}
else
{
blueprintCubeGridList.Add(new BlueprintGroupList(groupData.Children)
{
GroupName = group.Name ?? string.Empty
});
}
}
}
blueprintCubeGridList.ForEach(BlueprintCubeGridList.Add);
await Helper.Wait(() => ListViewDisplayService.IsPageLoaded);
ListViewDisplayService.Select(BlueprintCubeGridList.FirstOrDefault()?.FirstOrDefault());
}
private void LoadCubeProperties()
{
cubePropertyRoots.Clear();
if (SelectedCubeBlock is not null)
{
SelectedCubeBlock.ValueChanged = () => HasUnsavedChanges = true;
SpaceEngineersHelper.AnalyzeBlueprint(SelectedCubeBlock);
SelectedCubeBlock.Children.ForEach(child => child.ValueChanged = SelectedCubeBlock.ValueChanged);
cubePropertyRoots.AddRange(SelectedCubeBlock.Children);
}
PopulatePropertyTree(CubeBlockTreeViewService, cubePropertyRoots, PropertiesSearchText);
}
private static void PopulatePropertyTree(
ITreeViewService treeViewService,
IEnumerable<BlueprintPropertyViewData> source,
string searchText)
{
treeViewService.Clear();
foreach (var property in source.Where(property => property.MatchesSearch(searchText)))
{
treeViewService.Add(new TreeViewNode
{
Content = property,
HasUnrealizedChildren = property.CanExpand
});
}
}
[RelayCommand]
async Task OpenBlueprint()
{
loadingService?.StartLoading<BlueprintEditSubPage>($"{"LoadingDefinitions".GetLocalized()}...");
var openPicker = new FileOpenPicker();
InitializeWithWindow.Initialize(openPicker, WindowNative.GetWindowHandle(App.MainWindow));
openPicker.ViewMode = PickerViewMode.List;
openPicker.FileTypeFilter.Add(".sbc");
if (await openPicker.PickSingleFileAsync() is StorageFile file && File.Exists(file.Path))
{
if (await SpaceEngineersHelper.LoadBlueprintModel(file.Path) is BlueprintModel blueprintModel)
{
AutoNavigationParameterService.Parameter = blueprintModel;
if (blueprintModel.ViewData is not null)
tabViewTitleService?.SetTabViewTitle(blueprintModel.ViewData.Name, blueprintModel);
await SetDefinitions(blueprintModel.BlueprintDefinitions);
}
}
loadingService?.StopLoading<BlueprintEditSubPage>();
}
[RelayCommand]
void OpenInFolder()
{
if (currentBlueprintModel is not null && currentBlueprintModel.ViewData is not null)
Process.Start("explorer.exe", Path.GetDirectoryName(currentBlueprintModel.ViewData.FilePath) ?? string.Empty);
else
messageService?.ShowMessage("Warning_CantFindFile".GetLocalized(), "Warning".GetLocalized(), InfoBarSeverity.Warning);
}
[RelayCommand]
void ViewBlueprintsList() => navigationViewService?.NavigateTo<BlueprintsViewPage>("Local");
[RelayCommand]
async Task Save()
{
var path = currentBlueprintModel?.ViewData?.FilePath;
if (string.IsNullOrWhiteSpace(path))
{
await SaveAs();
return;
}
await SaveToPath(path);
}
[RelayCommand]
async Task SaveAs()
{
if (currentDefinitions is null)
{
return;
}
var savePicker = new FileSavePicker();
InitializeWithWindow.Initialize(savePicker, WindowNative.GetWindowHandle(App.MainWindow));
savePicker.FileTypeChoices.Add(new("BlueprintFile".GetLocalized(), [".sbc"]));
if (currentBlueprintModel?.ViewData?.FilePath is string currentPath && File.Exists(currentPath))
{
savePicker.SuggestedSaveFile = await StorageFile.GetFileFromPathAsync(currentPath);
}
savePicker.SuggestedFileName = "bp.sbc";
savePicker.DefaultFileExtension = ".sbc";
if (await savePicker.PickSaveFileAsync() is StorageFile file)
{
await SaveToPath(file.Path);
if (currentBlueprintModel is not null)
{
currentBlueprintModel.ViewData = SpaceEngineersHelper.LoadBlueprintInfo(file.Path)?.ToBlueprintInfoViewData();
}
}
}
private async Task SaveToPath(string path)
{
if (currentDefinitions is null)
{
return;
}
loadingService?.StartLoading<BlueprintEditSubPage>($"{"SavingBlueprint".GetLocalized()}...");
var cachePath = Path.Combine(AppPathHelper.AppCache, $"blueprint-{Guid.NewGuid():N}.json");
try
{
Directory.CreateDirectory(AppPathHelper.AppCache);
await File.WriteAllTextAsync(cachePath, JsonConvert.SerializeObject(
currentDefinitions,
new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto }));
var converterPath = GetBlueprintConverterPath();
using var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = converterPath,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
}
};
process.StartInfo.ArgumentList.Add("--input");
process.StartInfo.ArgumentList.Add(cachePath);
process.StartInfo.ArgumentList.Add("--output");
process.StartInfo.ArgumentList.Add(path);
process.Start();
var standardOutput = process.StandardOutput.ReadToEndAsync();
var standardError = process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
var result = await standardOutput;
var error = await standardError;
if (process.ExitCode != 0 || !string.Equals(result, "Successful", StringComparison.Ordinal))
{
throw new InvalidOperationException(
string.IsNullOrWhiteSpace(result) ? error : result.Replace("Error:", string.Empty));
}
HasUnsavedChanges = false;
messageService?.ShowMessage("SavingSuccessful".GetLocalized(), "Complete".GetLocalized(), InfoBarSeverity.Success);
}
catch (Exception ex)
{
messageService?.ShowMessage($"{"Failed_SaveFailed".GetLocalized()}: {ex.Message}", "Failed".GetLocalized(), InfoBarSeverity.Error);
}
finally
{
if (File.Exists(cachePath))
{
File.Delete(cachePath);
}
loadingService?.StopLoading<BlueprintEditSubPage>();
}
}
private static string GetBlueprintConverterPath()
{
var deployedPath = Path.Combine(
AppContext.BaseDirectory,
"Converter",
"SpaceEngineersBlueprintEditor.BlueprintConverter.exe");
if (File.Exists(deployedPath))
{
return deployedPath;
}
var parent = new DirectoryInfo(AppContext.BaseDirectory);
while (parent is not null)
{
foreach (var configuration in new[] { "Debug", "Release" })
{
var developmentPath = Path.Combine(
parent.FullName,
"SpaceEngineersBlueprintEditor.BlueprintConverter",
"bin", configuration, "net48",
"SpaceEngineersBlueprintEditor.BlueprintConverter.exe");
if (File.Exists(developmentPath))
{
return developmentPath;
}
}
parent = parent.Parent;
}
throw new FileNotFoundException("Blueprint converter was not deployed with the application.", deployedPath);
}
[RelayCommand]
async Task ConvertBlueprint(string command)
{
if (currentShipBlueprint is null)
{
return;
}
BlueprintTransformResult? transformResult = null;
var changed = false;
if (currentShipBlueprint is not null)
switch (command)
{
case "Destructible":
foreach (var grid in currentShipBlueprint.CubeGrids)
grid.DestructibleBlocks = true;
changed = true;
break;
case "Indestructible":
foreach (var grid in currentShipBlueprint.CubeGrids)
grid.DestructibleBlocks = false;
changed = true;
break;
case "Editable":
foreach (var grid in currentShipBlueprint.CubeGrids)
grid.Editable = true;
changed = true;
break;
case "Non-Editable":
foreach (var grid in currentShipBlueprint.CubeGrids)
grid.Editable = false;
changed = true;
break;
case "SmallGrid":
transformResult = BlueprintTransformService.ConvertGridSize(currentShipBlueprint, MyCubeSize.Small);
break;
case "LargeGrid":
transformResult = BlueprintTransformService.ConvertGridSize(currentShipBlueprint, MyCubeSize.Large);
break;
case "LightBlock":
transformResult = BlueprintTransformService.ConvertArmor(currentShipBlueprint, BlueprintArmorType.Light);
break;
case "HeavyBlock":
transformResult = BlueprintTransformService.ConvertArmor(currentShipBlueprint, BlueprintArmorType.Heavy);
break;
case "Static":
foreach (var grid in currentShipBlueprint.CubeGrids)
grid.IsStatic = true;
changed = true;
break;
case "Active":
foreach (var grid in currentShipBlueprint.CubeGrids)
grid.IsStatic = false;
changed = true;
break;
default:
break;
}
if (transformResult is not null)
{
changed = transformResult.ConvertedBlocks > 0 || transformResult.ChangedGrids > 0;
var summary = string.Format(
"BlueprintTransformSummary".GetLocalized(),
transformResult.ConvertedBlocks,
transformResult.AlreadyTargetBlocks,
transformResult.UnsupportedBlocks);
messageService?.ShowMessage(summary, "Complete".GetLocalized(), InfoBarSeverity.Success);
}
else
{
messageService?.ShowMessage("ConvertComplete".GetLocalized(), "Complete".GetLocalized(), InfoBarSeverity.Success);
}
if (changed)
{
HasUnsavedChanges = true;
await RefreshEditorViewsAsync();
}
}
public void SelectThreeDimensionalBlock(int id)
{
if (!sceneBlockReferences.TryGetValue(id, out var reference))
{
selectedSceneBlock = null;
SelectedThreeDimensionalBlockText = "Blueprint3D_NoSelection".GetLocalized();
return;
}
selectedSceneBlock = reference;
var block = reference.Block;
var gridName = string.IsNullOrWhiteSpace(reference.Grid.DisplayName)
? $"Grid {reference.GridIndex + 1}"
: reference.Grid.DisplayName;
SelectedThreeDimensionalBlockText =
$"{gridName} · {block.SubtypeName} · ({block.Min.X}, {block.Min.Y}, {block.Min.Z})";
}
public async Task HandleThreeDimensionalEditorActionAsync(string action)
{
switch (action)
{
case "moveXPositive":
await MoveSelectedBlockAsync(1, 0, 0);
break;
case "moveXNegative":
await MoveSelectedBlockAsync(-1, 0, 0);
break;
case "moveYPositive":
await MoveSelectedBlockAsync(0, 1, 0);
break;
case "moveYNegative":
await MoveSelectedBlockAsync(0, -1, 0);
break;
case "moveZPositive":
await MoveSelectedBlockAsync(0, 0, 1);
break;
case "moveZNegative":
await MoveSelectedBlockAsync(0, 0, -1);
break;
case "rotateX":
await RotateSelectedBlockAsync('X');
break;
case "rotateY":
await RotateSelectedBlockAsync('Y');
break;
case "rotateZ":
await RotateSelectedBlockAsync('Z');
break;
case "delete":
await DeleteSelectedBlockAsync();
break;
case "duplicate":
await DuplicateSelectedBlockAsync();
break;
case "undo":
await UndoThreeDimensionalEditAsync();
break;
case "redo":
await RedoThreeDimensionalEditAsync();
break;
case "save":
await Save();
break;
case "saveAs":
await SaveAs();
break;
case "refresh":
await RefreshThreeDimensionalSceneAsync();
break;
}
}
public async Task RefreshThreeDimensionalSceneAsync()
{
if (currentShipBlueprint is null)
{
return;
}
var generation = ++sceneGeneration;
var selectedBlock = selectedSceneBlock?.Block;
// Build from the live object-builder graph on the UI thread. WebView messages can
// otherwise mutate CubeBlocks while a background enumeration is still in progress.
var buildResult = Blueprint3DSceneBuilder.Build(currentShipBlueprint);
if (generation != sceneGeneration)
{
return;
}
int? selectedId = null;
if (selectedBlock is not null)
{
foreach (var pair in buildResult.BlockReferences)
{
if (ReferenceEquals(pair.Value.Block, selectedBlock))
{
selectedId = pair.Key;
selectedSceneBlock = pair.Value;
break;
}
}
}
buildResult.Scene.SelectedBlockId = selectedId;
sceneBlockReferences = buildResult.BlockReferences;
var sceneJson = await Task.Run(() => JsonConvert.SerializeObject(buildResult.Scene));
if (generation == sceneGeneration)
{
ThreeDimensionalSceneChanged?.Invoke(this, sceneJson);
}
}
private async Task RefreshEditorViewsAsync()
{
if (IsThreeDimensionalEditorVisible)
{
await RefreshThreeDimensionalSceneAsync();
}
else if (IsCubeGridListVisible)
{
LoadCubeGrids();
}
else if (IsShipBlueprintPropertyVisible)
{
LoadBlueprintPropertyDefinitions();
}
}
private async Task MoveSelectedBlockAsync(int x, int y, int z)
{
if (selectedSceneBlock is not { } reference)
{
return;
}
var oldMinimum = reference.Block.Min;
var newMinimum = new SerializableVector3I(
oldMinimum.X + x,
oldMinimum.Y + y,
oldMinimum.Z + z);
if (reference.Grid.CubeBlocks.Any(block =>
!ReferenceEquals(block, reference.Block) && SameMinimum(block.Min, newMinimum)))
{
messageService?.ShowMessage(
"Blueprint3D_PositionOccupied".GetLocalized(),
"Warning".GetLocalized(),
InfoBarSeverity.Warning);
return;
}
await ExecuteThreeDimensionalEditAsync(new BlueprintEditAction(
() => SetBlockMinimum(reference, newMinimum),
() => SetBlockMinimum(reference, oldMinimum)));
}
private async Task RotateSelectedBlockAsync(char axis)
{
if (selectedSceneBlock is not { } reference)
{
return;
}
var oldOrientation = reference.Block.BlockOrientation;
var newOrientation = new SerializableBlockOrientation(
RotateDirection(oldOrientation.Forward, axis),
RotateDirection(oldOrientation.Up, axis));
await ExecuteThreeDimensionalEditAsync(new BlueprintEditAction(
() => reference.Block.BlockOrientation = newOrientation,
() => reference.Block.BlockOrientation = oldOrientation));
}
private async Task DeleteSelectedBlockAsync()
{
if (selectedSceneBlock is not { } reference)
{
return;
}
var blocks = reference.Grid.CubeBlocks;
var originalIndex = blocks.IndexOf(reference.Block);
if (originalIndex < 0)
{
return;
}
var oldMinimum = new Vector3I(reference.Block.Min.X, reference.Block.Min.Y, reference.Block.Min.Z);
var groups = (reference.Grid.BlockGroups ?? [])
.Where(group => group.Blocks?.Any(position => position == oldMinimum) == true)
.ToArray();
await ExecuteThreeDimensionalEditAsync(new BlueprintEditAction(
() =>
{
blocks.Remove(reference.Block);
foreach (var group in groups)
{
group.Blocks.RemoveAll(position => position == oldMinimum);
}
selectedSceneBlock = null;
SelectedThreeDimensionalBlockText = "Blueprint3D_NoSelection".GetLocalized();
},
() =>
{
blocks.Insert(Math.Min(originalIndex, blocks.Count), reference.Block);
foreach (var group in groups)
{
if (!group.Blocks.Contains(oldMinimum))
{
group.Blocks.Add(oldMinimum);
}
}
selectedSceneBlock = reference;
}));
}
private async Task DuplicateSelectedBlockAsync()
{
if (selectedSceneBlock is not { } reference)
{
return;
}
var clone = (MyObjectBuilder_CubeBlock)MyObjectBuilderSerializer.Clone(reference.Block);
clone.EntityId = 0;
var offset = 1;
while (offset < 1024 && reference.Grid.CubeBlocks.Any(block =>
block.Min.X == reference.Block.Min.X + offset &&
block.Min.Y == reference.Block.Min.Y &&
block.Min.Z == reference.Block.Min.Z))
{
offset++;
}
clone.Min = new SerializableVector3I(
reference.Block.Min.X + offset,
reference.Block.Min.Y,
reference.Block.Min.Z);
var cloneReference = new Blueprint3DBlockReference
{
Grid = reference.Grid,
Block = clone,
GridIndex = reference.GridIndex
};
await ExecuteThreeDimensionalEditAsync(new BlueprintEditAction(
() =>
{
reference.Grid.CubeBlocks.Add(clone);
selectedSceneBlock = cloneReference;
},
() =>
{
reference.Grid.CubeBlocks.Remove(clone);
selectedSceneBlock = reference;
}));
}
private async Task ExecuteThreeDimensionalEditAsync(BlueprintEditAction editAction)
{
editAction.Apply();
undoActions.Push(editAction);
redoActions.Clear();
HasUnsavedChanges = true;
await RefreshThreeDimensionalSceneAsync();
}
private async Task UndoThreeDimensionalEditAsync()
{
if (!undoActions.TryPop(out var action))
{
return;
}
action.Undo();
redoActions.Push(action);
HasUnsavedChanges = true;
await RefreshThreeDimensionalSceneAsync();
}
private async Task RedoThreeDimensionalEditAsync()
{
if (!redoActions.TryPop(out var action))
{
return;
}
action.Apply();
undoActions.Push(action);
HasUnsavedChanges = true;
await RefreshThreeDimensionalSceneAsync();
}
private static void SetBlockMinimum(Blueprint3DBlockReference reference, SerializableVector3I minimum)
{
var oldMinimum = new Vector3I(reference.Block.Min.X, reference.Block.Min.Y, reference.Block.Min.Z);
reference.Block.Min = minimum;
var newMinimum = new Vector3I(minimum.X, minimum.Y, minimum.Z);
foreach (var group in reference.Grid.BlockGroups ?? [])
{
for (var index = 0; index < group.Blocks.Count; index++)
{
if (group.Blocks[index] == oldMinimum)
{
group.Blocks[index] = newMinimum;
}
}
}
}
private static bool SameMinimum(SerializableVector3I left, SerializableVector3I right)
{
return left.X == right.X && left.Y == right.Y && left.Z == right.Z;
}
private static Base6Directions.Direction RotateDirection(Base6Directions.Direction direction, char axis)
{
var vector = direction switch
{
Base6Directions.Direction.Forward => new Vector3I(0, 0, -1),
Base6Directions.Direction.Backward => new Vector3I(0, 0, 1),
Base6Directions.Direction.Left => new Vector3I(-1, 0, 0),
Base6Directions.Direction.Right => new Vector3I(1, 0, 0),
Base6Directions.Direction.Up => new Vector3I(0, 1, 0),
Base6Directions.Direction.Down => new Vector3I(0, -1, 0),
_ => new Vector3I(0, 0, -1)
};
var rotated = axis switch
{
'X' => new Vector3I(vector.X, -vector.Z, vector.Y),
'Y' => new Vector3I(vector.Z, vector.Y, -vector.X),
'Z' => new Vector3I(-vector.Y, vector.X, vector.Z),
_ => vector
};
return Base6Directions.GetDirection(rotated);
}
private sealed class BlueprintEditAction(Action apply, Action undo)
{
public Action Apply { get; } = apply;
public Action Undo { get; } = undo;
}
[RelayCommand]
void ConvertToSecBlueprint()
{
var exportHelper = new GridExportHelper();
//exportHelper.ExportGrid(currentShipBlueprint?.CubeGrids.First());
}
}