Add lever and jump pad components with activation logic

This commit is contained in:
2025-04-25 22:41:35 +02:00
parent 9b2ca61163
commit 8959fd4b9f
323 changed files with 12844 additions and 18 deletions

View File

@@ -0,0 +1,337 @@
@tool
extends RefCounted
class_name SS2D_Edge
## An SS2D_Edge represents an edge that will be rendered.
##
## It contains: [br]
## - A list of quads that should be rendered [br]
## - A [Material] that dictates how the edge should be rendered [br]
## What to encode in the color data (for use by shaders).
enum COLOR_ENCODING {
COLOR, ## Encode a diffuse value to offset the quads color by (currently only ever white).
NORMALS, ## Encode normal data in the colors to be unpacked by a shader later.
}
var quads: Array[SS2D_Quad] = []
var first_point_key: int = -1
var last_point_key: int = -1
var z_index: int = 0
var z_as_relative: bool = false
## If final point is connected to first point.
var wrap_around: bool = false
var material: Material = null
## Will return true if the 2 quads must be drawn in two calls.
static func different_render(q1: SS2D_Quad, q2: SS2D_Quad) -> bool:
if q1.matches_quad(q2):
return false
return true
static func get_consecutive_quads_for_mesh(_quads: Array[SS2D_Quad]) -> Array[Array]:
var quad_ranges: Array[Array] = []
if _quads.is_empty():
return quad_ranges
var quad_range: Array[SS2D_Quad] = []
quad_range.push_back(_quads[0])
for i in range(1, _quads.size(), 1):
var quad_prev: SS2D_Quad = _quads[i - 1]
var quad: SS2D_Quad = _quads[i]
if different_render(quad, quad_prev):
quad_ranges.push_back(quad_range)
quad_range = [quad]
else:
quad_range.push_back(quad)
quad_ranges.push_back(quad_range)
return quad_ranges
## Will generate normals for a given quad
## and interpolate with previous and next quads.
static func generate_normals_for_quad_interpolated(qp: SS2D_Quad, q: SS2D_Quad, qn: SS2D_Quad) -> Array[Color]:
# Interpolation and normalization
#First, consider everything to be a non corner
var tg_a: Vector2 = (q.tg_a + qp.tg_d)
var bn_a: Vector2 = (q.bn_a + qp.bn_d)
var tg_b: Vector2 = (q.tg_b + qp.tg_c)
var bn_b: Vector2 = (q.bn_b + qp.bn_c)
var tg_c: Vector2 = (q.tg_c + qn.tg_b)
var bn_c: Vector2 = (q.bn_c + qn.bn_b)
var tg_d: Vector2 = (q.tg_d + qn.tg_a)
var bn_d: Vector2 = (q.bn_d + qn.bn_a)
#then, fix values for corner cases (and edge ends)
if q.corner == q.CORNER.NONE:
if qp.corner == q.CORNER.NONE:
#check validity
if (not q.pt_a.is_equal_approx(qp.pt_d)) or (not q.pt_b.is_equal_approx(qp.pt_c)):
tg_a = q.tg_a
tg_b = q.tg_b
bn_a = q.bn_a
bn_b = q.bn_b
elif qp.corner == q.CORNER.INNER:
tg_a = (-qp.bn_d)
bn_a = (-qp.tg_d)
tg_b = (q.tg_b - qp.bn_a)
bn_b = (q.bn_b - qp.tg_a)
#check validity
if (not q.pt_a.is_equal_approx(qp.pt_d)) or (not q.pt_b.is_equal_approx(qp.pt_a)):
tg_a = q.tg_a
tg_b = q.tg_b
bn_a = q.bn_a
bn_b = q.bn_b
elif qp.corner == q.CORNER.OUTER:
tg_a = (q.tg_a + qp.bn_c)
bn_a = (q.bn_a - qp.tg_c)
tg_b = (qp.bn_b)
bn_b = (-qp.tg_b)
#check validity
if (not q.pt_a.is_equal_approx(qp.pt_c)) or (not q.pt_b.is_equal_approx(qp.pt_b)):
tg_a = q.tg_a
tg_b = q.tg_b
bn_a = q.bn_a
bn_b = q.bn_b
if qn.corner == q.CORNER.NONE:
#check validity
if (not q.pt_c.is_equal_approx(qn.pt_b)) or (not q.pt_d.is_equal_approx(qn.pt_a)):
tg_c = q.tg_c
tg_d = q.tg_d
bn_c = q.bn_c
bn_d = q.bn_d
elif qn.corner == q.CORNER.INNER:
tg_d = (-qn.tg_d)
bn_d = (qn.bn_d)
tg_c = (q.tg_c - qn.tg_c)
bn_c = (q.bn_c + qn.bn_c)
#check validity
if (not q.pt_c.is_equal_approx(qn.pt_c)) or (not q.pt_d.is_equal_approx(qn.pt_d)):
tg_c = q.tg_c
tg_d = q.tg_d
bn_c = q.bn_c
bn_d = q.bn_d
elif qn.corner == q.CORNER.OUTER:
tg_c = (qn.tg_b)
bn_c = (qn.bn_b)
#check validity
if (not q.pt_c.is_equal_approx(qn.pt_b)) or (not q.pt_d.is_equal_approx(qn.pt_a)):
tg_c = q.tg_c
tg_d = q.tg_d
bn_c = q.bn_c
bn_d = q.bn_d
elif q.corner == q.CORNER.INNER:
#common
tg_d = q.tg_d
bn_d = q.bn_d
tg_b = (q.tg_b)
bn_b = (q.bn_b)
#previous
tg_c = (q.tg_c - qp.tg_c)
bn_c = (q.bn_c + qp.bn_c)
#next
tg_a = (q.tg_a - qn.bn_b)
bn_a = (q.bn_a - qn.tg_b)
#check validity
if qp.corner != qp.CORNER.NONE or (not q.pt_c.is_equal_approx(qp.pt_c)) or (not q.pt_d.is_equal_approx(qp.pt_d)):
tg_c = q.tg_c
bn_c = q.bn_c
if qn.corner != qp.CORNER.NONE or (not q.pt_a.is_equal_approx(qn.pt_b)) or (not q.pt_d.is_equal_approx(qn.pt_a)):
tg_a = q.tg_a
bn_a = q.bn_a
elif q.corner == q.CORNER.OUTER:
tg_d = q.tg_d
bn_d = q.bn_d
tg_b = (q.tg_b)
bn_b = (q.bn_b)
#previous
tg_a = (q.tg_a + qp.tg_d)
bn_a = (q.bn_a + qp.bn_d)
#qn
tg_c = (q.tg_c - qn.bn_a)
bn_c = (q.bn_c + qn.tg_a)
#check validity
if qp.corner != qp.CORNER.NONE or (not q.pt_a.is_equal_approx(qp.pt_d)) or (not q.pt_b.is_equal_approx(qp.pt_c)):
tg_a = q.tg_a
bn_a = q.bn_a
if qn.corner != qp.CORNER.NONE or (not q.pt_b.is_equal_approx(qn.pt_b)) or (not q.pt_c.is_equal_approx(qn.pt_a)):
tg_c = q.tg_c
bn_c = q.bn_c
if q.flip_texture:
bn_a = -bn_a;
bn_b = -bn_b;
bn_c = -bn_c;
bn_d = -bn_d;
#Normalize the values
var half_vector: Vector2 = Vector2.ONE * 0.5
tg_a = tg_a.normalized()*0.5 + half_vector
tg_b = tg_b.normalized()*0.5 + half_vector
tg_c = tg_c.normalized()*0.5 + half_vector
tg_d = tg_d.normalized()*0.5 + half_vector
bn_a = bn_a.normalized()*0.5 + half_vector
bn_b = bn_b.normalized()*0.5 + half_vector
bn_c = bn_c.normalized()*0.5 + half_vector
bn_d = bn_d.normalized()*0.5 + half_vector
var normal_pt_a := Color(tg_a.x, tg_a.y, bn_a.x, bn_a.y)
var normal_pt_b := Color(tg_b.x, tg_b.y, bn_b.x, bn_b.y)
var normal_pt_c := Color(tg_c.x, tg_c.y, bn_c.x, bn_c.y)
var normal_pt_d := Color(tg_d.x, tg_d.y, bn_d.x, bn_d.y)
return [normal_pt_a, normal_pt_b, normal_pt_c, normal_pt_d]
## Assumes each quad in the sequence is of the same render type (same textures, values, etc...).[br]
## [param _quads] should have been generated by [method get_consecutive_quads_for_mesh].
static func generate_array_mesh_from_quad_sequence(_quads: Array[SS2D_Quad], _wrap_around: bool, color_encoding: int) -> ArrayMesh:
# FIXME: _wrap_around is unused.
if _quads.is_empty():
return ArrayMesh.new()
var total_length: float = 0.0
for q in _quads:
total_length += q.get_length_average()
if total_length == 0.0:
return ArrayMesh.new()
var first_quad: SS2D_Quad = _quads[0]
var tex: Texture2D = first_quad.texture
# The change in length required to apply to each quad
# to make the textures begin and end at the start and end of each texture
var change_in_length: float = -1.0
if tex != null:
# How many times the texture is repeated
var texture_reps: float = roundf(total_length / tex.get_size().x)
# Length required to display all the reps with the texture's full width
var texture_full_length: float = texture_reps * tex.get_size().x
# How much each quad's texture must be offset to make up the difference in full length vs total length
change_in_length = (texture_full_length / total_length)
if first_quad.fit_texture == SS2D_Material_Edge.FITMODE.CROP:
change_in_length = 1.0
var length_elapsed: float = 0.0
var st := SurfaceTool.new()
st.begin(Mesh.PRIMITIVE_TRIANGLES)
for q in _quads:
q.update_tangents()
for i in _quads.size():
var q: SS2D_Quad = _quads[i]
var section_length: float = q.get_length_average() * change_in_length
# var highest_value: float = max(q.get_height_left(), q.get_height_right())
# When welding and using different widths, quads can look a little weird
# This is because they are no longer parallelograms
# This is a tough problem to solve
# See http://reedbeta.com/blog/quadrilateral-interpolation-part-1/
var uv_a := Vector2(0, 0)
var uv_b := Vector2(0, 1)
var uv_c := Vector2(1, 1)
var uv_d := Vector2(1, 0)
# If we have a valid texture and this quad isn't a corner
if tex != null and q.corner == q.CORNER.NONE:
var x_left: float = (length_elapsed) / tex.get_size().x
var x_right: float = (length_elapsed + section_length) / tex.get_size().x
uv_a.x = x_left
uv_b.x = x_left
uv_c.x = x_right
uv_d.x = x_right
if q.flip_texture:
var t: Vector2 = uv_a
uv_a = uv_b
uv_b = t
t = uv_c
uv_c = uv_d
uv_d = t
var color_a := q.color
var color_b := q.color
var color_c := q.color
var color_d := q.color
if color_encoding == COLOR_ENCODING.NORMALS:
var next := _quads[wrapi(i + 1, 0, _quads.size())]
var prev := _quads[wrapi(i - 1, 0, _quads.size())]
var normals: Array[Color] = generate_normals_for_quad_interpolated(next, q, prev)
color_a = normals[0]
color_b = normals[1]
color_c = normals[2]
color_d = normals[3]
# A
_add_uv_to_surface_tool(st, uv_a)
st.set_color(color_a)
st.add_vertex(SS2D_Common_Functions.to_vector3(q.pt_a))
# B
_add_uv_to_surface_tool(st, uv_b)
st.set_color(color_b)
st.add_vertex(SS2D_Common_Functions.to_vector3(q.pt_b))
# C
_add_uv_to_surface_tool(st, uv_c)
st.set_color(color_c)
st.add_vertex(SS2D_Common_Functions.to_vector3(q.pt_c))
# A
_add_uv_to_surface_tool(st, uv_a)
st.set_color(color_a)
st.add_vertex(SS2D_Common_Functions.to_vector3(q.pt_a))
# C
_add_uv_to_surface_tool(st, uv_c)
st.set_color(color_c)
st.add_vertex(SS2D_Common_Functions.to_vector3(q.pt_c))
# D
_add_uv_to_surface_tool(st, uv_d)
st.set_color(color_d)
st.add_vertex(SS2D_Common_Functions.to_vector3(q.pt_d))
length_elapsed += section_length
st.index()
st.generate_normals()
return st.commit()
func get_meshes(color_encoding: SS2D_Edge.COLOR_ENCODING) -> Array[SS2D_Mesh]:
# Get Arrays of consecutive quads with the same mesh data.
# For each array, generate Mesh Data from the quad.
var consecutive_quad_arrays := SS2D_Edge.get_consecutive_quads_for_mesh(quads)
#print("Arrays: %s" % consecutive_quad_arrays.size())
var meshes: Array[SS2D_Mesh] = []
for consecutive_quads in consecutive_quad_arrays:
if consecutive_quads.is_empty():
continue
var array_mesh: ArrayMesh = SS2D_Edge.generate_array_mesh_from_quad_sequence(
consecutive_quads, wrap_around, color_encoding
)
var quad: SS2D_Quad = consecutive_quads[0]
var tex: Texture2D = quad.texture
var flip: bool = quad.flip_texture
var mesh_data := SS2D_Mesh.new(tex, flip, Transform2D(), [array_mesh], material)
mesh_data.force_no_tiling = quad.is_tapered or quad.corner != SS2D_Quad.CORNER.NONE
mesh_data.z_index = z_index
mesh_data.z_as_relative = z_as_relative
meshes.push_back(mesh_data)
return meshes
static func _add_uv_to_surface_tool(surface_tool: SurfaceTool, uv: Vector2) -> void:
surface_tool.set_uv(uv)
surface_tool.set_uv2(uv)

