@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()