Skip to content

Commit

Permalink
Refactor index-tuple implementation
Browse files Browse the repository at this point in the history
- Use Vector2i instead of Array[int]
  - Less error prone
  - No problems with typed arrays
  - Better suits the tuple type

- Refactor tuple utility functions
  - Provide helper function for array and dictionary access with
    index-tuples
  - O(1) access for dictionaries rather than O(n)
  - Code cleanup

- Bugfix in IndexMap.indicies_to_edges():
  The function now correctly operates on the array's values instead of
  only its indices.
  • Loading branch information
mphe committed Jun 5, 2024
1 parent 8ee4492 commit 40e2f79
Show file tree
Hide file tree
Showing 12 changed files with 507 additions and 390 deletions.
11 changes: 6 additions & 5 deletions addons/rmsmartshape/actions/action_delete_points.gd
Original file line number Diff line number Diff line change
Expand Up @@ -67,21 +67,22 @@ func undo() -> void:
_shape.set_point_properties(_keys[i], _properties[i])
# Restore point constraints.
for i in range(_keys.size()-1, -1, -1):
for tuple: Array[int] in _constraints[i]:
for tuple: Vector2i in _constraints[i]:
_shape.set_constraint(tuple[0], tuple[1], _constraints[i][tuple])
_shape.end_update()


func add_point_to_delete(key: int) -> void:
_keys.push_back(key)
var constraints: Dictionary = _shape.get_point_constraints(key)
var constraints := _shape.get_point_array().get_point_constraints(key)
# Save point constraints.
_constraints.append(_shape.get_point_constraints(key))
for tuple: Array[int] in constraints:
_constraints.append(constraints)

for tuple: Vector2i in constraints:
var constraint: SS2D_Point_Array.CONSTRAINT = constraints[tuple]
if constraint == SS2D_Point_Array.CONSTRAINT.NONE:
continue
var key_other: int = TUP.get_other_value_from_tuple(tuple, key)
var key_other := SS2D_IndexTuple.get_other_value(tuple, key)
if constraint & SS2D_Point_Array.CONSTRAINT.ALL:
if not _keys.has(key_other):
add_point_to_delete(key_other)
147 changes: 117 additions & 30 deletions addons/rmsmartshape/lib/tuple.gd
Original file line number Diff line number Diff line change
@@ -1,48 +1,135 @@
@tool
extends RefCounted
class_name SS2D_IndexTuple

## Everything in this script should be static.
## Provides utility functions for handling and storing indices of two related points using Vector2i.
##
## This script contains code that may referenced in multiple locations in the plugin.
## Index tuples are considered equal if their elements are equal, regardless of their order:
## T(X, Y) <=> T(Y, X).
##
## This is a simple script for a *very* simple tuple class
## Some notes:
## - "tuples" are just an array with two elements
## - Only integer Tuples are fully supported
## - Tuple(X,Y) is equal to Tuple(Y,X)
## For effectively working with containers, helper functions for arrays and dictionaries are
## provided that implement the above behavior.


static func create_tuple(a: int, b: int) -> Array[int]:
return [a, b]
## Returns the second tuple element that does not equal the given value.
## Returns -1 if neither element matches.
static func get_other_value(t: Vector2i, value: int) -> int:
if t.x == value:
return t.y
elif t.y == value:
return t.x
return -1


static func get_other_value_from_tuple(t: Array[int], value: int) -> int:
if t[0] == value:
return t[1]
elif t[1] == value:
return t[0]
return -1
## Returns whether two tuples are equal. Two tuples are considered equal when both contain the same values regardless of order.
static func are_equal(t1: Vector2i, t2: Vector2i) -> bool:
return t1 == t2 or t1 == flip_elements(t2)


static func tuples_are_equal(t1: Array[int], t2: Array[int]) -> bool:
return (t1[0] == t2[0] and t1[1] == t2[1]) or (t1[0] == t2[1] and t1[1] == t2[0])
## Returns true when the tuple contains the given value.
static func has(t: Vector2i, value: int) -> bool:
return t.x == value or t.y == value