View File

@@ -0,0 +1,264 @@
@tool
extends RefCounted
class_name SS2D_IndexMap
## Maps a set of indicies to an object.
var object: Variant = null
var indicies: PackedInt32Array
## Parameter [param subresources] has no effect, no subresources to duplicate.
func duplicate(_subresources: bool = false) -> SS2D_IndexMap:
return SS2D_IndexMap.new(indicies.duplicate(), object)
func _init(i: PackedInt32Array, o: Variant) -> void:
indicies = i
object = o
func _to_string() -> String:
return "[M_2_IDX] (%s) | %s" % [str(object), indicies]
static func is_index_array_valid(idx_array: PackedInt32Array) -> bool:
return idx_array.size() >= 2
func is_valid() -> bool:
return SS2D_IndexMap.is_index_array_valid(indicies)
# FIXME: Unused. Remove eventually
# func get_contiguous_segments() -> Array:
# if is_contiguous():
# return [indicies.duplicate()]
# var segments: Array = []
# var break_idx: int = find_break()
# var remainder: Array[int] = indicies.duplicate()
# while break_idx != -1:
# var new_slice: Array[int] = []
# for i in range(0, break_idx):
# new_slice.push_back(remainder[i])
# segments.push_back(new_slice)
# remainder = remainder.slice(break_idx, remainder.size())
# break_idx = SS2D_IndexMap.find_break_in_array(remainder)
# if not remainder.is_empty():
# segments.push_back(remainder)
# return segments
## Will join together segments that share the same idx,
## ex. [1,2], [4,5], and [2,3,4] become [1,2,3,4,5]
static func join_segments(segments: Array[PackedInt32Array]) -> Array[PackedInt32Array]:
var final_segments: Array[PackedInt32Array] = []
final_segments.assign(segments.duplicate())
var to_join_tuple: Vector2i
var join_performed := true
while join_performed:
join_performed = false
for i in range(0, final_segments.size()):
if join_performed:
break
for ii in range(i + 1, final_segments.size()):
var a := final_segments[i]
var b := final_segments[ii]
if a[-1] == b[0]:
to_join_tuple = Vector2i(i, ii)
join_performed = true
if b[-1] == a[0]:
to_join_tuple = Vector2i(ii, i)
join_performed = true
if join_performed:
break
if join_performed:
var idx_lowest: int = to_join_tuple[0]
var idx_highest: int = to_join_tuple[1]
var lowest: PackedInt32Array = final_segments[idx_lowest]
var highest: PackedInt32Array = final_segments[idx_highest]
final_segments.erase(lowest)
final_segments.erase(highest)
# pop the shared idx from lowest
lowest.remove_at(lowest.size() - 1)
var new_segment := lowest + highest
final_segments.push_back(new_segment)
return final_segments
## Does each index increment by 1 without any breaks.
func is_contiguous() -> bool:
return SS2D_IndexMap.is_array_contiguous(indicies)
static func is_array_contiguous(a: PackedInt32Array) -> bool:
return find_break_in_array(a) == -1
## Find a break in the indexes where they aren't contiguous.[br]
## Will return -1 if there's no break.[br]
func find_break() -> int:
return SS2D_IndexMap.find_break_in_array(indicies)
static func find_break_in_array(a: PackedInt32Array, offset: int = 0) -> int:
for i in range(offset, a.size() - 1, 1):
if is_break_at_index_in_array(a, i):
return i + 1
return -1
## Whether there is a break at the given index.[br]
## Will return -1 if there's no break.[br]
func is_break_at_index(i: int) -> bool:
return SS2D_IndexMap.is_break_at_index_in_array(indicies, i)
static func is_break_at_index_in_array(a: PackedInt32Array, i: int) -> bool:
var difference: int = absi((a[i]) - (a[i + 1]))
return difference != 1
func has_index(idx: int) -> bool:
return indicies.has(idx)
# FIXME: Unused, remove eventually.
# func lowest_index() -> int:
# return indicies.min()
#
#
# func highest_index() -> int:
# return indicies.max()
# FIXME: Unused, remove eventually
func _split_indicies_into_multiple_mappings(new_indicies: PackedInt32Array) -> Array[SS2D_IndexMap]:
var maps: Array[SS2D_IndexMap] = []
var break_idx := SS2D_IndexMap.find_break_in_array(new_indicies)
var offset := 0
var sub_indicies: PackedInt32Array
while break_idx != -1:
sub_indicies = new_indicies.slice(offset, break_idx)
if SS2D_IndexMap.is_index_array_valid(sub_indicies):
maps.push_back(SS2D_IndexMap.new(sub_indicies, object))
offset = break_idx
break_idx = SS2D_IndexMap.find_break_in_array(new_indicies, offset)
sub_indicies = new_indicies.slice(offset)
if SS2D_IndexMap.is_index_array_valid(sub_indicies):
maps.push_back(SS2D_IndexMap.new(sub_indicies, object))
return maps
## FIXME: Unused, remove eventually
## Will create a new set of SS2D_IndexMaps. [br][br]
##
## The new set will contain all of the indicies of the current set,
## minus the ones specified in the indicies parameter. [br][br]
##
## Example: [br]
## indicies = [0,1,2,3,4,5,6] [br]
## to_remove = [3,4] [br]
## new_sets = [0,1,2] [5,6] [br][br]
##
## This may split the IndexMap or make it invalid entirely.
## As a result, the returned array could have 0 or several IndexMaps.
func remove_indicies(to_remove: PackedInt32Array) -> Array[SS2D_IndexMap]:
var out: Array[SS2D_IndexMap] = []
var new_indicies := indicies.duplicate()
for r in to_remove:
var idx := new_indicies.find(r)
if idx >= 0:
new_indicies.remove_at(idx)
if not SS2D_IndexMap.is_index_array_valid(new_indicies):
return out
if SS2D_IndexMap.is_array_contiguous(new_indicies):
out.push_back(SS2D_IndexMap.new(new_indicies, object))
return out
return _split_indicies_into_multiple_mappings(new_indicies)
## Will create a new set of SS2D_IndexMaps. [br][br]
##
## The new set will contain all of the edges of the current set,
## minus the ones specified in the indicies parameter. [br][br]
##
## Example: [br]
## indicies = [0,1,2,3,4,5,6] [br]
## to_remove = [4,5] [br]
## new_sets = [0,1,2,3,4] [4,5,6] [br][br]
##
## This may split the IndexMap or make it invalid entirely.
## As a result, the returned array could have 0 or several IndexMaps.
func remove_edges(to_remove: PackedInt32Array) -> Array[SS2D_IndexMap]:
# Corner case
if to_remove.size() == 2:
var idx: int = indicies.find(to_remove[0])
if idx != indicies.size()-1:
if indicies[idx+1] == to_remove[1]:
# Need one split
var set_1 := indicies.slice(0, idx+1)
var set_2 := indicies.slice(idx+1, indicies.size())
var new_maps: Array[SS2D_IndexMap] = []
if SS2D_IndexMap.is_index_array_valid(set_1):
new_maps.push_back(SS2D_IndexMap.new(set_1, object))
if SS2D_IndexMap.is_index_array_valid(set_2):
new_maps.push_back(SS2D_IndexMap.new(set_2, object))
return new_maps
return [SS2D_IndexMap.new(indicies, object)]
# General case
var new_edges := SS2D_IndexMap.indicies_to_edges(indicies)
for i in range(0, to_remove.size() - 1, 1):
var idx1: int = to_remove[i]
var idx2: int = to_remove[i + 1]
var edges_to_remove := PackedInt32Array()
for ii in new_edges.size():
var edge := new_edges[ii]
if (edge[0] == idx1 or edge[0] == idx2) and (edge[1] == idx1 or edge[1] == idx2):
edges_to_remove.push_back(ii)
# Reverse iterate
for ii in range(edges_to_remove.size()-1, -1, -1):
new_edges.remove_at(edges_to_remove[ii])
new_edges = SS2D_IndexMap.join_segments(new_edges)
var new_index_mappings: Array[SS2D_IndexMap] = []
for e in new_edges:
new_index_mappings.push_back(SS2D_IndexMap.new(e, object))
return new_index_mappings
# NOTE: Even though it makes more sense to return an Array[Vector2i], we return PackedInt32Arrays
# instead because it makes things easier in the context where this function output is needed.
static func indicies_to_edges(p_indicies: PackedInt32Array) -> Array[PackedInt32Array]:
var edges: Array[PackedInt32Array] = []
for i in p_indicies.size() - 1:
var edge := PackedInt32Array([ i, i+1 ])
if absi(edge[0] - edge[1]) == 1:
edges.push_back(edge)
return edges
## Returns a Dict[Variant, Array[SS2D_IndexMap]]
static func index_map_array_sort_by_object(imaps: Array) -> Dictionary:
var dict := {}
for imap: SS2D_IndexMap in imaps:
if not dict.has(imap.object):
var arr: Array[SS2D_IndexMap] = [ imap ]
dict[imap.object] = arr
else:
var arr: Array[SS2D_IndexMap] = dict[imap.object]
arr.push_back(imap)
return dict

