Editor tool definitions for the AdvancedMappingTools architecture kit. ArchEditor, CurvedArchEditor, VaultEditor, PortalEditor and RoseWindowEditor each generate a preview mesh via MeshKit or place generator-backed parts, with settings classes, material pickers, and a shared ArchTool.Fill helper. ArchEditor.Dock snaps a new arch against the piers of already placed arches.
namespace AdvancedMappingTools;
/// <summary>
/// An arch of independent parts on two piers, or a row of connected arches.
/// The two clicks set the clear length between the outer piers.
/// </summary>
[ToolCategory( "Arches & Vaults" )]
[Title( "Arch" ), Icon( "architecture" )]
public sealed class ArchEditor( PrimitiveTool tool ) : LineEditor( tool )
{
public override IReadOnlySet<string> UnusedOptions { get; } = new HashSet<string> { "Surface" };
const float ConnectSlack = 24.0f;
const float StraightTolerance = 0.35f;
readonly ArchGenerator _generator = new();
public ArchSettings Settings { get; } = new();
public override object ExtraSettings => Settings;
protected override bool PlacesObjects => true;
protected override bool Connects => true;
protected override void BuildMesh( MeshKit kit ) { }
Transform Frame => new( Start, Facing );
/// <summary>
/// A click near the outer pier of a finished arch puts this arch right against that pier, so
/// the two stand against each other without a gap, in line or at a right angle from the front
/// or the back of the pier. It works for either end of the new arch.
/// </summary>
protected override Vector3 Dock( Vector3 point )
{
var docked = base.Dock( point );
if ( !ConnectFinished || SceneEditorSession.Active is null ) return docked;
var best = float.MaxValue;
var result = docked;
Vector3? onward = null;
var own = Settings.Piers && Settings.Shape != ArchShape.Circular ? MathF.Max( Settings.PierWidth, 1.0f ) : 0.0f;
foreach ( var part in SceneEditorSession.Active.Scene.GetAllComponents<AmtPart>() )
{
if ( part.Generator != _generator.Id || !PartReconciler.IsAlive( part ) || string.IsNullOrEmpty( part.Settings ) ) continue;
if ( PartService.Read( part.GameObject ).Settings is not ArchSettings arch || arch.GetType() != typeof( ArchSettings ) ) continue;
var pier = arch.Piers && arch.Shape != ArchShape.Circular ? MathF.Max( arch.PierWidth, 1.0f ) : 0.0f;
var world = part.WorldTransform;
var forward = world.Rotation.Forward;
var side = world.Rotation.Left;
foreach ( var (end, away) in new[] { (world.PointToWorld( Vector3.Zero ), -forward), (world.PointToWorld( Vector3.Forward * arch.Length ), forward) } )
{
var middle = end + away * pier * 0.5f;
// The outer face and the two faces of the pier a new arch can stand against.
foreach ( var (facing, reach) in new[] { (away, pier * 0.5f), (side, arch.Depth * 0.5f), (-side, arch.Depth * 0.5f) } )
{
var face = middle + facing * reach;
var seat = face + facing * own;
// This face is taken by the first point, the arch carries on from it.
if ( Points.Count > 0 && seat.WithZ( 0.0f ).Distance( Points[0].Position.WithZ( 0.0f ) ) < 1.0f )
{
onward = facing;
continue;
}
var distance = face.WithZ( 0.0f ).Distance( docked.WithZ( 0.0f ) );
if ( distance > DockRadius + pier + ConnectSlack || distance >= best ) continue;
best = distance;
result = seat;
}
}
}
if ( best < float.MaxValue )
{
DrawLabel( result, "Connects", Color.Green );
return result;
}
// Nearly in line with the arch it starts from: exactly in line, or the piers wedge apart.
if ( onward is Vector3 line )
{
var from = (docked - Points[0].Position).WithZ( 0.0f );
var along = from.Dot( line );
if ( along > 0.0f && (from - line * along).Length <= along * StraightTolerance )
return Points[0].Position + line * along;
}
return docked;
}
void Prepare()
{
Settings.Length = Length;
ArchTool.Fill( Settings, SurfaceMaterial, TrimMaterial );
}
protected override PolygonMesh BuildPreview()
{
Prepare();
return PartService.Plan( _generator, Settings ).Flatten( Frame );
}
protected override void PlaceObjects()
{
Prepare();
PartTool.Place( _generator, Settings, Frame );
}
}
/// <summary>
/// Arches that stand along a drawn line or a spiral and bend with it.
/// </summary>
[ToolCategory( "Arches & Vaults" )]
[Title( "Curved Arch" ), Icon( "looks" )]
public sealed class CurvedArchEditor : PathEditor
{
public override IReadOnlySet<string> UnusedOptions { get; } = new HashSet<string> { "CurveSegments", "Surface" };
readonly CurvedArchGenerator _generator = new();
public CurvedArchEditor( PrimitiveTool tool ) : base( tool ) => Smooth = true;
public CurvedArchSettings Settings { get; } = new();
public override object ExtraSettings => Settings;
protected override int MinPoints => 2;
protected override int MaxPoints => Settings.Layout == PathLayout.Spiral ? 2 : int.MaxValue;
protected override bool ConnectPoints => Settings.Layout == PathLayout.Drawn;
protected override bool PlacesObjects => true;
protected override bool AllowsHeight => true;
protected override bool IsShapeValid => Points.Count >= 2 && Points[0].Position.Distance( Points[1].Position ) > 1.0f;
protected override IEnumerable<string> Hints => Settings.Layout == PathLayout.Spiral
? ["Point 1: centre", "Point 2: radius and start"]
: [.. base.Hints, "Shift + drag a point: raise or lower it"];
protected override void BuildMesh( MeshKit kit ) { }
void Prepare()
{
Settings.Path = Settings.Layout == PathLayout.Spiral
? PathTool.Spiral( Points[0].Position, Points[1].Position, Settings.Turns, Settings.Clockwise )
: Curve();
ArchTool.Fill( Settings, SurfaceMaterial, TrimMaterial );
}
protected override PolygonMesh BuildPreview()
{
Prepare();
return PartService.Plan( _generator, Settings ).Flatten( new Transform( Vector3.Zero, Rotation.Identity ) );
}
protected override void PlaceObjects()
{
Prepare();
PartTool.Place( _generator, Settings, new Transform( Vector3.Zero, Rotation.Identity ) );
}
}
/// <summary>
/// A vault of independent webs over a rectangle set by three clicks, in one bay or a row of them.
/// </summary>
[ToolCategory( "Arches & Vaults" )]
[Title( "Vault" ), Icon( "roofing" )]
public sealed class VaultEditor( PrimitiveTool tool ) : RectEditor( tool )
{
public override IReadOnlySet<string> UnusedOptions { get; } = new HashSet<string> { "Surface", "Variation" };
readonly VaultGenerator _generator = new();
public VaultSettings Settings { get; } = new();
public override object ExtraSettings => Settings;
protected override bool PlacesObjects => true;
protected override void BuildMesh( MeshKit kit ) { }
void Prepare()
{
Settings.Length = RectLength;
Settings.Depth = RectDepth;
Settings.VaultMaterial = PartTool.Pick( Settings.VaultMaterial, SurfaceMaterial );
Settings.RibMaterial = PartTool.Pick( Settings.RibMaterial, TrimMaterial );
Settings.SupportMaterial = PartTool.Pick( Settings.SupportMaterial, SurfaceMaterial );
}
protected override PolygonMesh BuildPreview()
{
Prepare();
return PartService.Plan( _generator, Settings ).Flatten( RectFrame );
}
protected override void PlaceObjects()
{
Prepare();
PartTool.Place( _generator, Settings, RectFrame );
}
}
/// <summary>
/// What the arch tools have in common.
/// </summary>
public static class ArchTool
{
public static void Fill( ArchSettings settings, Material surface, Material trim )
{
settings.RingMaterial = PartTool.Pick( settings.RingMaterial, surface );
settings.KeystoneMaterial = PartTool.Pick( settings.KeystoneMaterial, trim );
settings.PierMaterial = PartTool.Pick( settings.PierMaterial, surface );
settings.TrimMaterial = PartTool.Pick( settings.TrimMaterial, trim );
settings.WallMaterial = PartTool.Pick( settings.WallMaterial, surface );
}
}
/// <summary>
/// A classical church entrance: paired pilasters, a stepped entablature, a door with a
/// broken pediment, a lunette window above and pinnacles on the corners.
/// The front is to the right of the drawn line.
/// </summary>
[ToolCategory( "Arches & Vaults" )]
[Title( "Portal" ), Icon( "temple_buddhist" )]
public sealed class PortalEditor( PrimitiveTool tool ) : LineEditor( tool )
{
[Property, Group( "Portal" ), Range( 64, 2048, slider: false ), WideMode]
public float Height { get; set; } = 256.0f;
[Property, Group( "Portal" ), Range( 8, 256, slider: false ), WideMode]
public float Thickness { get; set; } = 32.0f;
[Property, Group( "Door" ), Title( "Door Width" ), Range( 16, 512, slider: false ), WideMode]
public float DoorWidth { get; set; } = 72.0f;
[Property, Group( "Door" ), Title( "Door Height" ), Range( 16, 1024, slider: false ), WideMode]
public float DoorHeight { get; set; } = 144.0f;
[Property, Group( "Door" ), WideMode]
public bool Pediment { get; set; } = true;
[Property, Group( "Columns" ), Title( "Column Pairs" ), Range( 0, 8, slider: false ), WideMode]
public int ColumnPairs { get; set; } = 2;
[Property, Group( "Columns" ), Title( "Column Width" ), Range( 4, 128, slider: false ), WideMode]
public float ColumnWidth { get; set; } = 28.0f;
[Property, Group( "Columns" ), Title( "Column Depth" ), Range( 1, 64, slider: false ), WideMode]
public float ColumnDepth { get; set; } = 10.0f;
[Property, Group( "Top" ), Title( "Cornice Height" ), Range( 4, 128, slider: false ), WideMode]
public float CorniceHeight { get; set; } = 28.0f;
[Property, Group( "Top" ), Title( "Lunette" ), WideMode, Description( "A half round window over the cornice." )]
public bool Lunette { get; set; } = true;
[Property, ShowIf( nameof( Lunette ), true ), Group( "Top" ), Title( "Lunette Width" ), Range( 16, 512, slider: false ), WideMode]
public float LunetteWidth { get; set; } = 96.0f;
[Property, Group( "Top" ), WideMode]
public bool Pinnacles { get; set; } = true;
[Property, Group( "Steps" ), Title( "Round Steps" ), Range( 0, 16, slider: false ), WideMode, Description( "Half round steps in front of the door." )]
public int RoundSteps { get; set; } = 3;
protected override void BuildMesh( MeshKit kit )
{
var length = Length;
var center = length * 0.5f;
var width = MathF.Min( DoorWidth, length - 8.0f );
var head = MathF.Min( DoorHeight, Height - 8.0f );
var x0 = center - width * 0.5f;
var x1 = center + width * 0.5f;
var front = -Thickness * 0.5f;
kit.Frame = LineFrame;
kit.WallWithOpenings( length, Thickness, Height, [(x0, x1)], 0.0f, head );
kit.Material = TrimMaterial;
// Stepped plinth.
kit.Box( new Vector3( -4.0f, front - ColumnDepth - 4.0f, 0.0f ), new Vector3( x0, front, 16.0f ) );
kit.Box( new Vector3( x1, front - ColumnDepth - 4.0f, 0.0f ), new Vector3( length + 4.0f, front, 16.0f ) );
// Door surround.
kit.OpeningFrame( x0 - 8.0f, x1 + 8.0f, 0.0f, head + 8.0f, 8.0f, Thickness * 0.5f + 3.0f, false );
if ( Pediment )
{
var overhang = width * 0.5f + 20.0f;
var z = head + 12.0f;
kit.Box( new Vector3( center - overhang, front - 8.0f, z ), new Vector3( center + overhang, front, z + 6.0f ) );
foreach ( var sign in new[] { -1.0f, 1.0f } )
{
var eave = new Vector3( center + overhang * sign, front - 4.0f, z + 6.0f );
var apex = new Vector3( center + overhang * 0.25f * sign, front - 4.0f, z + 6.0f + overhang * 0.38f );
kit.Beam( eave, apex, 8.0f, 5.0f );
}
}
// Column pairs beside the door.
for ( var i = 0; i < ColumnPairs; i++ )
{
var side = MathF.Max( (x0 - 12.0f) / MathF.Max( ColumnPairs, 1 ), ColumnWidth );
var a = x0 - 16.0f - ColumnWidth - side * i;
var b = x1 + 16.0f + side * i;
foreach ( var x in new[] { a, b } )
{
if ( x < 0.0f || x + ColumnWidth > length ) continue;
kit.Box( new Vector3( x, front - ColumnDepth, 16.0f ), new Vector3( x + ColumnWidth, front, Height - CorniceHeight ) );
kit.Box( new Vector3( x - 3.0f, front - ColumnDepth - 3.0f, 16.0f ), new Vector3( x + ColumnWidth + 3.0f, front, 28.0f ) );
kit.Box( new Vector3( x - 3.0f, front - ColumnDepth - 3.0f, Height - CorniceHeight - 10.0f ), new Vector3( x + ColumnWidth + 3.0f, front, Height - CorniceHeight ) );
}
}
// Entablature in three steps, each standing further out.
var bottom = Height - CorniceHeight;
kit.Box( new Vector3( -4.0f, front - ColumnDepth - 2.0f, bottom ), new Vector3( length + 4.0f, front, bottom + CorniceHeight * 0.4f ) );
kit.Box( new Vector3( -8.0f, front - ColumnDepth - 6.0f, bottom + CorniceHeight * 0.4f ), new Vector3( length + 8.0f, front, bottom + CorniceHeight * 0.7f ) );
kit.Box( new Vector3( -14.0f, front - ColumnDepth - 12.0f, bottom + CorniceHeight * 0.7f ), new Vector3( length + 14.0f, Thickness * 0.5f + 4.0f, Height ) );
if ( Lunette ) BuildLunette( kit, center, front );
if ( Pinnacles )
{
foreach ( var x in new[] { 4.0f, length - 4.0f } )
kit.Lathe( new Vector3( x, front - 4.0f, Height ), [new( 0, 0 ), new( 9, 0 ), new( 9, 8 ), new( 6, 10 ), new( 1.5f, 44 ), new( 3, 46 ), new( 4, 49 ), new( 3, 52 ), new( 0, 54 )], 8 );
}
if ( RoundSteps > 0 )
{
kit.Material = TrimMaterial;
var baseRadius = width * 0.5f + 24.0f;
for ( var i = 0; i < RoundSteps; i++ )
{
var radius = baseRadius + 16.0f * (RoundSteps - 1 - i);
var z = 5.0f * (i + 1);
kit.Arc( new Vector3( center, front, 0.0f ), 0.0f, radius, 180.0f, 360.0f, 0.0f, z, 16 );
}
}
}
void BuildLunette( MeshKit kit, float center, float front )
{
var half = MathF.Max( LunetteWidth * 0.5f, 12.0f );
var band = MathF.Min( 8.0f, half * 0.4f );
var spring = Height + 4.0f;
var y = front + 2.0f;
var outer = MeshKit.ArchCurve( center - half, center + half, spring, half, ArchStyle.Round, 24 );
var inner = MeshKit.ArchCurve( center - half + band, center + half - band, spring, half - band, ArchStyle.Round, 24 );
kit.Material = SurfaceMaterial;
kit.Profile( [new( center - half - 16.0f, Height ), new( center + half + 16.0f, Height ), new( center + half + 16.0f, spring + 8.0f ), .. outer.Reverse(), new( center - half - 16.0f, spring + 8.0f )], y, y + 8.0f );
kit.Material = TrimMaterial;
kit.Band( inner, outer, y - 3.0f, y + 2.0f );
for ( var spoke = 1; spoke < 6; spoke++ )
{
var angle = MathF.PI * spoke / 6.0f;
var end = new Vector3( center - MathF.Cos( angle ) * (half - band), y - 1.0f, spring + MathF.Sin( angle ) * (half - band) );
kit.Beam( new Vector3( center, y - 1.0f, spring ), end, 1.5f, 1.5f );
}
kit.Box( new Vector3( center - half + band, y - 2.0f, spring - 1.0f ), new Vector3( center + half - band, y, spring + 1.0f ) );
}
}
/// <summary>
/// Round windows standing upright: a rose window with spokes and a ring, or a half round lunette.
/// </summary>
[ToolCategory( "Doors & Windows" )]
[Title( "Rose Window" ), Icon( "brightness_7" )]
public sealed class RoseWindowEditor( PrimitiveTool tool ) : StampEditor( tool )
{
public override IReadOnlySet<string> UnusedOptions { get; } = new HashSet<string> { "Surface" };
[Property, Group( "Rose Window" ), Range( 4, 1024, slider: false ), WideMode]
public float Radius { get; set; } = 48.0f;
[Property, Group( "Rose Window" ), Title( "Height Above Ground" ), Range( 0, 4096, slider: false ), WideMode]
public float HeightAboveGround { get; set; } = 128.0f;
[Property, Group( "Rose Window" ), Range( 3, 32, slider: false ), WideMode]
public int Spokes { get; set; } = 12;
[Property, Group( "Rose Window" ), Title( "Half Round" ), WideMode]
public bool HalfRound { get; set; } = false;
[Property, Group( "Rose Window" ), Title( "Frame Width" ), Range( 0.5f, 64, slider: false ), WideMode]
public float FrameWidth { get; set; } = 6.0f;
[Property, Group( "Rose Window" ), Range( 0.5f, 64, slider: false ), WideMode]
public float Depth { get; set; } = 3.0f;
[Property, Group( "Materials" ), Title( "Glass" ), WideMode]
public Material GlassMaterial { get; set; }
protected override void Stamp( MeshKit kit, Transform frame, Random random )
{
var center = new Vector3( 0.0f, 0.0f, HeightAboveGround );
var inner = MathF.Max( Radius - FrameWidth, 1.0f );
var from = HalfRound ? 0.0f : -180.0f;
var segments = HalfRound ? 24 : 48;
Vector2 At( float degrees, float radius ) => new(
MathF.Cos( degrees.DegreeToRadian() ) * radius,
HeightAboveGround + MathF.Sin( degrees.DegreeToRadian() ) * radius );
kit.Material = TrimMaterial;
// A full ring has no ends, its two halves are bands that close on each other.
var angles = Enumerable.Range( 0, segments + 1 ).Select( i => from + (180.0f - from) * i / segments ).ToArray();
if ( HalfRound )
{
kit.Band( [.. angles.Reverse().Select( a => At( a, inner ) )], [.. angles.Reverse().Select( a => At( a, Radius ) )], -Depth, Depth );
}
else
{
var halves = new[] { angles[..(segments / 2 + 1)], angles[(segments / 2)..] };
foreach ( var half in halves )
kit.Band( [.. half.Reverse().Select( a => At( a, inner ) )], [.. half.Reverse().Select( a => At( a, Radius ) )], -Depth, Depth );
}
var hub = inner * 0.18f;
var sweep = 180.0f - from;
for ( var s = 0; s < Spokes; s++ )
{
var degrees = from + sweep * (s + (HalfRound ? 1.0f : 0.0f)) / (HalfRound ? Spokes + 1f : Spokes);
var tip = At( degrees, inner );
var root = At( degrees, hub );
kit.Beam( new Vector3( root.x, 0.0f, root.y ), new Vector3( tip.x, 0.0f, tip.y ), Depth * 1.6f, MathF.Max( FrameWidth * 0.4f, 1.5f ) );
}
if ( HalfRound )
kit.Box( new Vector3( -Radius, -Depth, HeightAboveGround - FrameWidth ), new Vector3( Radius, Depth, HeightAboveGround ) );
kit.Material = GlassMaterial ?? TrimMaterial;
var pane = new List<Vector2>();
for ( var i = 0; i <= segments; i++ )
pane.Add( At( from + sweep * i / segments, inner ) );
kit.Profile( pane, -0.2f, 0.2f );
}
}