Files
przygody-pana-cegly/addons/rmsmartshape/shapes/point_array.gd

676 lines
18 KiB
GDScript

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