View File

@@ -0,0 +1,80 @@
@tool
extends RefCounted
class_name SS2D_Mesh
## Used to organize all requested meshes to be rendered by their textures.
var texture: Texture2D = null
var flip_texture: bool = false
var meshes: Array[ArrayMesh] = []
var mesh_transform: Transform2D = Transform2D()
var material: Material = null
var z_index: int = 0
var z_as_relative: bool = true
var show_behind_parent: bool = false
var force_no_tiling: bool = false
func _init(
t: Texture2D = null,
f: bool = false,
xform: Transform2D = Transform2D(),
m: Array[ArrayMesh] = [],
mat: Material = null
) -> void:
texture = t
flip_texture = f
meshes = m
mesh_transform = xform
material = mat
# Note: Not an override.
func duplicate(subresources: bool = false) -> SS2D_Mesh:
var copy := SS2D_Mesh.new()
copy.texture = texture
copy.flip_texture = flip_texture
copy.mesh_transform = mesh_transform
copy.material = material
copy.z_index = z_index
copy.z_as_relative = z_as_relative
copy.show_behind_parent = show_behind_parent
copy.force_no_tiling = force_no_tiling
copy.meshes = []
if subresources:
for m in meshes:
copy.meshes.push_back(m.duplicate(true))
return copy
func matches(tex: Texture2D, f: bool, t: Transform2D, m: Material, zi: int, zb: bool) -> bool:
return (
tex == texture
and f == flip_texture
and t == mesh_transform
and m == material
and zi == z_index
and zb == z_as_relative
)
func mesh_matches(m: SS2D_Mesh) -> bool:
return matches(
m.texture,
m.flip_texture,
m.mesh_transform,
m.material,
m.z_index,
m.z_as_relative
)
func debug_print_array_mesh(am: ArrayMesh) -> String:
var s := "Faces:%s | Surfs:%s | " % [am.get_faces(), am.get_surface_count()]
return s
func render(ci: CanvasItem) -> void:
#print("mesh count %s" % meshes.size())
for mesh in meshes:
ci.draw_mesh(mesh, texture)

