Editor-side catalog for architectural openings. Provides reading, saving and shipped presets for opening presets, and utilities for reading catalog files and creating filesystem-safe slugs.
using System.Collections.Generic;
using System.Linq;
namespace Sunless.Architecture;
public sealed class ArchOpeningCatalog : IArchCatalog
{
public const string Directory = "arch/openings";
public ArchShelf Shelf => ArchShelf.Openings;
public string Named( object entry ) => entry is ArchOpeningPreset preset ? preset.Name : null;
public IEnumerable<object> Authored()
{
return ArchCatalogFiles.Read<ArchOpeningPreset>( Directory, ".opening.json", preset => preset.Name );
}
public bool Save( object entry )
{
return entry is ArchOpeningPreset preset
&& !string.IsNullOrWhiteSpace( preset.Name )
&& ArchStorage.WriteDocument( PathFor( preset.Name ), preset );
}
public static string PathFor( string name ) => $"{Directory}/{ArchCatalogFiles.Slug( name, "opening" )}.opening.json";
public IEnumerable<object> Shipped() => Presets();
public static List<ArchOpeningPreset> Presets()
{
return new List<ArchOpeningPreset>
{
new() { Name = "doorway", Kind = OpeningKind.Archway, Width = 48f, Height = 96f, Leaf = false },
new() { Name = "door", Kind = OpeningKind.Door, Width = 48f, Height = 96f },
new() { Name = "door_double", Kind = OpeningKind.DoubleDoor, Width = 88f, Height = 100f },
new() { Name = "garage_single", Kind = OpeningKind.Garage, Width = 128f, Height = 112f, CasingWidth = 6f },
new() { Name = "garage_double", Kind = OpeningKind.Garage, Width = 216f, Height = 112f, CasingWidth = 6f },
// Kit presets so they show in the Opening grid and a recipe can name one.
new() { Name = "roller_shutter", Kind = OpeningKind.Garage, Width = 168f, Height = 168f, CasingWidth = 8f },
new() { Name = "loading_dock", Kind = OpeningKind.Garage, Width = 240f, Height = 192f, CasingWidth = 8f },
new() { Name = "loading_dock_open", Kind = OpeningKind.Garage, Width = 240f, Height = 192f, CasingWidth = 8f, StartOpen = true },
new() { Name = "hangar_door", Kind = OpeningKind.Garage, Width = 480f, Height = 264f, CasingWidth = 10f },
new() { Name = "hangar_door_open", Kind = OpeningKind.Garage, Width = 480f, Height = 264f, CasingWidth = 10f, StartOpen = true },
new() { Name = "bay_mouth", Kind = OpeningKind.Archway, Width = 240f, Height = 192f, Leaf = false, Cased = false },
new() { Name = "window", Kind = OpeningKind.Window, Width = 48f, Height = 48f, SillHeight = 40f, HasSill = true },
new() { Name = "window_wide", Kind = OpeningKind.Window, Width = 112f, Height = 48f, SillHeight = 40f, HasSill = true },
new() { Name = "window_tall", Kind = OpeningKind.Window, Width = 48f, Height = 80f, SillHeight = 24f, HasSill = true },
new() { Name = "window_strip", Kind = OpeningKind.Window, Width = 192f, Height = 24f, SillHeight = 88f, Cased = false },
// Named like a window: 6-over-6 is three across by two down, twice.
new()
{
Name = "sash_1over1", Kind = OpeningKind.Window, Width = 36f, Height = 60f, SillHeight = 36f,
HasSill = true, Glazed = true, Sashes = 2, PanesAcross = 1, PanesDown = 1
},
new()
{
Name = "sash_2over2", Kind = OpeningKind.Window, Width = 40f, Height = 66f, SillHeight = 34f,
HasSill = true, Glazed = true, Sashes = 2, PanesAcross = 2, PanesDown = 1
},
new()
{
Name = "sash_4over4", Kind = OpeningKind.Window, Width = 40f, Height = 66f, SillHeight = 34f,
HasSill = true, Glazed = true, Sashes = 2, PanesAcross = 2, PanesDown = 2
},
new()
{
Name = "sash_6over6", Kind = OpeningKind.Window, Width = 44f, Height = 84f, SillHeight = 26f,
HasSill = true, Glazed = true, Sashes = 2, PanesAcross = 3, PanesDown = 2
},
new()
{
Name = "sash_small", Kind = OpeningKind.Window, Width = 30f, Height = 30f, SillHeight = 52f,
HasSill = true, Glazed = true, Sashes = 1, PanesAcross = 2, PanesDown = 2
},
new()
{
Name = "sash_twin", Kind = OpeningKind.Window, Width = 76f, Height = 66f, SillHeight = 34f,
HasSill = true, Glazed = true, Lights = 2, Sashes = 2, PanesAcross = 2, PanesDown = 2
},
new()
{
Name = "sash_twin_tall", Kind = OpeningKind.Window, Width = 84f, Height = 84f, SillHeight = 26f,
HasSill = true, Glazed = true, Lights = 2, Sashes = 2, PanesAcross = 3, PanesDown = 2
},
new() { Name = "hatch", Kind = OpeningKind.Hatch, Width = 32f, Height = 32f, SillHeight = 56f }
};
}
}
// Both wall catalogs read a folder of named documents the same way, and the slug is what keeps a preset called
// "Sash 6/6" from being written to a path no filesystem will take.
public static class ArchCatalogFiles
{
public static List<T> Read<T>( string directory, string suffix, System.Func<T, string> named ) where T : class
{
var loaded = new List<T>();
foreach ( var file in ArchStorage.DocumentFiles( directory, suffix ) )
{
if ( ArchStorage.ReadDocument<T>( $"{directory}/{file}" ) is { } entry && !string.IsNullOrWhiteSpace( named( entry ) ) )
{
loaded.Add( entry );
}
}
return loaded.OrderBy( named ).ToList();
}
public static string Slug( string name, string fallback )
{
var slug = new string( (name ?? "")
.Trim()
.ToLowerInvariant()
.Select( character => char.IsLetterOrDigit( character ) ? character : '_' )
.ToArray() );
while ( slug.Contains( "__" ) )
{
slug = slug.Replace( "__", "_" );
}
slug = slug.Trim( '_' );
return string.IsNullOrWhiteSpace( slug ) ? fallback : slug;
}
}