This commit is contained in:
2026-05-13 18:52:00 +02:00
commit 2bb1acbece
404 changed files with 33353 additions and 0 deletions

View File

@@ -0,0 +1,228 @@
@tool
extends Object
class_name Geometry2DUtil
const THRESHOLD = 0.1
static func get_polygon_bounding_rect(points : PackedVector2Array) -> Rect2:
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)
static func get_polygon_center(points : PackedVector2Array) -> Vector2:
return get_polygon_bounding_rect(points).get_center()
static func slice_polygon_vertical(polygon : PackedVector2Array, slice_target : Vector2) -> Array[PackedVector2Array]:
var box := get_polygon_bounding_rect(polygon).grow(1.0)
if not box.has_point(slice_target):
return [polygon]
return Geometry2D.intersect_polygons([
box.position,
Vector2(slice_target.x, box.position.y),
Vector2(slice_target.x, box.position.y + box.size.y),
Vector2(box.position.x, box.position.y + box.size.y),
], polygon) + Geometry2D.intersect_polygons([
Vector2(slice_target.x, box.position.y),
Vector2(box.position.x + box.size.x, box.position.y),
box.position + box.size,
Vector2(slice_target.x, box.position.y + box.size.y),
], polygon)
static func apply_polygon_bool_operation_in_place(
current_polygons : Array[PackedVector2Array],
other_polygons : Array[PackedVector2Array],
operation : Geometry2D.PolyBooleanOperation) -> Array[PackedVector2Array]:
var holes : Array[PackedVector2Array] = []
for other_poly in other_polygons:
var result_polygons : Array[PackedVector2Array] = []
for current_points : PackedVector2Array in current_polygons:
if other_poly == current_points:
continue
var result = (
Geometry2D.merge_polygons(current_points, other_poly)
if operation == Geometry2D.PolyBooleanOperation.OPERATION_UNION else
Geometry2D.intersect_polygons(current_points, other_poly)
if operation == Geometry2D.PolyBooleanOperation.OPERATION_INTERSECTION else
Geometry2D.clip_polygons(current_points, other_poly)
)
for poly_points in result:
if Geometry2D.is_polygon_clockwise(poly_points):
holes.append(poly_points)
else:
result_polygons.append(poly_points)
current_polygons.clear()
current_polygons.append_array(result_polygons)
return holes
## TODO: document
static func apply_clips_to_polygon(
current_polygons : Array[PackedVector2Array],
clips : Array[PackedVector2Array],
operation : Geometry2D.PolyBooleanOperation) -> Array[PackedVector2Array]:
var holes := apply_polygon_bool_operation_in_place(
current_polygons, clips, operation
)
if not holes.is_empty():
slice_polygons_with_holes(current_polygons, holes)
return current_polygons
static func slice_polygons_with_holes(current_polygons : Array[PackedVector2Array], holes : Array[PackedVector2Array]) -> void:
var result_polygons : Array[PackedVector2Array] = []
for hole in holes:
for current_points : PackedVector2Array in current_polygons:
var slices := slice_polygon_vertical(
current_points, get_polygon_center(hole)
)
for slice in slices:
var result = Geometry2D.clip_polygons(slice, hole)
for poly_points in result:
if not Geometry2D.is_polygon_clockwise(poly_points):
result_polygons.append(poly_points)
current_polygons.clear()
current_polygons.append_array(result_polygons)
result_polygons.clear()
static func calculate_outlines(result : Array[PackedVector2Array]) -> Array[PackedVector2Array]:
if result.size() <= 1:
return result
var succesful_merges := true
var guard = 0
var holes : Array[PackedVector2Array] = []
while succesful_merges and result.size() > 1 and guard < 1000:
succesful_merges = false
guard += 1
var indices_to_be_removed : Dictionary[int, bool] = {}
var merged_to_be_appended : Array[PackedVector2Array] = []
for current_poly_idx in result.size():
if current_poly_idx in indices_to_be_removed:
continue
for other_poly_idx in result.size():
if current_poly_idx == other_poly_idx or other_poly_idx in indices_to_be_removed:
continue
var merge_result := Geometry2D.merge_polygons(
result[current_poly_idx], result[other_poly_idx])
var regular := merge_result.filter(func(x): return not Geometry2D.is_polygon_clockwise(x))
var clockwise := merge_result.filter(Geometry2D.is_polygon_clockwise)
if regular.size() == 1:
succesful_merges = true
indices_to_be_removed[current_poly_idx] = true
indices_to_be_removed[other_poly_idx] = true
merged_to_be_appended.append(regular[0])
holes.append_array(clockwise)
var sorted_indices = indices_to_be_removed.keys()
sorted_indices.sort()
sorted_indices.reverse()
for idx in sorted_indices:
result.remove_at(idx)
result.append_array(merged_to_be_appended)
return result + holes
static func calculate_polystroke(outline : PackedVector2Array, stroke_width : float,
end_mode : Geometry2D.PolyEndType, joint_mode : Geometry2D.PolyJoinType) -> Array[PackedVector2Array]:
if outline.is_empty():
return []
var poly_strokes := Geometry2D.offset_polyline(outline, stroke_width, joint_mode, end_mode)
var result_poly_strokes := Array(poly_strokes.filter(func(ps): return not Geometry2D.is_polygon_clockwise(ps)), TYPE_PACKED_VECTOR2_ARRAY, "", null)
var result_poly_holes := Array(poly_strokes.filter(Geometry2D.is_polygon_clockwise), TYPE_PACKED_VECTOR2_ARRAY, "", null)
if not result_poly_holes.is_empty():
slice_polygons_with_holes(result_poly_strokes, result_poly_holes)
return result_poly_strokes
static func get_polygon_indices(polygons : Array[PackedVector2Array], indices : Array) -> PackedVector2Array:
var result : PackedVector2Array = []
var p_count = 0
indices.clear()
for poly_points in polygons:
var p_range := range(p_count, poly_points.size() + p_count)
result.append_array(poly_points)
indices.append(p_range)
p_count += poly_points.size()
return result
static func is_point_on_segment(p : Vector2, s1 : Vector2, s2: Vector2) -> bool:
return Geometry2D.segment_intersects_circle(s1, s2, p, 0.01) > -1
static func get_progress_ratio_for_point_on_curve(p : Vector2, c : Curve2D, max_stages := 5,
tolerance_degrees := 4.0) -> float:
# Heuristic to find progress_ratio of cpc
var d := 0.0
var pts := c.tessellate(max_stages, tolerance_degrees)
var p1 := pts[0]
for i in range(1, pts.size()):
if Geometry2DUtil.is_point_on_segment(p, p1, pts[i]):
d += p1.distance_to(p)
break
d += p1.distance_to(pts[i])
p1 = pts[i]
return d / c.get_baked_length()
static func get_halfway_point_on_bezier(c : Curve2D, max_stages := 5, tolerance_degrees := 4.0) -> Vector2:
var pts := c.tessellate(max_stages, tolerance_degrees)
var tot_d := c.get_baked_length()
var d := 0.0
var p1 := pts[0]
for i in range(1, pts.size()):
var prev_d := d
d += p1.distance_to(pts[i])
if d >= tot_d * 0.5:
var d_ratio := 0.5 - (prev_d / tot_d) if prev_d > 0.0 else 0.5
var d_abs := tot_d * d_ratio
return pts[i-1] + pts[i-1].direction_to(pts[i]) * d_abs
p1 = pts[i]
return Vector2.ZERO
# Adapted from: https://stackoverflow.com/a/8405756/1081548
static func slice_bezier(p1: Vector2, cp2 : Vector2, cp3 : Vector2, p4 : Vector2,
t : float) -> Curve2D:
var x1 := p1.x
var y1 := p1.y
var x2 := x1 + cp2.x
var y2 := y1 + cp2.y
var x4 := p4.x
var y4 := p4.y
var x3 := x4 + cp3.x
var y3 := y4 + cp3.y
var x12 := (x2-x1)*t+x1
var y12 = (y2-y1)*t+y1
var x23 = (x3-x2)*t+x2
var y23 = (y3-y2)*t+y2
var x34 = (x4-x3)*t+x3
var y34 = (y4-y3)*t+y3
var x123 = (x23-x12)*t+x12
var y123 = (y23-y12)*t+y12
var x234 = (x34-x23)*t+x23
var y234 = (y34-y23)*t+y23
var x1234 = (x234-x123)*t+x123
var y1234 = (y234-y123)*t+y123
var sliced_curve := Curve2D.new()
sliced_curve.add_point(Vector2(x1, y1))
sliced_curve.add_point(Vector2(x1234, y1234))
sliced_curve.add_point(Vector2(x4, y4))
sliced_curve.set_point_out(0, Vector2(x12, y12) - sliced_curve.get_point_position(0))
sliced_curve.set_point_in(1, Vector2(x123, y123) - sliced_curve.get_point_position(1))
sliced_curve.set_point_out(1, Vector2(x234, y234) - sliced_curve.get_point_position(1))
sliced_curve.set_point_in(2, Vector2(x34, y34) - sliced_curve.get_point_position(2))
return sliced_curve