feat: Add project roadmap and initial specifications for dynamic vector props and terrain system
- Created ROADMAP.md outlining core features, implementation phases, and detailed breakdown for the Stickman Sandbox Builder project. - Introduced plans for dynamic vector props with `PropBlock` specification, including reusable components and utility functions for prop creation. - Developed a vector terrain system plan detailing the reusable `TerrainBlock` component and associated utility functions for geometry handling. - Implemented `PhysicsTestHarness` scene for testing physics interactions with dynamic props and terrain. - Added scripts for `PropBlock`, `PropUtils`, `TerrainBlock`, and `TerrainUtils` to support dynamic prop creation and terrain management.
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
class_name TerrainUtils
|
||||
extends RefCounted
|
||||
## TerrainUtils - Static geometry engine for vector terrain (Phase 3).
|
||||
##
|
||||
## Two responsibilities: sanitize raw input points into a clean, grid-snapped,
|
||||
## clockwise-ordered polygon; and spawn a configured TerrainBlock into a target
|
||||
## container. Consumed by the standalone physics test harness; not referenced by
|
||||
## the editor.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Constants
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
const DEFAULT_GRID_SIZE: float = 16.0
|
||||
|
||||
const DEFAULT_FILL_COLOR := Color(0.25, 0.55, 0.25, 1.0)
|
||||
const DEFAULT_OUTLINE_COLOR := Color(0.05, 0.10, 0.05, 1.0)
|
||||
const DEFAULT_OUTLINE_WIDTH: float = 2.0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Point sanitization
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Snap raw points to a grid, drop redundant nodes, and enforce clockwise
|
||||
## winding (required for solid collision decomposition). Returns a new array;
|
||||
## the input is not modified.
|
||||
static func sanitize_points(points: PackedVector2Array, grid_size: float = DEFAULT_GRID_SIZE) -> PackedVector2Array:
|
||||
var cleaned := PackedVector2Array()
|
||||
for p: Vector2 in points:
|
||||
cleaned.append(_snap_to_grid(p, grid_size))
|
||||
cleaned = _simplify_polyline(cleaned)
|
||||
if cleaned.size() >= 3 and not Geometry2D.is_polygon_clockwise(cleaned):
|
||||
cleaned.reverse()
|
||||
return cleaned
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Factory spawner
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Sanitize `points`, create a new TerrainBlock, apply the cleaned points and
|
||||
## styling, and add it to `container`. Returns the spawned block.
|
||||
static func spawn_block(
|
||||
container: Node,
|
||||
points: PackedVector2Array,
|
||||
grid_size: float = DEFAULT_GRID_SIZE,
|
||||
fill_color: Color = DEFAULT_FILL_COLOR,
|
||||
outline_color: Color = DEFAULT_OUTLINE_COLOR,
|
||||
outline_width: float = DEFAULT_OUTLINE_WIDTH
|
||||
) -> TerrainBlock:
|
||||
var block := TerrainBlock.new()
|
||||
block.name = "TerrainBlock"
|
||||
block.polygon_points = sanitize_points(points, grid_size)
|
||||
block.fill_color = fill_color
|
||||
block.outline_color = outline_color
|
||||
block.outline_width = outline_width
|
||||
container.add_child(block)
|
||||
return block
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
static func _snap_to_grid(p: Vector2, grid_size: float) -> Vector2:
|
||||
if grid_size <= 0.0:
|
||||
return p
|
||||
return Vector2(roundf(p.x / grid_size) * grid_size, roundf(p.y / grid_size) * grid_size)
|
||||
|
||||
|
||||
## Remove redundant nodes: consecutive duplicates (e.g. collapsed by grid
|
||||
## snapping) and collinear middle vertices (redundant for both render and
|
||||
## collision). NOTE: Geometry2D.simplify_polyline() does not exist in Godot
|
||||
## 4.7.1, so this local pass stands in for it.
|
||||
static func _simplify_polyline(points: PackedVector2Array) -> PackedVector2Array:
|
||||
var deduped := PackedVector2Array()
|
||||
for p: Vector2 in points:
|
||||
if not deduped.is_empty() and deduped[deduped.size() - 1].is_equal_approx(p):
|
||||
continue
|
||||
deduped.append(p)
|
||||
|
||||
# Drop a closing duplicate (last vertex == first vertex). Polygons are
|
||||
# treated as open here; otherwise the collinear pass below would treat the
|
||||
# first/last vertices as "collinear" with each other and erase the first
|
||||
# corner, producing a degenerate polygon.
|
||||
if deduped.size() >= 2 and deduped[0].is_equal_approx(deduped[deduped.size() - 1]):
|
||||
deduped.remove_at(deduped.size() - 1)
|
||||
|
||||
if deduped.size() < 3:
|
||||
return deduped
|
||||
|
||||
var simplified := PackedVector2Array()
|
||||
var n: int = deduped.size()
|
||||
for i: int in n:
|
||||
var prev := deduped[(i - 1 + n) % n]
|
||||
var curr := deduped[i]
|
||||
var next := deduped[(i + 1) % n]
|
||||
if _is_collinear(prev, curr, next):
|
||||
continue
|
||||
simplified.append(curr)
|
||||
|
||||
return simplified if simplified.size() >= 3 else deduped
|
||||
|
||||
|
||||
static func _is_collinear(a: Vector2, b: Vector2, c: Vector2) -> bool:
|
||||
return absf((b - a).cross(c - b)) < 0.001
|
||||
Reference in New Issue
Block a user