229 lines
8.1 KiB
GDScript
229 lines
8.1 KiB
GDScript
@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
|