Editor/Services/ArchRemap.cs
using System;
using System.Collections.Generic;
using Sandbox;
namespace Sunless.Architecture;
// Transforms a shape's old bounds into its new ones; shared by moves, corner drags and edge drags.
public readonly struct ArchRemap {
readonly Vector2 from;
readonly Vector2 scale;
readonly Vector2 to;
readonly float degrees;
ArchRemap( Vector2 from, Vector2 scale, Vector2 to, float degrees = 0f ) {
this.from = from;
this.scale = scale;
this.to = to;
this.degrees = Wrapped( degrees );
}
public static ArchRemap Translation( Vector2 shift ) => new( Vector2.Zero, Vector2.One, shift );
public static ArchRemap Turned( Vector2 pivot, int quarters, Vector2 to ) => Spun( pivot, quarters * 90f, to );
public static ArchRemap Spun( Vector2 pivot, float degrees ) => Spun( pivot, degrees, pivot );
public static ArchRemap Spun( Vector2 pivot, float degrees, Vector2 to ) => new( pivot, Vector2.One, to, degrees );
// A degenerate axis cannot be scaled, so it translates - a flat footprint still follows its drag.
public static ArchRemap Between( Vector2 fromMin, Vector2 fromMax, Vector2 toMin, Vector2 toMax ) {
var span = fromMax - fromMin;
var wanted = toMax - toMin;
return new ArchRemap( fromMin, new Vector2( Axis( span.x, wanted.x ), Axis( span.y, wanted.y ) ), toMin );
}
static float Axis( float span, float wanted ) => MathF.Abs( span ) < 0.01f ? 1f : wanted / span;
static float Wrapped( float degrees ) {
var turn = degrees % 360f;
return turn < 0f ? turn + 360f : turn;
}
public float Degrees => degrees;
public int Quarters => (int)MathF.Round( degrees / 90f ) % 4;
// Right-angle maps are exact; non-90 requires trig and cannot preserve axis-aligned bounds.
public bool KeepsAxes => MathF.Abs( degrees - MathF.Round( degrees / 90f ) * 90f ) < 0.001f;
public bool SwapsAxes => KeepsAxes && Quarters % 2 == 1;
// Exact swaps, no trig — a sine rotation drifts grid coordinates off the grid.
public static Vector2 Spun( Vector2 offset, int quarters ) => (((quarters % 4) + 4) % 4) switch {
1 => new Vector2( -offset.y, offset.x ),
2 => new Vector2( -offset.x, -offset.y ),
3 => new Vector2( offset.y, -offset.x ),
_ => offset
};
public Vector2 Spun( Vector2 direction ) => KeepsAxes
? Spun( direction, Quarters )
: ArchFootprint.Turned( direction, Vector2.Zero, degrees );
public float Spun( float yaw ) => yaw + degrees;
// Centre travels, size kept (swapped on odd quarters).
public void Centred( ref Vector2 min, ref Vector2 max ) {
var size = Swapped( max - min );
var at = Of( (min + max) * 0.5f );
min = at - size * 0.5f;
max = at + size * 0.5f;
}
public bool Axis( bool alongX ) {
if ( KeepsAxes ) {
return alongX ^ SwapsAxes;
}
var along = Spun( alongX ? new Vector2( 1f, 0f ) : new Vector2( 0f, 1f ) );
return MathF.Abs( along.x ) >= MathF.Abs( along.y );
}
public Vector2 Swapped( Vector2 pair ) => SwapsAxes ? new Vector2( pair.y, pair.x ) : pair;
public Vector2 Of( Vector2 point ) => to + Spun( (point - from) * scale );
public Vector3 Of( Vector3 point ) {
var flat = Of( new Vector2( point.x, point.y ) );
return new Vector3( flat.x, flat.y, point.z );
}
// Min/Max pairs must stay ordered: a negative scale would otherwise hand back an inside-out box.
public void Box( ref Vector2 min, ref Vector2 max ) {
var first = Of( min );
var second = Of( max );
min = Vector2.Min( first, second );
max = Vector2.Max( first, second );
}
public void Loop( List<Vector2> loop ) {
for ( var index = 0; index < loop.Count; index++ ) {
loop[index] = Of( loop[index] );
}
}
public void Path( List<Vector3> path ) {
for ( var index = 0; index < path.Count; index++ ) {
path[index] = Of( path[index] );
}
}
public void Nodes( List<ArchCurveNode> nodes ) {
foreach ( var node in nodes ) {
node.Position = Of( node.Position );
}
}
}