Editor-side helper for the procedural wall tool. Adds interior/exterior material fields and an opening toggle to ConnectedWallSettings, extends WallEditor with the matching material properties and UnusedOptions rules, and then assigns a material per face on every WallPanel part by picking the nearest path segment and testing the face normal against that segment's left vector, rebuilding the part's MaterialHash afterwards.
namespace AdvancedMappingTools;
public sealed partial class ConnectedWallSettings
{
[Property, Group( "Openings" ), Title( "Use Openings" ), WideMode]
public bool UseOpenings { get; set; } = true;
[Property, Group( "Materials" ), WideMode]
public bool SeparateWallMaterials { get; set; }
[Property, Group( "Materials" ), ResourceType( "vmat" ), WideMode]
public string InteriorMaterial { get; set; }
[Property, Group( "Materials" ), ResourceType( "vmat" ), WideMode]
public string ExteriorMaterial { get; set; }
}
public sealed partial class WallEditor
{
public override bool ChangesOtherSettings( string property ) => property is nameof( AdvancedCorners ) or nameof( OpeningGrid ) or nameof( OpeningKind ) or nameof( UseWallOpenings ) or nameof( SeparateWallMaterials ) or nameof( Mode ) or nameof( Placement ) or nameof( TopStyle ) or nameof( ConnectedParts );
public override IReadOnlySet<string> UnusedOptions
{
get
{
var h = new HashSet<string>( base.UnusedOptions );
if ( !SeparateWallMaterials )
h.UnionWith( [nameof( InteriorSurface ), nameof( ExteriorSurface )] );
if ( !UsesConnections )
h.UnionWith( [nameof( UseWallOpenings ), nameof( OpeningGrid ), nameof( AdvancedCorners )] );
if ( !UsesConnections || !AdvancedCorners )
h.Add( nameof( CornerProfile ) );
if ( !UsesConnections || !OpeningGrid || !UseWallOpenings )
h.UnionWith( [nameof( OpeningColumns ), nameof( OpeningRows ), nameof( OpeningRowSpacing ), nameof( OpeningKind ), nameof( GridOpeningWidth ), nameof( GridOpeningHeight ), nameof( GridSill )] );
if ( OpeningKind != WallOpeningKind.Window )
h.Add( nameof( GridSill ) );
if ( !HasCap )
h.UnionWith( [nameof( CapHeight ), nameof( CapOverhang )] );
return h;
}
}
[Property, Group( "Materials" ), WideMode]
public bool SeparateWallMaterials { get; set; }
[Property, Group( "Materials" ), Title( "Interior (Left Side)" ), WideMode]
public Material InteriorSurface { get; set; }
[Property, Group( "Materials" ), Title( "Exterior (Right Side)" ), WideMode]
public Material ExteriorSurface { get; set; }
[Property, Group( "Openings" ), Title( "Use Wall Openings" ), WideMode]
public bool UseWallOpenings { get; set; } = true;
}
internal static class WallDetailMaterials
{
public static void Apply( PartPlan root, ConnectedWallSettings s, GenContext context, PathTrack track )
{
if ( !s.SeparateWallMaterials )
return;
var inside = context.Material( s.InteriorMaterial ?? s.Material );
var outside = context.Material( s.ExteriorMaterial ?? s.Material );
foreach ( var part in root.All().Where( p => p.Kind == "WallPanel" && p.Mesh != null ) )
foreach ( var face in part.Mesh.FaceHandles )
{
var vertices = part.Mesh.GetFaceVertices( face ).Select( part.Mesh.GetVertexPosition ).ToArray();
if ( vertices.Length < 3 )
continue;
var normal = Vector3.Cross( vertices[1] - vertices[0], vertices[2] - vertices[0] ).Normal;
if ( MathF.Abs( normal.z ) > .5f )
continue;
var center = vertices.Aggregate( Vector3.Zero, ( sum, p ) => sum + p ) / vertices.Length;
var segment = Enumerable.Range( 0, Math.Max( 1, track.Count - 1 ) ).OrderBy( i =>
{
var a = track.Frames[i].Position;
var b = track.Frames[Math.Min( i + 1, track.Count - 1 )].Position;
var d = (b - a).WithZ( 0 );
var t = d.LengthSquared > .0001f ? Math.Clamp( (center - a).WithZ( 0 ).Dot( d ) / d.LengthSquared, 0, 1 ) : 0;
return (center - (a + d * t)).WithZ( 0 ).LengthSquared;
} ).First();
var direction = (track.Frames[Math.Min( segment + 1, track.Count - 1 )].Position - track.Frames[segment].Position).WithZ( 0 );
if ( direction.Length < .01f )
direction = Vector3.Forward;
var left = new Vector3( -direction.y, direction.x, 0 ).Normal;
if ( MathF.Abs( normal.Dot( left ) ) > .5f )
part.Mesh.SetFaceMaterial( face, normal.Dot( left ) > 0 ? inside : outside );
}
PartPlan Rehash( PartPlan p )
{
for ( var i = 0; i < p.Children.Count; i++ )
p.Children[i] = Rehash( p.Children[i] );
if ( p.Kind != "WallPanel" || p.Mesh == null )
return p;
var copy = new PartPlan
{
Id = p.Id,
Name = p.Name,
Kind = p.Kind,
Category = p.Category,
Seed = p.Seed,
UsesSeed = p.UsesSeed,
Local = p.Local,
Mesh = p.Mesh,
Dimensions = p.Dimensions,
GeometryHash = p.GeometryHash,
MaterialHash = PartHash.Materials( p.Mesh ),
Smoothing = p.Smoothing,
Tint = p.Tint
};
foreach ( var c in p.Children )
copy.Add( c );
return copy;
}
Rehash( root );
}
}