142 lines
4.9 KiB
GDScript
142 lines
4.9 KiB
GDScript
@tool
|
|
extends Path2D
|
|
## A custom node that extends Path2D so it can be drawn as a Line2D
|
|
## Original adapted code: https://www.hedberggames.com/blog/rendering-curves-in-godot
|
|
## @deprecated: Use [ScalableVectorShape2D] instead.
|
|
class_name DrawablePath2D
|
|
|
|
## Emitted when a new set of points was calculated for a connected Line2D, Polygon2D, or CollisionPolygon2D
|
|
signal path_changed(new_points : PackedVector2Array)
|
|
|
|
## This signal is used internally in editor-mode to tell the DrawablePath2D tool that
|
|
## the instance of assigned Line2D, Polygon2D, or CollisionPolygon2D has changed
|
|
signal assigned_node_changed()
|
|
|
|
|
|
## The Polygon2D controlled by this Path2D
|
|
@export var polygon: Polygon2D:
|
|
set(_poly):
|
|
polygon = _poly
|
|
assigned_node_changed.emit()
|
|
|
|
## The Line2D controlled by this Path2D
|
|
@export var line: Line2D:
|
|
set(_line):
|
|
line = _line
|
|
assigned_node_changed.emit()
|
|
|
|
## The CollisionPolygon2D controlled by this Path2D
|
|
@export var collision_polygon: CollisionPolygon2D:
|
|
set(_poly):
|
|
collision_polygon = _poly
|
|
assigned_node_changed.emit()
|
|
|
|
## Controls whether the path is treated as static (only update in editor) or dynamic (can be updated during runtime)
|
|
## If you set this to true, be alert for potential performance issues
|
|
@export var update_curve_at_runtime: bool = false
|
|
|
|
## Controls the paramaters used to divide up the line in segments.
|
|
## These settings are prefilled with the default values.
|
|
@export_group("Tesselation settings")
|
|
## Controls how many subdivisions a curve segment may face before it is considered approximate enough.
|
|
## Each subdivision splits the segment in half, so the default 5 stages may mean up to 32 subdivisions
|
|
## per curve segment. Increase with care!
|
|
@export_range(1, 10) var max_stages : int = 5:
|
|
set(_max_stages):
|
|
max_stages = _max_stages
|
|
assigned_node_changed.emit()
|
|
## Controls how many degrees the midpoint of a segment may deviate from the real curve, before the
|
|
## segment has to be subdivided.
|
|
@export_range(0.0, 180.0) var tolerance_degrees := 4.0:
|
|
set(_tolerance_degrees):
|
|
tolerance_degrees = _tolerance_degrees
|
|
assigned_node_changed.emit()
|
|
|
|
var lock_assigned_shapes := true
|
|
|
|
# Wire up signals at runtime
|
|
func _ready():
|
|
if update_curve_at_runtime:
|
|
if not curve.changed.is_connected(curve_changed):
|
|
curve.changed.connect(curve_changed)
|
|
|
|
|
|
# Wire up signals on enter tree for the editor
|
|
func _enter_tree():
|
|
if Engine.is_editor_hint():
|
|
if not curve.changed.is_connected(curve_changed):
|
|
curve.changed.connect(curve_changed)
|
|
if not assigned_node_changed.is_connected(_on_assigned_node_changed):
|
|
assigned_node_changed.connect(_on_assigned_node_changed)
|
|
# handles update when reparenting
|
|
if update_curve_at_runtime:
|
|
if not curve.changed.is_connected(curve_changed):
|
|
curve.changed.connect(curve_changed)
|
|
|
|
|
|
# Clean up signals (ie. when closing scene) to prevent error messages in the editor
|
|
func _exit_tree():
|
|
if curve.changed.is_connected(curve_changed):
|
|
curve.changed.disconnect(curve_changed)
|
|
|
|
|
|
func _on_assigned_node_changed():
|
|
if is_instance_valid(line):
|
|
if lock_assigned_shapes:
|
|
line.set_meta("_edit_lock_", true)
|
|
curve_changed()
|
|
if is_instance_valid(polygon):
|
|
if lock_assigned_shapes:
|
|
polygon.set_meta("_edit_lock_", true)
|
|
curve_changed()
|
|
if is_instance_valid(collision_polygon):
|
|
if lock_assigned_shapes:
|
|
collision_polygon.set_meta("_edit_lock_", true)
|
|
curve_changed()
|
|
|
|
|
|
# Redraw the line based on the new curve, using its tesselate method
|
|
func curve_changed():
|
|
if (not is_instance_valid(line) and not is_instance_valid(polygon)
|
|
and not is_instance_valid(collision_polygon)
|
|
and not path_changed.has_connections()):
|
|
# guard against needlessly invoking expensive tesselate operation
|
|
return
|
|
|
|
var new_points := curve.tessellate(max_stages, tolerance_degrees)
|
|
# Fixes cases start- and end-node are so close to each other that
|
|
# polygons won't fill and closed lines won't cap nicely
|
|
if new_points[0].distance_to(new_points[new_points.size()-1]) < 0.001:
|
|
new_points.remove_at(new_points.size() - 1)
|
|
if is_instance_valid(line):
|
|
line.points = new_points
|
|
if is_instance_valid(polygon):
|
|
polygon.polygon = new_points
|
|
if is_instance_valid(collision_polygon):
|
|
collision_polygon.polygon = new_points
|
|
path_changed.emit(new_points)
|
|
|
|
|
|
func get_bounding_rect() -> Rect2:
|
|
var points := curve.tessellate(max_stages, tolerance_degrees)
|
|
if points.size() < 1:
|
|
# Cannot calculate a center for 0 points
|
|
return Rect2(Vector2.ZERO, Vector2.ZERO)
|
|
var minx := INF
|
|
var miny := INF
|
|
var maxx := -INF
|
|
var maxy := -INF
|
|
for p : Vector2 in points:
|
|
minx = p.x if p.x < minx else minx
|
|
miny = p.y if p.y < miny else miny
|
|
maxx = p.x if p.x > maxx else maxx
|
|
maxy = p.y if p.y > maxy else maxy
|
|
return Rect2(minx, miny, maxx - minx, maxy - miny)
|
|
|
|
|
|
func set_position_to_center() -> void:
|
|
var c = get_bounding_rect().get_center()
|
|
position += c
|
|
for i in range(curve.get_point_count()):
|
|
curve.set_point_position(i, curve.get_point_position(i) - c)
|