Editor/FenceLibrary/FenceDefinitionEditorUtility.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox.Volumes;
namespace Editor;
internal sealed class FencePrototype
{
public FenceDefinitionEntry Entry { get; init; }
public string DisplayName { get; init; }
public Model Model { get; init; }
public PrefabFile Prefab { get; init; }
public Angles RotationOffset { get; init; }
public float Weight { get; init; }
public float CanonicalLength { get; init; }
}
internal sealed class FencePlacementSegment
{
public FencePrototype Prototype { get; init; }
public Vector3 LinePoint { get; init; }
public Vector3 SurfacePoint { get; init; }
public Vector3 Direction { get; init; }
public Vector3 Up { get; init; }
public Angles RandomRotationOffset { get; init; }
public float Length { get; init; }
public float StartDistance { get; init; }
public float EndDistance { get; init; }
}
internal sealed class FencePlacementPlan
{
public string RootName { get; set; }
public Vector3 Origin { get; set; }
public Vector3 Direction { get; set; }
public float TotalLength { get; set; }
public bool CreateBlockerVolumes { get; set; }
public List<FencePlacementSegment> Segments { get; set; } = [];
}
internal enum FenceToolPlacementMode
{
Line,
Spline
}
internal readonly record struct FenceSplineControlPoint( Vector3 Position, Vector3 Normal );
internal readonly record struct FenceSplineSample( Vector3 Position, Vector3 Normal );
internal readonly record struct FenceSplineDistanceSample( Vector3 Position, Vector3 Normal, Vector3 Tangent );
internal sealed class FenceToolSettings
{
/// <summary>
/// Fence definition asset used for the next placement run.
/// </summary>
[Property]
public FenceDefinition Definition { get; set; }
/// <summary>
/// Selects whether the tool places a straight line run or a surface-drawn spline run.
/// </summary>
[Property]
public FenceToolPlacementMode Mode { get; set; } = FenceToolPlacementMode.Line;
/// <summary>
/// When enabled, each segment start is projected down onto scene geometry before placement.
/// </summary>
[Property]
public bool SnapToGround { get; set; } = true;
/// <summary>
/// Vertical distance used for the downward scene trace when snapping fence segments to the ground.
/// </summary>
[Property]
public float GroundTraceHeight { get; set; } = 256.0f;
/// <summary>
/// Optional scene tag filter for the ground trace. When empty, any visible scene geometry may be used.
/// </summary>
[Property]
public string SurfaceTag { get; set; } = string.Empty;
/// <summary>
/// Keeps fence segments upright by ignoring vertical slope when resolving their forward direction.
/// </summary>
[Property]
public bool KeepUpright { get; set; } = true;
/// <summary>
/// Minimum world-space distance between captured points while drawing spline fence runs.
/// </summary>
[Property]
public float SplinePointSpacing { get; set; } = 24.0f;
/// <summary>
/// Number of interpolated curve samples generated between captured spline points for preview and placement.
/// </summary>
[Property]
public int SplineInterpolationSteps { get; set; } = 6;
/// <summary>
/// Seed used for deterministic random selection when multiple entries fit a run segment.
/// </summary>
[Property]
public int Seed { get; set; } = 1;
/// <summary>
/// Increments the seed after a successful placement so the next run can vary automatically.
/// </summary>
[Property]
public bool IncrementSeedAfterPlacement { get; set; } = true;
}
internal readonly record struct FenceProjectionRange( float Min, float Max )
{
public float Length => Max - Min;
}
internal sealed class FencePlacementCandidate
{
public FencePrototype Prototype { get; init; }
public float Length { get; init; }
}
internal static class FenceDefinitionEditorUtility
{
internal const string PreviewTag = "__fence_preview";
private const float MinimumSegmentLength = 0.01f;
internal static int ComputeDefinitionHash( FenceDefinition definition )
{
if ( definition is null )
return 0;
var hash = new HashCode();
hash.Add( definition.RootName ?? string.Empty );
hash.Add( definition.UseWeightedRandom );
hash.Add( definition.MaximumSegments );
hash.Add( definition.SegmentGap );
hash.Add( definition.RotationRandomizationMin.pitch );
hash.Add( definition.RotationRandomizationMin.yaw );
hash.Add( definition.RotationRandomizationMin.roll );
hash.Add( definition.RotationRandomizationMax.pitch );
hash.Add( definition.RotationRandomizationMax.yaw );
hash.Add( definition.RotationRandomizationMax.roll );
hash.Add( definition.DistanceRandomizationMin );
hash.Add( definition.DistanceRandomizationMax );
hash.Add( definition.CreateBlockerVolumes );
hash.Add( definition.Entries?.Count ?? 0 );
if ( definition.Entries is not null )
{
foreach ( var entry in definition.Entries )
{
if ( entry is null )
continue;
hash.Add( entry.DisplayName ?? string.Empty );
hash.Add( entry.Model?.ResourcePath ?? string.Empty );
hash.Add( entry.Prefab?.ResourcePath ?? string.Empty );
hash.Add( entry.RotationOffset.pitch );
hash.Add( entry.RotationOffset.yaw );
hash.Add( entry.RotationOffset.roll );
hash.Add( entry.Weight );
}
}
return hash.ToHashCode();
}
internal static List<FencePrototype> BuildPrototypes( FenceDefinition definition )
{
var prototypes = new List<FencePrototype>();
if ( definition?.Entries is null )
return prototypes;
foreach ( var entry in definition.Entries )
{
if ( entry is null || (entry.Prefab is null && entry.Model is null) )
continue;
var prototype = BuildPrototype( entry );
if ( prototype is not null )
{
prototypes.Add( prototype );
}
}
return prototypes;
}
internal static FencePlacementPlan BuildPlacementPlan( FenceDefinition definition, IReadOnlyList<FencePrototype> prototypes, FenceToolSettings settings, Scene scene, Vector3 startPoint, Vector3 endPoint )
{
var plan = new FencePlacementPlan
{
RootName = string.IsNullOrWhiteSpace( definition?.RootName ) ? "Fence" : definition.RootName,
Origin = startPoint,
Direction = Vector3.Forward,
TotalLength = 0.0f
};
if ( definition is null || prototypes is null || prototypes.Count == 0 )
return plan;
var rawDirection = endPoint - startPoint;
if ( rawDirection.Length < 1.0f )
return plan;
var runDirection = ResolveRunDirection( rawDirection, settings.KeepUpright, Vector3.Up );
if ( runDirection.Length < 0.001f )
return plan;
var totalLength = Vector3.Dot( rawDirection, runDirection );
if ( totalLength <= 0.0f )
return plan;
plan.Direction = runDirection;
plan.TotalLength = totalLength;
plan.CreateBlockerVolumes = definition.CreateBlockerVolumes;
var rng = new Random( settings.Seed );
var cursorDistance = 0.0f;
var maximumSegments = Math.Max( definition.MaximumSegments, 1 );
var baseGap = Math.Max( definition.SegmentGap, 0.0f );
for ( var segmentIndex = 0; segmentIndex < maximumSegments; segmentIndex++ )
{
var remaining = totalLength - cursorDistance;
if ( remaining <= MinimumSegmentLength )
break;
var linePoint = startPoint + runDirection * cursorDistance;
TrySampleSurface( scene, settings, linePoint, out var surfacePoint, out var surfaceNormal );
var up = settings.KeepUpright ? Vector3.Up : (surfaceNormal.Length < 0.001f ? Vector3.Up : surfaceNormal.Normal);
var forward = ResolveRunDirection( runDirection, settings.KeepUpright, up );
if ( forward.Length < 0.001f )
break;
var candidates = new List<FencePlacementCandidate>();
foreach ( var prototype in prototypes )
{
if ( prototype.CanonicalLength <= remaining + 0.01f )
{
candidates.Add( new FencePlacementCandidate
{
Prototype = prototype,
Length = prototype.CanonicalLength
} );
}
}
if ( candidates.Count == 0 )
break;
var chosen = PickCandidate( candidates, definition.UseWeightedRandom, rng );
if ( chosen is null )
break;
var endDistance = cursorDistance + chosen.Length;
plan.Segments.Add( new FencePlacementSegment
{
Prototype = chosen.Prototype,
LinePoint = linePoint,
SurfacePoint = surfacePoint,
Direction = forward,
Up = up,
RandomRotationOffset = SampleRotationRandomization( definition, rng ),
Length = chosen.Length,
StartDistance = cursorDistance,
EndDistance = endDistance
} );
cursorDistance = Math.Max( endDistance + SampleDistanceGap( definition, baseGap, rng ), cursorDistance + MinimumSegmentLength );
}
return plan;
}
internal static FencePlacementPlan BuildSplinePlacementPlan( FenceDefinition definition, IReadOnlyList<FencePrototype> prototypes, FenceToolSettings settings, Scene scene, IReadOnlyList<FenceSplineControlPoint> controlPoints )
{
var origin = controlPoints is not null && controlPoints.Count > 0 ? controlPoints[0].Position : Vector3.Zero;
var plan = new FencePlacementPlan
{
RootName = string.IsNullOrWhiteSpace( definition?.RootName ) ? "Fence" : definition.RootName,
Origin = origin,
Direction = Vector3.Forward,
TotalLength = 0.0f
};
if ( definition is null || prototypes is null || prototypes.Count == 0 || controlPoints is null || controlPoints.Count < 2 )
return plan;
var samples = BuildSplinePreviewPath( controlPoints, settings.SplineInterpolationSteps );
var totalLength = MeasureSplineLength( samples );
if ( totalLength < 1.0f )
return plan;
var initialSample = SampleSplineAtDistance( samples, 0.0f );
var initialUp = settings.KeepUpright ? Vector3.Up : NormalizeNormal( initialSample.Normal );
var initialDirection = ResolveRunDirection( initialSample.Tangent, settings.KeepUpright, initialUp );
plan.Direction = initialDirection.Length < 0.001f ? Vector3.Forward : initialDirection;
plan.TotalLength = totalLength;
plan.CreateBlockerVolumes = definition.CreateBlockerVolumes;
var rng = new Random( settings.Seed );
var cursorDistance = 0.0f;
var maximumSegments = Math.Max( definition.MaximumSegments, 1 );
var baseGap = Math.Max( definition.SegmentGap, 0.0f );
for ( var segmentIndex = 0; segmentIndex < maximumSegments; segmentIndex++ )
{
var remaining = totalLength - cursorDistance;
if ( remaining <= MinimumSegmentLength )
break;
var candidates = new List<FencePlacementCandidate>();
foreach ( var prototype in prototypes )
{
if ( prototype.CanonicalLength <= remaining + 0.01f )
{
candidates.Add( new FencePlacementCandidate
{
Prototype = prototype,
Length = prototype.CanonicalLength
} );
}
}
if ( candidates.Count == 0 )
break;
var chosen = PickCandidate( candidates, definition.UseWeightedRandom, rng );
if ( chosen is null )
break;
var startSample = SampleSplineAtDistance( samples, cursorDistance );
var endDistance = cursorDistance + chosen.Length;
var endSample = SampleSplineAtDistance( samples, Math.Min( endDistance, totalLength ) );
var up = settings.KeepUpright ? Vector3.Up : NormalizeNormal( startSample.Normal );
var chord = endSample.Position - startSample.Position;
var forwardSource = chord.Length > 0.001f ? chord : startSample.Tangent;
var forward = ResolveRunDirection( forwardSource, settings.KeepUpright, up );
if ( forward.Length < 0.001f )
break;
plan.Segments.Add( new FencePlacementSegment
{
Prototype = chosen.Prototype,
LinePoint = startSample.Position,
SurfacePoint = startSample.Position,
Direction = forward,
Up = up,
RandomRotationOffset = SampleRotationRandomization( definition, rng ),
Length = chosen.Length,
StartDistance = cursorDistance,
EndDistance = endDistance
} );
cursorDistance = Math.Max( endDistance + SampleDistanceGap( definition, baseGap, rng ), cursorDistance + MinimumSegmentLength );
}
return plan;
}
internal static List<FenceSplineSample> BuildSplinePreviewPath( IReadOnlyList<FenceSplineControlPoint> controlPoints, int interpolationSteps )
{
var samples = new List<FenceSplineSample>();
if ( controlPoints is null || controlPoints.Count == 0 )
return samples;
if ( controlPoints.Count == 1 )
{
var point = NormalizeSplinePoint( controlPoints[0] );
samples.Add( new FenceSplineSample( point.Position, point.Normal ) );
return samples;
}
var steps = Math.Clamp( interpolationSteps, 1, 32 );
var first = NormalizeSplinePoint( controlPoints[0] );
samples.Add( new FenceSplineSample( first.Position, first.Normal ) );
for ( var index = 0; index < controlPoints.Count - 1; index++ )
{
var p0 = controlPoints[Math.Max( index - 1, 0 )];
var p1 = controlPoints[index];
var p2 = controlPoints[index + 1];
var p3 = controlPoints[Math.Min( index + 2, controlPoints.Count - 1 )];
for ( var step = 1; step <= steps; step++ )
{
var t = (float)step / steps;
var position = CatmullRom( p0.Position, p1.Position, p2.Position, p3.Position, t );
var normal = InterpolateNormal( p1.Normal, p2.Normal, t );
if ( samples.Count > 0 && Vector3.DistanceBetween( samples[^1].Position, position ) < 0.001f )
continue;
samples.Add( new FenceSplineSample( position, normal ) );
}
}
return samples;
}
internal static GameObject SpawnPreviewHierarchy( FencePlacementPlan plan )
{
if ( plan is null || plan.Segments.Count == 0 )
return null;
var root = new GameObject( true, $"{plan.RootName} Preview" );
root.Flags |= GameObjectFlags.NotSaved | GameObjectFlags.Hidden | GameObjectFlags.EditorOnly;
root.Tags.Add( PreviewTag );
foreach ( var segment in plan.Segments )
{
var placed = SpawnSegmentObject( segment.Prototype, previewOnly: true );
if ( !placed.IsValid() )
continue;
ApplySegmentTransform( placed, segment );
placed.SetParent( root, true );
MarkHierarchyAsPreview( placed );
}
return root;
}
internal static GameObject CommitPlacementPlan( FencePlacementPlan plan )
{
if ( plan is null || plan.Segments.Count == 0 )
return null;
var root = new GameObject( true, string.IsNullOrWhiteSpace( plan.RootName ) ? "Fence" : plan.RootName );
foreach ( var segment in plan.Segments )
{
var placed = SpawnSegmentObject( segment.Prototype, previewOnly: false );
if ( !placed.IsValid() )
continue;
ApplySegmentTransform( placed, segment );
placed.SetParent( root, true );
if ( plan.CreateBlockerVolumes )
{
AddOrUpdateBlockerVolume( placed );
}
}
return root;
}
internal static int AddOrUpdateBlockerVolumes( GameObject root )
{
if ( !root.IsValid() || IsDedicatedBlockerObject( root ) )
return 0;
var count = 0;
foreach ( var target in ResolveBlockerTargets( root ) )
{
if ( AddOrUpdateBlockerVolume( target ) )
{
count++;
}
}
return count;
}
internal static bool TryUpdatePreviewHierarchy( GameObject root, FencePlacementPlan plan )
{
if ( !root.IsValid() || plan is null )
return false;
if ( root.Children.Count != plan.Segments.Count )
return false;
for ( var index = 0; index < plan.Segments.Count; index++ )
{
var child = root.Children[index];
var segment = plan.Segments[index];
if ( !child.IsValid() || !string.Equals( child.Name, segment.Prototype.DisplayName, StringComparison.Ordinal ) )
return false;
}
for ( var index = 0; index < plan.Segments.Count; index++ )
{
ApplySegmentTransform( root.Children[index], plan.Segments[index] );
}
return true;
}
internal static FenceProjectionRange MeasureProjectedRange( BBox bounds, Vector3 direction )
{
var normal = direction.Normal;
var min = float.PositiveInfinity;
var max = float.NegativeInfinity;
foreach ( var corner in GetCorners( bounds ) )
{
var projection = Vector3.Dot( corner, normal );
min = Math.Min( min, projection );
max = Math.Max( max, projection );
}
if ( float.IsInfinity( min ) || float.IsInfinity( max ) )
return new FenceProjectionRange( 0.0f, 0.0f );
return new FenceProjectionRange( min, max );
}
internal static IEnumerable<Vector3> GetCorners( BBox bounds )
{
var mins = bounds.Mins;
var maxs = bounds.Maxs;
yield return new Vector3( mins.x, mins.y, mins.z );
yield return new Vector3( mins.x, mins.y, maxs.z );
yield return new Vector3( mins.x, maxs.y, mins.z );
yield return new Vector3( mins.x, maxs.y, maxs.z );
yield return new Vector3( maxs.x, mins.y, mins.z );
yield return new Vector3( maxs.x, mins.y, maxs.z );
yield return new Vector3( maxs.x, maxs.y, mins.z );
yield return new Vector3( maxs.x, maxs.y, maxs.z );
}
internal static void DestroyHierarchy( GameObject root )
{
if ( root.IsValid() )
{
root.Destroy();
}
}
private static FencePrototype BuildPrototype( FenceDefinitionEntry entry )
{
var displayName = ResolveDisplayName( entry );
var tempObject = SpawnSegmentObject( entry, displayName, previewOnly: true );
if ( !tempObject.IsValid() )
return null;
tempObject.WorldRotation = entry.RotationOffset.ToRotation();
tempObject.WorldPosition = Vector3.Zero;
var range = MeasureProjectedRange( tempObject.GetBounds(), Vector3.Forward );
var canonicalLength = Math.Max( range.Length, 1.0f );
tempObject.Destroy();
return new FencePrototype
{
Entry = entry,
DisplayName = displayName,
Model = entry.Model,
Prefab = entry.Prefab,
RotationOffset = entry.RotationOffset,
Weight = entry.Weight,
CanonicalLength = canonicalLength
};
}
private static FencePlacementCandidate PickCandidate( List<FencePlacementCandidate> candidates, bool useWeightedRandom, Random rng )
{
if ( candidates is null || candidates.Count == 0 )
return null;
if ( candidates.Count == 1 )
return candidates[0];
if ( !useWeightedRandom )
return candidates[rng.Next( candidates.Count )];
var totalWeight = 0.0f;
foreach ( var candidate in candidates )
{
totalWeight += Math.Max( candidate.Prototype.Weight, 0.01f );
}
if ( totalWeight <= 0.0f )
return candidates[rng.Next( candidates.Count )];
var pick = (float)rng.NextDouble() * totalWeight;
foreach ( var candidate in candidates )
{
pick -= Math.Max( candidate.Prototype.Weight, 0.01f );
if ( pick <= 0.0f )
return candidate;
}
return candidates[^1];
}
private static void TrySampleSurface( Scene scene, FenceToolSettings settings, Vector3 linePoint, out Vector3 surfacePoint, out Vector3 surfaceNormal )
{
surfacePoint = linePoint;
surfaceNormal = Vector3.Up;
if ( scene is null || !settings.SnapToGround )
return;
var traceHeight = Math.Max( settings.GroundTraceHeight, 1.0f );
var traceStart = linePoint + Vector3.Up * traceHeight;
var traceEnd = linePoint + Vector3.Down * traceHeight;
var trace = scene.Trace.Ray( traceStart, traceEnd )
.UseRenderMeshes( true )
.UsePhysicsWorld( false )
.WithoutTags( PreviewTag )
.WithoutTags( "hidden" );
var result = default(SceneTraceResult);
if ( !string.IsNullOrWhiteSpace( settings.SurfaceTag ) )
{
result = trace.WithTag( settings.SurfaceTag ).Run();
}
if ( !result.Hit )
{
result = trace.Run();
}
if ( !result.Hit )
return;
surfacePoint = result.HitPosition;
surfaceNormal = result.Normal;
}
private static Vector3 ResolveRunDirection( Vector3 direction, bool keepUpright, Vector3 up )
{
if ( keepUpright )
{
var flat = new Vector3( direction.x, direction.y, 0.0f );
return flat.Length < 0.001f ? Vector3.Forward : flat.Normal;
}
var projected = direction - up * Vector3.Dot( direction, up );
return projected.Length < 0.001f ? Vector3.Forward : projected.Normal;
}
private static float MeasureSplineLength( IReadOnlyList<FenceSplineSample> samples )
{
if ( samples is null || samples.Count < 2 )
return 0.0f;
var length = 0.0f;
for ( var index = 1; index < samples.Count; index++ )
{
length += Vector3.DistanceBetween( samples[index - 1].Position, samples[index].Position );
}
return length;
}
private static FenceSplineDistanceSample SampleSplineAtDistance( IReadOnlyList<FenceSplineSample> samples, float distance )
{
if ( samples is null || samples.Count == 0 )
return new FenceSplineDistanceSample( Vector3.Zero, Vector3.Up, Vector3.Forward );
if ( samples.Count == 1 )
return new FenceSplineDistanceSample( samples[0].Position, NormalizeNormal( samples[0].Normal ), Vector3.Forward );
var remaining = Math.Max( distance, 0.0f );
var lastTangent = Vector3.Forward;
for ( var index = 1; index < samples.Count; index++ )
{
var previous = samples[index - 1];
var current = samples[index];
var delta = current.Position - previous.Position;
var segmentLength = delta.Length;
if ( segmentLength <= 0.001f )
continue;
lastTangent = delta.Normal;
if ( remaining <= segmentLength )
{
var t = remaining / segmentLength;
var position = previous.Position + delta * t;
var normal = InterpolateNormal( previous.Normal, current.Normal, t );
return new FenceSplineDistanceSample( position, normal, lastTangent );
}
remaining -= segmentLength;
}
var last = samples[^1];
return new FenceSplineDistanceSample( last.Position, NormalizeNormal( last.Normal ), lastTangent );
}
private static FenceSplineControlPoint NormalizeSplinePoint( FenceSplineControlPoint point )
{
return new FenceSplineControlPoint( point.Position, NormalizeNormal( point.Normal ) );
}
private static Vector3 NormalizeNormal( Vector3 normal )
{
return normal.Length < 0.001f ? Vector3.Up : normal.Normal;
}
private static Vector3 InterpolateNormal( Vector3 start, Vector3 end, float t )
{
var normal = NormalizeNormal( start ) + (NormalizeNormal( end ) - NormalizeNormal( start )) * Math.Clamp( t, 0.0f, 1.0f );
return NormalizeNormal( normal );
}
private static Vector3 CatmullRom( Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t )
{
var t2 = t * t;
var t3 = t2 * t;
return (p1 * 2.0f
+ (p2 - p0) * t
+ (p0 * 2.0f - p1 * 5.0f + p2 * 4.0f - p3) * t2
+ (Vector3.Zero - p0 + p1 * 3.0f - p2 * 3.0f + p3) * t3) * 0.5f;
}
private static GameObject SpawnSegmentObject( FencePrototype prototype, bool previewOnly )
{
if ( prototype is null )
return null;
return SpawnSegmentObject( prototype.Entry, prototype.DisplayName, previewOnly );
}
private static GameObject SpawnSegmentObject( FenceDefinitionEntry entry, string displayName, bool previewOnly )
{
if ( entry?.Prefab is not null )
{
var prefabScene = SceneUtility.GetPrefabScene( entry.Prefab );
var clone = prefabScene?.Clone();
if ( clone is null )
return null;
clone.Name = displayName;
if ( previewOnly )
{
MarkHierarchyAsPreview( clone );
}
return clone;
}
if ( entry?.Model is null )
return null;
var root = new GameObject( true, displayName );
if ( previewOnly )
{
MarkHierarchyAsPreview( root );
}
if ( !previewOnly && (entry.Model.Physics?.Parts.Count ?? 0) > 0 )
{
var prop = root.AddComponent<Prop>();
prop.Model = entry.Model;
prop.IsStatic = ResolveModelIsStatic( entry.Model );
}
else if ( entry.Model.BoneCount > 0 )
{
var skinnedRenderer = root.AddComponent<SkinnedModelRenderer>();
skinnedRenderer.PlayAnimationsInEditorScene = true;
skinnedRenderer.Model = entry.Model;
}
else
{
var renderer = root.AddComponent<ModelRenderer>();
renderer.Model = entry.Model;
}
return root;
}
private static bool ResolveModelIsStatic( Model model )
{
if ( model is null || string.IsNullOrWhiteSpace( model.ResourcePath ) )
return false;
var asset = AssetSystem.FindByPath( model.ResourcePath );
var archetype = asset?.FindStringEditInfo( "model_archetype_id" );
return string.Equals( archetype, "static_prop_model", StringComparison.Ordinal );
}
private static void ApplySegmentTransform( GameObject placed, FencePlacementSegment segment )
{
if ( !placed.IsValid() || segment is null )
return;
var rotationOffset = CombineAngles( segment.Prototype.RotationOffset, segment.RandomRotationOffset );
placed.WorldRotation = Rotation.LookAt( segment.Direction, segment.Up ) * rotationOffset.ToRotation();
placed.WorldPosition = segment.SurfacePoint;
var forwardRange = MeasureProjectedRange( placed.GetBounds(), segment.Direction );
var targetStart = Vector3.Dot( segment.LinePoint, segment.Direction );
placed.WorldPosition += segment.Direction * (targetStart - forwardRange.Min);
var upRange = MeasureProjectedRange( placed.GetBounds(), segment.Up );
var targetBottom = Vector3.Dot( segment.SurfacePoint, segment.Up );
placed.WorldPosition += segment.Up * (targetBottom - upRange.Min);
}
internal static bool AddOrUpdateBlockerVolume( GameObject segmentRoot )
{
if ( !segmentRoot.IsValid() || !TryGetBlockerLocalBounds( segmentRoot, out var localBounds ) )
return false;
RemoveDirectBlockerObjects( segmentRoot );
var area = segmentRoot.Components.Get<NavMeshArea>( FindMode.EverythingInSelf ) ?? segmentRoot.Components.Create<NavMeshArea>();
area.SceneVolume = new SceneVolume
{
Type = SceneVolume.VolumeTypes.Box,
Box = ClampBlockerBounds( localBounds )
};
area.IsBlocker = true;
area.Enabled = true;
return true;
}
private static IEnumerable<GameObject> ResolveBlockerTargets( GameObject root )
{
if ( !root.IsValid() || IsDedicatedBlockerObject( root ) )
yield break;
if ( HasOwnSegmentContent( root ) || HasOwnBlockerArea( root ) || root.Parent.IsValid() )
{
yield return root;
yield break;
}
var yieldedChild = false;
foreach ( var child in root.Children )
{
if ( !child.IsValid() || IsDedicatedBlockerObject( child ) )
continue;
yieldedChild = true;
yield return child;
}
if ( !yieldedChild )
{
yield return root;
}
}
private static bool TryGetBlockerLocalBounds( GameObject segmentRoot, out BBox localBounds )
{
localBounds = default;
var hasBounds = false;
foreach ( var gameObject in EnumerateHierarchy( segmentRoot ) )
{
if ( IsDedicatedBlockerObject( gameObject ) )
continue;
foreach ( var collider in gameObject.Components.GetAll<Collider>( FindMode.EverythingInSelf ) )
{
if ( collider is null )
continue;
var bounds = ConvertLocalBoundsToLocal( collider.LocalBounds, collider.GameObject.WorldTransform, segmentRoot.WorldTransform );
localBounds = hasBounds ? localBounds.AddBBox( bounds ) : bounds;
hasBounds = true;
}
}
if ( hasBounds )
return true;
localBounds = ConvertWorldBoundsToLocal( segmentRoot.GetBounds(), segmentRoot.WorldTransform );
return localBounds.Size.Length > 0.001f;
}
private static bool HasOwnSegmentContent( GameObject gameObject )
{
return gameObject.Components.Get<Collider>( FindMode.EverythingInSelf ) is not null
|| gameObject.Components.Get<ModelRenderer>( FindMode.EverythingInSelf ) is not null
|| gameObject.Components.Get<SkinnedModelRenderer>( FindMode.EverythingInSelf ) is not null
|| gameObject.Components.Get<Prop>( FindMode.EverythingInSelf ) is not null;
}
private static bool HasOwnBlockerArea( GameObject gameObject )
{
return gameObject.Components.Get<NavMeshArea>( FindMode.EverythingInSelf ) is not null;
}
private static void RemoveDirectBlockerObjects( GameObject segmentRoot )
{
foreach ( var child in segmentRoot.Children.ToArray() )
{
if ( IsDedicatedBlockerObject( child ) )
{
child.Destroy();
}
}
}
private static bool IsDedicatedBlockerObject( GameObject gameObject )
{
return gameObject.IsValid() && string.Equals( gameObject.Name, "NavBlocker", StringComparison.Ordinal );
}
private static BBox ConvertLocalBoundsToLocal( BBox sourceBounds, Transform sourceSpace, Transform targetSpace )
{
var bounds = default(BBox);
var hasPoint = false;
foreach ( var corner in GetCorners( sourceBounds ) )
{
var targetPoint = targetSpace.PointToLocal( sourceSpace.PointToWorld( corner ) );
if ( !hasPoint )
{
bounds = BBox.FromPositionAndSize( targetPoint, 0.0f );
hasPoint = true;
}
else
{
bounds = bounds.AddPoint( targetPoint );
}
}
return bounds;
}
private static BBox ConvertWorldBoundsToLocal( BBox worldBounds, Transform localSpace )
{
var bounds = default(BBox);
var hasPoint = false;
foreach ( var corner in GetCorners( worldBounds ) )
{
var localPoint = localSpace.PointToLocal( corner );
if ( !hasPoint )
{
bounds = BBox.FromPositionAndSize( localPoint, 0.0f );
hasPoint = true;
}
else
{
bounds = bounds.AddPoint( localPoint );
}
}
return bounds;
}
private static BBox ClampBlockerBounds( BBox bounds )
{
var sourceSize = bounds.Size;
var size = new Vector3(
Math.Max( sourceSize.x, 1.0f ),
Math.Max( sourceSize.y, 1.0f ),
Math.Max( sourceSize.z, 1.0f ) );
return BBox.FromPositionAndSize( bounds.Center, size );
}
private static void MarkHierarchyAsPreview( GameObject root )
{
foreach ( var gameObject in EnumerateHierarchy( root ) )
{
gameObject.Flags |= GameObjectFlags.NotSaved | GameObjectFlags.Hidden | GameObjectFlags.EditorOnly;
gameObject.Tags.Add( PreviewTag );
}
}
private static IEnumerable<GameObject> EnumerateHierarchy( GameObject root )
{
if ( !root.IsValid() )
yield break;
yield return root;
foreach ( var child in root.Children )
{
foreach ( var descendant in EnumerateHierarchy( child ) )
{
yield return descendant;
}
}
}
private static string ResolveDisplayName( FenceDefinitionEntry entry )
{
if ( entry is null )
return "Fence Segment";
if ( !string.IsNullOrWhiteSpace( entry.DisplayName ) )
return entry.DisplayName.Trim();
if ( entry.Prefab is not null )
return System.IO.Path.GetFileNameWithoutExtension( entry.Prefab.ResourcePath );
if ( entry.Model is not null )
return System.IO.Path.GetFileNameWithoutExtension( entry.Model.ResourcePath );
return "Fence Segment";
}
private static Angles SampleRotationRandomization( FenceDefinition definition, Random rng )
{
if ( definition is null )
return Angles.Zero;
return new Angles(
SampleRange( definition.RotationRandomizationMin.pitch, definition.RotationRandomizationMax.pitch, rng ),
SampleRange( definition.RotationRandomizationMin.yaw, definition.RotationRandomizationMax.yaw, rng ),
SampleRange( definition.RotationRandomizationMin.roll, definition.RotationRandomizationMax.roll, rng ) );
}
private static float SampleDistanceGap( FenceDefinition definition, float baseGap, Random rng )
{
if ( definition is null )
return baseGap;
return baseGap + SampleRange( definition.DistanceRandomizationMin, definition.DistanceRandomizationMax, rng );
}
private static float SampleRange( float a, float b, Random rng )
{
var min = Math.Min( a, b );
var max = Math.Max( a, b );
if ( MathF.Abs( max - min ) < 0.001f )
return min;
return min + (max - min) * (float)rng.NextDouble();
}
private static Angles CombineAngles( Angles a, Angles b )
{
return new Angles(
a.pitch + b.pitch,
a.yaw + b.yaw,
a.roll + b.roll );
}
}