[Tool]
public partial class Grid : Area3D
{
public const float StepSize = 0.05f; // 5cm
const float Offset = 0.001f;
const int ThickLineEvery = 4;
const float ThickLineWidth = 0.006f;
const float ThinLineWidth = 0.004f;
static readonly Color ThickLineColor = new(0.25f, 0.25f, 0.25f);
static readonly Color ThinLineColor = new(0.35f, 0.35f, 0.35f);
// TODO: Make GridSize be three-dimensional?
Vector2I _gridSize = new(16, 16);
Vector3 _halfGridActualSize = new(8.0f, 0.0f, 8.0f);
[Export] public Vector2I GridSize {
get => _gridSize;
set {
_gridSize = value;
// Helper value for converting grid pos to local pos and back.
_halfGridActualSize = new Vector3(GridSize.X, 0, GridSize.Y) * (StepSize / 2.0f);
Update();
}
}
public override void _Ready()
=> Update();
void Update()
{
UpdateCollisionShape();
UpdateImmediateMesh();
}
/// Returns whether the specified item is contained in this or any nested grids.
public bool ContainsItem(Item item)
{
while (item?.GetParentOrNull() is Grid grid) {
if (grid == this) return true;
item = grid.GetParent() as Item;
}
return false;
}
public Vector3I LocalToGrid(Vector3 pos)
=> (Vector3I)((pos + _halfGridActualSize) / StepSize);
public Vector3 GridToLocal(Vector3I pos)
=> (pos + Vector3.One * 0.5f) * StepSize - _halfGridActualSize;
public Vector3I GlobalToGrid(Vector3 pos)
=> LocalToGrid(ToLocal(pos));
public Vector3 GridToGlobal(Vector3I pos)
=> ToGlobal(GridToLocal(pos));
public static Aabb LocalItemTransformToLocalAabb(Transform3D transform, Item item)
{
var bounds = (transform.Basis * ((Vector3)item.Size * StepSize)).Abs();
return new(transform.Origin - bounds / 2, bounds);
}
///
/// Returns whether the item can be placed at the specified global transform.
/// The transform needs to be grid-aligned, such as by calling .
///
public bool CanPlaceAt(Item item, Transform3D transform)
{
transform = GlobalTransform.AffineInverse() * transform;
var region = LocalItemTransformToLocalAabb(transform, item).Grow(-0.01f);
foreach (var other in GetChildren().OfType- ())
if (region.Intersects(LocalItemTransformToLocalAabb(other.Transform, other)))
return false;
return true;
}
///
/// Snaps a global transform to line up with the grid.
/// The transform will be "pushed out" into the normal vector's
/// direction according to the current rotation and item's size.
///
public Transform3D Snap(Transform3D transform, Vector3 normal, Item item)
{
var halfSize = (Vector3)item.Size * StepSize / 2;
// Get grid-local values of the global transform and normal.
var inverse = GlobalTransform.AffineInverse();
var pos = inverse * transform.Origin;
var basis = inverse.Basis * transform.Basis;
normal = inverse.Basis * normal;
// Snap rotation to nearest axis.
basis = Basis.FromEuler(basis.GetEuler().Snapped(Tau / 4));
// Offset / "push out" by half of the item's size.
var axis = (normal * basis).Abs().MaxAxisIndex();
pos += halfSize[(int)axis] * normal;
// Snap the position to the grid.
var halfOff = (basis * halfSize + _halfGridActualSize).PosMod(1.0f);
pos = halfOff + (pos - halfOff).Snapped(StepSize);
return new(GlobalBasis * basis, GlobalTransform * pos);
}
static StandardMaterial3D _material;
static StandardMaterial3D GetOrCreateMaterial()
{
if (_material == null) {
_material = new StandardMaterial3D();
_material.VertexColorUseAsAlbedo = true;
}
return _material;
}
ConvexPolygonShape3D _shape;
void UpdateCollisionShape()
{
if (Engine.IsEditorHint()) return;
if (_shape == null) {
_shape = new ConvexPolygonShape3D();
AddChild(new CollisionShape3D { Shape = _shape }, true);
}
const float Offset = 0.001f;
var x = GridSize.X * StepSize / 2;
var y = GridSize.Y * StepSize / 2;
_shape.Points = [
new(-x, Offset, -y),
new( x, Offset, -y),
new( x, Offset, y),
new(-x, Offset, y),
];
}
ImmediateMesh _mesh;
void UpdateImmediateMesh()
{
static bool IsThickLine(int line)
=> (line % ThickLineEvery) == 0;
static (float, Color) GetLineWidthAndColor(int line)
=> IsThickLine(line) ? (ThickLineWidth, ThickLineColor) : (ThinLineWidth, ThinLineColor);
static void FlatQuad(ImmediateMesh mesh, float x1, float x2, float y1, float y2) {
mesh.SurfaceAddVertex(new(x1, Offset, y1)); // 1--2
mesh.SurfaceAddVertex(new(x2, Offset, y1)); // | /
mesh.SurfaceAddVertex(new(x1, Offset, y2)); // 3'
mesh.SurfaceAddVertex(new(x2, Offset, y1)); // .1
mesh.SurfaceAddVertex(new(x2, Offset, y2)); // / |
mesh.SurfaceAddVertex(new(x1, Offset, y2)); // 3--2
}
_mesh = (ImmediateMesh)GetNodeOrNull("MeshInstance3D")?.Mesh;
if (_mesh == null) {
var meshInstance = new MeshInstance3D();
meshInstance.Mesh = _mesh = new();
AddChild(meshInstance, true);
} else
_mesh.ClearSurfaces();
_mesh.SurfaceBegin(Mesh.PrimitiveType.Triangles);
// Horizontal Lines
var half_width = GridSize.X * StepSize / 2;
for (var line = 0; line <= GridSize.Y; line++) {
var (line_width, line_color) = GetLineWidthAndColor(line);
var y = StepSize * (line - GridSize.Y / 2.0f);
_mesh.SurfaceSetColor(line_color);
FlatQuad(_mesh, -half_width, +half_width, y - line_width / 2, y + line_width / 2);
}
// Vertical Lines
var half_depth = GridSize.Y * StepSize / 2;
for (var line = 0; line <= GridSize.X; line++) {
var (line_width, line_color) = GetLineWidthAndColor(line);
var x = StepSize * (line - GridSize.X / 2.0f);
_mesh.SurfaceSetColor(line_color);
FlatQuad(_mesh, x - line_width / 2, x + line_width / 2, -half_depth, +half_depth);
}
_mesh.SurfaceEnd();
_mesh.SurfaceSetMaterial(0, GetOrCreateMaterial());
}
}