Editor service that handles grid snapping and sizing for the scene editor. It provides methods to snap coordinates to a base grid, subgrid and finest granularity, compute subgrid sizes, rectangle bounds, stepped movement and divide spans into subdivisions.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// The grid is the EDITOR'S — the one drawn in the viewport, with the spacing and the snap toggle already sitting
// on the scene view's own bar. The tool held a second one of its own, which meant two answers to one question and
// a cursor that could land on a rung nothing had drawn.
//
// Sizes always answer, because a reach or a minimum run is a measurement and does not stop being one when snapping
// is off. Only a COORDINATE asks whether to snap, and it asks here rather than at any call site.
public sealed class ArchGridService
{
public const float FinestSize = 0.25f;
// The angle ladder every authored turn ratchets onto. It stands beside the size ladder because stepping and
// snapping are one invariant, and a module choosing its own would author off the grid with nothing to say so.
public const float AngleStep = 15f;
public static bool Snapping => EditorScene.GizmoSettings.SnapToGrid;
public float BaseSize => Math.Clamp( EditorScene.GizmoSettings.GridSpacing, FinestSize, 128f );
public float Base( float value )
{
return Snapping ? Snap( value, BaseSize ) : value;
}
public Vector2 Base( Vector2 point )
{
return Snapping ? Snap( point, BaseSize ) : point;
}
public Vector3 Base( Vector3 point )
{
return Snapping ? Snap( point, BaseSize ) : point;
}
public float Subgrid( float value, int divisions = 8 )
{
return Snapping ? Snap( value, SubgridSize( divisions ) ) : value;
}
public Vector2 Subgrid( Vector2 point, int divisions = 8 )
{
return Snapping ? Snap( point, SubgridSize( divisions ) ) : point;
}
public Vector3 Subgrid( Vector3 point, int divisions = 8 )
{
return Snapping ? Snap( point, SubgridSize( divisions ) ) : point;
}
public Vector3 CurveControl( Vector3 point )
{
var flat = Base( new Vector2( point.x, point.y ) );
return new Vector3( flat.x, flat.y, Height( point.z ) );
}
// A control DRAPED on ground: the flat lands on the ladder and the height does not, because a height read off
// the terrain is measured rather than authored - rung it and the run sinks into the ground it was laid on.
public Vector3 Draped( Vector3 point )
{
var flat = Base( new Vector2( point.x, point.y ) );
return new Vector3( flat.x, flat.y, point.z );
}
// No second grid for z - the same ladder a plan coordinate lands on.
public float Height( float value )
{
return Base( value );
}
public List<Vector2> Base( IEnumerable<Vector2> points )
{
return points.Select( Base ).ToList();
}
public (Vector2 Min, Vector2 Max) Rectangle( Vector2 first, Vector2 second )
{
var min = Vector2.Min( first, second );
var max = Vector2.Max( first, second );
return (Base( min ), Base( max ));
}
public float SubgridSize( int divisions = 8 )
{
var count = PowerOfTwo( Math.Max( 1, divisions ) );
return MathF.Max( FinestSize, BaseSize / count );
}
public ArchDivision DivideAtMost( float span, float spacing, int divisions = 8 )
{
var length = MathF.Abs( Subgrid( span, divisions ) );
if ( length < FinestSize )
{
return new ArchDivision { Span = 0f, Count = 0 };
}
var unit = SubgridSize( divisions );
var units = Math.Max( 1, (int)MathF.Round( length / unit ) );
var minimum = Math.Max( 1, (int)MathF.Ceiling( length / MathF.Max( unit, spacing ) ) );
var count = minimum;
while ( count < units && units % count != 0 )
{
count++;
}
return new ArchDivision { Span = length, Count = Math.Min( count, units ) };
}
public static float Fine( float value )
{
return Snap( value, FinestSize );
}
public static Vector2 Fine( Vector2 point )
{
return Snap( point, FinestSize );
}
public static Vector3 Fine( Vector3 point )
{
return Snap( point, FinestSize );
}
public static float Snap( float value, float size )
{
return MathF.Round( value / size ) * size;
}
// Where a DRAGGED edit lands: the step is what goes on the ladder, never the coordinate. An existing shape is
// wherever it was authored - a turned loop's corners are nowhere near a rung, and a cut's band is wherever it
// was pulled to - so snapping the absolute coordinate yanks the whole thing onto the nearest one the moment a
// handle touches it. On a base grid of 64 a nudge along x dropped a cut standing at -40 straight to 0, which
// is the shape jumping a storey for a gesture that never touched its height. Placement still snaps outright:
// a NEW shape is authored on the grid, and an edit keeps the offset it already had.
public static float Stepped( float from, float to, float size )
{
return from + Snap( to - from, size );
}
public static Vector2 Stepped( Vector2 from, Vector2 to, float size )
{
return new Vector2( Stepped( from.x, to.x, size ), Stepped( from.y, to.y, size ) );
}
static Vector2 Snap( Vector2 point, float size )
{
return new Vector2( Snap( point.x, size ), Snap( point.y, size ) );
}
static Vector3 Snap( Vector3 point, float size )
{
return new Vector3( Snap( point.x, size ), Snap( point.y, size ), Snap( point.z, size ) );
}
static int PowerOfTwo( int value )
{
var result = 1;
while ( result < value )
{
result *= 2;
}
return result;
}
}