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 sceneBlockReferences = new Dictionary(); private Blueprint3DBlockReference? selectedSceneBlock; private readonly Stack undoActions = new(); private readonly Stack redoActions = new(); private readonly List blueprintPropertyRoots = []; private readonly List cubePropertyRoots = []; private readonly ILoadingService? loadingService = ServiceManager.GetGlobalService(); private readonly IMessageService? messageService = ServiceManager.GetGlobalService(); private readonly INavigationViewService? navigationViewService = ServiceManager.GetGlobalService(); private readonly ITabViewTitleService? tabViewTitleService = ServiceManager.GetGlobalService(); public ObservableCollection BlueprintCubeGridList { get; } = []; public IBackgroundImageService? BackgroundImageService { get; set; } = ServiceManager.GetGlobalService(); public IListViewDisplayService ListViewDisplayService { get; set; } = new ListViewDisplayService(); public IAutoNavigationParameterService AutoNavigationParameterService { get; set; } = ServiceManager.GetService>(); 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(); public event EventHandler? 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($"{"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(); } 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(); 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($"{"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(); } 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(); 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(); 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 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($"{"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(); } [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("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($"{"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(); } } 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()); } }