editor/AdvancedMappingTools/Core/BuilderStore.cs

JSON persistence for the Advanced Mapping Tools editor package. Stores per-tool presets and per-object builder records (type, plane height, origin, points, settings) under ProjectSettings/AdvancedMappingTools, and supports exporting and importing preset files.

using System.IO;
using System.Text.Json;

namespace AdvancedMappingTools;
/// <summary>
/// What a builder needs to pick a finished object up again.
/// </summary>
public sealed class BuilderRecord
{
	public string Type { get; set; }
	public bool Closed { get; set; }
	public float PlaneHeight { get; set; }
	public float[] Origin { get; set; }
	public List<float[]> Points { get; set; } = [];
	public Dictionary<string, string> Settings { get; set; } = [];
}

/// <summary>
/// Keeps presets and the records of built objects as JSON in the project settings.
/// </summary>
public static class BuilderStore
{
	static string Root => Path.Combine( Project.Current.GetRootPath(), "ProjectSettings", "AdvancedMappingTools" );
	static string PresetFile( string type ) => Path.Combine( Root, "presets", $"{type}.json" );
	static string RecordFile( Guid id ) => Path.Combine( Root, "objects", $"{id}.json" );

	public static Dictionary<string, Dictionary<string, string>> LoadPresets( string type )
	{
		var path = PresetFile( type );
		if ( !File.Exists( path ) )
			return[];
		var loaded = StoreSafety.Read<Dictionary<string, Dictionary<string, string>>>( path ) ?? [];
		if ( loaded.Values.Any( v => v is null ) )
		{
			StoreSafety.Warn( path, "A preset contains no settings" );
			return loaded.Where( p => p.Value is not null ).ToDictionary( p => p.Key, p => p.Value );
		}

		return loaded;
	}

	public static void SavePresets( string type, Dictionary<string, Dictionary<string, string>> presets )
	{
		var path = PresetFile( type );
		Directory.CreateDirectory( Path.GetDirectoryName( path ) );
		StoreSafety.Write( path, JsonSerializer.Serialize( presets, new JsonSerializerOptions { WriteIndented = true } ) );
	}

	/// <summary>
	/// A file with the presets of one builder, to hand to someone else or keep elsewhere.
	/// </summary>
	public sealed class PresetFileContent
	{
		public string Tool { get; set; }
		public Dictionary<string, Dictionary<string, string>> Presets { get; set; } = [];
	}

	/// <summary>
	/// Write the presets of a builder to a file. Without any saved preset, the current settings
	/// go in as one called "Current", so an export is never empty.
	/// </summary>
	public static int ExportPresets( string type, string path, Dictionary<string, string> current )
	{
		var presets = LoadPresets( type );

		if ( presets.Count == 0 ) presets["Current"] = current;

		File.WriteAllText( path, JsonSerializer.Serialize( new PresetFileContent { Tool = type, Presets = presets }, new JsonSerializerOptions { WriteIndented = true } ) );

		return presets.Count;
	}

	/// <summary>
	/// Add the presets from a file to the ones of a builder. A name that exists already gets a
	/// number. Returns how many were added, or the reason nothing was.
	/// </summary>
	public static (int Added, string Problem) ImportPresets( string type, string path )
	{
		PresetFileContent content;
		try
		{
			content = JsonSerializer.Deserialize<PresetFileContent>( File.ReadAllText( path ) );
		}
		catch ( Exception e )
		{
			return (0, $"The file could not be read: {e.Message}");
		}

		if ( content?.Presets is null || content.Presets.Count == 0 )
			return (0, "The file holds no presets.");
		if ( content.Tool != type )
			return (0, $"The presets are for {content.Tool}, not for this tool.");
		var presets = LoadPresets( type );
		foreach ( var(name, settings)in content.Presets )
		{
			var unique = name;
			for ( var i = 2; presets.ContainsKey( unique ); i++ )
				unique = $"{name} ({i})";
			if ( settings is null )
				return (0, "A preset contains no settings.");
			presets[unique] = settings;
		}

		SavePresets( type, presets );
		return (content.Presets.Count, null);
	}

	public static void SaveRecord( Guid id, BuilderRecord record )
	{
		var path = RecordFile( id );
		Directory.CreateDirectory( Path.GetDirectoryName( path ) );
		StoreSafety.Write( path, JsonSerializer.Serialize( record ) );
		LineSnap.Forget( id );
	}

	public static BuilderRecord LoadRecord( Guid id )
	{
		var path = RecordFile( id );
		var record = StoreSafety.Read<BuilderRecord>( path );
		if ( record is null )
			return null;
		if ( string.IsNullOrWhiteSpace( record.Type ) || record.Origin is null || record.Origin.Length < 3 || record.Origin.Take( 3 ).Any( v => !float.IsFinite( v ) ) || record.Points is null || record.Points.Count == 0 || record.Points.Any( p => p is null || p.Length < 3 || p.Take( 3 ).Any( v => !float.IsFinite( v ) ) ) )
		{
			StoreSafety.Warn( path, "Incomplete object coordinates" );
			return null;
		}

		record.Settings ??= [];
		return record;
	}

	public static void DeleteRecord( Guid id )
	{
		var path = RecordFile( id );
		if ( File.Exists( path ) ) File.Delete( path );
	}
}