init
This commit is contained in:
228
addons/curved_lines_2d/geometry_2d_util.gd
Normal file
228
addons/curved_lines_2d/geometry_2d_util.gd
Normal 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
|
||||
Reference in New Issue
Block a user