Editor primitive tool that draws a floor or ceiling slab from a closed polygon path. Docks points onto nearby wall corners (foot or top), offsets the outline in or out by the wall margin to fit inside, on centre of, or outside the walls, and builds the top, bottom and side faces at the chosen thickness and elevation.
using System.Runtime.InteropServices;
namespace AdvancedMappingTools;
public enum FloorFit
{
[Title( "Inside The Walls" )] Inside,
[Title( "Wall Centre" )] Center,
[Title( "Outside The Walls" )] Outside
}
/// <summary>
/// Draw the outline of a floor or ceiling slab, straight or curved. Points near the corners of
/// finished walls land on them, at the foot for a floor and at the top for a ceiling, and the
/// slab then fits inside the walls, on their centre or over them.
/// </summary>
[ToolCategory( "Walls & Floors" )]
[Title( "Floor" ), Icon( "grid_on" )]
public sealed class FloorEditor( PrimitiveTool tool ) : PathEditor( tool )
{
protected override int MinPoints => 3;
protected override bool AlwaysClosed => true;
[Property, Range( 0, 512, slider: false ), WideMode, Description( "Slab thickness below the drawn outline. Zero makes a single face." )]
public float Thickness { get; set; } = 8.0f;
[Property, Range( -4096, 4096, slider: false ), WideMode, Description( "Raises the slab above the drawn outline." )]
public float Elevation { get; set; } = 0.0f;
[Property, Group( "Walls" ), WideMode, Description( "Where a slab drawn on wall corners ends: on the inner face, on the centre of the walls, or on their outer face." )]
public FloorFit Fit { get; set; } = FloorFit.Inside;
[Property, Advanced, Group( "Walls" ), Title( "Wall Margin" ), Range( 0, 256, slider: false ), WideMode, Description( "Half the wall, set when the points snap onto wall corners. Fit moves the outline in or out by this much." )]
public float WallMargin { get; set; } = 0.0f;
protected override bool Connects => true;
const float CornerReach = 16.0f;
protected override Vector3? UnderCursor( Vector3 point ) => WallCornerUnderCursor( point, CornerReach ) ?? base.UnderCursor( point );
/// <summary>
/// A point near a wall corner lands on it, at its foot or its top, whichever the click was nearer to.
/// </summary>
protected override Vector3 Dock( Vector3 point )
{
var docked = base.Dock( point );
if ( !ConnectFinished || SceneEditorSession.Active is null ) return docked;
var corner = WallTops.Nearest( SceneEditorSession.Active.Scene, docked, DockRadius + CornerReach );
if ( Points.Count == 0 ) WallMargin = corner?.Half ?? 0.0f;
if ( corner is not { } found ) return docked;
WallMargin = MathF.Max( WallMargin, found.Half );
// The click itself says which: docking may have pulled it onto a corner of the wall mesh.
var top = MathF.Abs( point.z - found.Top.z ) < MathF.Abs( point.z - found.Foot.z );
var spot = top ? found.Top : found.Foot;
DrawLabel( spot, top ? "Wall top" : "Wall foot", Color.Green );
return spot;
}
/// <summary>
/// The outline moved out (positive) or in (negative) by a distance, corner by corner.
/// </summary>
static List<Vector3> Offset( List<Vector3> ring, float distance )
{
if ( MathF.Abs( distance ) < 0.001f ) return ring;
var result = new List<Vector3>( ring.Count );
var limit = MathF.Abs( distance ) * 4.0f;
for ( var i = 0; i < ring.Count; i++ )
{
var before = ring[(i + ring.Count - 1) % ring.Count];
var at = ring[i];
var after = ring[(i + 1) % ring.Count];
// Outward normals of the two edges of a counter clockwise outline.
var a = (at - before).WithZ( 0.0f ).Normal;
var b = (after - at).WithZ( 0.0f ).Normal;
var na = new Vector3( a.y, -a.x, 0.0f );
var nb = new Vector3( b.y, -b.x, 0.0f );
var bisector = (na + nb).Normal;
var cos = bisector.Dot( na );
var length = cos > 0.05f ? MathF.Min( distance / cos, limit ) : distance;
result.Add( at + bisector * (MathF.Abs( length ) * MathF.Sign( distance )) );
}
return result;
}
protected override bool IsShapeValid
{
get
{
var ring = SamplePath();
return ring.Count >= 3 && Mesh.TriangulatePolygon( CollectionsMarshal.AsSpan( ring ) ).Length >= 3;
}
}
protected override void BuildMesh( MeshKit kit )
{
var ring = SamplePath();
if ( SignedArea( ring ) < 0.0f ) ring.Reverse();
var margin = MathF.Max( WallMargin, 0.0f );
ring = Offset( ring, Fit switch { FloorFit.Inside => -margin, FloorFit.Outside => margin, _ => 0.0f } );
var top = Vector3.Up * Elevation;
var bottom = Vector3.Up * (Elevation - Thickness);
kit.Face( [.. ring.Select( x => x + top )] );
if ( Thickness <= 0.0f ) return;
kit.Material = TrimMaterial;
kit.Face( [.. ring.AsEnumerable().Reverse().Select( x => x + bottom )] );
for ( var i = 0; i < ring.Count; i++ )
{
var a = ring[i];
var b = ring[(i + 1) % ring.Count];
kit.Face( a + bottom, b + bottom, b + top, a + top );
}
}
}