View File

@@ -0,0 +1,72 @@
@tool
extends Resource
class_name SS2D_Point
@export var position: Vector2 : set = _set_position
@export var point_in: Vector2 : set = _set_point_in
@export var point_out: Vector2 : set = _set_point_out
@export var properties: SS2D_VertexProperties : set = _set_properties
# If class members are written to, the 'changed' signal may not be emitted
# Signal is only emitted when data is actually changed
# If assigned data is the same as the existing data, no signal is emitted
func _init(pos: Vector2 = Vector2(0, 0)) -> void:
position = pos
point_in = Vector2(0, 0)
point_out = Vector2(0, 0)
properties = SS2D_VertexProperties.new()
func equals(other: SS2D_Point) -> bool:
if position != other.position:
return false
if point_in != other.point_in:
return false
if point_out != other.point_out:
return false
print ("E! %s" % properties.equals(other.properties))
if not properties.equals(other.properties):
return false
return true
func _set_position(v: Vector2) -> void:
if position != v:
position = v
emit_changed()
func _set_point_in(v: Vector2) -> void:
if point_in != v:
point_in = v
emit_changed()
func _set_point_out(v: Vector2) -> void:
if point_out != v:
point_out = v
emit_changed()
func _set_properties(other: SS2D_VertexProperties) -> void:
# FIXME: What if other is null?
if properties == null or not properties.equals(other):
if properties:
properties.changed.disconnect(_on_properties_changed)
properties = other
if properties:
properties.changed.connect(_on_properties_changed)
emit_changed()
func _to_string() -> String:
return "<SS2D_Point %s>" % [position]
func _on_properties_changed() -> void:
emit_changed()

View File