static func find_tuple_in_array_of_tuples(tuple_array: Array, t: Array[int]) -> int:
for i in range(tuple_array.size()):
var other: Array[int] = []
other.assign(tuple_array[i])
if tuples_are_equal(t, other):
## Searches for an equal tuple in the given array and returns the index or -1 if not found.
## Incorporates the equality behavior.
static func array_find(tuple_array: Array[Vector2i], t: Vector2i) -> int:
for i in tuple_array.size():
if are_equal(tuple_array[i], t):
return i
return -1


static func is_tuple_in_array_of_tuples(tuple_array: Array, t: Array[int]) -> bool:
return find_tuple_in_array_of_tuples(tuple_array, t) != -1
## Returns whether the given tuple exists in the given array.
## Incorporates the equality behavior.
static func array_has(tuple_array: Array[Vector2i], t: Vector2i) -> bool:
return array_find(tuple_array, t) != -1


## Returns a list indices to tuples that contain the given value.
static func array_find_partial(tuple_array: Array[Vector2i], value: int) -> Array[int]:
var out: Array[int] = []

for i in tuple_array.size():
if tuple_array[i].x == value or tuple_array[i].y == value:
out.push_back(i)

return out


## Returns a tuple with elements in ascending order.
static func normalize_tuple(tuple: Vector2i) -> Vector2i:
if tuple.x <= tuple.y:
return tuple
return flip_elements(tuple)


## Returns a tuple with x and y components switched.
static func flip_elements(tuple: Vector2i) -> Vector2i:
return Vector2i(tuple.y, tuple.x)


## Validates the keys of a dictionary to be correct tuple values and converts all Arrays to
## corresponding Vector2i values.
## Optionally also validates that values are of the given type.
## Exists mostly for backwards compatibility to allow a seamless transition from Array to Vector2i tuples.
static func dict_validate(dict: Dictionary, value_type: Variant = null) -> void:
# TODO: Maybe don't use asserts but push_warning and return true if successful
for key: Variant in dict.keys():
var value: Variant = dict[key]

if value_type != null:
assert(is_instance_of(value, value_type), "Incorrect value type in dictionary: " + var_to_str(value))

if key is Array:
var converted := Vector2i(int(key[0]), int(key[1]))
dict.erase(key)
dict[converted] = value
else:
assert(key is Vector2i, "Invalid tuple representation: %s. Should be Vector2i." % var_to_str(key))


## Get the value in a dictionary with the given tuple as key or a default value if it does not exist.
## Incorporates the equality behavior.
static func dict_get(dict: Dictionary, tuple: Vector2i, default: Variant = null) -> Variant:
if dict.has(tuple):
return dict[tuple]
return dict.get(flip_elements(tuple), default)


static func dict_has(dict: Dictionary, tuple: Vector2i) -> bool:
if dict.has(tuple):
return true
return dict.has(flip_elements(tuple))


static func dict_set(dict: Dictionary, tuple: Vector2i, value: Variant) -> void:
dict[dict_get_key(dict, tuple)] = value


## Removes the given entry from the dictionary. Returns true if a corresponding key existed, otherwise false.
static func dict_erase(dict: Dictionary, tuple: Vector2i) -> bool:
return dict.erase(dict_get_key(dict, tuple))


## Checks if there is an existing key for the given tuple or its flipped variant and returns it.
## If a key does not exist, returns the tuple as it is.
## Usually this function does not need to be invoked manually, as helpers for dictionary and array access exist.
static func dict_get_key(dict: Dictionary, tuple: Vector2i) -> Vector2i:
if not dict.has(tuple):
var flipped := flip_elements(tuple)

if dict.has(flipped):
return flipped

return tuple


## Returns a list of all dictionary keys (tuples) that contain the given value.
static func dict_find_partial(dict: Dictionary, value: int) -> Array[Vector2i]:
var out: Array[Vector2i] = []

for t: Vector2i in dict.keys():
if t.x == value or t.y == value:
out.push_back(t)

static func is_tuple(thing: Variant) -> bool:
if not thing is Array:
return false
var arr := thing as Array
return arr and arr.size() == 2
return out
59 changes: 26 additions & 33 deletions addons/rmsmartshape/plugin.gd
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ var edge_data: SS2D_Edge = null
var on_width_handle: bool = false
const WIDTH_HANDLE_OFFSET: float = 60.0
var closest_key: int
var closest_edge_keys: Array[int] = [-1, -1]
var closest_edge_keys := Vector2i(-1, -1)
var width_scaling: float

