[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 if the item could be considered for placement /// at the specified local position and normal. /// public bool CanPlaceAgainst(Item item, Vector3 position, Vector3 normal) { return normal.IsEqualApprox(Vector3.Up); } /// /// Returns whether the item can be placed at the specified local transform. /// The transform needs to be grid-aligned, such as by calling . /// public bool CanPlaceAt(Item item, Transform3D 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 local 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) { // Snap rotation to nearest axis. transform.Basis = Basis.FromEuler(transform.Basis.GetEuler().Snapped(Tau / 4)); // Offset / "push out" by half of the item's size. var halfSize = (Vector3)item.Size * StepSize / 2; var axis = (normal * transform.Basis).Abs().MaxAxisIndex(); transform.Origin += halfSize[(int)axis] * normal; // Snap the position to the grid. var halfOff = (transform.Basis * halfSize + _halfGridActualSize).PosMod(1.0f); transform.Origin = halfOff + (transform.Origin - halfOff).Snapped(StepSize); return transform; } static StandardMaterial3D _material; static StandardMaterial3D GetOrCreateMaterial() => _material ??= new() { VertexColorUseAsAlbedo = true }; 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()); } }