@@ -0,0 +1,675 @@
@tool
extends Resource
class_name SS2D_Point_Array
enum CONSTRAINT { NONE = 0, AXIS_X = 1, AXIS_Y = 2, CONTROL_POINTS = 4, PROPERTIES = 8, ALL = 15 }
# Maps a key to each point: Dict[int, SS2D_Point]
@export var _points: Dictionary = {} : set = _set_points
# Contains all keys; the order of the keys determines the order of the points
@export var _point_order := PackedInt32Array() : set = set_point_order
## Dict[Vector2i, CONSTRAINT]
## Key is tuple of point_keys; Value is the CONSTRAINT enum.
@export var _constraints: Dictionary = {} : set = _set_constraints
# Next key value to generate
@export var _next_key: int = 0 : set = set_next_key
## Dict[Vector2i, SS2D_Material_Edge_Metadata]
## Dictionary of specific materials to use for specific tuples of points.
## Key is tuple of two point keys, value is material.
@export var _material_overrides: Dictionary = {} : set = set_material_overrides
## Controls how many subdivisions a curve segment may face before it is considered
## approximate enough.
@export_range(0, 8, 1)
var tessellation_stages: int = 3 : set = set_tessellation_stages
## Controls how many degrees the midpoint of a segment may deviate from the real
## curve, before the segment has to be subdivided.
@export_range(0.1, 16.0, 0.1, "or_greater", "or_lesser")
var tessellation_tolerance: float = 6.0 : set = set_tessellation_tolerance
@export_range(1, 512) var curve_bake_interval: float = 20.0 : set = set_curve_bake_interval
var _constraints_enabled: bool = true
var _updating_constraints := false
var _keys_to_update_constraints := PackedInt32Array()
var _changed_during_update := false
var _updating := false
# Point caches
var _point_cache_dirty := true
var _vertex_cache := PackedVector2Array()
var _curve := Curve2D.new()
var _curve_no_control_points := Curve2D.new()
var _tesselation_cache := PackedVector2Array()
var _tess_vertex_mapping := SS2D_TesselationVertexMapping.new()
## Gets called when points were modified.
## In comparison to the "changed" signal, "update_finished" will only be called once after
## begin/end_update() blocks, while "changed" will be called for every singular change.
## Hence, this signal is usually better suited to react to point updates.
signal update_finished()
signal constraint_removed(key1: int, key2: int)
signal material_override_changed(tuple: Vector2i)
###################
# HANDLING POINTS #
###################
func _init() -> void:
# Required by Godot to correctly make unique instances of this resource
_points = {}
_constraints = {}
_next_key = 0
# Assigning an empty dict to _material_overrides this way
# instead of assigning in the declaration appears to bypass
# a weird Godot bug where _material_overrides of one shape
# interfere with another
if _material_overrides == null:
_material_overrides = {}
func clone(deep: bool = false) -> SS2D_Point_Array:
var copy := SS2D_Point_Array.new()
copy._next_key = _next_key
copy.tessellation_stages = tessellation_stages
copy.tessellation_tolerance = tessellation_tolerance
copy.curve_bake_interval = curve_bake_interval
if deep:
var new_point_dict := {}
for k: int in _points:
new_point_dict[k] = get_point(k).duplicate(true)
copy._points = new_point_dict
copy._point_order = _point_order.duplicate()
copy._constraints = _constraints.duplicate()
copy._material_overrides = _material_overrides.duplicate()
else:
copy._points = _points
copy._point_order = _point_order
copy._constraints = _constraints
copy._material_overrides = _material_overrides
return copy
## Called by Godot when loading from a saved scene
func _set_points(ps: Dictionary) -> void:
_points = ps
for k: int in _points:
_hook_point(k)
_changed()
func set_point_order(po: PackedInt32Array) -> void:
_point_order = po
_changed()
func _set_constraints(cs: Dictionary) -> void:
_constraints = cs
# For backwards compatibility (Array to Vector2i transition)
# FIXME: Maybe remove during the next breaking release
SS2D_IndexTuple.dict_validate(_constraints, TYPE_INT)
func set_next_key(i: int) -> void:
_next_key = i
func __generate_key(next: int) -> int:
if not is_key_valid(next):
return __generate_key(maxi(next + 1, 0))
return next
## Reserve a key. It will not be generated again.
func reserve_key() -> int:
var next: int = __generate_key(_next_key)
_next_key = next + 1
return next
## Returns next key that would be generated when adding a new point, e.g. when [method add_point] is called.
func get_next_key() -> int:
return __generate_key(_next_key)
func is_key_valid(k: int) -> bool:
return k >= 0 and not _points.has(k)
## Add a point and insert it at the given index or at the end by default.
## Returns the key of the added point.
func add_point(point: Vector2, idx: int = -1, use_key: int = -1) -> int:
# print("Add Point :: ", point, " | idx: ", idx, " | key: ", use_key, " |")
if use_key == -1 or not is_key_valid(use_key):
use_key = reserve_key()
if use_key == _next_key:
_next_key += 1
_points[use_key] = SS2D_Point.new(point)
_hook_point(use_key)
_point_order.push_back(use_key)
if idx != -1:
set_point_index(use_key, idx)
_changed()
return use_key
## Deprecated. There is no reason to use this function, points can be modified directly.
## @deprecated
func set_point(key: int, value: SS2D_Point) -> void:
if has_point(key):
# FIXME: Should there be a call to remove_constraints() like in remove_point()? Because
# we're technically deleting a point and replacing it with another.
_unhook_point(get_point(key))
_points[key] = value
_hook_point(key)
_changed()
## Connects the changed signal of the given point. Requires that the point exists in _points.
func _hook_point(key: int) -> void:
var p := get_point(key)
if not p.changed.is_connected(_on_point_changed):
p.changed.connect(_on_point_changed.bind(key))
## Disconnects the changed signal of the given point. See also _hook_point().
func _unhook_point(p: SS2D_Point) -> void:
if not p.changed.is_connected(_on_point_changed):
p.changed.disconnect(_on_point_changed)
func is_index_in_range(idx: int) -> bool:
return idx >= 0 and idx < _point_order.size()
func get_point_key_at_index(idx: int) -> int:
return _point_order[idx]
func get_edge_keys_for_indices(indices: Vector2i) -> Vector2i:
return Vector2i(
get_point_key_at_index(indices.x),
get_point_key_at_index(indices.y)
)
func get_point_at_index(idx: int) -> SS2D_Point:
return _points[_point_order[idx]]
## Returns the point with the given key as reference or null if it does not exist.
func get_point(key: int) -> SS2D_Point:
return _points.get(key)
func get_point_count() -> int:
return _point_order.size()
func get_point_index(key: int) -> int:
if has_point(key):
var idx := 0
for k in _point_order:
if key == k:
return idx
idx += 1
return -1
## Reverse order of points in point array.[br]
## I.e. [1, 2, 3, 4] will become [4, 3, 2, 1].[br]
func invert_point_order() -> void:
# Postpone `changed` and disable constraints.
var was_updating: bool = _updating
_updating = true
disable_constraints()
_point_order.reverse()
# Swap Bezier points.
for p: SS2D_Point in _points.values():
if p.point_out != p.point_in:
var tmp: Vector2 = p.point_out
p.point_out = p.point_in
p.point_in = tmp
# Re-enable contraits and emit `changed`.
enable_constraints()
_updating = was_updating
_changed()
func set_point_index(key: int, idx: int) -> void:
if not has_point(key):
return
var old_idx: int = get_point_index(key)
if idx < 0 or idx >= _points.size():
idx = _points.size() - 1
if idx == old_idx:
return
_point_order.remove_at(old_idx)
_point_order.insert(idx, key)
_changed()
func has_point(key: int) -> bool:
return _points.has(key)
func get_all_point_keys() -> PackedInt32Array:
# _point_order should contain every single point ONLY ONCE
return _point_order
func remove_point(key: int) -> bool:
if has_point(key):
# print("Remove Point :: ", get_point_position(key), " | idx: ", get_point_index(key), " | key: ", key, " |")
remove_constraints(key)
_unhook_point(get_point(key))
_point_order.remove_at(get_point_index(key))
_points.erase(key)
_changed()
return true
return false
func remove_point_at_index(idx: int) -> void:
remove_point(get_point_key_at_index(idx))
## Remove all points from point array.
func clear() -> void:
_points.clear()
_point_order.clear()
_constraints.clear()
_next_key = 0
_changed()
## point_in controls the edge leading from the previous vertex to this one
func set_point_in(key: int, value: Vector2) -> void:
if has_point(key):
_points[key].point_in = value
_changed()
func get_point_in(key: int) -> Vector2:
if has_point(key):
return _points[key].point_in
return Vector2(0, 0)
## point_out controls the edge leading from this vertex to the next
func set_point_out(key: int, value: Vector2) -> void:
if has_point(key):
_points[key].point_out = value
_changed()
func get_point_out(key: int) -> Vector2:
if has_point(key):
return _points[key].point_out
return Vector2(0, 0)
func set_point_position(key: int, value: Vector2) -> void:
if has_point(key):
_points[key].position = value
_changed()
func get_point_position(key: int) -> Vector2:
if has_point(key):
return _points[key].position
return Vector2(0, 0)
func set_point_properties(key: int, value: SS2D_VertexProperties) -> void:
if has_point(key):
_points[key].properties = value
_changed()
func get_point_properties(key: int) -> SS2D_VertexProperties:
var p := get_point(key)
return p.properties if p else null
## Returns the corresponding key for a given point or -1 if it does not exist.
func get_key_from_point(p: SS2D_Point) -> int:
for k: int in _points:
if p == _points[k]:
return k
return -1
func _on_point_changed(key: int) -> void:
if _updating_constraints:
_keys_to_update_constraints.push_back(key)
else:
update_constraints(key)
## Begin updating the shape.[br]
## Shape mesh and curve will only be updated after [method end_update] is called.
func begin_update() -> void:
_updating = true
## End updating the shape.[br]
## Mesh and curve will be updated, if changes were made to points array after
## [method begin_update] was called.
func end_update() -> bool:
var was_dirty := _changed_during_update
_updating = false
_changed_during_update = false
if was_dirty:
update_finished.emit()
return was_dirty
## Is shape in the middle of being updated.
## Returns [code]true[/code] after [method begin_update] and before [method end_update].
func is_updating() -> bool:
return _updating
func _changed() -> void:
_point_cache_dirty = true
emit_changed()
if _updating:
_changed_during_update = true
else:
update_finished.emit()
###############
# CONSTRAINTS #
###############
func disable_constraints() -> void:
_constraints_enabled = false
func enable_constraints() -> void:
_constraints_enabled = true
func _update_constraints(src: int) -> void:
if not _constraints_enabled:
return
var constraints := get_point_constraints_tuples(src)
for tuple in constraints:
var constraint: CONSTRAINT = SS2D_IndexTuple.dict_get(_constraints, tuple)
if constraint == CONSTRAINT.NONE:
continue
var dst: int = SS2D_IndexTuple.get_other_value(tuple, src)
if constraint & CONSTRAINT.AXIS_X:
set_point_position(dst, Vector2(get_point_position(src).x, get_point_position(dst).y))
if constraint & CONSTRAINT.AXIS_Y:
set_point_position(dst, Vector2(get_point_position(dst).x, get_point_position(src).y))
if constraint & CONSTRAINT.CONTROL_POINTS:
set_point_in(dst, get_point_in(src))
set_point_out(dst, get_point_out(src))
if constraint & CONSTRAINT.PROPERTIES:
set_point_properties(dst, get_point_properties(src))
## Will mutate points based on constraints.[br]
## Values from Passed key will be used to update constrained points.[br]
func update_constraints(src: int) -> void:
if not has_point(src) or _updating_constraints:
return
_updating_constraints = true
# Initial pass of updating constraints
_update_constraints(src)
# Subsequent required passes of updating constraints
while not _keys_to_update_constraints.is_empty():
var key_set := _keys_to_update_constraints
_keys_to_update_constraints = PackedInt32Array()
for k in key_set:
_update_constraints(k)
_updating_constraints = false
_changed()
## Returns all point constraint that include the given point key.
## Returns a Dictionary[Vector2i, CONSTRAINT].
func get_point_constraints(key1: int) -> Dictionary:
var constraints := {}
var tuples := get_point_constraints_tuples(key1)
for t in tuples:
constraints[t] = get_point_constraint(t.x, t.y)
return constraints
## Returns all point constraint tuples that include the given point key.
func get_point_constraints_tuples(key1: int) -> Array[Vector2i]:
return SS2D_IndexTuple.dict_find_partial(_constraints, key1)
## Returns the constraint for a pair of keys or CONSTRAINT.NONE if no constraint exists.
func get_point_constraint(key1: int, key2: int) -> CONSTRAINT:
return SS2D_IndexTuple.dict_get(_constraints, Vector2i(key1, key2), CONSTRAINT.NONE)
## Set a constraint between two points. If the constraint is NONE, remove_constraint() is called instead.
func set_constraint(key1: int, key2: int, constraint: CONSTRAINT) -> void:
var t := Vector2i(key1, key2)
if constraint == CONSTRAINT.NONE:
remove_constraint(t)
return
SS2D_IndexTuple.dict_set(_constraints, t, constraint)
update_constraints(key1)
_changed()
## Remove all constraints involving the given point key.
func remove_constraints(key1: int) -> void:
for tuple in get_point_constraints_tuples(key1):
remove_constraint(tuple)
## Remove the constraint between the two point indices of the given tuple.
func remove_constraint(point_index_tuple: Vector2i) -> void:
if SS2D_IndexTuple.dict_erase(_constraints, point_index_tuple):
emit_signal("constraint_removed", point_index_tuple.x, point_index_tuple.y)
########
# MISC #
########
func debug_print() -> void:
for k in get_all_point_keys():
var pos: Vector2 = get_point_position(k)
var _in: Vector2 = get_point_in(k)
var out: Vector2 = get_point_out(k)
print("%s = P:%s | I:%s | O:%s" % [k, pos, _in, out])
######################
# MATERIAL OVERRIDES #
######################
## dict: Dict[Vector2i, SS2D_Material_Edge_Metadata]
func set_material_overrides(dict: Dictionary) -> void:
# For backwards compatibility (Array to Vector2i transition)
# FIXME: Maybe remove during the next breaking release
SS2D_IndexTuple.dict_validate(dict, SS2D_Material_Edge_Metadata)
if _material_overrides != null:
for old: SS2D_Material_Edge_Metadata in _material_overrides.values():
_unhook_mat(old)
_material_overrides = dict
for tuple: Vector2i in _material_overrides:
_hook_mat(tuple, _material_overrides[tuple])
func has_material_override(tuple: Vector2i) -> bool:
return SS2D_IndexTuple.dict_has(_material_overrides, tuple)
func remove_material_override(tuple: Vector2i) -> void:
var old := get_material_override(tuple)
if old != null:
_unhook_mat(old)
SS2D_IndexTuple.dict_erase(_material_overrides, tuple)
_on_material_override_changed(tuple)
func set_material_override(tuple: Vector2i, mat: SS2D_Material_Edge_Metadata) -> void:
var old := get_material_override(tuple)
if old != null:
if old == mat:
return
else:
_unhook_mat(old)
_hook_mat(tuple, mat)
SS2D_IndexTuple.dict_set(_material_overrides, tuple, mat)
_on_material_override_changed(tuple)
## Returns the material override for the edge defined by the given point index tuple, or null if
## there is no override.
func get_material_override(tuple: Vector2i) -> SS2D_Material_Edge_Metadata:
return SS2D_IndexTuple.dict_get(_material_overrides, tuple)
func _hook_mat(tuple: Vector2i, mat: SS2D_Material_Edge_Metadata) -> void:
if not mat.changed.is_connected(_on_material_override_changed):
mat.changed.connect(_on_material_override_changed.bind(tuple))
func _unhook_mat(mat: SS2D_Material_Edge_Metadata) -> void:
if mat.changed.is_connected(_on_material_override_changed):
mat.changed.disconnect(_on_material_override_changed)
## Returns a list of index tuples for wich material overrides exist.
func get_material_overrides() -> Array[Vector2i]:
var keys: Array[Vector2i] = []
keys.assign(_material_overrides.keys())
return keys
func clear_all_material_overrides() -> void:
_material_overrides = {}
## Returns a PackedVector2Array with all points of the shape.
func get_vertices() -> PackedVector2Array:
_update_cache()
return _vertex_cache
## Returns a Curve2D representing the shape including bezier handles.
func get_curve() -> Curve2D:
_update_cache()
return _curve
## Returns a Curve2D representing the shape, disregarding bezier handles.
func get_curve_no_control_points() -> Curve2D:
_update_cache()
return _curve_no_control_points
## Returns a PackedVector2Array with all points
func get_tessellated_points() -> PackedVector2Array:
_update_cache()
return _tesselation_cache
func set_tessellation_stages(value: int) -> void:
tessellation_stages = value
_changed()
func set_tessellation_tolerance(value: float) -> void:
tessellation_tolerance = value
_changed()
func set_curve_bake_interval(f: float) -> void:
curve_bake_interval = f
_curve.bake_interval = f
_changed()
func get_tesselation_vertex_mapping() -> SS2D_TesselationVertexMapping:
_update_cache()
return _tess_vertex_mapping
func _update_cache() -> void:
# NOTE: Theoretically one could differentiate between vertex list dirty, curve dirty and
# tesselation dirty to never waste any computation time.
# However, 99% of the time, the cache will be dirty due to vertex updates, so we don't bother.
if not _point_cache_dirty:
return
var keys := get_all_point_keys()
_vertex_cache.resize(keys.size())
_curve.clear_points()
_curve_no_control_points.clear_points()
for i in keys.size():
var key := keys[i]
var pos := get_point_position(keys[i])
# Vertex cache
_vertex_cache[i] = pos
# Curves
_curve.add_point(pos, get_point_in(key), get_point_out(key))
_curve_no_control_points.add_point(pos)
# Tesselation
# Point 0 will be the same on both the curve points and the vertices
# Point size - 1 will be the same on both the curve points and the vertices
_tesselation_cache = _curve.tessellate(tessellation_stages, tessellation_tolerance)
if _tesselation_cache.size() >= 2:
_tesselation_cache[0] = _curve.get_point_position(0)
_tesselation_cache[-1] = _curve.get_point_position(_curve.get_point_count() - 1)
_tess_vertex_mapping.build(_tesselation_cache, _vertex_cache)
_point_cache_dirty = false
func _to_string() -> String:
return "<SS2D_Point_Array points: %s order: %s>" % [_points.keys(), _point_order]
func _on_material_override_changed(tuple: Vector2i) -> void:
material_override_changed.emit(tuple)