# Vertex paint mode stuff
Expand Down Expand Up @@ -342,13 +342,13 @@ func _gui_update_edge_info_panel() -> void:
# Don't update if already visible
if gui_edge_info_panel.visible:
return
var indicies: Array[int] = [-1, -1]
var indicies := Vector2i(-1, -1)
var override: SS2D_Material_Edge_Metadata = null
if on_edge:
var t: Transform2D = get_et() * shape.get_global_transform()
var offset: float = shape.get_closest_offset_straight_edge(t.affine_inverse() * edge_point)
var keys: Array[int] = _get_edge_point_keys_from_offset(offset, true)
indicies = [shape.get_point_index(keys[0]), shape.get_point_index(keys[1])]
var keys: Vector2i = _get_edge_point_keys_from_offset(offset, true)
indicies = Vector2i(shape.get_point_index(keys.x), shape.get_point_index(keys.y))
if shape.get_point_array().has_material_override(keys):
override = shape.get_point_array().get_material_override(keys)
gui_edge_info_panel.set_indicies(indicies)
Expand Down Expand Up @@ -585,12 +585,7 @@ func connect_shape(s: SS2D_Shape) -> void:


func get_material_override_from_indicies() -> SS2D_Material_Edge_Metadata:
var keys: Array[int] = []
var indices := gui_edge_info_panel.indicies
for i in indices:
keys.push_back(shape.get_point_key_at_index(i))
if not shape.get_point_array().has_material_override(keys):
return null
var keys := shape.get_point_array().get_edge_keys_for_indices(gui_edge_info_panel.indicies)
return shape.get_point_array().get_material_override(keys)


Expand Down Expand Up @@ -619,17 +614,15 @@ func _on_edge_material_changed(m: SS2D_Material_Edge) -> void:


func _on_edge_material_override_toggled(enabled: bool) -> void:
var indicies: Array[int] = gui_edge_info_panel.indicies
if indicies.has(-1) or indicies.size() != 2:
var indices := gui_edge_info_panel.indicies

if SS2D_IndexTuple.has(indices, -1):
return
var keys: Array[int] = []
for i in indicies:
keys.push_back(shape.get_point_key_at_index(i))

var keys := shape.get_point_array().get_edge_keys_for_indices(indices)

# Get the relevant Override data if any exists
var override: SS2D_Material_Edge_Metadata = null
if shape.get_point_array().has_material_override(keys):
override = shape.get_point_array().get_material_override(keys)
var override: SS2D_Material_Edge_Metadata = shape.get_point_array().get_material_override(keys)

