Editor utility for the AdvancedMappingTools package. Clones builder objects in rows with a per-copy offset and yaw under one undo step, and reflects builder geometry in place: path points in BuilderStore records, mirrored PathCurve settings for generated parts, and side swapping for HouseSettings doors and windows.
namespace AdvancedMappingTools;
public enum MirrorAxis
{
[Title( "Across X (left to right)" )] X,
[Title( "Across Y (front to back)" )] Y
}
/// <summary>
/// Copies in a row and mirror images of finished objects. A copy keeps the record of the
/// object it came from, so it snaps, joins and can be edited like the original.
/// </summary>
public static class Arrange
{
/// <summary>
/// <paramref name="copies"/> copies of every object, each one <paramref name="offset"/>
/// further on and turned <paramref name="yaw"/> degrees more, as one undo step.
/// </summary>
public static List<GameObject> Array( IReadOnlyList<GameObject> objects, int copies, Vector3 offset, float yaw )
{
var made = new List<GameObject>();
if ( SceneEditorSession.Active is null || objects.Count == 0 || copies < 1 ) return made;
using var scope = SceneEditorSession.Scope();
using ( SceneEditorSession.Active.UndoScope( "Array" ).WithGameObjectCreations().Push() )
{
foreach ( var go in objects.Where( x => x.IsValid() ) )
{
var record = BuilderStore.LoadRecord( go.Id );
for ( var i = 1; i <= copies; i++ )
{
var clone = go.Clone();
clone.WorldPosition = go.WorldPosition + offset * i;
clone.WorldRotation = Rotation.FromYaw( yaw * i ) * go.WorldRotation;
clone.WorldScale = go.WorldScale;
if ( record is not null ) BuilderStore.SaveRecord( clone.Id, record );
made.Add( clone );
}
}
}
return made;
}
/// <summary>
/// Mirror an object made by a builder: its points are reflected across its own middle and it
/// is built again in place. Returns why it could not be mirrored, or null.
/// </summary>
public static string Mirror( GameObject go, MirrorAxis axis )
{
var record = go.IsValid() ? BuilderStore.LoadRecord( go.Id ) : null;
if ( record is null || record.Points.Count == 0 )
return MirrorPart( go, axis );
var points = record.Points.Select( x => new PathPoint( new Vector3( x[0], x[1], x[2] ), x.Length > 3 ? new Vector3( x[3], x[4], x[5] ) : null ) ).ToList();
var low = points.Select( x => x.Position ).Aggregate( ( a, b ) => Vector3.Min( a, b ) );
var high = points.Select( x => x.Position ).Aggregate( ( a, b ) => Vector3.Max( a, b ) );
var middle = (low + high) * 0.5f;
Vector3 Flip( Vector3 p ) => axis == MirrorAxis.X ? p.WithX( middle.x * 2.0f - p.x ) : p.WithY( middle.y * 2.0f - p.y );
// A mirror image runs the other way round, or every outline would turn inside out.
var flipped = new List<PathPoint>( points.Count );
for ( var i = points.Count - 1; i >= 0; i-- )
{
var bend = record.Closed ? points[(i - 1 + points.Count) % points.Count].Bend : i > 0 ? points[i - 1].Bend : null;
flipped.Add( new PathPoint( Flip( points[i].Position ), bend is Vector3 b ? Flip( b ) : null ) );
}
BuilderEditor.Rebuild( go, x => x.ReplacePoints( flipped ) );
return null;
}
/// <summary>
/// A part object has no points of its own: mirror the path in its settings and let the
/// generator build it again, so walls, pipes and houses keep their parts and their record.
/// </summary>
static string MirrorPart( GameObject go, MirrorAxis axis )
{
if ( !go.IsValid() ) return "Nothing selected.";
var root = PartService.RootOf( go );
if ( !root.IsValid() ) return $"{go.Name} is not a Davelix Construct object.";
var (generator, settings) = PartService.Read( root );
if ( generator is null || settings is null ) return $"{go.Name} has no generator settings to mirror.";
if ( settings is HouseSettings house )
{
MirrorHouse( house, axis );
RegeneratePart( root, settings );
return null;
}
var pathProperty = settings.GetType().GetProperty( "Path" );
if ( pathProperty?.GetValue( settings ) is not PathCurve curve || curve.Nodes.Count < 2 )
return $"{go.Name} has no path in its settings to mirror.";
pathProperty.SetValue( settings, MirrorPath( curve, axis ) );
RegeneratePart( root, settings );
return null;
}
static void RegeneratePart( GameObject root, GeneratorSettings settings ) =>
PartService.Transaction( "Mirror Part", root, () => PartService.Regenerate( root, RegenerationOptions.Everything, true, settings ) );
/// <summary>
/// Reflect a path across its own middle and run it the other way round, so its outlines
/// keep facing outwards.
/// </summary>
public static PathCurve MirrorPath( PathCurve curve, MirrorAxis axis )
{
var points = curve.Nodes.Select( x => x.Position ).ToList();
var low = points.Aggregate( ( a, b ) => Vector3.Min( a, b ) );
var high = points.Aggregate( ( a, b ) => Vector3.Max( a, b ) );
var middle = (low + high) * 0.5f;
Vector3 Flip( Vector3 p ) => axis == MirrorAxis.X ? p.WithX( middle.x * 2.0f - p.x ) : p.WithY( middle.y * 2.0f - p.y );
// Tangents are offsets from their point, so they are reflected as directions.
Vector3 Turn( Vector3 d ) => axis == MirrorAxis.X ? d.WithX( -d.x ) : d.WithY( -d.y );
var mirrored = new PathCurve { Closed = curve.Closed, Interpolation = curve.Interpolation };
var count = curve.Nodes.Count;
for ( var i = count - 1; i >= 0; i-- )
{
var old = curve.Nodes[i];
var node = old.Copy();
node.Position = Flip( old.Position );
// Running the other way, the handle that pointed on now points back.
node.TangentIn = Turn( old.TangentOut );
node.TangentOut = Turn( old.TangentIn );
// Reflecting and reversing both swap left and right, so a lean stays as it is.
node.Roll = old.Roll;
// A slope belongs to the stretch to the next point, which is now the one before.
var before = i > 0 ? curve.Nodes[i - 1] : curve.Closed ? curve.Nodes[count - 1] : null;
node.Slope = before?.Slope is float slope ? -slope : null;
mirrored.Nodes.Add( node );
}
return mirrored;
}
/// <summary>
/// A house is a rectangle, so a mirror only swaps which side has windows and the door.
/// </summary>
static void MirrorHouse( HouseSettings house, MirrorAxis axis )
{
if ( axis == MirrorAxis.X )
{
(house.WindowsLeft, house.WindowsRight) = (house.WindowsRight, house.WindowsLeft);
house.DoorSide = house.DoorSide switch
{
HouseSide.Left => HouseSide.Right,
HouseSide.Right => HouseSide.Left,
_ => house.DoorSide
};
}
else
{
(house.WindowsBack, house.WindowsFront) = (house.WindowsFront, house.WindowsBack);
house.DoorSide = house.DoorSide switch
{
HouseSide.Front => HouseSide.Back,
HouseSide.Back => HouseSide.Front,
_ => house.DoorSide
};
}
}
}