View File

@@ -0,0 +1,210 @@
@tool
extends RefCounted
class_name SS2D_Quad
enum ORIENTATION { COLINEAR = 0, CCW, CW }
enum CORNER { NONE = 0, OUTER, INNER }
var pt_a: Vector2
var pt_b: Vector2
var pt_c: Vector2
var pt_d: Vector2
var tg_a : Vector2
var tg_b : Vector2
var tg_c : Vector2
var tg_d : Vector2
var bn_a : Vector2
var bn_b : Vector2
var bn_c : Vector2
var bn_d : Vector2
var texture: Texture2D = null
var color: Color = Color(1.0, 1.0, 1.0, 1.0)
var is_tapered: bool = false
var ignore_weld_next: bool = false
var flip_texture: bool = false
# Deprecated, should remove control_point_index
var control_point_index: int
var fit_texture := SS2D_Material_Edge.FITMODE.SQUISH_AND_STRETCH
# Contains value from CORNER enum
var corner: int = 0
# Will return two quads split down the middle of this one
func bisect() -> Array[SS2D_Quad]:
var delta: Vector2 = pt_d - pt_a
var delta_normal := delta.normalized()
var quad_left: SS2D_Quad = duplicate()
var quad_right: SS2D_Quad = duplicate()
var mid_point := Vector2(get_length_average(), 0.0) * delta_normal
quad_left.pt_d = pt_a + mid_point
quad_left.pt_c = pt_b + mid_point
quad_right.pt_a = pt_d - mid_point
quad_right.pt_b = pt_c - mid_point
return [quad_left, quad_right]
func _to_string() -> String:
return "[Quad] A:%s B:%s C:%s D:%s | Corner: %s" % [pt_a, pt_b, pt_c, pt_d, corner]
func matches_quad(q: SS2D_Quad) -> bool:
return (
texture == q.texture
and color == q.color
and flip_texture == q.flip_texture
and fit_texture == q.fit_texture
)
func duplicate() -> SS2D_Quad:
var q := SS2D_Quad.new()
q.pt_a = pt_a
q.pt_b = pt_b
q.pt_c = pt_c
q.pt_d = pt_d
q.texture = texture
q.color = color
q.flip_texture = flip_texture
q.control_point_index = control_point_index
q.corner = corner
return q
func update_tangents() -> void:
tg_a = (pt_d-pt_a).normalized()
tg_b = (pt_c-pt_b).normalized()
tg_c = tg_b
tg_d = tg_a
bn_a = (pt_b - pt_a).normalized()
bn_b = bn_a
bn_c = (pt_c - pt_d).normalized()
bn_d = bn_c
func _init(
a: Vector2 = Vector2.ZERO,
b: Vector2 = Vector2.ZERO,
c: Vector2 = Vector2.ZERO,
d: Vector2 = Vector2.ZERO,
t: Texture2D = null,
f: bool = false
) -> void:
pt_a = a
pt_b = b
pt_c = c
pt_d = d
texture = t
flip_texture = f
func get_rotation() -> float:
return SS2D_NormalRange.get_angle_from_vector(pt_c - pt_a)
## Given three colinear points p, q, r, the function checks if
## point q lies on line segment 'pr'.
func on_segment(p: Vector2, q: Vector2, r: Vector2) -> bool:
return (
(q.x <= maxf(p.x, r.x))
and (q.x >= minf(p.x, r.x))
and (q.y <= maxf(p.y, r.y))
and (q.y >= minf(p.y, r.y))
)
## Returns CCW, CW, or colinear.[br]
## see https://www.geeksforgeeks.org/check-if-two-given-line-segments-intersect/
func get_orientation(a: Vector2, b: Vector2, c: Vector2) -> ORIENTATION:
var val := (float(b.y - a.y) * (c.x - b.x)) - (float(b.x - a.x) * (c.y - b.y))
if val > 0:
return ORIENTATION.CW
elif val < 0:
return ORIENTATION.CCW
return ORIENTATION.COLINEAR
## Return true if line segments p1q1 and p2q2 intersect.
func edges_intersect(p1: Vector2, q1: Vector2, p2: Vector2, q2: Vector2) -> bool:
var o1 := get_orientation(p1, q1, p2)
var o2 := get_orientation(p1, q1, q2)
var o3 := get_orientation(p2, q2, p1)
var o4 := get_orientation(p2, q2, q1)
# General case
if (o1 != o2) and (o3 != o4):
return true
# Special Cases
# p1 , q1 and p2 are colinear and p2 lies on segment p1q1
if (o1 == 0) and on_segment(p1, p2, q1):
return true
# p1 , q1 and q2 are colinear and q2 lies on segment p1q1
if (o2 == 0) and on_segment(p1, q2, q1):
return true
# p2 , q2 and p1 are colinear and p1 lies on segment p2q2
if (o3 == 0) and on_segment(p2, p1, q2):
return true
# p2 , q2 and q1 are colinear and q1 lies on segment p2q2
if (o4 == 0) and on_segment(p2, q1, q2):
return true
return false
func self_intersects() -> bool:
return edges_intersect(pt_a, pt_d, pt_b, pt_c) or edges_intersect(pt_a, pt_b, pt_d, pt_c)
func render_lines(ci: CanvasItem) -> void:
ci.draw_line(pt_a, pt_b, color)
ci.draw_line(pt_b, pt_c, color)
ci.draw_line(pt_c, pt_d, color)
ci.draw_line(pt_d, pt_a, color)
func render_points(rad: float, intensity: float, ci: CanvasItem) -> void:
ci.draw_circle(pt_a, rad, Color(intensity, 0, 0))
ci.draw_circle(pt_b, rad, Color(0, 0, intensity))
ci.draw_circle(pt_c, rad, Color(0, intensity, 0))
ci.draw_circle(pt_d, rad, Color(intensity, 0, intensity))
func get_height_average() -> float:
return (get_height_left() + get_height_right()) / 2.0
func get_height_left() -> float:
return pt_a.distance_to(pt_b)
func get_height_right() -> float:
return pt_d.distance_to(pt_c)
## Returns the difference in height between the left and right sides.
func get_height_difference() -> float:
return get_height_left() - get_height_right()
func get_length_average() -> float:
return (get_length_top() + get_length_bottom()) / 2.0
func get_length_top() -> float:
return pt_d.distance_to(pt_a)
func get_length_bottom() -> float:
return pt_c.distance_to(pt_b)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,208 @@
@tool
@icon("../assets/Anchor.svg")
extends Node2D
class_name SS2D_Shape_Anchor
const DEBUG_DRAW_LINE_LENGTH := 128.0
@export var shape_path: NodePath : set = set_shape_path
@export var shape_point_index: int = 0 : set = set_shape_point_index
@export_range (0.0, 1.0) var shape_point_offset: float = 0.0 : set = set_shape_point_offset
@export_range (0, 3.14) var child_rotation: float = 3.14 : set = set_child_rotation
@export var use_shape_scale: bool = false : set = set_use_shape_scale
@export var debug_draw: bool = false : set = set_debug_draw
var cached_shape_transform: Transform2D = Transform2D.IDENTITY
var shape: SS2D_Shape = null
###########
#-SETTERS-#
###########
func set_shape_path(value: NodePath) -> void:
# Assign path value
shape_path = value
set_shape()
notify_property_list_changed()
refresh()
func set_shape() -> void:
# Disconnect old shape
if shape != null:
disconnect_shape(shape)
# Set shape if path is valid and connect
shape = null
if has_node(shape_path):
var new_node: Node = get_node(shape_path)
if not new_node is SS2D_Shape:
push_error("Shape Path isn't a valid subtype of SS2D_Shape! Aborting...")
return
shape = new_node
connect_shape(shape)
shape_point_index = get_shape_index_range(shape, shape_point_index)
func get_shape_index_range(s: SS2D_Shape, idx: int) -> int:
var point_count: int = s.get_point_count()
# Subtract 2;
# 'point_count' is out of bounds; subtract 1
# cannot use final idx as starting point_index; subtract another 1
var final_idx: int = point_count - 2
if idx < 0:
idx = final_idx
idx = idx % (final_idx + 1)
return idx
func set_shape_point_index(value: int) -> void:
if value == shape_point_index:
return
if shape == null:
shape_point_index = value
return
shape_point_index = get_shape_index_range(shape, value)
#notify_property_list_changed()
refresh()
func set_shape_point_offset(value: float) -> void:
shape_point_offset = value
#notify_property_list_changed()
refresh()
func set_use_shape_scale(value: bool) -> void:
use_shape_scale = value
#notify_property_list_changed()
refresh()
func set_child_rotation(value: float) -> void:
child_rotation = value
#notify_property_list_changed()
refresh()
func set_debug_draw(v: bool) -> void:
debug_draw = v
#notify_property_list_changed()
refresh()
##########
#-EVENTS-#
##########
func _process(_delta: float) -> void:
if shape == null:
set_shape()
return
if shape.is_queued_for_deletion():
return
if shape.get_global_transform() != cached_shape_transform:
cached_shape_transform = shape.get_global_transform()
refresh()
func _monitored_node_leaving() -> void:
pass
func _handle_point_change() -> void:
refresh()
#########
#LOGIC-#
#########
func _cubic_bezier(p0: Vector2, p1: Vector2, p2: Vector2, p3: Vector2, t: float) -> Vector2:
var q0 := p0.lerp(p1, t)
var q1 := p1.lerp(p2, t)
var q2 := p2.lerp(p3, t)
var r0 := q0.lerp(q1, t)
var r1 := q1.lerp(q2, t)
var s := r0.lerp(r1, t)
return s
func disconnect_shape(s: SS2D_Shape) -> void:
s.disconnect("points_modified", self._handle_point_change)
s.disconnect("tree_exiting", self._monitored_node_leaving)
func connect_shape(s: SS2D_Shape) -> void:
s.connect("points_modified", self._handle_point_change)
s.connect("tree_exiting", self._monitored_node_leaving)
func refresh() -> void:
if shape == null:
return
if not is_instance_valid(shape):
return
if shape.is_queued_for_deletion():
disconnect_shape(shape)
shape = null
return
# Subtract one, cannot use final point as starting index
# var point_count: int = shape.get_point_count() - 1
var pt_a_index: int = shape_point_index
var pt_b_index: int = shape_point_index + 1
var pt_a_key: int = shape.get_point_key_at_index(pt_a_index)
var pt_b_key: int = shape.get_point_key_at_index(pt_b_index)
var pt_a: Vector2 = shape.global_transform * shape.get_point_position(pt_a_key)
var pt_b: Vector2 = shape.global_transform * shape.get_point_position(pt_b_key)
var pt_a_handle: Vector2
var pt_b_handle: Vector2
var n_pt: Vector2
var n_pt_a: Vector2
var n_pt_b: Vector2
var angle := 0.0
pt_a_handle = shape.global_transform * (
shape.get_point_position(pt_a_key) + shape.get_point_out(pt_a_key)
)
pt_b_handle = shape.global_transform * (
shape.get_point_position(pt_b_key) + shape.get_point_in(pt_b_key)
)
# If this segment uses no bezier curve, use linear interpolation instead
if pt_a_handle != pt_a or pt_b_handle != pt_b:
n_pt = _cubic_bezier(pt_a, pt_a_handle, pt_b_handle, pt_b, shape_point_offset)
else:
n_pt = pt_a.lerp(pt_b, shape_point_offset)
n_pt_a = _cubic_bezier(
pt_a, pt_a_handle, pt_b_handle, pt_b, clampf(shape_point_offset - 0.1, 0.0, 1.0)
)
n_pt_b = _cubic_bezier(
pt_a, pt_a_handle, pt_b_handle, pt_b, clampf(shape_point_offset + 0.1, 0.0, 1.0)
)
angle = atan2(n_pt_a.y - n_pt_b.y, n_pt_a.x - n_pt_b.x)
self.global_transform = Transform2D(angle + child_rotation, n_pt)
if use_shape_scale:
self.scale = shape.scale
queue_redraw()
func _draw() -> void:
if Engine.is_editor_hint() and debug_draw:
draw_line(Vector2.ZERO, Vector2(0, -DEBUG_DRAW_LINE_LENGTH), self.modulate)