if enabled:
if override == null:
Expand Down Expand Up @@ -791,16 +784,16 @@ func draw_mode_edit_edge(overlay: Control, color_normal: Color, color_highlight:
draw_vert_handles(overlay, t, verts, false)

if current_action.type == ACTION_VERT.MOVE_VERT:
var edge_point_keys: Array[int] = current_action.keys
var edge_point_keys := current_action.keys
var p1: Vector2 = shape.get_point_position(edge_point_keys[0])
var p2: Vector2 = shape.get_point_position(edge_point_keys[1])
overlay.draw_line(t * p1, t * p2, Color.BLACK, 8.0, true)
overlay.draw_line(t * p1, t * p2, color_highlight, 4.0, true)
elif on_edge:
var offset: float = shape.get_closest_offset_straight_edge(t.affine_inverse() * edge_point)
var edge_point_keys: Array[int] = _get_edge_point_keys_from_offset(offset, true)
var p1: Vector2 = shape.get_point_position(edge_point_keys[0])
var p2: Vector2 = shape.get_point_position(edge_point_keys[1])
var edge_point_keys := _get_edge_point_keys_from_offset(offset, true)
var p1: Vector2 = shape.get_point_position(edge_point_keys.x)
var p2: Vector2 = shape.get_point_position(edge_point_keys.y)
overlay.draw_line(t * p1, t * p2, Color.BLACK, 8.0, true)
overlay.draw_line(t * p1, t * p2, color_highlight, 4.0, true)

Expand All @@ -812,9 +805,9 @@ func draw_mode_cut_edge(overlay: Control) -> void:
# Draw "X" marks along the edge that is selected
var t: Transform2D = get_et() * shape.get_global_transform()
var offset: float = shape.get_closest_offset_straight_edge(t.affine_inverse() * edge_point)
var edge_point_keys: Array[int] = _get_edge_point_keys_from_offset(offset, true)
var from: Vector2 = t * shape.get_point_position(edge_point_keys[0])
var to: Vector2 = t * shape.get_point_position(edge_point_keys[1])
var edge_point_keys := _get_edge_point_keys_from_offset(offset, true)
var from: Vector2 = t * shape.get_point_position(edge_point_keys.x)
var to: Vector2 = t * shape.get_point_position(edge_point_keys.y)
var dir: Vector2 = (to - from).normalized()
var angle: float = dir.angle()
var length: float = (to - from).length()
Expand Down Expand Up @@ -1134,17 +1127,17 @@ func _input_handle_left_click(
var offset: float = shape.get_closest_offset_straight_edge(
t.affine_inverse() * edge_point
)
var edge_point_keys: Array[int] = _get_edge_point_keys_from_offset(offset, true)
select_vertices_to_move([edge_point_keys[0], edge_point_keys[1]], vp_m_pos)
var edge_point_keys := _get_edge_point_keys_from_offset(offset, true)
select_vertices_to_move([edge_point_keys.x, edge_point_keys.y], vp_m_pos)
if _defer_mesh_updates:
shape.begin_update()
return true
elif current_mode == MODE.CUT_EDGE:
if not on_edge:
return true
var offset: float = shape.get_closest_offset_straight_edge(t.affine_inverse() * edge_point)
var edge_keys: Array[int] = _get_edge_point_keys_from_offset(offset, true)
perform_action(ActionCutEdge.new(shape, edge_keys[0], edge_keys[1]))
var edge_keys := _get_edge_point_keys_from_offset(offset, true)
perform_action(ActionCutEdge.new(shape, edge_keys.x, edge_keys.y))
on_edge = false
return true
elif current_mode == MODE.FREEHAND:
Expand Down Expand Up @@ -1348,7 +1341,7 @@ func _input_move_control_points(mb: InputEventMouseButton, vp_m_pos: Vector2, gr

func _get_edge_point_keys_from_offset(
offset: float, straight: bool = false, _position := Vector2(0, 0)
) -> Array[int]:
) -> Vector2i:
for i in range(0, shape.get_point_count() - 1, 1):
var key: int = shape.get_point_key_at_index(i)
var key_next: int = shape.get_point_key_at_index(i + 1)
Expand All @@ -1362,11 +1355,11 @@ func _get_edge_point_keys_from_offset(
next_offset = shape.get_closest_offset(shape.get_point_position(key_next))

if offset >= this_offset and offset <= next_offset:
return [key, key_next]
return Vector2i(key, key_next)
# for when the shape is closed and the final point has an offset of 0
if next_offset == 0 and offset >= this_offset:
return [key, key_next]
return [-1, -1]
return Vector2i(key, key_next)
return Vector2i(-1, -1)


func _input_motion_is_on_edge(mm: InputEventMouseMotion, grab_threshold: float) -> bool:
Expand Down
9 changes: 4 additions & 5 deletions addons/rmsmartshape/scenes/GUI_Edge_InfoPanel.gd
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@
extends PanelContainer
class_name SS2D_EdgeInfoPanel


signal material_override_toggled(enabled: bool)
signal render_toggled(enabled: bool)
signal weld_toggled(enabled: bool)
signal z_index_changed(value: int)
signal edge_material_changed(value: SS2D_Material_Edge)

var indicies: Array[int] = [-1, -1] : set = set_indicies
var indicies := Vector2i(-1, -1) : set = set_indicies
var edge_material: SS2D_Material_Edge = null
var edge_material_selector := FileDialog.new()

Expand Down Expand Up @@ -63,9 +62,9 @@ func _on_set_edge_material_file_selected(f: String) -> void:
set_edge_material(rsc)


func set_indicies(a: Array[int]) -> void:
indicies = a
idx_label.text = "IDX: [%s, %s]" % [indicies[0], indicies[1]]
func set_indicies(t: Vector2i) -> void:
indicies = t
idx_label.text = "IDX: %s" % indicies


func set_material_override(enabled: bool) -> void:
Expand Down
Loading

0 comments on commit 40e2f79

Please sign in to comment.