Editor/Services/ArchOutput.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
public sealed class ArchDoorRequest {
public ArchOpening Opening { get; init; }
public ArchWall Wall { get; init; }
public Vector3 HingeLocal { get; init; }
// Wall leaves inherit yaw from the wall's node; anything else sets its own.
public Rotation Rotation { get; init; } = Rotation.Identity;
public float LeafWidth { get; init; }
public float LeafHeight { get; init; }
public float LeafThickness { get; init; }
public bool Mirrored { get; init; }
public ArchBrush Brush { get; init; }
public ArchAimedPrefab Art { get; init; }
public string MaterialGroup { get; init; }
public bool Hung => Art.Standing;
}
public sealed class ArchGlassRequest {
public ArchOpening Opening { get; init; }
public ArchMesh Canvas { get; init; }
}
public sealed class ArchBuiltPart {
public string Path { get; init; }
public Transform NodeTransform { get; init; }
public ArchMesh Canvas { get; init; }
public List<ArchDoorRequest> Doors { get; } = new();
public List<ArchGlassRequest> Glass { get; } = new();
public List<ArchFittingRequest> Fittings { get; } = new();
public int Owner { get; init; }
public string Piece { get; init; } = "";
public ArchCollisionMode? Asked { get; init; }
public ArchCollisionMode? Scope { get; init; }
public bool? AskedShadows { get; init; }
public bool? ScopeShadows { get; init; }
public object Source { get; init; }
public ArchCollisionMode Resolved { get; set; } = ArchCollisionMode.Complex;
public bool ResolvedShadows { get; set; } = true;
// Geometry only — author settings ride Settings, not Key
public ulong Key {
get {
var hash = ArchHash.Fold( Canvas.Content, NodeTransform.Position );
hash = ArchHash.Fold( hash, NodeTransform.Rotation.Forward );
foreach ( var door in Doors ) {
hash = ArchHash.Fold( hash, door.Opening?.Id ?? 0 );
hash = ArchHash.Fold( hash, door.Opening?.StartOpen ?? false );
hash = ArchHash.Fold( hash, (int)(door.Opening?.Kind ?? default) );
hash = ArchHash.Fold( hash, door.HingeLocal );
hash = ArchHash.Fold( hash, door.Rotation.Forward );
hash = ArchHash.Fold( hash, door.LeafWidth );
hash = ArchHash.Fold( hash, door.LeafHeight );
hash = ArchHash.Fold( hash, door.LeafThickness );
hash = ArchHash.Fold( hash, door.Mirrored );
hash = ArchHash.Fold( hash, door.Brush.Material?.Name );
hash = ArchHash.Fold( hash, door.MaterialGroup );
hash = door.Art.Fold( hash );
}
foreach ( var pane in Glass ) {
hash = ArchHash.Fold( hash, pane.Opening?.Id ?? 0 );
hash = ArchHash.Fold( hash, pane.Canvas?.Content ?? 0ul );
}
foreach ( var fitting in Fittings ) {
hash = fitting.Fold( hash );
}
return hash;
}
}
// Moves no vertex — a toggle writes flags without re-cooking the mesh
public ulong Settings => ArchHash.Fold( ArchHash.Fold( ArchHash.Seed, (int)Resolved ), ResolvedShadows );
}
public sealed class ArchOutput {
readonly List<ArchBuiltPart> parts = new();
readonly Dictionary<string, Transform> frames = new();
readonly ArchPlan plan;
ArchCollisionMode? scope;
bool? shadowScope;
public ArchOutput( ArchPlan plan = null ) {
this.plan = plan;
}
public IReadOnlyDictionary<string, Transform> Frames => frames;
public IDisposable Within( ArchCollisionMode? asked ) {
var held = scope;
scope = asked ?? held;
return new ScopeReset( this, held );
}
public IDisposable WithinShadows( int itemId ) {
var held = shadowScope;
shadowScope = ArchParts.AskedShadows( plan, itemId, "" ) ?? held;
return new ShadowScopeReset( this, held );
}
readonly struct ScopeReset( ArchOutput output, ArchCollisionMode? restore ) : IDisposable {
public void Dispose() => output.scope = restore;
}
readonly struct ShadowScopeReset( ArchOutput output, bool? restore ) : IDisposable {
public void Dispose() => output.shadowScope = restore;
}
public ArchOutput Draw<TPart>(
string path,
Transform nodeTransform,
Transform projection,
TPart source,
bool welded,
Action<ArchMesh, TPart, ArchBuiltPart> create ) {
var canvas = new ArchMesh( projection ).Splitting();
if ( welded ) {
canvas.Welded();
}
ArchParts.Split( path, out var owner, out var piece );
var part = new ArchBuiltPart {
Path = path,
NodeTransform = nodeTransform,
Canvas = canvas,
Source = source,
Owner = owner,
Piece = piece,
Asked = ArchParts.Asked( plan, owner, piece ) ?? (source as IArchCollides)?.Collision,
Scope = scope,
AskedShadows = ArchParts.AskedShadows( plan, owner, piece ),
ScopeShadows = shadowScope
};
create( canvas, source, part );
File( part, canvas, source );
return this;
}
public ArchOutput Add( string path, Transform nodeTransform, ArchMesh canvas ) {
if ( canvas is null || !canvas.Emitted ) {
return this;
}
ArchParts.Split( path, out var owner, out var piece );
File( new ArchBuiltPart {
Path = path,
NodeTransform = nodeTransform,
Canvas = canvas,
Owner = owner,
Piece = piece,
Asked = ArchParts.Asked( plan, owner, piece ),
Scope = scope,
AskedShadows = ArchParts.AskedShadows( plan, owner, piece ),
ScopeShadows = shadowScope
}, canvas, null );
return this;
}
public ArchOutput Add( string path, Transform nodeTransform, Action<ArchMesh> create ) {
var canvas = new ArchMesh( Transform.Zero ).Welded().Splitting();
create( canvas );
return Add( path, nodeTransform, canvas );
}
// An empty host is not filed — the cache would read it as a vanished node
void File( ArchBuiltPart part, ArchMesh canvas, object source ) {
if ( !canvas.IsEmpty ) {
parts.Add( part );
}
if ( canvas.Pieces.Count == 0 ) {
return;
}
frames[part.Path] = part.NodeTransform;
var orphaned = canvas.IsEmpty && (part.Doors.Count > 0 || part.Glass.Count > 0 || part.Fittings.Count > 0)
? part
: null;
foreach ( var piece in canvas.Pieces ) {
Filed( part, source, piece, "", ref orphaned );
}
}
void Filed( ArchBuiltPart host, object source, ArchMeshPiece piece, string under, ref ArchBuiltPart orphaned ) {
var below = ArchParts.Joined( under, piece.Name );
var named = ArchParts.Joined( host.Piece, below );
var part = new ArchBuiltPart {
Path = $"{host.Path}/{below}",
NodeTransform = Transform.Zero,
Canvas = piece.Canvas,
Source = source,
Owner = host.Owner,
Piece = named,
Asked = ArchParts.Asked( plan, host.Owner, named ) ?? host.Asked,
Scope = host.Scope,
AskedShadows = ArchParts.AskedShadows( plan, host.Owner, named ) ?? host.AskedShadows,
ScopeShadows = host.ScopeShadows
};
if ( !piece.Canvas.IsEmpty ) {
if ( orphaned is not null ) {
part.Doors.AddRange( orphaned.Doors );
part.Glass.AddRange( orphaned.Glass );
part.Fittings.AddRange( orphaned.Fittings );
orphaned = null;
}
parts.Add( part );
}
foreach ( var nested in piece.Canvas.Pieces ) {
Filed( host, source, nested, below, ref orphaned );
}
}
public IReadOnlyList<ArchBuiltPart> Create() {
return parts.ToList();
}
}