Editor/Data/ArchCabinetFrontPreset.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// A front an author DREW, standing beside the four the tool extrudes. Everything the scene pass needs to hang the
// prefab on a mount is here: which prefab, where on it the mount takes hold, and how it is turned once it is there.
//
// The anchor is picked on the prefab in the designer rather than modelled to a convention, so a mesh built around
// whatever pivot Blender left behind lands where the author meant it to.
public sealed class ArchCabinetFrontPreset : IArchAimedPrefab {
public string Name { get; set; } = "";
public string Prefab { get; set; } = "";
public string Detail { get; set; } = "";
// The picked point in the PREFAB's own space: a vertex is that vertex, an edge is its midpoint.
public Vector3 Anchor { get; set; }
// The picked edge's direction, prefab-local and unit length. Zero where a vertex was picked.
public Vector3 AnchorAlong { get; set; }
// Turned in the MOUNT's frame, after the base mapping and the edge correction. A vertex pick carries no
// rotation of its own, so this is the whole escape hatch from guessing how the author modelled it.
public Angles AnchorTurn { get; set; }
// Standing at the size it was modelled unless the author asks for it to be stretched into the opening.
public bool Scales { get; set; }
public bool Standing => !string.IsNullOrWhiteSpace( Prefab );
public bool Anchored => Anchor != Vector3.Zero || AnchorAlong != Vector3.Zero;
public bool OnAnEdge => AnchorAlong.Length > 0.01f;
public ArchCabinetFrontPreset Copy() => (ArchCabinetFrontPreset)MemberwiseClone();
}
// Author fronts stand on disk beside the carcass types, in their own folder - a front is fitted to any type and
// belongs to none of them, so filing it under one would make it that type's private art.
public static class ArchCabinetFronts {
public static string Directory => ArchDataRoot.For( "cabinets/fronts" );
public static List<ArchCabinetFrontPreset> Authored() {
var loaded = new List<ArchCabinetFrontPreset>();
foreach ( var file in ArchStorage.DocumentFiles( Directory, ".front.json" ) ) {
if ( ArchStorage.ReadDocument<ArchCabinetFrontPreset>( $"{Directory}/{file}" ) is { Name.Length: > 0 } preset ) {
loaded.Add( preset );
}
}
return loaded.OrderBy( preset => preset.Name ).ToList();
}
public static bool Save( ArchCabinetFrontPreset preset ) {
return preset is { Name.Length: > 0 } && ArchStorage.WriteDocument( PathFor( preset.Name ), preset );
}
public static bool Remove( string name ) {
return !string.IsNullOrWhiteSpace( name ) && ArchStorage.DeleteDocument( PathFor( name ) );
}
public static ArchCabinetFrontPreset Find( string name ) {
return string.IsNullOrWhiteSpace( name )
? null
: ArchStorage.ReadDocument<ArchCabinetFrontPreset>( PathFor( name ) );
}
// A fitting stores the PREFAB PATH, which is what the scene pass hangs - so an author front is recognised by the
// art it points at. A prefab nobody wrote a front for still stands, as one with no anchor of its own.
public static ArchCabinetFrontPreset Resolve( string reference ) {
if ( string.IsNullOrWhiteSpace( reference ) ) {
return null;
}
return Authored().FirstOrDefault( preset => string.Equals( preset.Prefab, reference, StringComparison.OrdinalIgnoreCase ) )
?? new ArchCabinetFrontPreset { Name = Titled( reference ), Prefab = reference };
}
// The prefab's own file name, which is what a dropped asset is called until the author types over it.
public static string Titled( string path ) {
if ( string.IsNullOrWhiteSpace( path ) ) {
return "";
}
var file = path.Replace( '\\', '/' ).Split( '/' ).Last();
var stem = file.EndsWith( ".prefab", StringComparison.OrdinalIgnoreCase ) ? file[..^7] : file;
var spaced = stem.Replace( '_', ' ' ).Trim();
return spaced.Length == 0 ? "Front" : char.ToUpperInvariant( spaced[0] ) + spaced[1..];
}
static string PathFor( string name ) => $"{Directory}/{Slug( name )}.front.json";
static string Slug( string name ) {
var cleaned = new string( name.Select( letter => char.IsLetterOrDigit( letter ) ? char.ToLowerInvariant( letter ) : '_' ).ToArray() );
return cleaned.Trim( '_' ).Length == 0 ? "front" : cleaned.Trim( '_' );
}
}