You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
171 lines
5.3 KiB
171 lines
5.3 KiB
[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(); |
|
} |
|
|
|
|
|
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); |
|
} |
|
|
|
|
|
/// <summary> |
|
/// Returns whether the item can be placed at the specified global transform. |
|
/// The transform needs to be grid-aligned, such as by calling <see cref="Snap"/>. |
|
/// </summary> |
|
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<Item>()) |
|
if (region.Intersects(LocalItemTransformToLocalAabb(other.Transform, other))) |
|
return false; |
|
return true; |
|
} |
|
|
|
/// <summary> |
|
/// 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. |
|
/// </summary> |
|
public Transform3D Snap(Transform3D transform, Vector3 normal, Item item) |
|
{ |
|
var inverseTransform = GlobalTransform.AffineInverse(); |
|
var inverseBasis = GlobalBasis.Inverse(); |
|
var halfSize = (Vector3)item.Size * StepSize / 2; |
|
|
|
// Get grid-local values of the global transform and normal. |
|
var pos = inverseTransform * transform.Origin; |
|
var basis = inverseBasis * transform.Basis; |
|
normal = inverseBasis * 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 offsetAxis = (int)(normal * basis).Abs().MaxAxisIndex(); |
|
pos += halfSize[offsetAxis] * normal; |
|
|
|
// Snap the position to the grid. |
|
var halfOff = basis * halfSize.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; |
|
} |
|
|
|
BoxShape3D _shape; |
|
void UpdateCollisionShape() |
|
{ |
|
if (Engine.IsEditorHint()) return; |
|
|
|
if (_shape == null) { |
|
_shape = new BoxShape3D(); |
|
AddChild(new CollisionShape3D { Shape = _shape }, true); |
|
} |
|
|
|
_shape.Size = new(GridSize.X * StepSize, 0.001f, GridSize.Y * StepSize); |
|
} |
|
|
|
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>("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()); |
|
} |
|
}
|
|
|