Editor side procedural wall generator. ConnectedWallSettings holds the wall line, height, thickness, alignment, openings and materials; ConnectedWallGenerator builds wall panels around openings, drops in doors and windows, adds plinths and junction prisms from the section data. The WallLinker static class re-solves connections between walls in a scene, converts solved corners back to local space, snaps new walls onto existing ones and can weld walls into one mesh.
namespace AdvancedMappingTools;
/// <summary>
/// An opening kept as a value, so a wall can be rebuilt or reconnected without losing it.
/// </summary>
public sealed class WallOpeningSpec
{
[Property, ResourceType( "vmat" )]
public string FrameMaterial { get; set; }
[Property, ResourceType( "vmat" )]
public string GlassMaterial { get; set; }
[Property, ResourceType( "vmat" )]
public string LeafMaterial { get; set; }
[Property]
public bool Enabled { get; set; } = true;
[Property]
public bool Frame { get; set; } = true;
[Property]
public bool Glass { get; set; } = true;
[Property]
public bool Leaf { get; set; } = true;
[Property]
public float Along { get; set; }
[Property]
public float Width { get; set; } = 48.0f;
[Property]
public float Sill { get; set; } = 36.0f;
[Property]
public float Height { get; set; } = 56.0f;
[Property]
public bool Window { get; set; } = true;
/// <summary>
/// A door with frame and leaf stands in the opening.
/// </summary>
[Property]
public bool Door { get; set; }
}
public sealed partial class ConnectedWallSettings : GeneratorSettings, IProfiledFrameSettings
{
/// <summary>
/// The line of the wall, in the space of the root object.
/// </summary>
public PathCurve Path { get; set; } = PathCurve.FromPoints( [Vector3.Zero, new Vector3( 256.0f, 0.0f, 0.0f )] );
/// <summary>
/// How the wall is cut where it meets others, set by the connection engine.
/// </summary>
public List<WallSection> Sections { get; set; } = [];
/// <summary>
/// Outlines of the pieces that fill the middle of a junction.
/// </summary>
public List<List<Vector3>> Hubs { get; set; } = [];
/// <summary>
/// The walls this one is connected to, by object id.
/// </summary>
public List<Guid> Neighbours { get; set; } = [];
[Property, Group( "Placed Openings" ), WideMode]
public List<WallOpeningSpec> PlacedOpenings { get; set; } = [];
[Property, Group( "Wall" ), Range( 1, 4096, slider: false ), WideMode]
public float Height { get; set; } = 128.0f;
[Property, Group( "Wall" ), Range( 0.25f, 512, slider: false ), WideMode]
public float Thickness { get; set; } = 8.0f;
[Property, Group( "Wall" ), WideMode, Description( "Where the wall sits relative to its line." )]
public WallAlignment Alignment { get; set; } = WallAlignment.Center;
[Property, Group( "Connection" ), Title( "Connect" ), WideMode, Description( "Off leaves this wall out of every connection." )]
public bool Connected { get; set; } = true;
[Property, Group( "Connection" ), Title( "Join" ), WideMode, Description( "How two ends meet at a corner. Overlap leaves both square." )]
public WallJoinStyle Join { get; set; } = WallJoinStyle.Miter;
[Property, Group( "Openings" ), Range( 0, 64, slider: false ), WideMode, Description( "Openings spread evenly along the wall." )]
public int Openings { get; set; } = 0;
[Property, HideIf( nameof( Openings ), 0 ), Group( "Openings" ), Title( "Opening Width" ), Range( 4, 1024, slider: false ), WideMode]
public float OpeningWidth { get; set; } = 48.0f;
[Property, HideIf( nameof( Openings ), 0 ), Group( "Openings" ), Title( "Sill Height" ), Range( 0, 1024, slider: false ), WideMode]
public float SillHeight { get; set; } = 36.0f;
[Property, HideIf( nameof( Openings ), 0 ), Group( "Openings" ), Title( "Opening Height" ), Range( 4, 1024, slider: false ), WideMode]
public float OpeningHeight { get; set; } = 56.0f;
[Property, HideIf( nameof( Openings ), 0 ), Group( "Openings" ), WideMode, Description( "A window frame with glass in every opening." )]
public bool Windows { get; set; } = true;
[Property, Group( "Materials" ), ResourceType( "vmat" ), WideMode]
public string Material { get; set; }
[Property, ShowIf( nameof( Windows ), true ), Group( "Materials" ), Title( "Windows" ), ResourceType( "vmat" ), WideMode]
public string WindowMaterial { get; set; }
[Property, ShowIf( nameof( Windows ), true ), Group( "Materials" ), Title( "Glass" ), ResourceType( "vmat" ), WideMode]
public string GlassMaterial { get; set; }
[Property, ShowIf( nameof( Windows ), true ), Group( "Openings" ), Title( "Profiled Frames" ), WideMode, Description( "Profiled window frames with real corner joints, glass seated in them and glazing bars. Off: the plain bars of earlier versions." )]
public bool ProfiledFrames { get; set; }
[Property, ShowIf( nameof( ProfiledFrames ), true ), Group( "Openings" ), Title( "Frame Corners" ), WideMode]
public FrameCorner FrameCorner { get; set; } = FrameCorner.Mitred;
[Property, ShowIf( nameof( ProfiledFrames ), true ), Group( "Openings" ), Title( "Frame Profile" ), WideMode]
public FrameProfile FrameProfile { get; set; } = FrameProfile.Chamfered;
[Property, ShowIf( nameof( ProfiledFrames ), true ), Group( "Openings" ), Title( "Frame Detail" ), WideMode]
public WindowDetail FrameDetail { get; set; } = WindowDetail.Standard;
[Property, ShowIf( nameof( ProfiledFrames ), true ), Group( "Openings" ), Title( "Glazing" ), WideMode]
public Glazing FrameGlazing { get; set; } = Glazing.Double;
public (float Left, float Right) Faces() => Alignment switch
{
WallAlignment.Inside => (Thickness, 0.0f),
WallAlignment.Outside => (0.0f, -Thickness),
_ => (Thickness * 0.5f, -Thickness * 0.5f)
};
}
/// <summary>
/// A wall along a line, as independent panels around its openings. Where it meets other walls
/// it ends on the corners the connection engine worked out. It only reads them, so a
/// wall can be rebuilt on its own at any time.
/// </summary>
public sealed partial class ConnectedWallGenerator : IPartGenerator
{
public const string GeneratorId = "connectedwall";
const string WallCategory = "wall";
const float MinimumPanel = 0.05f;
const float PierClearance = 4.0f;
public string Id => GeneratorId;
public string Title => "Wall";
public Type SettingsType => typeof( ConnectedWallSettings );
public IReadOnlyList<string> SeedNames => [WallCategory, FrameGenerator.FrameCategory, FrameGenerator.GlassCategory];
/// <summary>
/// The sampled line of a wall.
/// </summary>
public static PathTrack Line( ConnectedWallSettings s ) => s.Path.Track( 1024.0f );
public PartPlan Generate( GeneratorSettings settings, GenContext context )
{
var s = (ConnectedWallSettings)settings;
var root = context.Group( "root", "Wall", "Building", WallCategory );
if ( s.Path is null || s.Path.SegmentCount == 0 )
return root;
var track = Line( s );
var shape = new WallShape( s, track );
var sections = s.Sections.Count > 0 ? s.Sections : [new WallSection
{
From = 0.0f,
To = track.Length
}
];
var z0 = track.Frames[0].Position.z;
var z1 = z0 + s.Height;
var material = context.Material( s.Material );
for ( var i = 0; i < sections.Count; i++ )
root.Add( Section( s, context, shape, sections[i], i, z0, z1, material ) );
for ( var i = 0; i < s.Hubs.Count; i++ )
{
var outline = s.Hubs[i];
root.Add( context.Solid( $"junction{i:00}", "Junction", "WallJunction", WallCategory, material, kit => kit.Prism( outline, z0, z1 ) ) );
}
if ( s.Plinth )
AddPlinth( root, s, context, track, sections, z0 );
WallDetailMaterials.Apply( root, s, context, track );
return root;
}
/// <summary>Where the wall panels begin: on top of the plinth when there is one.</summary>
static float Floor( ConnectedWallSettings s, float z0 ) => s.Plinth ? z0 + MathF.Min( Math.Clamp( s.PlinthHeight, 1, 256 ), s.Height ) : z0;
static PartPlan Section( ConnectedWallSettings s, GenContext context, WallShape shape, WallSection section, int index, float z0, float z1, Material material )
{
var id = $"section{index:00}";
var group = context.Group( id, $"Wall Section {index + 1}", "Wall", WallCategory );
var windowMaterial = context.Material( s.WindowMaterial );
var glass = context.Material( s.GlassMaterial );
var cursor = section.From;
var number = 0;
// With a plinth the wall stands on it instead of reaching down through it.
var floor = Floor( s, z0 );
void Panel( string name, string label, float from, float to, float low, float high )
{
low = MathF.Max( low, floor );
if ( to - from < MinimumPanel || high - low < MinimumPanel )
return;
var piece = 0;
foreach ( var(start, end)in shape.Pieces( from, to ) )
{
var partId = $"{id}/{name}{(piece > 0 ? $"_{piece}" : "")}";
group.Add( context.Solid( partId, label, "WallPanel", WallCategory, material, kit => kit.Prism( shape.Outline( section, start, end ), low, high ) ) );
piece++;
}
}
var openings = OpeningsIn( s, section ).ToArray();
if ( openings.Where( ( a, i ) => openings.Skip( i + 1 ).Any( b => MathF.Abs( a.Along - b.Along ) < (a.Width + b.Width) * .5f - .01f ) ).Any() )
return LayeredSection( s, context, shape, section, index, z0, z1, material, openings );
foreach ( var opening in openings )
{
var half = opening.Width * 0.5f;
// An opening stays clear of the ends of its section, a corner needs its pier.
var from = MathF.Max( opening.Along - half, section.From + PierClearance );
var to = MathF.Min( opening.Along + half, section.To - PierClearance );
if ( to - from < 1.0f )
continue;
Panel( $"pier{number}", $"Pier {number + 1}", cursor, from, z0, z1 );
Panel( $"below{number}", $"Below Opening {number + 1}", from, to, z0, z0 + opening.Sill );
Panel( $"above{number}", $"Above Opening {number + 1}", from, to, z0 + opening.Sill + opening.Height, z1 );
if ( opening.Door )
group.Add( Door( s, shape, context, $"{id}/door{number}", number, from, to, z0 + opening.Sill, MathF.Min( opening.Height, z1 - z0 - opening.Sill ), windowMaterial, material, opening ) );
else if ( opening.Window && s.Windows )
group.Add( Window( s, context, shape, $"{id}/window{number}", number, from, to, z0 + opening.Sill, MathF.Min( opening.Height, z1 - z0 - opening.Sill ), windowMaterial, glass, opening ) );
cursor = to;
number++;
}
Panel( $"pier{number}", $"Pier {number + 1}", cursor, section.To, z0, z1 );
return group;
}
/// <summary>
/// A door or window is straight, a wall may bend. It spans the straight line between the two
/// sides of its opening, and is deep enough to fill the wall where the wall bows away.
/// </summary>
static (Vector3 Center, float Yaw, float Width, float Depth) Fit( ConnectedWallSettings s, WallShape shape, float from, float to, float sill )
{
var middle = (shape.Left + shape.Right) * 0.5f;
var start = shape.Track.PlaceAt( from, middle );
var end = shape.Track.PlaceAt( to, middle );
var center = (start + end) * 0.5f;
var chord = (end - start).WithZ( 0.0f );
var bow = shape.Track.PlaceAt( (from + to) * 0.5f, middle ).WithZ( 0.0f ).Distance( center.WithZ( 0.0f ) );
return (center.WithZ( sill ), MathF.Atan2( chord.y, chord.x ).RadianToDegree(), chord.Length, s.Thickness + 2.0f + bow * 2.0f);
}
static PartPlan Door( ConnectedWallSettings s, WallShape shape, GenContext context, string id, int number, float from, float to, float sill, float height, Material frameMaterial, Material leaf, WallOpeningSpec opening )
{
var(center, yaw, width, depth) = Fit( s, shape, from, to, sill );
var plan = DoorGenerator.Generate( new DoorRequest( id, $"Door {number + 1}", width, height, depth, 3.0f, context.Material( opening.FrameMaterial ?? s.WindowMaterial ), context.Material( opening.LeafMaterial ?? s.Material ) ), context ).At( center, yaw );
plan.Children.RemoveAll( p => (p.Kind == "FrameBar" && !opening.Frame) || (p.Kind is "DoorLeaf" or "Handle" && !opening.Leaf) );
return plan;
}
static PartPlan Window( ConnectedWallSettings s, GenContext context, WallShape shape, string id, int number, float from, float to, float sill, float height, Material trim, Material glass, WallOpeningSpec opening )
{
var(center, yaw, width, depth) = Fit( s, shape, from, to, sill );
var plan = FrameGenerator.Generate( new FrameRequest( id, $"Window {number + 1}", width, height, depth, 3.0f, FrameStyle.Wooden, 1, 1, opening.Frame, opening.Glass, 0.0f, s.Variation, context.Material( opening.FrameMaterial ?? s.WindowMaterial ), context.Material( opening.GlassMaterial ?? s.GlassMaterial ), FrameGenerator.Spec( s ) ), context ).At( center, yaw );
if ( !opening.Frame )
plan.Children.RemoveAll( p => p.Kind == "Frame" );
return plan;
}
/// <summary>
/// The openings of a section: placed ones first, then the evenly spread ones.
/// </summary>
static IEnumerable<WallOpeningSpec> OpeningsIn( ConnectedWallSettings s, WallSection section )
{
if ( !s.UseOpenings )
return[];
var all = s.PlacedOpenings.Where( x => x.Enabled && x.Along >= section.From && x.Along <= section.To ).ToList();
var length = section.To - section.From;
for ( var row = 0; row < (s.OpeningGrid ? Math.Clamp( s.OpeningRows, 1, 16 ) : 1); row++ )
for ( var i = 0; i < Math.Clamp( s.Openings, 0, 64 ); i++ )
{
var window = !s.OpeningGrid || s.AutomaticOpeningKind == WallOpeningKind.Window;
var sill = (window ? s.SillHeight : 0) + row * Math.Clamp( s.OpeningRowSpacing, 8, 1024 );
if ( sill + s.OpeningHeight > s.Height || sill < 0 )
continue;
var item = new WallOpeningSpec
{
Along = section.From + length * (i + .5f) / s.Openings,
Width = s.OpeningWidth,
Sill = sill,
Height = s.OpeningHeight,
Window = window,
Door = s.OpeningGrid && s.AutomaticOpeningKind == WallOpeningKind.Door
};
if ( all.Any( o => MathF.Abs( o.Along - item.Along ) < (o.Width + item.Width) * .5f && o.Sill < item.Sill + item.Height && o.Sill + o.Height > item.Sill ) )
continue;
all.Add( item );
}
return all.OrderBy( x => x.Along );
}
/// <summary>
/// The footprint of a wall, addressed by distance along its line.
/// </summary>
public sealed class WallShape
{
public PathTrack Track { get; }
public float Left { get; }
public float Right { get; }
readonly Vector2[] _leftFace;
readonly Vector2[] _rightFace;
public WallShape( ConnectedWallSettings s, PathTrack track, float expansion = 0 )
{
Track = track;
(Left, Right) = s.Faces();
Left += expansion;
Right -= expansion;
_leftFace = [..Enumerable.Range( 0, track.Count ).Select( i => Flat( track.Place( i, Left ) ) )];
_rightFace = [..Enumerable.Range( 0, track.Count ).Select( i => Flat( track.Place( i, Right ) ) )];
}
static Vector2 Flat( Vector3 v ) => new( v.x, v.y );
static Vector3 Lift( Vector2 v ) => new( v.x, v.y, 0.0f );
/// <summary>
/// How far along the wall the point of a face lies.
/// </summary>
float AlongFace( Vector2[] face, Vector3 point )
{
var target = Flat( point );
var best = (Distance: float.MaxValue, Along: 0.0f);
for ( var i = 0; i + 1 < face.Length; i++ )
{
var a = face[i];
var d = face[i + 1] - a;
var t = d.LengthSquared > 0.0f ? (target - a).Dot( d ) / d.LengthSquared : 0.0f;
// The first and last piece carry on: a mitre ends past the line of the wall.
var low = i == 0 ? -8.0f : 0.0f;
var high = i == face.Length - 2 ? 8.0f : 1.0f;
t = Math.Clamp( t, low, high );
var distance = (a + d * t).Distance( target );
if ( distance < best.Distance )
best = (distance, Track.Frames[i].Distance + (Track.Frames[i + 1].Distance - Track.Frames[i].Distance) * t);
}
return best.Along;
}
List<Vector3> Chain( Vector2[] face, float side, float from, float to, Vector3? start, Vector3? end, IReadOnlyList<Vector3> stops )
{
var first = start is Vector3 s ? AlongFace( face, s ) : from;
var last = end is Vector3 e ? AlongFace( face, e ) : to;
var points = new List<(float Along, Vector3 Point)> { (first, start ?? Lift( Flat( Track.PlaceAt( from, side ) ) )) };
// The very ends of the line are no corners: a face that runs on past them stays straight.
for ( var i = 1; i < Track.Count - 1; i++ )
{
var distance = Track.Frames[i].Distance;
if ( distance > first + 0.01f && distance < last - 0.01f )
points.Add( (distance, Lift( face[i] )) );
}
foreach ( var stop in stops )
{
var distance = AlongFace( face, stop );
if ( distance > first + 0.01f && distance < last - 0.01f )
points.Add( (distance, stop) );
}
points.Add( (last, end ?? Lift( Flat( Track.PlaceAt( to, side ) ) )) );
return [.. points.OrderBy( x => x.Along ).Select( x => x.Point )];
}
/// <summary>
/// A stretch of a wall as outlines. All the way around a closed wall the outline would be a
/// ring, so that stretch comes in two halves.
/// </summary>
public IEnumerable<(float From, float To)> Pieces( float from, float to )
{
if ( !Track.Closed || to - from < Track.Length * 0.75f )
{
yield return (from, to);
yield break;
}
var middle = (from + to) * 0.5f;
yield return (from, middle);
yield return (middle, to);
}
/// <summary>
/// The outline between two distances. At the ends of a section it uses the solved corners.
/// </summary>
public List<Vector3> Outline( WallSection section, float from, float to )
{
var atStart = from <= section.From + 0.01f;
var atEnd = to >= section.To - 0.01f;
var right = Chain( _rightFace, Right, from, to, atStart ? section.FromRight : null, atEnd ? section.ToRight : null, section.RightStops );
var left = Chain( _leftFace, Left, from, to, atStart ? section.FromLeft : null, atEnd ? section.ToLeft : null, section.LeftStops );
left.Reverse();
if ( atEnd ) right.AddRange( section.ToEdge );
right.AddRange( left );
if ( atStart ) right.AddRange( section.FromEdge.AsEnumerable().Reverse() );
return right;
}
}
}
/// <summary>
/// Keeps the walls of a scene connected. Every wall stays its own object, a change only
/// touches settings and each wall then regenerates itself.
/// </summary>
public static class WallLinker
{
static readonly ConnectedWallGenerator Generator = new();
public static List<GameObject> Walls( Scene scene ) =>
[.. scene.GetAllComponents<AmtPart>()
.Where( x => PartReconciler.IsAlive( x ) && x.Generator == ConnectedWallGenerator.GeneratorId && !string.IsNullOrEmpty( x.Settings ) )
.Select( x => x.GameObject )];
static ConnectedWallSettings Read( GameObject wall ) => (ConnectedWallSettings)PartService.Read( wall ).Settings;
/// <summary>
/// The walls a change to this one may touch. For undo.
/// </summary>
public static List<GameObject> Network( GameObject wall ) => Walls( wall.Scene );
/// <summary>
/// Work out all connections again and rebuild the walls whose ends changed. Parts changed by
/// hand keep their changes, walls that were not touched are not rebuilt at all.
/// </summary>
public static List<GameObject> Reconnect( Scene scene )
{
// A fixed order: which of two equal walls runs through must not change from run to run.
var walls = Walls( scene ).OrderBy( x => x.Id ).ToList();
var settings = walls.Select( Read ).ToList();
var linked = Enumerable.Range( 0, walls.Count ).Where( i => settings[i].Connected ).ToList();
var inputs = linked.Select( i =>
{
var s = settings[i];
var world = walls[i].WorldTransform;
var(left, right) = s.Faces();
var line = ConnectedWallGenerator.Line( s );
var path = line.Frames.Take( s.Path.Closed ? line.Count - 1 : line.Count ).Select( x => world.PointToWorld( x.Position ) ).ToList();
// The solver works in world units, settings and generated geometry in local units.
var tangent = (line.Frames[1].Position - line.Frames[0].Position).WithZ( 0 ).Normal;
var normal = new Vector3( -tangent.y, tangent.x, 0 );
var worldTangent = (world.PointToWorld( tangent ) - world.Position).WithZ( 0 ).Normal;
var worldNormal = (world.PointToWorld( normal ) - world.Position).WithZ( 0 );
var widthScale = worldNormal.Dot( new Vector3( -worldTangent.y, worldTangent.x, 0 ) );
var a = left * widthScale;
var b = right * widthScale;
var endZ = world.PointToWorld( line.Frames[0].Position + Vector3.Up * s.Height ).z;
return new WallInput( path, s.Path.Closed, MathF.Max( a, b ), MathF.Min( a, b ), s.Join, MathF.Min( path[0].z, endZ ), MathF.Max( path[0].z, endZ ) );
} ).ToList();
var solved = WallNetwork.Solve( inputs );
var changed = new List<GameObject>();
for ( var n = 0; n < linked.Count; n++ )
{
var i = linked[n];
var wall = walls[i];
var s = settings[i];
var solution = solved[n];
var world = wall.WorldTransform;
Vector3? Local( Vector3? point ) => point is Vector3 p ? world.PointToLocal( p ).WithZ( 0.0f ) : null;
var localLine = ConnectedWallGenerator.Line( s );
float LocalDistance( float distance )
{
var walked = 0.0f;
for ( var k = 1; k < localLine.Count; k++ )
{
var previous = localLine.Frames[k - 1];
var current = localLine.Frames[k];
var span = world.PointToWorld( previous.Position ).Distance( world.PointToWorld( current.Position ) );
if ( span <= 0.00001f )
continue;
if ( distance <= walked + span || k == localLine.Count - 1 )
return previous.Distance + (distance - walked) / span * (current.Distance - previous.Distance);
walked += span;
}
return 0;
}
var mirrored = world.Scale.x * world.Scale.y < 0;
var sections = solution.Sections.Select( x => new WallSection { From = LocalDistance( x.From ), To = LocalDistance( x.To ), FromLeft = Local( mirrored ? x.FromRight : x.FromLeft ), FromRight = Local( mirrored ? x.FromLeft : x.FromRight ), ToLeft = Local( mirrored ? x.ToRight : x.ToLeft ), ToRight = Local( mirrored ? x.ToLeft : x.ToRight ), LeftStops = [..(mirrored ? x.RightStops : x.LeftStops).Select( p => world.PointToLocal( p ).WithZ( 0.0f ) )], RightStops = [..(mirrored ? x.LeftStops : x.RightStops).Select( p => world.PointToLocal( p ).WithZ( 0.0f ) )], FromEdge = [..(mirrored ? x.FromEdge.AsEnumerable().Reverse() : x.FromEdge).Select( p => world.PointToLocal( p ).WithZ( 0.0f ) )], ToEdge = [..(mirrored ? x.ToEdge.AsEnumerable().Reverse() : x.ToEdge).Select( p => world.PointToLocal( p ).WithZ( 0.0f ) )] } ).ToList();
var hubs = solution.Hubs.Select( x => x.Select( p => world.PointToLocal( p ).WithZ( 0.0f ) ).ToList() ).ToList();
var neighbours = solution.Neighbours.Select( x => walls[linked[x]].Id ).OrderBy( x => x ).ToList();
string Cuts() => System.Text.Json.JsonSerializer.Serialize( s.Sections ) + System.Text.Json.JsonSerializer.Serialize( s.Hubs );
var before = Cuts();
s.Sections = sections;
s.Hubs = hubs;
s.Neighbours = neighbours;
if ( before == Cuts() )
{
PartReconciler.RootPart( wall ).Settings = GeneratorRegistry.Save( s );
continue;
}
PartService.Regenerate( wall, RegenerationOptions.Everything, true, s );
changed.Add( wall );
}
scene.ProcessDeletes();
return changed;
}
/// <summary>
/// Put a point on a wall line near it, on the end if one is close. That is what makes
/// a new wall connect: it starts or ends on the old one, at the height of its foot.
/// A click on a wall hits its top, a new wall must not start up there.
/// </summary>
public static Vector3 Snap( Scene scene, Vector3 point, float radius, bool stack = false ) => TrySnap( scene, point, radius, stack, out var result ) ? result : point;
public static bool TrySnap( Scene scene, Vector3 point, float radius, bool stack, out Vector3 snapped )
{
var flat = point.WithZ( 0.0f );
var best = float.MaxValue;
var result = point;
foreach ( var wall in Walls( scene ) )
{
var s = Read( wall );
var line = ConnectedWallGenerator.Line( s );
var world = wall.WorldTransform;
var reach = radius + s.Thickness * .5f * MathF.Max( MathF.Abs( world.Scale.x ), MathF.Abs( world.Scale.y ) );
var footZ = world.PointToWorld( line.Frames[0].Position ).z;
var endZ = world.PointToWorld( line.Frames[0].Position + Vector3.Up * s.Height ).z;
var topZ = MathF.Max( footZ, endZ );
footZ = MathF.Min( footZ, endZ );
// A click on a wall from above lands on its top too, and that has to stay a T junction.
// Only a tool that was told to stack looks at the top edge.
var plane = stack && MathF.Abs( point.z - topZ ) < MathF.Abs( point.z - footZ ) ? topZ : footZ;
var last = line.Closed ? line.Count - 1 : line.Count;
// Ends win over the side of a wall, they are what a corner is made of.
foreach ( var i in new[]
{
0,
last - 1
}
)
{
var end = world.PointToWorld( line.Frames[i].Position ).WithZ( 0.0f );
var horizontal = end.Distance( flat );
var vertical = plane - point.z;
var distance = MathF.Sqrt( horizontal * horizontal * 0.0625f + vertical * vertical );
if ( end.Distance( flat ) > reach || distance >= best )
continue;
best = distance;
result = end.WithZ( plane );
}
for ( var i = 0; i + 1 < line.Count; i++ )
{
var a = world.PointToWorld( line.Frames[i].Position ).WithZ( 0.0f );
var b = world.PointToWorld( line.Frames[i + 1].Position ).WithZ( 0.0f );
var d = b - a;
var t = d.LengthSquared > 0.0f ? Math.Clamp( (flat - a).Dot( d ) / d.LengthSquared, 0.0f, 1.0f ) : 0.0f;
var on = a + d * t;
var horizontal = on.Distance( flat );
var vertical = plane - point.z;
var distance = MathF.Sqrt( horizontal * horizontal + vertical * vertical );
if ( horizontal > reach || distance >= best )
continue;
best = distance;
result = on.WithZ( plane );
}
}
snapped = result;
return best < float.MaxValue;
}
/// <summary>
/// One connected mesh out of walls, on request. The walls stay as they are. Faces where two
/// walls meet exactly disappear, which leaves one body without anything inside it.
/// </summary>
public static PolygonMesh Weld( IEnumerable<GameObject> walls, Material material )
{
var kit = new MeshKit { Material = material };
foreach ( var wall in walls )
{
var s = Read( wall );
var track = ConnectedWallGenerator.Line( s );
var shape = new ConnectedWallGenerator.WallShape( s, track );
var z0 = track.Frames[0].Position.z;
var world = wall.WorldTransform;
kit.Frame = world;
foreach ( var section in s.Sections.Count > 0 ? s.Sections : [new WallSection { From = 0.0f, To = track.Length }] )
{
foreach ( var (from, to) in shape.Pieces( section.From, section.To ) )
kit.Prism( shape.Outline( section, from, to ), z0, z0 + s.Height );
}
foreach ( var hub in s.Hubs )
kit.Prism( hub, z0, z0 + s.Height );
}
return kit.Finish();
}
}