View File

@@ -0,0 +1,58 @@
@tool
@icon("../assets/closed_shape.png")
extends SS2D_Shape
class_name SS2D_Shape_Closed
## DEPRECATED: Use [SS2D_Shape] instead.
## @deprecated
# UNUSED FUNCTIONS:
## Returns true if line segment 'a1a2' and 'b1b2' intersect.[br]
## Find the four orientations needed for general and special cases.[br]
#func do_edges_intersect(a1: Vector2, a2: Vector2, b1: Vector2, b2: Vector2) -> bool:
# var o1: int = get_points_orientation([a1, a2, b1])
# var o2: int = get_points_orientation([a1, a2, b2])
# var o3: int = get_points_orientation([b1, b2, a1])
# var o4: int = get_points_orientation([b1, b2, a2])
#
# # General case
# if o1 != o2 and o3 != o4:
# return true
#
# # Special Cases
# # a1, a2 and b1 are colinear and b1 lies on segment p1q1
# if o1 == ORIENTATION.COLINEAR and on_segment(a1, b1, a2):
# return true
#
# # a1, a2 and b2 are colinear and b2 lies on segment p1q1
# if o2 == ORIENTATION.COLINEAR and on_segment(a1, b2, a2):
# return true
#
# # b1, b2 and a1 are colinear and a1 lies on segment p2q2
# if o3 == ORIENTATION.COLINEAR and on_segment(b1, a1, b2):
# return true
#
# # b1, b2 and a2 are colinear and a2 lies on segment p2q2
# if o4 == ORIENTATION.COLINEAR and on_segment(b1, a2, b2):
# return true
#
# # Doesn't fall in any of the above cases
# return false
#static func get_edge_intersection(a1: Vector2, a2: Vector2, b1: Vector2, b2: Vector2) -> Variant:
# var den: float = (b2.y - b1.y) * (a2.x - a1.x) - (b2.x - b1.x) * (a2.y - a1.y)
#
# # Check if lines are parallel or coincident
# if den == 0:
# return null
#
# var ua: float = ((b2.x - b1.x) * (a1.y - b1.y) - (b2.y - b1.y) * (a1.x - b1.x)) / den
# var ub: float = ((a2.x - a1.x) * (a1.y - b1.y) - (a2.y - a1.y) * (a1.x - b1.x)) / den
#
# if ua < 0 or ub < 0 or ua > 1 or ub > 1:
# return null
#
# return Vector2(a1.x + ua * (a2.x - a1.x), a1.y + ua * (a2.y - a1.y))

View File

@@ -0,0 +1,7 @@
@tool
@icon("../assets/open_shape.png")
extends SS2D_Shape
class_name SS2D_Shape_Open
## DEPRECATED: Use [SS2D_Shape] instead.
## @deprecated

View File

@@ -0,0 +1,31 @@
@tool
extends Node2D
class_name SS2D_Shape_Render
## Node is used to render shape geometry.
var mesh: SS2D_Mesh = null : set = set_mesh
func set_mesh(m: SS2D_Mesh) -> void:
mesh = m
if m != null:
if m.force_no_tiling:
texture_repeat = CanvasItem.TEXTURE_REPEAT_DISABLED
else:
texture_repeat = CanvasItem.TEXTURE_REPEAT_PARENT_NODE
material = mesh.material
z_index = mesh.z_index
z_as_relative = mesh.z_as_relative
show_behind_parent = mesh.show_behind_parent
else:
material = null
z_index = 0
z_as_relative = true
show_behind_parent = false
queue_redraw()
func _draw() -> void:
if mesh != null:
mesh.render(self)