Files
fps_project_1/addons/proton_scatter/src/shapes/path_shape.gd

250 lines
5.7 KiB
GDScript

@tool
class_name ProtonScatterPathShape
extends ProtonScatterBaseShape
const Bounds := preload("../common/bounds.gd")
@export var closed := true:
set(val):
closed = val
emit_changed()
@export var thickness := 0.0:
set(val):
thickness = max(0, val) # Width cannot be negative
_half_thickness_squared = pow(thickness * 0.5, 2)
emit_changed()
@export var curve: Curve3D:
set(val):
# Disconnect previous signal
if curve and curve.changed.is_connected(_on_curve_changed):
curve.changed.disconnect(_on_curve_changed)
curve = val
curve.changed.connect(_on_curve_changed)
emit_changed()
var _polygon: PolygonPathFinder
var _half_thickness_squared: float
var _bounds: Bounds
func is_point_inside(point: Vector3, global_transform: Transform3D) -> bool:
if not _polygon:
_update_polygon_from_curve()
if not _polygon:
return false
point = global_transform.affine_inverse() * point
if thickness > 0:
var closest_point_on_curve: Vector3 = curve.get_closest_point(point)
var dist2 = closest_point_on_curve.distance_squared_to(point)
if dist2 < _half_thickness_squared:
return true
if closed:
return _polygon.is_point_inside(Vector2(point.x, point.z))
return false
func get_corners_global(gt: Transform3D) -> Array:
var res := []
if not curve:
return res
var half_thickness = thickness * 0.5
var corners = [
Vector3(-1, -1, -1),
Vector3(1, -1, -1),
Vector3(1, -1, 1),
Vector3(-1, -1, 1),
Vector3(-1, 1, -1),
Vector3(1, 1, -1),
Vector3(1, 1, 1),
Vector3(-1, 1, 1),
]
var points = curve.tessellate(3, 10)
for p in points:
res.push_back(gt * p)
if thickness > 0:
for offset in corners:
res.push_back(gt * (p + offset * half_thickness))
return res
func get_bounds() -> Bounds:
if not _bounds:
_update_polygon_from_curve()
return _bounds
func get_copy():
var copy = get_script().new()
copy.thickness = thickness
copy.closed = closed
if curve:
copy.curve = curve.duplicate()
return copy
func copy_from(source) -> void:
thickness = source.thickness
if source.curve:
curve = source.curve.duplicate() # TODO, update signals
# TODO: create points in the middle of the path
func create_point(position: Vector3) -> void:
if not curve:
curve = Curve3D.new()
curve.add_point(position)
func remove_point(index):
if index > curve.get_point_count() - 1:
return
curve.remove_point(index)
func get_closest_to(position):
if curve.get_point_count() == 0:
return -1
var closest = -1
var dist_squared = -1
for i in curve.get_point_count():
var point_pos: Vector3 = curve.get_point_position(i)
var point_dist: float = point_pos.distance_squared_to(position)
if (closest == -1) or (dist_squared > point_dist):
closest = i
dist_squared = point_dist
var threshold = 16 # Ignore if the closest point is farther than this
if dist_squared >= threshold:
return -1
return closest
func get_closed_edges(shape_t: Transform3D) -> Array[PackedVector2Array]:
if not closed and thickness <= 0:
return []
if not curve:
return []
var edges: Array[PackedVector2Array] = []
var polyline := PackedVector2Array()
var shape_t_inverse := shape_t.affine_inverse()
var points := curve.tessellate(5, 5) # TODO: find optimal values
for p in points:
p *= shape_t_inverse # Apply the shape node transform
polyline.push_back(Vector2(p.x, p.z))
if closed:
# Ensure the polygon is closed
var first_point: Vector3 = points[0]
var last_point: Vector3 = points[-1]
if first_point != last_point:
first_point *= shape_t_inverse
polyline.push_back(Vector2(first_point.x, first_point.z))
# Prevents the polyline to be considered as a hole later.
if Geometry2D.is_polygon_clockwise(polyline):
polyline.reverse()
# Expand the polyline to get the outer edge of the path.
if thickness > 0:
# WORKAROUND. We cant specify the round end caps resolution, but it's tied to the polyline
# size. So we scale everything up before calling offset_polyline(), then scale the result
# down so we get rounder caps.
var scale = 5.0 * thickness
var delta = (thickness / 2.0) * scale
var t2 = Transform2D().scaled(Vector2.ONE * scale)
var result := Geometry2D.offset_polyline(polyline * t2, delta, Geometry2D.JOIN_ROUND, Geometry2D.END_ROUND)
t2 = Transform2D().scaled(Vector2.ONE * (1.0 / scale))
for polygon in result:
edges.push_back(polygon * t2)
if closed and thickness == 0.0:
edges.push_back(polyline)
return edges
func get_open_edges(shape_t: Transform3D) -> Array[Curve3D]:
if not curve or closed or thickness > 0:
return []
var res := Curve3D.new()
var shape_t_inverse := shape_t.affine_inverse()
for i in curve.get_point_count():
var pos = curve.get_point_position(i)
var pos_t = pos * shape_t_inverse
var p_in = (curve.get_point_in(i) + pos) * shape_t_inverse - pos_t
var p_out = (curve.get_point_out(i) + pos) * shape_t_inverse - pos_t
res.add_point(pos_t, p_in, p_out)
return [res]
func _update_polygon_from_curve() -> void:
var connections = PackedInt32Array()
var polygon_points = PackedVector2Array()
if not _bounds:
_bounds = Bounds.new()
_bounds.clear()
_polygon = PolygonPathFinder.new()
if not curve:
curve = Curve3D.new()
if curve.get_point_count() == 0:
return
var baked_points = curve.tessellate(4, 6)
var steps := baked_points.size()
for i in baked_points.size():
var point = baked_points[i]
var projected_point = Vector2(point.x, point.z)
_bounds.feed(point)
polygon_points.push_back(projected_point)
connections.append(i)
if i == steps - 1:
connections.append(0)
else:
connections.append(i + 1)
_bounds.compute_bounds()
_polygon.setup(polygon_points, connections)
func _on_curve_changed() -> void:
_update_polygon_from_curve()
emit_changed()