Phase 14 - Organized directories.
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
@tool
|
||||
class_name TerrainBlock
|
||||
extends StaticBody2D
|
||||
## TerrainBlock - Reusable StaticBody2D vector-terrain component (Phase 1).
|
||||
##
|
||||
## A self-contained terrain block: a StaticBody2D root with three children built
|
||||
## in code — a Polygon2D (interior fill), a Line2D (crisp vector outline, closed
|
||||
## by appending the first vertex, rounded joints/caps), and a CollisionPolygon2D
|
||||
## configured with BUILD_SOLIDS so concave terrain blocks collide correctly.
|
||||
## Fully @tool: exported properties update the children live in the editor.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Child node names
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
const POLYGON_NODE_NAME := "Polygon2D"
|
||||
const OUTLINE_NODE_NAME := "Outline"
|
||||
const COLLISION_NODE_NAME := "CollisionPolygon2D"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Exported properties
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## The terrain polygon's vertices (local space). The unified setter pushes the
|
||||
## array to the Polygon2D and CollisionPolygon2D, and closes the Line2D loop by
|
||||
## appending the first vertex to the end.
|
||||
@export var polygon_points: PackedVector2Array = PackedVector2Array():
|
||||
set(value):
|
||||
polygon_points = value
|
||||
_apply_points()
|
||||
|
||||
## Interior fill color (Polygon2D).
|
||||
@export var fill_color: Color = Color(0.25, 0.55, 0.25, 1.0):
|
||||
set(value):
|
||||
fill_color = value
|
||||
if _polygon != null:
|
||||
_polygon.color = value
|
||||
|
||||
## Outline color (Line2D).
|
||||
@export var outline_color: Color = Color(0.05, 0.10, 0.05, 1.0):
|
||||
set(value):
|
||||
outline_color = value
|
||||
if _outline != null:
|
||||
_outline.default_color = value
|
||||
|
||||
## Outline width in pixels (Line2D).
|
||||
@export var outline_width: float = 2.0:
|
||||
set(value):
|
||||
outline_width = value
|
||||
if _outline != null:
|
||||
_outline.width = value
|
||||
|
||||
## Registry id of the spawn template that produced this block (Phase 4b). Used
|
||||
## by the terrain drag-painting three-state overlap query to detect same-type
|
||||
## overlaps. Plain var (not inspector-editable).
|
||||
var spawn_id: String = ""
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal node references (built in _ready, @tool-safe)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
var _polygon: Polygon2D
|
||||
var _outline: Line2D
|
||||
var _collision: CollisionPolygon2D
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lifecycle
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _ready() -> void:
|
||||
_ensure_children()
|
||||
_apply_points()
|
||||
_apply_style()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal build / apply
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _ensure_children() -> void:
|
||||
_polygon = get_node_or_null(NodePath(POLYGON_NODE_NAME)) as Polygon2D
|
||||
if _polygon == null:
|
||||
_polygon = Polygon2D.new()
|
||||
_polygon.name = POLYGON_NODE_NAME
|
||||
add_child(_polygon)
|
||||
|
||||
_outline = get_node_or_null(NodePath(OUTLINE_NODE_NAME)) as Line2D
|
||||
if _outline == null:
|
||||
_outline = Line2D.new()
|
||||
_outline.name = OUTLINE_NODE_NAME
|
||||
_outline.joint_mode = Line2D.LINE_JOINT_ROUND
|
||||
_outline.begin_cap_mode = Line2D.LINE_CAP_ROUND
|
||||
_outline.end_cap_mode = Line2D.LINE_CAP_ROUND
|
||||
add_child(_outline)
|
||||
|
||||
_collision = get_node_or_null(NodePath(COLLISION_NODE_NAME)) as CollisionPolygon2D
|
||||
if _collision == null:
|
||||
_collision = CollisionPolygon2D.new()
|
||||
_collision.name = COLLISION_NODE_NAME
|
||||
_collision.build_mode = CollisionPolygon2D.BUILD_SOLIDS
|
||||
add_child(_collision)
|
||||
|
||||
|
||||
## Pushes the current polygon_points to the Polygon2D and CollisionPolygon2D,
|
||||
## and appends the first vertex to the Line2D to close the border loop.
|
||||
func _apply_points() -> void:
|
||||
if _polygon != null:
|
||||
_polygon.polygon = polygon_points
|
||||
if _collision != null:
|
||||
_collision.polygon = polygon_points if polygon_points.size() >= 3 else PackedVector2Array()
|
||||
if _outline != null:
|
||||
var outline_points := polygon_points.duplicate()
|
||||
if not outline_points.is_empty():
|
||||
outline_points.append(polygon_points[0])
|
||||
_outline.points = outline_points
|
||||
|
||||
|
||||
func _apply_style() -> void:
|
||||
if _polygon != null:
|
||||
_polygon.color = fill_color
|
||||
if _outline != null:
|
||||
_outline.default_color = outline_color
|
||||
_outline.width = outline_width
|
||||
@@ -0,0 +1 @@
|
||||
uid://bw31hvuc1uako
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://cduiqhk7xpm4m
|
||||
Reference in New Issue
Block a user