class_name PropVisuals extends RefCounted ## Shared custom-prop drawing and collision approximation. const CIRCLE_SEGMENTS: int = 24 static func build(parent: Node2D, shapes: Array[Dictionary]) -> Rect2: var bounds := Rect2() var has_bounds := false for index: int in shapes.size(): var shape: Dictionary = shapes[index] var points := _shape_points(shape) if points.size() < 2: push_warning("PropVisuals: skipped shape with insufficient points.") continue var group := Node2D.new() group.name = "Shape_%d" % index parent.add_child(group) var closed := bool(shape.get("closed", String(shape.get("shape_type", "")) != "line")) var fill := Color.from_string(String(shape.get("fill_color", "")), Color.WHITE) var outline := Color.from_string(String(shape.get("outline_color", "")), Color.BLACK) var width: float = maxf(0.0, float(shape.get("outline_width", 2.0))) if closed and points.size() >= 3: var polygon := Polygon2D.new() polygon.name = "Polygon2D" polygon.polygon = points polygon.color = fill group.add_child(polygon) var line := Line2D.new() line.name = "Line2D" var loop: PackedVector2Array = points.duplicate() if closed: loop.append(points[0]) line.points = loop line.default_color = outline line.width = width line.joint_mode = Line2D.LINE_JOINT_ROUND line.begin_cap_mode = Line2D.LINE_CAP_ROUND line.end_cap_mode = Line2D.LINE_CAP_ROUND group.add_child(line) for point: Vector2 in points: if not has_bounds: bounds = Rect2(point, Vector2.ZERO) has_bounds = true else: bounds = bounds.expand(point) return bounds static func collision_hull(shapes: Array[Dictionary]) -> PackedVector2Array: var points := PackedVector2Array() for shape: Dictionary in shapes: var closed := bool(shape.get("closed", String(shape.get("shape_type", "")) != "line")) if closed: points.append_array(_shape_points(shape)) if points.size() < 3: return PackedVector2Array() var hull: PackedVector2Array = Geometry2D.convex_hull(points) if hull.size() < 3 or absf(_polygon_area(hull)) <= 0.001: return PackedVector2Array() return hull static func _shape_points(shape: Dictionary) -> PackedVector2Array: var raw: Variant = shape.get("points", PackedVector2Array()) var points := PackedVector2Array() if raw is PackedVector2Array: points = raw as PackedVector2Array elif raw is Array: for item: Variant in raw as Array: if item is Vector2: points.append(item as Vector2) elif item is Dictionary: var data: Dictionary = item as Dictionary points.append(Vector2(float(data.get("x", 0.0)), float(data.get("y", 0.0)))) if points.is_empty() and String(shape.get("shape_type", "")) == "circle": var radius: float = maxf(0.0, float(shape.get("radius", 24.0))) for index: int in CIRCLE_SEGMENTS: var angle: float = TAU * float(index) / float(CIRCLE_SEGMENTS) points.append(Vector2(cos(angle), sin(angle)) * radius) return points static func _polygon_area(points: PackedVector2Array) -> float: var area: float = 0.0 for index: int in points.size(): var next: Vector2 = points[(index + 1) % points.size()] area += points[index].cross(next) return area * 0.5