diff --git a/addons/rmsmartshape/actions/action.gd b/addons/rmsmartshape/actions/action.gd
new file mode 100644
index 0000000..5125edc
--- /dev/null
+++ b/addons/rmsmartshape/actions/action.gd
@@ -0,0 +1,23 @@
+class_name SS2D_Action
+extends RefCounted
+
+## Base class for all plugin actions.
+##
+## UndoRedo system will call [method do] and [method undo].
+
+# @virtual
+## Returns string to be used as a name in editor History tab.
+func get_name() -> String:
+ return "UntitledAction"
+
+
+# @virtual
+## Do action here.
+func do() -> void:
+ pass
+
+
+# @virtual
+## Undo action here.
+func undo() -> void:
+ pass
diff --git a/addons/rmsmartshape/actions/action_add_collision_nodes.gd b/addons/rmsmartshape/actions/action_add_collision_nodes.gd
new file mode 100644
index 0000000..648f67d
--- /dev/null
+++ b/addons/rmsmartshape/actions/action_add_collision_nodes.gd
@@ -0,0 +1,56 @@
+extends SS2D_Action
+
+## ActionAddCollisionNodes
+
+var _shape: SS2D_Shape
+
+var _saved_index: int
+var _saved_pos: Vector2
+
+
+func _init(shape: SS2D_Shape) -> void:
+ _shape = shape
+
+
+func get_name() -> String:
+ return "Add Collision Nodes"
+
+
+func do() -> void:
+ _saved_index = _shape.get_index()
+ _saved_pos = _shape.position
+
+ var owner := _shape.owner
+ var static_body := StaticBody2D.new()
+ static_body.position = _shape.position
+ _shape.position = Vector2.ZERO
+
+ _shape.get_parent().add_child(static_body, true)
+ static_body.owner = owner
+
+ _shape.get_parent().remove_child(_shape)
+ static_body.add_child(_shape, true)
+ _shape.owner = owner
+
+ var poly: CollisionPolygon2D = CollisionPolygon2D.new()
+ static_body.add_child(poly, true)
+ poly.owner = owner
+ # TODO: Make this a option at some point
+ poly.modulate.a = 0.3
+ poly.visible = false
+ _shape.collision_polygon_node_path = _shape.get_path_to(poly)
+
+
+func undo() -> void:
+ var owner := _shape.owner
+ var parent := _shape.get_parent()
+ var grandparent := _shape.get_parent().get_parent()
+ parent.remove_child(_shape)
+ grandparent.remove_child(parent)
+ parent.free()
+
+ grandparent.add_child(_shape)
+ _shape.owner = owner
+ grandparent.move_child(_shape, _saved_index)
+ _shape.position = _saved_pos
+
diff --git a/addons/rmsmartshape/actions/action_add_point.gd b/addons/rmsmartshape/actions/action_add_point.gd
new file mode 100644
index 0000000..017eadc
--- /dev/null
+++ b/addons/rmsmartshape/actions/action_add_point.gd
@@ -0,0 +1,44 @@
+extends SS2D_Action
+
+## ActionAddPoint
+
+const ActionInvertOrientation := preload("res://addons/rmsmartshape/actions/action_invert_orientation.gd")
+var _invert_orientation: ActionInvertOrientation
+
+var _commit_update: bool
+var _shape: SS2D_Shape
+var _key: int
+var _position: Vector2
+var _idx: int
+
+
+func _init(shape: SS2D_Shape, position: Vector2, idx: int = -1, commit_update: bool = true) -> void:
+ _shape = shape
+ _position = position
+ _commit_update = commit_update
+ _idx = _shape.adjust_add_point_index(idx)
+ _key = _shape.reserve_key()
+ _invert_orientation = ActionInvertOrientation.new(shape)
+
+
+func get_name() -> String:
+ return "Add Point at (%d, %d)" % [_position.x, _position.y]
+
+
+func do() -> void:
+ _shape.begin_update()
+ _key = _shape.add_point(_position, _idx, _key)
+ _invert_orientation.do()
+ if _commit_update:
+ _shape.end_update()
+
+
+func undo() -> void:
+ _shape.begin_update()
+ _invert_orientation.undo()
+ _shape.remove_point(_key)
+ _shape.end_update()
+
+
+func get_key() -> int:
+ return _key
diff --git a/addons/rmsmartshape/actions/action_close_shape.gd b/addons/rmsmartshape/actions/action_close_shape.gd
new file mode 100644
index 0000000..d935209
--- /dev/null
+++ b/addons/rmsmartshape/actions/action_close_shape.gd
@@ -0,0 +1,40 @@
+extends SS2D_Action
+
+## ActionCloseShape
+
+const ActionInvertOrientation := preload("res://addons/rmsmartshape/actions/action_invert_orientation.gd")
+var _invert_orientation: ActionInvertOrientation
+
+var _shape: SS2D_Shape
+var _key: int
+var _performed: bool
+
+
+func _init(shape: SS2D_Shape) -> void:
+ _shape = shape
+ _invert_orientation = ActionInvertOrientation.new(shape)
+
+
+func get_name() -> String:
+ return "Close Shape"
+
+
+func do() -> void:
+ _performed = _shape.can_close()
+ if _performed:
+ _shape.begin_update()
+ _key = _shape.close_shape(_key)
+ _invert_orientation.do()
+ _shape.end_update()
+
+
+func undo() -> void:
+ if _performed:
+ _shape.begin_update()
+ _invert_orientation.undo()
+ _shape.remove_point(_key)
+ _shape.end_update()
+
+
+func get_key() -> int:
+ return _key
diff --git a/addons/rmsmartshape/actions/action_cut_edge.gd b/addons/rmsmartshape/actions/action_cut_edge.gd
new file mode 100644
index 0000000..5fc3f65
--- /dev/null
+++ b/addons/rmsmartshape/actions/action_cut_edge.gd
@@ -0,0 +1,41 @@
+extends SS2D_Action
+
+## ActionCutEdge
+##
+## A delegate action that selects an action to perform based on the edge
+## location and shape state.
+
+var ActionOpenShape := preload("res://addons/rmsmartshape/actions/action_open_shape.gd")
+var ActionDeletePoint := preload("res://addons/rmsmartshape/actions/action_delete_point.gd")
+var ActionSplitShape := preload("res://addons/rmsmartshape/actions/action_split_shape.gd")
+
+var _shape: SS2D_Shape
+var _action: SS2D_Action
+
+
+func _init(shape: SS2D_Shape, key_edge_start: int, key_edge_end: int) -> void:
+ _shape = shape
+
+ var key_first: int = shape.get_point_key_at_index(0)
+ var key_last: int = shape.get_point_key_at_index(shape.get_point_count()-1)
+ if _shape.is_shape_closed():
+ _action = ActionOpenShape.new(shape, key_edge_start)
+ elif key_edge_start == key_first:
+ _action = ActionDeletePoint.new(shape, key_edge_start)
+ elif key_edge_end == key_last:
+ _action = ActionDeletePoint.new(shape, key_edge_end)
+ else:
+ _action = ActionSplitShape.new(shape, key_edge_start)
+
+
+func get_name() -> String:
+ return _action.get_name()
+
+
+func do() -> void:
+ _action.do()
+
+
+func undo() -> void:
+ _action.undo()
+
diff --git a/addons/rmsmartshape/actions/action_delete_control_point.gd b/addons/rmsmartshape/actions/action_delete_control_point.gd
new file mode 100644
index 0000000..56a2bdc
--- /dev/null
+++ b/addons/rmsmartshape/actions/action_delete_control_point.gd
@@ -0,0 +1,45 @@
+extends SS2D_Action
+
+## ActionDeleteControlPoint
+
+enum PointType {POINT_IN, POINT_OUT}
+
+const ActionInvertOrientation := preload("res://addons/rmsmartshape/actions/action_invert_orientation.gd")
+var _invert_orientation: ActionInvertOrientation
+
+var _shape: SS2D_Shape
+var _key: int
+var _point_type: PointType
+var _old_value: Vector2
+
+
+func _init(shape: SS2D_Shape, key: int, point_type: PointType) -> void:
+ _shape = shape
+ _key = key
+ _point_type = point_type
+ _old_value = shape.get_point_in(key) if _point_type == PointType.POINT_IN else shape.get_point_out(key)
+ _invert_orientation = ActionInvertOrientation.new(shape)
+
+
+func get_name() -> String:
+ return "Delete Control Point " + ("In" if _point_type == PointType.POINT_IN else "Out")
+
+
+func do() -> void:
+ _shape.begin_update()
+ if _point_type == PointType.POINT_IN:
+ _shape.set_point_in(_key, Vector2.ZERO)
+ else:
+ _shape.set_point_out(_key, Vector2.ZERO)
+ _invert_orientation.do()
+ _shape.end_update()
+
+
+func undo() -> void:
+ _shape.begin_update()
+ _invert_orientation.undo()
+ if _point_type == PointType.POINT_IN:
+ _shape.set_point_in(_key, _old_value)
+ else:
+ _shape.set_point_out(_key, _old_value)
+ _shape.end_update()
diff --git a/addons/rmsmartshape/actions/action_delete_point.gd b/addons/rmsmartshape/actions/action_delete_point.gd
new file mode 100644
index 0000000..ad63e85
--- /dev/null
+++ b/addons/rmsmartshape/actions/action_delete_point.gd
@@ -0,0 +1,13 @@
+extends "res://addons/rmsmartshape/actions/action_delete_points.gd"
+
+## ActionDeletePoint
+
+
+func _init(shape: SS2D_Shape, key: int, commit_update: bool = true) -> void:
+ var keys: PackedInt32Array = [key]
+ super(shape, keys, commit_update)
+
+
+func get_name() -> String:
+ var pos := _shape.get_point_position(_keys[0])
+ return "Delete Point at (%d, %d)" % [pos.x, pos.y]
diff --git a/addons/rmsmartshape/actions/action_delete_points.gd b/addons/rmsmartshape/actions/action_delete_points.gd
new file mode 100644
index 0000000..60ec5ba
--- /dev/null
+++ b/addons/rmsmartshape/actions/action_delete_points.gd
@@ -0,0 +1,88 @@
+extends SS2D_Action
+
+## ActionDeletePoints
+
+const TUP := preload("res://addons/rmsmartshape/lib/tuple.gd")
+
+const ActionInvertOrientation := preload("res://addons/rmsmartshape/actions/action_invert_orientation.gd")
+var _invert_orientation: ActionInvertOrientation
+
+const ActionCloseShape := preload("res://addons/rmsmartshape/actions/action_close_shape.gd")
+var _close_shape: ActionCloseShape
+
+var _shape: SS2D_Shape
+var _keys: PackedInt32Array
+var _indicies: PackedInt32Array
+var _positions: PackedVector2Array
+var _points_in: PackedVector2Array
+var _points_out: PackedVector2Array
+var _properties: Array[SS2D_VertexProperties]
+var _constraints: Array[Dictionary]
+var _was_closed: bool
+var _commit_update: bool
+
+
+func _init(shape: SS2D_Shape, keys: PackedInt32Array, commit_update: bool = true) -> void:
+ _shape = shape
+ _invert_orientation = ActionInvertOrientation.new(shape)
+ _close_shape = ActionCloseShape.new(shape)
+ _commit_update = commit_update
+
+ for k in keys:
+ add_point_to_delete(k)
+
+
+func get_name() -> String:
+ return "Delete Points %s" % [_keys]
+
+
+func do() -> void:
+ _shape.begin_update()
+ _was_closed = _shape.is_shape_closed()
+ var first_run := _positions.size() == 0
+ for k in _keys:
+ if first_run:
+ _indicies.append(_shape.get_point_index(k))
+ _positions.append(_shape.get_point_position(k))
+ _points_in.append(_shape.get_point_in(k))
+ _points_out.append(_shape.get_point_out(k))
+ _properties.append(_shape.get_point_properties(k))
+ _shape.remove_point(k)
+ if _was_closed:
+ _close_shape.do()
+ _invert_orientation.do()
+ if _commit_update:
+ _shape.end_update()
+
+
+func undo() -> void:
+ _shape.begin_update()
+ _invert_orientation.undo()
+ if _was_closed:
+ _close_shape.undo()
+ for i in range(_keys.size()-1, -1, -1):
+ _shape.add_point(_positions[i], _indicies[i], _keys[i])
+ _shape.set_point_in(_keys[i], _points_in[i])
+ _shape.set_point_out(_keys[i], _points_out[i])
+ _shape.set_point_properties(_keys[i], _properties[i])
+ # Restore point constraints.
+ for i in range(_keys.size()-1, -1, -1):
+ 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 := _shape.get_point_array().get_point_constraints(key)
+ # Save point 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 := 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)
diff --git a/addons/rmsmartshape/actions/action_invert_orientation.gd b/addons/rmsmartshape/actions/action_invert_orientation.gd
new file mode 100644
index 0000000..0f41504
--- /dev/null
+++ b/addons/rmsmartshape/actions/action_invert_orientation.gd
@@ -0,0 +1,33 @@
+extends SS2D_Action
+
+## ActionInvertOrientation
+
+var _shape: SS2D_Shape
+var _performed: bool
+
+
+func _init(shape: SS2D_Shape) -> void:
+ _shape = shape
+
+
+func get_name() -> String:
+ return "Invert Orientation"
+
+
+func do() -> void:
+ _performed = should_invert_orientation(_shape)
+ if _performed:
+ _shape.invert_point_order()
+
+
+func undo() -> void:
+ if _performed:
+ _shape.invert_point_order()
+
+
+func should_invert_orientation(s: SS2D_Shape) -> bool:
+ if s == null:
+ return false
+ if not s.is_shape_closed():
+ return false
+ return not s.are_points_clockwise() and s.get_point_count() >= 3
diff --git a/addons/rmsmartshape/actions/action_make_shape_unique.gd b/addons/rmsmartshape/actions/action_make_shape_unique.gd
new file mode 100644
index 0000000..28a064a
--- /dev/null
+++ b/addons/rmsmartshape/actions/action_make_shape_unique.gd
@@ -0,0 +1,26 @@
+extends SS2D_Action
+
+## ActionMakeShapeUnique
+
+var _shape: SS2D_Shape
+var _old_array: SS2D_Point_Array
+var _new_array: SS2D_Point_Array
+
+
+func _init(shape: SS2D_Shape) -> void:
+ _shape = shape
+ _old_array = shape.get_point_array()
+ _new_array = _shape.get_point_array().clone(true)
+
+
+func get_name() -> String:
+ return "Make Shape Unique"
+
+
+func do() -> void:
+ _shape.set_point_array(_new_array)
+
+
+func undo() -> void:
+ _shape.set_point_array(_old_array)
+
diff --git a/addons/rmsmartshape/actions/action_move_control_points.gd b/addons/rmsmartshape/actions/action_move_control_points.gd
new file mode 100644
index 0000000..5af9e67
--- /dev/null
+++ b/addons/rmsmartshape/actions/action_move_control_points.gd
@@ -0,0 +1,43 @@
+extends SS2D_Action
+
+## ActionMoveControlPoints
+
+var _shape: SS2D_Shape
+var _keys: PackedInt32Array
+var _old_points_in: PackedVector2Array
+var _old_points_out: PackedVector2Array
+var _new_points_in: PackedVector2Array
+var _new_points_out: PackedVector2Array
+
+
+func _init(s: SS2D_Shape, keys: PackedInt32Array,
+ old_points_in: PackedVector2Array, old_points_out: PackedVector2Array) -> void:
+ _shape = s
+ _keys = keys
+ _old_points_in = old_points_in
+ _old_points_out = old_points_out
+ for key in _keys:
+ _new_points_in.append(_shape.get_point_in(key))
+ _new_points_out.append(_shape.get_point_out(key))
+
+
+func get_name() -> String:
+ return "Move Control Point"
+
+
+func do() -> void:
+ _assign_points_in_out(_keys, _new_points_in, _new_points_out)
+
+
+func undo() -> void:
+ _assign_points_in_out(_keys, _old_points_in, _old_points_out)
+
+
+func _assign_points_in_out(keys: PackedInt32Array, in_positions: PackedVector2Array, out_positions: PackedVector2Array) -> void:
+ _shape.begin_update()
+ for i in keys.size():
+ if _shape.get_point_in(keys[i]) != in_positions[i]:
+ _shape.set_point_in(keys[i], in_positions[i])
+ if _shape.get_point_out(keys[i]) != out_positions[i]:
+ _shape.set_point_out(keys[i], out_positions[i])
+ _shape.end_update()
diff --git a/addons/rmsmartshape/actions/action_move_verticies.gd b/addons/rmsmartshape/actions/action_move_verticies.gd
new file mode 100644
index 0000000..f8d4a47
--- /dev/null
+++ b/addons/rmsmartshape/actions/action_move_verticies.gd
@@ -0,0 +1,44 @@
+extends SS2D_Action
+
+## ActionMoveVerticies
+
+const ActionInvertOrientation := preload("res://addons/rmsmartshape/actions/action_invert_orientation.gd")
+var _invert_orientation: ActionInvertOrientation
+
+var _shape: SS2D_Shape
+var _keys: PackedInt32Array
+var _old_positions: PackedVector2Array
+var _new_positions: PackedVector2Array
+
+
+func _init(s: SS2D_Shape, keys: PackedInt32Array, old_positions: PackedVector2Array) -> void:
+ _shape = s
+ _keys = keys.duplicate()
+ _old_positions = old_positions.duplicate()
+ _new_positions = PackedVector2Array()
+ for k in _keys:
+ _new_positions.append(_shape.get_point_position(k))
+ _invert_orientation = ActionInvertOrientation.new(_shape)
+
+
+func get_name() -> String:
+ if _keys.size() == 1:
+ return "Move Vertex to (%d, %d)" % [_new_positions[0].x, _new_positions[0].y]
+ else:
+ return "Move Verticies"
+
+
+func do() -> void:
+ _shape.begin_update()
+ for i in _keys.size():
+ _shape.set_point_position(_keys[i], _new_positions[i])
+ _invert_orientation.do()
+ _shape.end_update()
+
+
+func undo() -> void:
+ _shape.begin_update()
+ _invert_orientation.undo()
+ for i in range(_keys.size() - 1, -1, -1):
+ _shape.set_point_position(_keys[i], _old_positions[i])
+ _shape.end_update()
diff --git a/addons/rmsmartshape/actions/action_open_shape.gd b/addons/rmsmartshape/actions/action_open_shape.gd
new file mode 100644
index 0000000..adbbb1f
--- /dev/null
+++ b/addons/rmsmartshape/actions/action_open_shape.gd
@@ -0,0 +1,31 @@
+extends SS2D_Action
+
+## ActionOpenShape
+
+var _shape: SS2D_Shape
+var _cut_idx: int
+var _closing_key: int
+
+
+func _init(shape: SS2D_Shape, edge_start_key: int) -> void:
+ _shape = shape
+ _cut_idx = shape.get_point_index(edge_start_key)
+
+
+func get_name() -> String:
+ return "Open Shape"
+
+
+func do() -> void:
+ _shape.begin_update()
+ var last_idx: int = _shape.get_point_count() - 1
+ _closing_key = _shape.get_point_key_at_index(last_idx)
+ _shape.open_shape_at_edge(_cut_idx)
+ _shape.end_update()
+
+
+func undo() -> void:
+ _shape.begin_update()
+ _shape.undo_open_shape_at_edge(_cut_idx, _closing_key)
+ _shape.end_update()
+
diff --git a/addons/rmsmartshape/actions/action_set_pivot.gd b/addons/rmsmartshape/actions/action_set_pivot.gd
new file mode 100644
index 0000000..6157337
--- /dev/null
+++ b/addons/rmsmartshape/actions/action_set_pivot.gd
@@ -0,0 +1,44 @@
+extends SS2D_Action
+
+## ActionSetPivot
+
+var _shape: SS2D_Shape
+
+var _new_pos: Vector2
+var _old_pos: Vector2
+
+
+func _init(s: SS2D_Shape, pos: Vector2) -> void:
+ _shape = s
+ _new_pos = pos
+ _old_pos = _shape.global_position
+
+
+func get_name() -> String:
+ return "Set Pivot"
+
+
+func do() -> void:
+ _set_pivot(_new_pos)
+
+
+func undo() -> void:
+ _set_pivot(_old_pos)
+
+
+func _set_pivot(shape_position: Vector2) -> void:
+ var shape_gt: Transform2D = _shape.get_global_transform()
+
+ _shape.global_position = shape_position
+
+ _shape.begin_update()
+ _shape.disable_constraints()
+
+ for i in _shape.get_point_count():
+ var key: int = _shape.get_point_key_at_index(i)
+ var point: Vector2 = _shape.get_point_position(key)
+ _shape.set_point_position(key, _shape.to_local(shape_gt * point))
+
+ _shape.enable_constraints()
+ _shape.end_update()
+
diff --git a/addons/rmsmartshape/actions/action_split_curve.gd b/addons/rmsmartshape/actions/action_split_curve.gd
new file mode 100644
index 0000000..a4ca489
--- /dev/null
+++ b/addons/rmsmartshape/actions/action_split_curve.gd
@@ -0,0 +1,10 @@
+extends "res://addons/rmsmartshape/actions/action_add_point.gd"
+
+## ActionSplitCurve
+
+func _init(shape: SS2D_Shape, idx: int, gpoint: Vector2, xform: Transform2D, commit_update: bool = true) -> void:
+ super._init(shape, xform.affine_inverse() * gpoint, idx, commit_update)
+
+
+func get_name() -> String:
+ return "Split Curve at (%d, %d)" % [_position.x, _position.y]
diff --git a/addons/rmsmartshape/actions/action_split_shape.gd b/addons/rmsmartshape/actions/action_split_shape.gd
new file mode 100644
index 0000000..8fb2a28
--- /dev/null
+++ b/addons/rmsmartshape/actions/action_split_shape.gd
@@ -0,0 +1,74 @@
+extends SS2D_Action
+
+## ActionSplitShape
+##
+## How it's done:
+## 1. First, the shape is copied and added to the scene tree.
+## 2. Then, points of the splitted shape are deleted from first point to split point.
+## 3. Finally, points of the original shape are deleted from the point after split point to last point.
+
+const ActionDeletePoints := preload("res://addons/rmsmartshape/actions/action_delete_points.gd")
+var _delete_points_from_original: ActionDeletePoints
+
+var _shape: SS2D_Shape
+var _splitted: SS2D_Shape
+var _splitted_collision: CollisionPolygon2D
+var _split_idx: int
+
+
+func _init(shape: SS2D_Shape, split_point_key: int) -> void:
+ assert(shape.is_shape_closed() == false)
+ _shape = shape
+ _split_idx = shape.get_point_index(split_point_key)
+ _splitted = null
+ _splitted_collision = null
+ _delete_points_from_original = null
+
+
+func get_name() -> String:
+ return "Split Shape"
+
+
+func do() -> void:
+ if not is_instance_valid(_splitted):
+ _splitted = _shape.clone()
+ _splitted.begin_update()
+ for i in range(0, _split_idx + 1):
+ _splitted.remove_point_at_index(0)
+ _splitted.end_update()
+ _shape.get_parent().add_child(_splitted, true)
+ _splitted.set_owner(_shape.get_tree().get_edited_scene_root())
+ # Add a collision shape node if the original shape has one.
+ if (not _shape.collision_polygon_node_path.is_empty() and _shape.has_node(_shape.collision_polygon_node_path)):
+ var collision_polygon_original := _shape.get_node(_shape.collision_polygon_node_path) as CollisionPolygon2D
+ if not is_instance_valid(_splitted_collision):
+ _splitted_collision = CollisionPolygon2D.new()
+ _splitted_collision.visible = collision_polygon_original.visible
+ _splitted_collision.modulate = collision_polygon_original.modulate
+ collision_polygon_original.get_parent().add_child(_splitted_collision, true)
+ _splitted_collision.set_owner(collision_polygon_original.get_tree().get_edited_scene_root())
+ _splitted.collision_polygon_node_path = _splitted.get_path_to(_splitted_collision)
+
+ if _delete_points_from_original == null:
+ var delete_keys := PackedInt32Array()
+ for i in range(_shape.get_point_count() - 1, _split_idx, -1):
+ delete_keys.append(_shape.get_point_key_at_index(i))
+ _delete_points_from_original = ActionDeletePoints.new(_shape, delete_keys)
+ _delete_points_from_original.do()
+
+
+func undo() -> void:
+ _splitted.set_owner(null)
+ _splitted.get_parent().remove_child(_splitted)
+ if is_instance_valid(_splitted_collision):
+ _splitted_collision.set_owner(null)
+ _splitted_collision.get_parent().remove_child(_splitted_collision)
+ _delete_points_from_original.undo()
+
+
+func _notification(what: int) -> void:
+ if what == NOTIFICATION_PREDELETE:
+ if is_instance_valid(_splitted) and _splitted.get_parent() == null:
+ _splitted.queue_free()
+ if is_instance_valid(_splitted_collision) and _splitted_collision.get_parent() == null:
+ _splitted_collision.queue_free()
diff --git a/addons/rmsmartshape/asset_library_icon-new.png b/addons/rmsmartshape/asset_library_icon-new.png
new file mode 100644
index 0000000..44db166
Binary files /dev/null and b/addons/rmsmartshape/asset_library_icon-new.png differ
diff --git a/addons/rmsmartshape/asset_library_icon-new.png.import b/addons/rmsmartshape/asset_library_icon-new.png.import
new file mode 100644
index 0000000..554070c
--- /dev/null
+++ b/addons/rmsmartshape/asset_library_icon-new.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://5wllxq74p8kq"
+path="res://.godot/imported/asset_library_icon-new.png-60877ecd9837f02c48423d4ec08e9284.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/rmsmartshape/asset_library_icon-new.png"
+dest_files=["res://.godot/imported/asset_library_icon-new.png-60877ecd9837f02c48423d4ec08e9284.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/addons/rmsmartshape/asset_library_icon.png b/addons/rmsmartshape/asset_library_icon.png
new file mode 100644
index 0000000..1ff18e6
Binary files /dev/null and b/addons/rmsmartshape/asset_library_icon.png differ
diff --git a/addons/rmsmartshape/asset_library_icon.png.import b/addons/rmsmartshape/asset_library_icon.png.import
new file mode 100644
index 0000000..6d146eb
--- /dev/null
+++ b/addons/rmsmartshape/asset_library_icon.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cbde02owb6iuu"
+path="res://.godot/imported/asset_library_icon.png-57d4f9e357cb3293fe7f718c0ef3633c.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/rmsmartshape/asset_library_icon.png"
+dest_files=["res://.godot/imported/asset_library_icon.png-57d4f9e357cb3293fe7f718c0ef3633c.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/addons/rmsmartshape/assets/Anchor.svg b/addons/rmsmartshape/assets/Anchor.svg
new file mode 100644
index 0000000..a9caae0
--- /dev/null
+++ b/addons/rmsmartshape/assets/Anchor.svg
@@ -0,0 +1,12 @@
+
diff --git a/addons/rmsmartshape/assets/Anchor.svg.import b/addons/rmsmartshape/assets/Anchor.svg.import
new file mode 100644
index 0000000..57a43f2
--- /dev/null
+++ b/addons/rmsmartshape/assets/Anchor.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b2h8c634u1120"
+path="res://.godot/imported/Anchor.svg-1c23ca67e353b480fc5d8186bb0064b9.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/rmsmartshape/assets/Anchor.svg"
+dest_files=["res://.godot/imported/Anchor.svg-1c23ca67e353b480fc5d8186bb0064b9.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/addons/rmsmartshape/assets/CenterView.svg b/addons/rmsmartshape/assets/CenterView.svg
new file mode 100644
index 0000000..dc4052f
--- /dev/null
+++ b/addons/rmsmartshape/assets/CenterView.svg
@@ -0,0 +1 @@
+
diff --git a/addons/rmsmartshape/assets/CenterView.svg.import b/addons/rmsmartshape/assets/CenterView.svg.import
new file mode 100644
index 0000000..41a9077
--- /dev/null
+++ b/addons/rmsmartshape/assets/CenterView.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://byx10wt6pl1vo"
+path="res://.godot/imported/CenterView.svg-f5e9c7178a59ab586ba9d8fac61799bc.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/rmsmartshape/assets/CenterView.svg"
+dest_files=["res://.godot/imported/CenterView.svg-f5e9c7178a59ab586ba9d8fac61799bc.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/addons/rmsmartshape/assets/InterpLinear.svg b/addons/rmsmartshape/assets/InterpLinear.svg
new file mode 100644
index 0000000..241a82f
--- /dev/null
+++ b/addons/rmsmartshape/assets/InterpLinear.svg
@@ -0,0 +1 @@
+
diff --git a/addons/rmsmartshape/assets/InterpLinear.svg.import b/addons/rmsmartshape/assets/InterpLinear.svg.import
new file mode 100644
index 0000000..c2dc335
--- /dev/null
+++ b/addons/rmsmartshape/assets/InterpLinear.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://umo7sgb6w70s"
+path="res://.godot/imported/InterpLinear.svg-c9d61ab817f2809471482a31f06cda10.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/rmsmartshape/assets/InterpLinear.svg"
+dest_files=["res://.godot/imported/InterpLinear.svg-c9d61ab817f2809471482a31f06cda10.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/addons/rmsmartshape/assets/SourceCodeVariable-Roman.ttf b/addons/rmsmartshape/assets/SourceCodeVariable-Roman.ttf
new file mode 100644
index 0000000..42e619f
Binary files /dev/null and b/addons/rmsmartshape/assets/SourceCodeVariable-Roman.ttf differ
diff --git a/addons/rmsmartshape/assets/SourceCodeVariable-Roman.ttf.import b/addons/rmsmartshape/assets/SourceCodeVariable-Roman.ttf.import
new file mode 100644
index 0000000..0b511de
--- /dev/null
+++ b/addons/rmsmartshape/assets/SourceCodeVariable-Roman.ttf.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="font_data_dynamic"
+type="FontFile"
+uid="uid://cjgo8gf0cp441"
+path="res://.godot/imported/SourceCodeVariable-Roman.ttf-cb1e4f55679e973e90075716efcdf601.fontdata"
+
+[deps]
+
+source_file="res://addons/rmsmartshape/assets/SourceCodeVariable-Roman.ttf"
+dest_files=["res://.godot/imported/SourceCodeVariable-Roman.ttf-cb1e4f55679e973e90075716efcdf601.fontdata"]
+
+[params]
+
+Rendering=null
+antialiasing=1
+generate_mipmaps=false
+disable_embedded_bitmaps=true
+multichannel_signed_distance_field=false
+msdf_pixel_range=8
+msdf_size=48
+allow_system_fallback=true
+force_autohinter=false
+hinting=1
+subpixel_positioning=1
+oversampling=0.0
+Fallbacks=null
+fallbacks=[]
+Compress=null
+compress=true
+preload=[]
+language_support={}
+script_support={}
+opentype_features={}
diff --git a/addons/rmsmartshape/assets/closed_shape.png b/addons/rmsmartshape/assets/closed_shape.png
new file mode 100644
index 0000000..e2834e3
Binary files /dev/null and b/addons/rmsmartshape/assets/closed_shape.png differ
diff --git a/addons/rmsmartshape/assets/closed_shape.png.import b/addons/rmsmartshape/assets/closed_shape.png.import
new file mode 100644
index 0000000..dbc0499
--- /dev/null
+++ b/addons/rmsmartshape/assets/closed_shape.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://8r4xp6j0ro3y"
+path="res://.godot/imported/closed_shape.png-c1d349f5c2caecc40ec65e52f86a8145.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/rmsmartshape/assets/closed_shape.png"
+dest_files=["res://.godot/imported/closed_shape.png-c1d349f5c2caecc40ec65e52f86a8145.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/addons/rmsmartshape/assets/freehand.png b/addons/rmsmartshape/assets/freehand.png
new file mode 100644
index 0000000..ee1125a
Binary files /dev/null and b/addons/rmsmartshape/assets/freehand.png differ
diff --git a/addons/rmsmartshape/assets/freehand.png.import b/addons/rmsmartshape/assets/freehand.png.import
new file mode 100644
index 0000000..85093e0
--- /dev/null
+++ b/addons/rmsmartshape/assets/freehand.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bjbq000i2n7qd"
+path="res://.godot/imported/freehand.png-de4c7fa877a42fa4776ee9cd166cdabd.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/rmsmartshape/assets/freehand.png"
+dest_files=["res://.godot/imported/freehand.png-de4c7fa877a42fa4776ee9cd166cdabd.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/addons/rmsmartshape/assets/gui_theme.res b/addons/rmsmartshape/assets/gui_theme.res
new file mode 100644
index 0000000..55d0270
Binary files /dev/null and b/addons/rmsmartshape/assets/gui_theme.res differ
diff --git a/addons/rmsmartshape/assets/icon.png b/addons/rmsmartshape/assets/icon.png
new file mode 100644
index 0000000..a9d3337
Binary files /dev/null and b/addons/rmsmartshape/assets/icon.png differ
diff --git a/addons/rmsmartshape/assets/icon.png.import b/addons/rmsmartshape/assets/icon.png.import
new file mode 100644
index 0000000..2f57b16
--- /dev/null
+++ b/addons/rmsmartshape/assets/icon.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://h0jpajdrldwm"
+path="res://.godot/imported/icon.png-cf4ff9c0f4595c1f390bb463008250b4.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/rmsmartshape/assets/icon.png"
+dest_files=["res://.godot/imported/icon.png-cf4ff9c0f4595c1f390bb463008250b4.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/addons/rmsmartshape/assets/icon_collision_polygon_2d.svg b/addons/rmsmartshape/assets/icon_collision_polygon_2d.svg
new file mode 100644
index 0000000..9d90a66
--- /dev/null
+++ b/addons/rmsmartshape/assets/icon_collision_polygon_2d.svg
@@ -0,0 +1,5 @@
+
diff --git a/addons/rmsmartshape/assets/icon_collision_polygon_2d.svg.import b/addons/rmsmartshape/assets/icon_collision_polygon_2d.svg.import
new file mode 100644
index 0000000..25f570c
--- /dev/null
+++ b/addons/rmsmartshape/assets/icon_collision_polygon_2d.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c3n1j5ybtgjit"
+path="res://.godot/imported/icon_collision_polygon_2d.svg-d52c533e615c0baed724848cec175fe6.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/rmsmartshape/assets/icon_collision_polygon_2d.svg"
+dest_files=["res://.godot/imported/icon_collision_polygon_2d.svg-d52c533e615c0baed724848cec175fe6.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/addons/rmsmartshape/assets/icon_curve_create.svg b/addons/rmsmartshape/assets/icon_curve_create.svg
new file mode 100644
index 0000000..1181111
--- /dev/null
+++ b/addons/rmsmartshape/assets/icon_curve_create.svg
@@ -0,0 +1,7 @@
+
diff --git a/addons/rmsmartshape/assets/icon_curve_create.svg.import b/addons/rmsmartshape/assets/icon_curve_create.svg.import
new file mode 100644
index 0000000..eb6db2b
--- /dev/null
+++ b/addons/rmsmartshape/assets/icon_curve_create.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://qf0hmmvirgbn"
+path="res://.godot/imported/icon_curve_create.svg-24c8317cfb8ced81d4c832ca7b3310d5.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/rmsmartshape/assets/icon_curve_create.svg"
+dest_files=["res://.godot/imported/icon_curve_create.svg-24c8317cfb8ced81d4c832ca7b3310d5.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/addons/rmsmartshape/assets/icon_curve_delete.svg b/addons/rmsmartshape/assets/icon_curve_delete.svg
new file mode 100644
index 0000000..901a08e
--- /dev/null
+++ b/addons/rmsmartshape/assets/icon_curve_delete.svg
@@ -0,0 +1,7 @@
+
diff --git a/addons/rmsmartshape/assets/icon_curve_delete.svg.import b/addons/rmsmartshape/assets/icon_curve_delete.svg.import
new file mode 100644
index 0000000..9577201
--- /dev/null
+++ b/addons/rmsmartshape/assets/icon_curve_delete.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ce2cbowctirhc"
+path="res://.godot/imported/icon_curve_delete.svg-9121c11be9a22040b2d567743d44ead0.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/rmsmartshape/assets/icon_curve_delete.svg"
+dest_files=["res://.godot/imported/icon_curve_delete.svg-9121c11be9a22040b2d567743d44ead0.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/addons/rmsmartshape/assets/icon_curve_edit.svg b/addons/rmsmartshape/assets/icon_curve_edit.svg
new file mode 100644
index 0000000..8f09ca6
--- /dev/null
+++ b/addons/rmsmartshape/assets/icon_curve_edit.svg
@@ -0,0 +1,7 @@
+
diff --git a/addons/rmsmartshape/assets/icon_curve_edit.svg.import b/addons/rmsmartshape/assets/icon_curve_edit.svg.import
new file mode 100644
index 0000000..639cb49
--- /dev/null
+++ b/addons/rmsmartshape/assets/icon_curve_edit.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://crjuwuq4vp15w"
+path="res://.godot/imported/icon_curve_edit.svg-bd7d5e1af7c72f4525dff1d79ad0af94.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/rmsmartshape/assets/icon_curve_edit.svg"
+dest_files=["res://.godot/imported/icon_curve_edit.svg-bd7d5e1af7c72f4525dff1d79ad0af94.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/addons/rmsmartshape/assets/icon_editor_freehand.svg b/addons/rmsmartshape/assets/icon_editor_freehand.svg
new file mode 100644
index 0000000..d354946
--- /dev/null
+++ b/addons/rmsmartshape/assets/icon_editor_freehand.svg
@@ -0,0 +1,13 @@
+
+
+
diff --git a/addons/rmsmartshape/assets/icon_editor_freehand.svg.import b/addons/rmsmartshape/assets/icon_editor_freehand.svg.import
new file mode 100644
index 0000000..eda11a4
--- /dev/null
+++ b/addons/rmsmartshape/assets/icon_editor_freehand.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://da64iqyjipw4f"
+path="res://.godot/imported/icon_editor_freehand.svg-4eb5cac550480a5ffb50b9c62fba3af1.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/rmsmartshape/assets/icon_editor_freehand.svg"
+dest_files=["res://.godot/imported/icon_editor_freehand.svg-4eb5cac550480a5ffb50b9c62fba3af1.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/addons/rmsmartshape/assets/icon_editor_handle.svg b/addons/rmsmartshape/assets/icon_editor_handle.svg
new file mode 100644
index 0000000..328dc04
--- /dev/null
+++ b/addons/rmsmartshape/assets/icon_editor_handle.svg
@@ -0,0 +1 @@
+
diff --git a/addons/rmsmartshape/assets/icon_editor_handle.svg.import b/addons/rmsmartshape/assets/icon_editor_handle.svg.import
new file mode 100644
index 0000000..5b6c811
--- /dev/null
+++ b/addons/rmsmartshape/assets/icon_editor_handle.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ccnyogs0qbq8b"
+path="res://.godot/imported/icon_editor_handle.svg-ace1b9f89612a442855e8ad0888fdf3e.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/rmsmartshape/assets/icon_editor_handle.svg"
+dest_files=["res://.godot/imported/icon_editor_handle.svg-ace1b9f89612a442855e8ad0888fdf3e.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.5
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/addons/rmsmartshape/assets/icon_editor_handle_add.svg b/addons/rmsmartshape/assets/icon_editor_handle_add.svg
new file mode 100644
index 0000000..a8bc1fd
--- /dev/null
+++ b/addons/rmsmartshape/assets/icon_editor_handle_add.svg
@@ -0,0 +1,5 @@
+
diff --git a/addons/rmsmartshape/assets/icon_editor_handle_add.svg.import b/addons/rmsmartshape/assets/icon_editor_handle_add.svg.import
new file mode 100644
index 0000000..d8998c0
--- /dev/null
+++ b/addons/rmsmartshape/assets/icon_editor_handle_add.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ci73r1ncmyrrw"
+path="res://.godot/imported/icon_editor_handle_add.svg-7258971e5edcfe4396b7b9c27c0ba1cc.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/rmsmartshape/assets/icon_editor_handle_add.svg"
+dest_files=["res://.godot/imported/icon_editor_handle_add.svg-7258971e5edcfe4396b7b9c27c0ba1cc.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.5
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/addons/rmsmartshape/assets/icon_editor_handle_bezier.svg b/addons/rmsmartshape/assets/icon_editor_handle_bezier.svg
new file mode 100644
index 0000000..b498345
--- /dev/null
+++ b/addons/rmsmartshape/assets/icon_editor_handle_bezier.svg
@@ -0,0 +1 @@
+
diff --git a/addons/rmsmartshape/assets/icon_editor_handle_bezier.svg.import b/addons/rmsmartshape/assets/icon_editor_handle_bezier.svg.import
new file mode 100644
index 0000000..c6cf95a
--- /dev/null
+++ b/addons/rmsmartshape/assets/icon_editor_handle_bezier.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cycko7tpd5xk5"
+path="res://.godot/imported/icon_editor_handle_bezier.svg-b0150f05772e8974a66bc735765e0aeb.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/rmsmartshape/assets/icon_editor_handle_bezier.svg"
+dest_files=["res://.godot/imported/icon_editor_handle_bezier.svg-b0150f05772e8974a66bc735765e0aeb.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.5
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/addons/rmsmartshape/assets/icon_editor_handle_control.svg b/addons/rmsmartshape/assets/icon_editor_handle_control.svg
new file mode 100644
index 0000000..ea69f4e
--- /dev/null
+++ b/addons/rmsmartshape/assets/icon_editor_handle_control.svg
@@ -0,0 +1 @@
+
diff --git a/addons/rmsmartshape/assets/icon_editor_handle_control.svg.import b/addons/rmsmartshape/assets/icon_editor_handle_control.svg.import
new file mode 100644
index 0000000..9208cb7
--- /dev/null
+++ b/addons/rmsmartshape/assets/icon_editor_handle_control.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dgcshah05p0bm"
+path="res://.godot/imported/icon_editor_handle_control.svg-8383942d01a7ee5e839992ed99e53aaf.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/rmsmartshape/assets/icon_editor_handle_control.svg"
+dest_files=["res://.godot/imported/icon_editor_handle_control.svg-8383942d01a7ee5e839992ed99e53aaf.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.5
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/addons/rmsmartshape/assets/icon_editor_handle_selected.svg b/addons/rmsmartshape/assets/icon_editor_handle_selected.svg
new file mode 100644
index 0000000..ac96ca6
--- /dev/null
+++ b/addons/rmsmartshape/assets/icon_editor_handle_selected.svg
@@ -0,0 +1,3 @@
+
diff --git a/addons/rmsmartshape/assets/icon_editor_handle_selected.svg.import b/addons/rmsmartshape/assets/icon_editor_handle_selected.svg.import
new file mode 100644
index 0000000..4b670dd
--- /dev/null
+++ b/addons/rmsmartshape/assets/icon_editor_handle_selected.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dh3vdfbsl5o65"
+path="res://.godot/imported/icon_editor_handle_selected.svg-356ef806610ee2a60b5838a989f58598.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/rmsmartshape/assets/icon_editor_handle_selected.svg"
+dest_files=["res://.godot/imported/icon_editor_handle_selected.svg-356ef806610ee2a60b5838a989f58598.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.25
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/addons/rmsmartshape/assets/icon_editor_position.svg b/addons/rmsmartshape/assets/icon_editor_position.svg
new file mode 100644
index 0000000..7657eb5
--- /dev/null
+++ b/addons/rmsmartshape/assets/icon_editor_position.svg
@@ -0,0 +1,4 @@
+
diff --git a/addons/rmsmartshape/assets/icon_editor_position.svg.import b/addons/rmsmartshape/assets/icon_editor_position.svg.import
new file mode 100644
index 0000000..fb6f20f
--- /dev/null
+++ b/addons/rmsmartshape/assets/icon_editor_position.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c18veogqnx5ht"
+path="res://.godot/imported/icon_editor_position.svg-1f6e1977293add549ed436510f8834c1.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/rmsmartshape/assets/icon_editor_position.svg"
+dest_files=["res://.godot/imported/icon_editor_position.svg-1f6e1977293add549ed436510f8834c1.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/addons/rmsmartshape/assets/icon_editor_snap.svg b/addons/rmsmartshape/assets/icon_editor_snap.svg
new file mode 100644
index 0000000..a4a1f33
--- /dev/null
+++ b/addons/rmsmartshape/assets/icon_editor_snap.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/addons/rmsmartshape/assets/icon_editor_snap.svg.import b/addons/rmsmartshape/assets/icon_editor_snap.svg.import
new file mode 100644
index 0000000..e2de758
--- /dev/null
+++ b/addons/rmsmartshape/assets/icon_editor_snap.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bojrgd0r131xh"
+path="res://.godot/imported/icon_editor_snap.svg-f0374ecd8cb313d78a2acb46b093a7f1.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/rmsmartshape/assets/icon_editor_snap.svg"
+dest_files=["res://.godot/imported/icon_editor_snap.svg-f0374ecd8cb313d78a2acb46b093a7f1.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/addons/rmsmartshape/assets/icon_width_handle.svg.import b/addons/rmsmartshape/assets/icon_width_handle.svg.import
new file mode 100644
index 0000000..a1e995e
--- /dev/null
+++ b/addons/rmsmartshape/assets/icon_width_handle.svg.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="StreamTexture"
+path="res://.import/icon_width_handle.svg-55d6fdc3b3c08e53dc0bed806d2f0a8f.stex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/rmsmartshape/assets/icon_width_handle.svg"
+dest_files=[ "res://.import/icon_width_handle.svg-55d6fdc3b3c08e53dc0bed806d2f0a8f.stex" ]
+
+[params]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_mode=0
+compress/bptc_ldr=0
+compress/normal_map=0
+flags/repeat=0
+flags/filter=true
+flags/mipmaps=false
+flags/anisotropic=false
+flags/srgb=2
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/HDR_as_SRGB=false
+process/invert_color=false
+stream=false
+size_limit=0
+detect_3d=true
+svg/scale=1.0
diff --git a/addons/rmsmartshape/assets/icon_width_handle0.svg.import b/addons/rmsmartshape/assets/icon_width_handle0.svg.import
new file mode 100644
index 0000000..3d70ef9
--- /dev/null
+++ b/addons/rmsmartshape/assets/icon_width_handle0.svg.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="StreamTexture"
+path="res://.import/icon_width_handle0.svg-9a486a252970ecb72608cabecc20a9f6.stex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/rmsmartshape/assets/icon_width_handle0.svg"
+dest_files=[ "res://.import/icon_width_handle0.svg-9a486a252970ecb72608cabecc20a9f6.stex" ]
+
+[params]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_mode=0
+compress/bptc_ldr=0
+compress/normal_map=0
+flags/repeat=0
+flags/filter=true
+flags/mipmaps=false
+flags/anisotropic=false
+flags/srgb=2
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/HDR_as_SRGB=false
+process/invert_color=false
+stream=false
+size_limit=0
+detect_3d=false
+svg/scale=1.0
diff --git a/addons/rmsmartshape/assets/icon_width_handle1.svg.import b/addons/rmsmartshape/assets/icon_width_handle1.svg.import
new file mode 100644
index 0000000..c4edbdc
--- /dev/null
+++ b/addons/rmsmartshape/assets/icon_width_handle1.svg.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="StreamTexture"
+path="res://.import/icon_width_handle1.svg-a1da7c369cb74b89d2283c3679312805.stex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/rmsmartshape/assets/icon_width_handle1.svg"
+dest_files=[ "res://.import/icon_width_handle1.svg-a1da7c369cb74b89d2283c3679312805.stex" ]
+
+[params]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_mode=0
+compress/bptc_ldr=0
+compress/normal_map=0
+flags/repeat=0
+flags/filter=true
+flags/mipmaps=false
+flags/anisotropic=false
+flags/srgb=2
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/HDR_as_SRGB=false
+process/invert_color=false
+stream=false
+size_limit=0
+detect_3d=false
+svg/scale=1.0
diff --git a/addons/rmsmartshape/assets/icon_width_handle2.svg.import b/addons/rmsmartshape/assets/icon_width_handle2.svg.import
new file mode 100644
index 0000000..fd84d56
--- /dev/null
+++ b/addons/rmsmartshape/assets/icon_width_handle2.svg.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="StreamTexture"
+path="res://.import/icon_width_handle2.svg-b02ee2da69342d687c1fd9680123e834.stex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/rmsmartshape/assets/icon_width_handle2.svg"
+dest_files=[ "res://.import/icon_width_handle2.svg-b02ee2da69342d687c1fd9680123e834.stex" ]
+
+[params]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_mode=0
+compress/bptc_ldr=0
+compress/normal_map=0
+flags/repeat=0
+flags/filter=true
+flags/mipmaps=false
+flags/anisotropic=false
+flags/srgb=2
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/HDR_as_SRGB=false
+process/invert_color=false
+stream=false
+size_limit=0
+detect_3d=false
+svg/scale=1.0
diff --git a/addons/rmsmartshape/assets/icon_width_handle3.svg.import b/addons/rmsmartshape/assets/icon_width_handle3.svg.import
new file mode 100644
index 0000000..751beb8
--- /dev/null
+++ b/addons/rmsmartshape/assets/icon_width_handle3.svg.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="StreamTexture"
+path="res://.import/icon_width_handle3.svg-3dcf9d2b715a3978009539d7a6640342.stex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/rmsmartshape/assets/icon_width_handle3.svg"
+dest_files=[ "res://.import/icon_width_handle3.svg-3dcf9d2b715a3978009539d7a6640342.stex" ]
+
+[params]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_mode=0
+compress/bptc_ldr=0
+compress/normal_map=0
+flags/repeat=0
+flags/filter=true
+flags/mipmaps=false
+flags/anisotropic=false
+flags/srgb=2
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/HDR_as_SRGB=false
+process/invert_color=false
+stream=false
+size_limit=0
+detect_3d=false
+svg/scale=1.0
diff --git a/addons/rmsmartshape/assets/icon_width_handle4.svg.import b/addons/rmsmartshape/assets/icon_width_handle4.svg.import
new file mode 100644
index 0000000..957a8ee
--- /dev/null
+++ b/addons/rmsmartshape/assets/icon_width_handle4.svg.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="StreamTexture"
+path="res://.import/icon_width_handle4.svg-47fd86c3a89419c03da7bc2def15fbfa.stex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/rmsmartshape/assets/icon_width_handle4.svg"
+dest_files=[ "res://.import/icon_width_handle4.svg-47fd86c3a89419c03da7bc2def15fbfa.stex" ]
+
+[params]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_mode=0
+compress/bptc_ldr=0
+compress/normal_map=0
+flags/repeat=0
+flags/filter=true
+flags/mipmaps=false
+flags/anisotropic=false
+flags/srgb=2
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/HDR_as_SRGB=false
+process/invert_color=false
+stream=false
+size_limit=0
+detect_3d=true
+svg/scale=1.0
diff --git a/addons/rmsmartshape/assets/light1-1.png b/addons/rmsmartshape/assets/light1-1.png
new file mode 100644
index 0000000..4523497
Binary files /dev/null and b/addons/rmsmartshape/assets/light1-1.png differ
diff --git a/addons/rmsmartshape/assets/light1-1.png.import b/addons/rmsmartshape/assets/light1-1.png.import
new file mode 100644
index 0000000..30774a7
--- /dev/null
+++ b/addons/rmsmartshape/assets/light1-1.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b350jwt2tgchl"
+path="res://.godot/imported/light1-1.png-433fe7882cc83fa28e02c4948a40f886.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/rmsmartshape/assets/light1-1.png"
+dest_files=["res://.godot/imported/light1-1.png-433fe7882cc83fa28e02c4948a40f886.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/addons/rmsmartshape/assets/meta_shape.png b/addons/rmsmartshape/assets/meta_shape.png
new file mode 100644
index 0000000..2e8259d
Binary files /dev/null and b/addons/rmsmartshape/assets/meta_shape.png differ
diff --git a/addons/rmsmartshape/assets/meta_shape.png.import b/addons/rmsmartshape/assets/meta_shape.png.import
new file mode 100644
index 0000000..424753a
--- /dev/null
+++ b/addons/rmsmartshape/assets/meta_shape.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://olmio6gxe5d6"
+path="res://.godot/imported/meta_shape.png-462cb0c943fa311edd3a47aeb27824d9.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/rmsmartshape/assets/meta_shape.png"
+dest_files=["res://.godot/imported/meta_shape.png-462cb0c943fa311edd3a47aeb27824d9.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/addons/rmsmartshape/assets/open_shape.png b/addons/rmsmartshape/assets/open_shape.png
new file mode 100644
index 0000000..8e1978d
Binary files /dev/null and b/addons/rmsmartshape/assets/open_shape.png differ
diff --git a/addons/rmsmartshape/assets/open_shape.png.import b/addons/rmsmartshape/assets/open_shape.png.import
new file mode 100644
index 0000000..9ac56fa
--- /dev/null
+++ b/addons/rmsmartshape/assets/open_shape.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cpg65u2v4mthq"
+path="res://.godot/imported/open_shape.png-f18727d78226e221aef881edda953bbd.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/rmsmartshape/assets/open_shape.png"
+dest_files=["res://.godot/imported/open_shape.png-f18727d78226e221aef881edda953bbd.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/addons/rmsmartshape/assets/shape.png.import b/addons/rmsmartshape/assets/shape.png.import
new file mode 100644
index 0000000..7af62f8
--- /dev/null
+++ b/addons/rmsmartshape/assets/shape.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="StreamTexture"
+path="res://.import/shape.png-00d53041e0aeffe00cc49849dfbcc65f.stex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/rmsmartshape/shape.png"
+dest_files=[ "res://.import/shape.png-00d53041e0aeffe00cc49849dfbcc65f.stex" ]
+
+[params]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_mode=0
+compress/bptc_ldr=0
+compress/normal_map=0
+flags/repeat=0
+flags/filter=false
+flags/mipmaps=false
+flags/anisotropic=false
+flags/srgb=2
+process/fix_alpha_border=false
+process/premult_alpha=false
+process/HDR_as_SRGB=false
+process/invert_color=false
+stream=false
+size_limit=0
+detect_3d=true
+svg/scale=1.0
diff --git a/addons/rmsmartshape/assets/shape_anchor.png.import b/addons/rmsmartshape/assets/shape_anchor.png.import
new file mode 100644
index 0000000..86c89d7
--- /dev/null
+++ b/addons/rmsmartshape/assets/shape_anchor.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="StreamTexture"
+path="res://.import/shape_anchor.png-c4527dad8f7093abc2088dd0157f0c72.stex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/rmsmartshape/shape_anchor.png"
+dest_files=[ "res://.import/shape_anchor.png-c4527dad8f7093abc2088dd0157f0c72.stex" ]
+
+[params]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_mode=0
+compress/bptc_ldr=0
+compress/normal_map=0
+flags/repeat=0
+flags/filter=true
+flags/mipmaps=false
+flags/anisotropic=false
+flags/srgb=2
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/HDR_as_SRGB=false
+process/invert_color=false
+stream=false
+size_limit=0
+detect_3d=true
+svg/scale=1.0
diff --git a/addons/rmsmartshape/common_functions.gd b/addons/rmsmartshape/common_functions.gd
new file mode 100644
index 0000000..3fa40af
--- /dev/null
+++ b/addons/rmsmartshape/common_functions.gd
@@ -0,0 +1,33 @@
+@tool
+extends Node
+class_name SS2D_Common_Functions
+
+
+static func sort_z(a, b) -> bool:
+ if a.z_index < b.z_index:
+ return true
+ return false
+
+
+static func sort_int_ascending(a: int, b: int) -> bool:
+ if a < b:
+ return true
+ return false
+
+
+static func sort_int_descending(a: int, b: int) -> bool:
+ if a < b:
+ return false
+ return true
+
+
+static func to_vector3(vector: Vector2) -> Vector3:
+ return Vector3(vector.x, vector.y, 0)
+
+
+static func merge_arrays(arrays: Array) -> Array:
+ var new_array := []
+ for array: Array in arrays:
+ for v: Variant in array:
+ new_array.push_back(v)
+ return new_array
diff --git a/addons/rmsmartshape/documentation/.gdignore b/addons/rmsmartshape/documentation/.gdignore
new file mode 100644
index 0000000..e69de29
diff --git a/addons/rmsmartshape/documentation/Controls.md b/addons/rmsmartshape/documentation/Controls.md
new file mode 100644
index 0000000..424b2d8
--- /dev/null
+++ b/addons/rmsmartshape/documentation/Controls.md
@@ -0,0 +1,47 @@
+# SmartShape2D - Controls and Hotkeys
+
+
+
+## Controls - Point Create
+
+- Add Point
+ - Left Click Anywhere in the viewport
+
+- Leave Point Create Mode
+ - ESCAPE
+
+## Controls - Point Edit
+
+- Add Point
+ - Either:
+ - Hold ALT and Left Click Anywhere in the viewport
+ - Click on an edge between two points
+
+- Grab closest point
+ - Hold CTRL
+
+- Cycle through texture indices of a point
+ - Mouseover a point and MOUSEWHEEL up or down to increment / decrement the texture index
+
+- Flip texture
+ - Mouseover a point and press SPACE
+
+- Change texture width property
+ - Mouseover a point, hold SHIFT, then MOUSEWHEEL up or down to increment / decrement the texture width
+
+- Add Bezier curve
+ - Mouseover a point, hold SHIFT, then click and drag to create control points on the point
+
+- Create New Shape
+ - Hold SHFT + ALT and click
+ - The location of the click will be the the first point of a newly created Shape Node
+
+### Overlap
+
+When multiple points and edges overlap, it can be ambiguous what clicking will do.
+SmartShape adheres the following rules:
+- If a control point overlaps a vertex, the control point takes priority
+- If a control point or vertex overlaps an edge:
+ - Clicking will move the control point or vert
+ - Clicking while holding ALT will create new point on the edge
+
diff --git a/addons/rmsmartshape/documentation/FAQ.md b/addons/rmsmartshape/documentation/FAQ.md
new file mode 100644
index 0000000..ca83607
--- /dev/null
+++ b/addons/rmsmartshape/documentation/FAQ.md
@@ -0,0 +1,47 @@
+# SmartShape2D - FAQ
+
+
+
+## Why aren't my textures repeating?
+
+If your textures aren't repeating and look something like this:
+
+
+
+The issue is most likely that you need to set the texture's import options in Godot:
+
+
+
+## Why isn't my shape updaing when I change the Light Mask?
+
+Each shape is currently rendered by multiple-subnodes (Children Node2Ds).
+Their owner isn't set, making them invisible in the editor (unless you have debug mode on).
+
+Unfortunately, there is no accessible signal when changing the Light Mask setting in editor.
+That means changing the Light Mask setting in editor won't trigger the children nodes to change THIER Light Mask
+
+The Shape nodes CAN update their children's light mask, they just need to be triggered to do it manually.
+If you update the shape in any way, the shape will update its children's light mask.
+
+When playing the game, the render Node children are regenerated, ensuring that the light mask will be set
+correctly.
+
+The actual shape has set\_light\_mask overloaded, so changing the light\_mask in code should work without issue
+
+If you need to manually tell the shape to update its rendering, call the set\_as\_dirty() method
+
+If anyone has any insights on this issue, please feel free to open an issue on this subject
+and let us know how we might be able to fix it
+
+## Why does changing the width look so ugly?
+
+Changing the width of the quads generally looks best with welding turned off.
+
+If welding is on, you can still change the width of the quads, but you may need to play with it a bit.
+It's best that you change the width gradually in small increments instead of sharply.
+Sharply changing the width will result in odd looking shapes.
+
+[Non-perspective rendering to a non-parallelogram is kinda tough](http://reedbeta.com/blog/quadrilateral-interpolation-part-1/)
+
+If anyone has any insights on this issue, please feel free to open an issue on this subject
+and let us know how we might be able to fix it
diff --git a/addons/rmsmartshape/documentation/Godot4.md b/addons/rmsmartshape/documentation/Godot4.md
new file mode 100644
index 0000000..93423a3
--- /dev/null
+++ b/addons/rmsmartshape/documentation/Godot4.md
@@ -0,0 +1,40 @@
+# Using SmartShape2D with Godot 4
+
+Godot 4 moved `repeat` for textures as an import option to a per-node option. On how to make textures repeat and
+set normal maps see [section below.](#repeating-textures-and-normal-textures-with-canvastexture)
+
+By default, **shape resources are shared** when the shape is copy-pasted. Editing points will edit every copy of that shape.
+To make point geometry unique, press **"Make Unique"** property button in Geometry property group in the inspector:
+
+
+
+## Repeating Textures and Normal Textures with CanvasTexture
+
+CanvasItem, the base class of Node2D has a Texture section with `repeat`. If you aren't using a normal, you can set it here.
+By default, this setting is inherited by children nodes so you could set it on a parent node (assuming all the children need
+repeating textures or it's easier to disable for a few specific nodes than turn it on for most).
+
+
+
+Normal textures are no longer set in a material resource.
+
+To set normal textures, you can create a `CanvasTexture` resource in the inspector on any property, that allows setting a `Texture2D`.
+
+
+
+`CanvasTexture` allows you to assign diffuse texture and normal map texture, as well as set textures to `repeat`:
+
+
+
+## Converting Projects from Godot 3.x
+
+Scene files with shapes saved in Godot 3.x should load in Godot 4 project. However, you may encounter some issues. Here is a list of expected problems:
+1. Textures are looking weird, not repeated.
+2. Normal textures are not used.
+
+Please read the section on [how to set repeat and use normal textures in Godot 4](#repeating-textures-and-normal-textures-with-canvastexture).
+
+## Removed Features
+
+- The Godot 4 version of this addon does not support 1.x RMSmartShape2D nodes anymore.
+- SS2D_Shape_Meta node was removed, since its functionality is available copy-pasted shapes by default.
diff --git a/addons/rmsmartshape/documentation/Install.md b/addons/rmsmartshape/documentation/Install.md
new file mode 100644
index 0000000..3a3d0ca
--- /dev/null
+++ b/addons/rmsmartshape/documentation/Install.md
@@ -0,0 +1,20 @@
+# SmartShape2D - Install
+
+## Asset Library
+
+- After installing the plugin, you may encounter an error, this is normal.
+- You need to restart Godot before using the plugin.
+
+## Manual Install
+
+- Clone the repository at https://github.com/SirRamEsq/SmartShape2D.
+- Move the "addons/rmsmartshape" folder to your project's "addons" folder.
+- Open your project in Godot to have the addon install.
+- After installing the plugin, you may encounter an error, this is normal.
+- Restart Godot.
+
+## Activate Plugin
+
+- After Installing the plugin, activate the plugin by navigating to `Project -> Project Settings... -> Plugins`
+
+
diff --git a/addons/rmsmartshape/documentation/Normals.md b/addons/rmsmartshape/documentation/Normals.md
new file mode 100644
index 0000000..91c3e7b
--- /dev/null
+++ b/addons/rmsmartshape/documentation/Normals.md
@@ -0,0 +1,60 @@
+# Normals
+
+## Default Normals
+
+Even if you assign normal textures to your Edge Material, the normals will look wrong.
+
+For example, consider the following image:
+
+
+
+The normals in the image clearly don't line up with where the light is actually coming from.
+
+## Encoding Normal data in the canvas_item Vertex Shader Color Parameter
+
+As SmartShape renders the edges, the textures and their normal textures are also rotated.
+This will result in incorrect normals.
+To solve this, we can use a shader to correctly calculate the normals.
+
+The tricky part lies in how few ways there are to pass data to a canvasItem shader on a per-vertex basis.
+See here for the full list:
+https://docs.godotengine.org/en/stable/tutorials/shading/shading_reference/canvas_item_shader.html#doc-canvas-item-shader
+
+Fortunately, the COLOR parameter can be used for this purpose.
+The COLOR ENCODING paramter of an Edge Material can be used to choose what data is encoded in the
+Vertex Shader's Color Parameter.
+
+If we set the COLOR ENCODING value to "Normals", we get this:
+
+
+
+Ok, so now the normals still look wrong, but the colors look wrong too. Great.
+
+There is one final step before our normals will be correct. We need to decode the normal data in a shader.
+
+
+## Writing a Shader
+
+You'll want to assign a new shader to the Edge Material's material property.
+This shader will determine how each edge is rendered.
+
+Here's a sample shader to decode our normal data and set our actual color to that of our diffuse texture:
+
+```glsl
+shader_type canvas_item;
+
+varying mat2 NORMAL_MATRIX;
+
+void vertex() {
+ NORMAL_MATRIX = mat2(COLOR.rg, COLOR.ba)*2.0 - mat2(vec2(1.0), vec2(1.0));
+}
+
+void fragment() {
+ NORMAL.xy = NORMAL_MATRIX*NORMAL.xy;
+ COLOR = texture(TEXTURE, UV);
+}
+```
+
+After assigning this shader the the Edge Material's material property, our normals finally look right:
+
+
diff --git a/addons/rmsmartshape/documentation/Quickstart.md b/addons/rmsmartshape/documentation/Quickstart.md
new file mode 100644
index 0000000..14554b0
--- /dev/null
+++ b/addons/rmsmartshape/documentation/Quickstart.md
@@ -0,0 +1,136 @@
+# SmartShape2D - QuickStart
+---
+
+
+If you feel like your workflow with SmartShape2D is a little bit slow, try reading [Controls and Hotkeys](./Controls.md). The hotkeys may help you work with the tool more effectively.
+
+## Basic understanding
+SmartShapes work similarly to [tilesets](https://docs.godotengine.org/en/latest/tutorials/2d/using_tilesets.html) but are not bound to a grid. They can be used to create polygons and even organic shapes. This allows for level design akin to [Rayman Legends](https://youtu.be/WFu1utKAZ18?si=_33TaErpHSh-r732&t=916) (based on the UbiArt Framework).
+
+Each SmartShape is made up of multiple textures that are responsible for rendering different aspects like corners or edges:
+
+
+
+## Creating a Shape
+
+
+
+- First, instance a node of either:
+ - SS2D_Shape_Open
+ - **SS2D_Shape_Closed**
+ - **We'll use a closed shape for this Quickstart demo**
+- SS2D_Shape_Base cannot be instanced directly
+- SS2D_Shape_Anchor is a node that attaches to a shape
+- The following Nodes are legacy nodes and are deprecated:
+ - RMSmartShape2D
+ - RMSmartShape2DAnchor
+
+
+
+## Editing the Shape
+
+- After creating the shape node, make sure it's selected and the toolbar appears and is in Point Edit mode
+ - 
+- Hold ALT and Left Click on the viewport to add points
+ - If this is a closed shape, the polygon will close after adding the 3rd point
+- You should now have a shape consisting of a few points and lines:
+
+
+
+## Setting the Fill Texture of the Shape (Closed Shape only)
+
+- To give it some life, we'll want to edit the "Shape Material" in the Inspector
+- Under "Shape Material" Expand "Fill Textures" and you'll see an empty array
+- Set the Array's size to '1'
+- Assign a texture to the newly created slot in the array
+- After assigning the shape should now have a valid texture
+ - If nothing happens after setting the texture, try to force the shape to update by adjusting one of the points
+- **Note that "Fill Textures" does not affect open shapes at all**
+- If you want to add a normal_texture, you would add it using the "Fill Texture Normals" property
+
+
+
+## Texturing the Edges
+
+- This where the rubber hits the road, the real meat of the tool
+- Under "Shape Material" add an element to the "Edge Meta Materials" property
+ - Shape Material -> Edge Meta Materials
+- Set the resource of the newly created element to "SS2D_Material_Edge_Metadata"
+ - Unfortunately, due to Godot limitations, every avaiable resource will offered to you instead of the one you want
+ - The options are alphabetized though, which helps in finding the resource you want
+- Expand the first element of the "Edge Meta Materials" that you just set
+ - Shape Material -> Edge Meta Materials -> element 1
+- Set the value of the "Edge Material" property to a new resource of type "SS2D_Material_Edge"
+ - Shape Material -> Edge Meta Materials -> element 1 -> Edge Material
+- Expand "Edge Material" that you just set
+- Add an element to "Textures" and assign the texture to one that you want to use as an edge
+- The shape's edges should now update using the texture you set
+ - If nothing happens after setting the texture, try to force the shape to update by adjusting one of the points
+- If you want to add a normal_texture, you would add it using the "Texture Normals" property
+- Godot should now look something like this:
+
+
+
+### Corners
+
+- If your shape has sharp 90-degree corners, the texture can look a bit warped in those places
+- You can specify a unique texture to use for inner and outer corners for each Edge Material
+- The following Edge Material properties are used for corners
+ - Textures Corner Inner
+ - Texture Normals Corner Inner
+ - Textures Corner Outer
+ - Texture Normals Corner Outer
+- See how the addition of outer corner textures improves the square created earlier
+
+
+
+### Multiple Edge Materials in One Edge
+
+- You can add as many Edge Meta Materials as you want to a Shape Material, each with their own Edge Material
+- For instance, you can add an additional egde with a rock texture (and its own set of corner textures) and have it render behind the grass
+ - To have it render behind the grass, Set the Z index of the meta material
+
+
+
+### Normal Range
+
+- Each Meta material has a Normal Range
+- The Normal Range indicates when a texture should be rendered
+ - If the normal range is 0 - 360 or 0 - 0, then any angle is considered in range and the edge will always render
+ - Angle "0" is Facing directly Right
+ - Angle "90" is Facing directly Up
+ - Angle "180" is Facing directly Left
+ - Angle "270" is Facing directly Down
+
+- If you wanted to, for example:
+ - Have rocks display on the bottom part of the shape only
+ - Have grass display on the sides and top of the shape only
+- You could:
+ - Set the grass Normal Range to 0 - 180
+ - Set the rock Normal Range to 181 - 359
+
+
+
+### Material Overrides
+
+- Using Material Overrides will allow you to change how specific edges are rendered
+- For Example, to prevent the left edge from rendering, we'll do the following:
+ - Select the edge edit button from the toolbar 
+ - Right Click the left edge of the shape
+ - Press the "Material Override" Button
+ - Uncheck the "Render" Checkbox
+
+
+
+- You can use material overrrides to also specify a specific material for a single edge
+- For example, Checking Render and choosing the "Rock" Edge Material will cause the edge to render as rocks, even though the NormalRange configuration would otherwise have it render as grass
+
+### Multiple Textures
+
+- If more than one texture is specified for an Edge Material, you can specify which texture should be used
+- Enter Point Edit mode, mouseover a point, and scroll up or down to change the texture index
+
+## Anchoring Nodes to the Shape
+
+- To anchor nodes directly to the SmartShape2D node, use SmartSahpeAnchor2D
+- You can then make nodes children to the anchor
diff --git a/addons/rmsmartshape/documentation/Resources.md b/addons/rmsmartshape/documentation/Resources.md
new file mode 100644
index 0000000..92c5678
--- /dev/null
+++ b/addons/rmsmartshape/documentation/Resources.md
@@ -0,0 +1,115 @@
+# SmartShape2D - Resources
+
+
+
+## Shape Materials
+
+Shape materials provide all the texture and collision information needed by the SmartShape nodes.
+Once a shape material is defined, it can be easily reused by any number of SmartShape2D nodes.
+
+- Edge Meta Materials
+ - An array of resources of type SS2D_Material_Edge_Metadata
+- Fill Textures
+ - Used as the texture for the inside of the polygon for Closed Shapes
+ - Currently, only the first texture can be used, multiple textures may be supported at a later date
+- Normal Textures
+ - In Godot 4, you can set normal textures with `CanvasTexture` resource.
+- Fill Texture Z Index
+ - Each Edge Meta Material has a ZIndex indicating which edges are drawn first
+ - This sets the ZIndex for the fill texture
+ - This allows the user to draw the fill texture in front of some edges or behind others
+- Fill Mesh Offset
+ - The Offset of the Fill Mesh
+ - Can be used to grow / shrink the fill mesh
+- Render Offset
+ - Every edge in the shape will be offset by this amount
+- Weld
+ - Whether or not to weld the last quad of an edge with the first quad of the next edge
+
+## Edge Meta Material
+
+An Edge Meta Material doesn't contain the actual textures used to render an edge like **Edge Material** does.
+Instead, this resource contains a single **Edge Material** and describes how and when to render the edge.
+
+- Normal Range
+ - The angles at which an edge is allowed to be rendered
+- Weld
+ - Whether or not to weld the quads in this edge
+- Taper Sharp Corners
+ - Edge vertices sharper than 90° that aren't rendered as corners, will be tapered and not welded
+ - Will not work properly on shapes with curves
+- Render
+ - Whether or not this edge is visible
+- Z Index
+ - Dictates the order in which edges are drawn
+- Offset
+ - Offset of the edge
+ - Can use a positive or negative value to draw the edges closer or further from the shape
+
+## Normal Range
+
+The Normal Range indicates when a texture should be rendered.
+Each shape will compare the Surface Normal of an edge to the Normal Range in each Edge Meta Material.
+If the edge's Normal is inside a Meta Material's Normal Range, the Meta Material's Edge Material is rendered.
+
+
+
+- A Normal Range is specified in Degrees
+ - If the normal range is 0 - 360 or 0 - 0, any angle is considered in range and the edge will always render
+ - Angle "0" is Facing directly Right
+ - Angle "90" is Facing directly Up
+ - Angle "180" is Facing directly Left
+ - Angle "270" is Facing directly Down
+
+## Edge Material
+
+The actual textures used to define an edge
+
+### Textures
+
+- The primary textures used for the edge
+- At least one texture must be defined
+- Example: 
+
+### Taper Textures
+
+These textures will be used as the first or last quad in an edge.
+They're named "Taper Textures" because the purpose is to show the edge "tapering off"
+- Textures_Taper_Left is the first quad in an edge
+ - Example: 
+- Textures_Taper_Right is the final quad in an edge
+ - Example: 
+
+### Corner Textures
+
+These textures will be used when the edge forms a sharp corner (80 degrees - 100 degrees)
+These are used because corners can look warped when using only regular textures
+- Texture_Corner_Inner is used when the corner forms an inner corner
+ - Example: 
+- Texture_Corner_Outer is used when the corner forms an outer angle
+ - Example: 
+
+### Normal Texture and Repeat
+
+To use normal textures, you can create a `CanvasTexture` resource in the inspector on any property,
+that allows to set a texture. There you can assign your texture and your normal texture, as well as set
+those to `repeat`.
+
+### Repeat Textures
+
+See previous section.
+
+### Fit Mode
+
+Most likely, the textures you use will not *perfectly* fit the polygon.
+This setting allows you to determine how SmartShape will rectify this.
+
+Differt options may look better or worse depending on the art-style.
+
+- Sqush and Stretch
+ - Texture will be mutated
+ - Either slightly squished or stretched to fit the polygon
+- Crop
+ - Texture will not be mutated
+ - Texture will simply be cropped when changing from one texture to the next
+
diff --git a/addons/rmsmartshape/documentation/Shapes.md b/addons/rmsmartshape/documentation/Shapes.md
new file mode 100644
index 0000000..70afa84
--- /dev/null
+++ b/addons/rmsmartshape/documentation/Shapes.md
@@ -0,0 +1,88 @@
+# SmartShape2D - Shapes
+
+Each shape consists of a set of points. You can directly edit either the points or the edges between the points in the viewport.
+
+Shapes are configured to use a [Shape Material](./Resources.md#ShapeMaterial) which determines how the shape is rendered.
+
+A shape can be open or closed. Each new shape starts open. To close a shape, simply add a point on top of the first one.
+
+
+## Properties
+
+> 🛈 Most properties now have documentation comments.
+
+
+
+### Editor Debug
+
+- Will show the bounding box for each quad in the mesh of edges.
+- Can be helpful to illustrate why a shape doesn't look the way you expect.
+
+### Flip Edges
+
+- Will flip the edges of the shape (invert y).
+
+### Render Edges
+
+- Whether or not the edges of the shape should be rendered.
+
+### Collision Size
+
+- Size of the collision shape.
+
+### Collision Offset
+
+- Offset of where the collision shape starts and ends.
+- A **positive** value offsets the collision shape **outwards**.
+- A **negative** value offsets the collision shape **inwards**.
+
+### Tessellation Stages
+
+- Number of stages in the curve tessellation process (Uses Curve2D Internally).
+- First Param in Curve2D.tessellate.
+- See [Curve2D Documentation](https://docs.godotengine.org/en/3.2/classes/class_curve2d.html#class-curve2d-method-tessellate).
+
+### Tessellation Tolerence
+
+- Tolerence Degrees in the curve tessellation process (Uses Curve2D Internally).
+- Second Param in Curve2D.tessellate.
+- See [Curve2D Documentation](https://docs.godotengine.org/en/3.2/classes/class_curve2d.html#class-curve2d-method-tessellate).
+
+### Collision Generation Method
+
+- Controls which method should be used to generate the collision shape.
+- See also in-engine documentation.
+
+### Collision Update Mode
+
+- Controls when to update collisions.
+- See also in-engine documentation.
+
+### Curve Bake Interval
+
+- Bake interval value for Curve2D.
+- See [Curve2D Documentation](https://docs.godotengine.org/en/3.2/classes/class_curve2d.html#class-curve2d-property-bake-interval).
+
+### Collision Polygon Node Path
+
+- The path to the CollisionShape that the SmartShape will use for collision.
+- Is Autoset when pressing the generate collision button.
+
+### Shape Material
+
+- The material that this shape will use to render itself.
+- For backwards compatibility `fill_texture_z_index` defaults to `-10`. Set this to `0` and enable `fill_texture_show_behind_parent` in order to preserve Godot's normal z-sorting when layering with other nodes.
+
+### Points
+
+- **There is no need to edit this property by hand, but you can if you'd like.**
+- Contains all of the points and meta-data for the points contained in this shape.
+- This data structure is updated as you manipulate the shape.
+
+### Material Overrides
+
+- **There is no need to edit this property by hand, but you can if you'd like.**
+- When an edge is given a "Material Override" the data for that edge is stored here.
+- This data structure is updated as you manipulate the shape.
+
+
diff --git a/addons/rmsmartshape/documentation/Toolbar.md b/addons/rmsmartshape/documentation/Toolbar.md
new file mode 100644
index 0000000..ffd31b8
--- /dev/null
+++ b/addons/rmsmartshape/documentation/Toolbar.md
@@ -0,0 +1,53 @@
+# SmartShape2D - Toolbar
+
+
+
+
+
+## Create Mode
+
+- In this mode you can start creating a new shape.
+- Left-Click anywhere to add a new point.
+- Press ESCAPE to exit create mode.
+- Hold down ALT and Left-Click to create a point between the two points closest to your mouse.
+
+## Point Mode
+
+- In this mode you can add, delete, and move all of the points that make up a shape
+- To **Add** a new point to the shape:
+ - Hold down ALT and Left-Click anywhere on the viewport to add a point between the two points closest to your mouse.
+ - Left-Click on an edge between two points.
+- To **Move** a point, Left-Click on any point and drag
+- To **Delete** a point, Right-Click on any point
+- To set the **Control Points** of a point (for curves), hold **Shift**, Left Click on any point and drag
+ - After the Control Points have been set, you can edit them individually by Left-Clicking and dragging
+ - You can delete control points by right clicking them
+ - To make an empty clone SmartShape2D, hold down ALT + SHIFT and Left-Click anywhere in the viewport.
+
+## Edge Mode
+
+- In this mode you can Move Eges and choose how specific edges are rendered
+- To **Move** an Edge, Left Click and Drag the Edge
+- To **Change an Edges Rendering**, right click the edge and press "**Material Override**"
+
+
+- This popup allows you to **change how edges are rendered**
+ - **Render** will toggle whether or not this edge will be drawn with Edge Materials
+ - **Set Material** allows you to choose a specific Edge Material to use to render this edge
+
+## Origin Set
+
+- This tool allows you to set the origin of any SmartShape
+- To **Set the Origin** Left Click anywhere on the viewport
+
+## Generate Collision
+
+- If you want your shape to have collision, press this button to autogenerate the collision nodes
+- The shape will be made a child of a newly created **StaticBody2D**
+- A sibling node, **CollisionPolygon2D** will also be created and added to the SceneTree
+ - The "Collision Polygon" parameter of the Shape will be set to this sibling **CollisionPolygon2D**
+
+## Snapping
+
+When Moving / Adding points, snapping will cause the positions of the points to snap to the grid. This works the same as Godot's built-in snapping.
+You can have snapping either use Global Space, or space relative to the shape's origin.
diff --git a/addons/rmsmartshape/documentation/VersionHistory.md b/addons/rmsmartshape/documentation/VersionHistory.md
new file mode 100644
index 0000000..e924a95
--- /dev/null
+++ b/addons/rmsmartshape/documentation/VersionHistory.md
@@ -0,0 +1,117 @@
+# Version History
+
+## 2.x
+### 2.2
+January 4th 2021
+### Fix
+- Fix for crash that would occur when points were aligned *just* right
+- See issue 66
+ + https://github.com/SirRamEsq/SmartShape2D/issues/66
+### Features
+- Each Edge Material can now have a Material (Shader)
+- Each Edge Material Meta can have a z-index and z-as-relative set
+- See issue 64
+ + https://github.com/SirRamEsq/SmartShape2D/issues/64
+
+### 2.1
+December 14th 2020
+#### Significant Changes from 2.0
+- Improved Width handling
+- Improved Welding
+- Rendering is now achieved by having multiple child-nodes each render a piece of the shape
+ + Previously, all the rendering was done by the shape node
+ + Improves performance
+ + Fixes lighting bugs
+- Point Creation mode reimplemented
+ + Mode active by default
+ + Can be exited by pressing ESC
+- Several usability additions
+ + Hotkey for grabbing closest point
+ + Hotkey for creating new shape at point
+ + Width Grabber for closest point
+ + Preview for adding points
+- Several Bug fixes and issues closed
+#### New Features
+- Meta Shapes Introduced
+- "Fit mode" added to edge material
+ + Can either squash and stretch the texture or crop it
+#### Minor Changes
+- Changes to GUI Theme
+ + More in line with standard Godot
+- Add windows scripts for running unit tests
+- Changed default snap settings to 8x8 pixels
+
+### 2.0
+September 7th 2020
+#### Significant Changes from 1.0
+- Edge Textures are no longer determined by a cardinal direction (UP, DOWN, LEFT, RIGHT)
+ - Instead, a starting and ending normal angle is specified for each edge
+- Textures are now defined per-edge instead of per-shape
+#### New Features
+- Taper textures
+ - Instead of simply ending, the user can have an edge "taper-off"
+- Editing by Edges
+- Material Overrides
+#### Internal Changes
+- Completely overhauled everything
+- A rudimentary constraint system is in place
+ - Closed shapes will add a point when closing, then constrain the added point's position to the first point
+- Points are no longer refered to by index, they are refered to by keys
+ - This enables points to have relationships that aren't affected when:
+ - Adding/Removing a point
+ - Changing orientation of the poly
+- Many Unit and Integration tests
+ - Refactored original working code to better support testing
+- Kept original scripts and classes from version 1.0 to ease importing
+
+## 1.x
+### Changes in 1.3
+This update primarily fixes bugs and improves existing features to be more usable.
+#### Changes
+- Merged top/left/right/bottom offset into one variable. render offset
+#### Fixes
+- Input bugs
+- Edge Flipping
+- Polygon orientation bugs
+- Quad Welding
+- Corer quad generation and welding
+- Collision variables in the RMSmartShapeMaterial working as intended
+
+### Changes in 1.2
+#### Tweaks
+- Refactoring
+- Toolbar takes less space
+- Minor bug fixes
+
+#### New Features
+- Bezier Curves!
+ - Hold shift on a control point to create a curve
+- Corner Quads!
+ - Both inner and outer corner quads are now generated
+ - Textures can be speciied for each direction of both inner and outer quads
+- Edge Moving!
+ - Can move an edge (two points) by pressing SHIFT in move mode and dragging the edge
+
+### Changes in 1.1
+- Refactoring
+- Fixed Errors Occuring when Texture Arrays are size '0' but not null
+- Fixed sync between texture, flip, and width indicies
+ - Would sometimes share a single array between the 3 vars
+ - Are all unique now
+
+- Snapping
+- More informative toolbar
+
+### Changes in 1.0
+- Fixed many debug errors reported related to indexing beyond array sizes
+- Fixed control point wrapping of RMSmartShapeAnchor2D nodes anchored to RMSmartShape2D nodes.
+- Tested on newly released 3.2 Godot.
+
+### Changes in 0.91
+- Edges are calculated in relationship to object space instead of screen space
+- Added option to allow user to let the object recalculate edges based on screen space.
+- Fixed uv calculations for flipped textures.
+- Fixed uv bug for edge sections less than half the size of texture width
+- Added option to allow for a RMSmartShapeAnchor to mimic scale of monitored node
+- Removed sections of code related to clockwise versus clockwise checks, very specifically regarding the direction of texture edges.
+- Corrected normal texture bug for fill and edge rendering
diff --git a/addons/rmsmartshape/documentation/imgs/AngleExplaination.png b/addons/rmsmartshape/documentation/imgs/AngleExplaination.png
new file mode 100644
index 0000000..5b74564
Binary files /dev/null and b/addons/rmsmartshape/documentation/imgs/AngleExplaination.png differ
diff --git a/addons/rmsmartshape/documentation/imgs/EdgeEdit-MaterialOverride.png b/addons/rmsmartshape/documentation/imgs/EdgeEdit-MaterialOverride.png
new file mode 100644
index 0000000..56c0070
Binary files /dev/null and b/addons/rmsmartshape/documentation/imgs/EdgeEdit-MaterialOverride.png differ
diff --git a/addons/rmsmartshape/documentation/imgs/EdgeEdit-NoRender.png b/addons/rmsmartshape/documentation/imgs/EdgeEdit-NoRender.png
new file mode 100644
index 0000000..789ac59
Binary files /dev/null and b/addons/rmsmartshape/documentation/imgs/EdgeEdit-NoRender.png differ
diff --git a/addons/rmsmartshape/documentation/imgs/Inpsector-EdgeMaterial.png b/addons/rmsmartshape/documentation/imgs/Inpsector-EdgeMaterial.png
new file mode 100644
index 0000000..e031a52
Binary files /dev/null and b/addons/rmsmartshape/documentation/imgs/Inpsector-EdgeMaterial.png differ
diff --git a/addons/rmsmartshape/documentation/imgs/Inpsector-EdgeMaterialCornerOuter.png b/addons/rmsmartshape/documentation/imgs/Inpsector-EdgeMaterialCornerOuter.png
new file mode 100644
index 0000000..42bb12b
Binary files /dev/null and b/addons/rmsmartshape/documentation/imgs/Inpsector-EdgeMaterialCornerOuter.png differ
diff --git a/addons/rmsmartshape/documentation/imgs/Inpsector-EdgeMaterials2.png b/addons/rmsmartshape/documentation/imgs/Inpsector-EdgeMaterials2.png
new file mode 100644
index 0000000..f03a474
Binary files /dev/null and b/addons/rmsmartshape/documentation/imgs/Inpsector-EdgeMaterials2.png differ
diff --git a/addons/rmsmartshape/documentation/imgs/Inpsector-EdgeMaterialsNormalRange.png b/addons/rmsmartshape/documentation/imgs/Inpsector-EdgeMaterialsNormalRange.png
new file mode 100644
index 0000000..cfc3a68
Binary files /dev/null and b/addons/rmsmartshape/documentation/imgs/Inpsector-EdgeMaterialsNormalRange.png differ
diff --git a/addons/rmsmartshape/documentation/imgs/NewNode-SS2D_Nodes.png b/addons/rmsmartshape/documentation/imgs/NewNode-SS2D_Nodes.png
new file mode 100644
index 0000000..df063e4
Binary files /dev/null and b/addons/rmsmartshape/documentation/imgs/NewNode-SS2D_Nodes.png differ
diff --git a/addons/rmsmartshape/documentation/imgs/NormalColors.png b/addons/rmsmartshape/documentation/imgs/NormalColors.png
new file mode 100644
index 0000000..904f83a
Binary files /dev/null and b/addons/rmsmartshape/documentation/imgs/NormalColors.png differ
diff --git a/addons/rmsmartshape/documentation/imgs/NormalCorrect.png b/addons/rmsmartshape/documentation/imgs/NormalCorrect.png
new file mode 100644
index 0000000..2c899e2
Binary files /dev/null and b/addons/rmsmartshape/documentation/imgs/NormalCorrect.png differ
diff --git a/addons/rmsmartshape/documentation/imgs/NormalWrong.png b/addons/rmsmartshape/documentation/imgs/NormalWrong.png
new file mode 100644
index 0000000..79c188e
Binary files /dev/null and b/addons/rmsmartshape/documentation/imgs/NormalWrong.png differ
diff --git a/addons/rmsmartshape/documentation/imgs/PluginActivate.png b/addons/rmsmartshape/documentation/imgs/PluginActivate.png
new file mode 100644
index 0000000..1a96705
Binary files /dev/null and b/addons/rmsmartshape/documentation/imgs/PluginActivate.png differ
diff --git a/addons/rmsmartshape/documentation/imgs/ShapeClosed-FillTextured.png b/addons/rmsmartshape/documentation/imgs/ShapeClosed-FillTextured.png
new file mode 100644
index 0000000..d768f8e
Binary files /dev/null and b/addons/rmsmartshape/documentation/imgs/ShapeClosed-FillTextured.png differ
diff --git a/addons/rmsmartshape/documentation/imgs/ShapeClosed-Untextured.png b/addons/rmsmartshape/documentation/imgs/ShapeClosed-Untextured.png
new file mode 100644
index 0000000..275b25c
Binary files /dev/null and b/addons/rmsmartshape/documentation/imgs/ShapeClosed-Untextured.png differ
diff --git a/addons/rmsmartshape/documentation/imgs/Toolbar-PointEdit.png b/addons/rmsmartshape/documentation/imgs/Toolbar-PointEdit.png
new file mode 100644
index 0000000..8bc8b56
Binary files /dev/null and b/addons/rmsmartshape/documentation/imgs/Toolbar-PointEdit.png differ
diff --git a/addons/rmsmartshape/documentation/imgs/canvas-item-repeat.png b/addons/rmsmartshape/documentation/imgs/canvas-item-repeat.png
new file mode 100644
index 0000000..1045c52
Binary files /dev/null and b/addons/rmsmartshape/documentation/imgs/canvas-item-repeat.png differ
diff --git a/addons/rmsmartshape/documentation/imgs/faq-texture-repeat-import.png b/addons/rmsmartshape/documentation/imgs/faq-texture-repeat-import.png
new file mode 100644
index 0000000..f5963d7
Binary files /dev/null and b/addons/rmsmartshape/documentation/imgs/faq-texture-repeat-import.png differ
diff --git a/addons/rmsmartshape/documentation/imgs/faq-texture-repeat.png b/addons/rmsmartshape/documentation/imgs/faq-texture-repeat.png
new file mode 100644
index 0000000..1ceedbf
Binary files /dev/null and b/addons/rmsmartshape/documentation/imgs/faq-texture-repeat.png differ
diff --git a/addons/rmsmartshape/documentation/imgs/godot4-assign-normal-tex.png b/addons/rmsmartshape/documentation/imgs/godot4-assign-normal-tex.png
new file mode 100644
index 0000000..cc64e74
Binary files /dev/null and b/addons/rmsmartshape/documentation/imgs/godot4-assign-normal-tex.png differ
diff --git a/addons/rmsmartshape/documentation/imgs/godot4-create-texture-res.png b/addons/rmsmartshape/documentation/imgs/godot4-create-texture-res.png
new file mode 100644
index 0000000..4a0e417
Binary files /dev/null and b/addons/rmsmartshape/documentation/imgs/godot4-create-texture-res.png differ
diff --git a/addons/rmsmartshape/documentation/imgs/godot4-make-points-unique.png b/addons/rmsmartshape/documentation/imgs/godot4-make-points-unique.png
new file mode 100644
index 0000000..9b1e49d
Binary files /dev/null and b/addons/rmsmartshape/documentation/imgs/godot4-make-points-unique.png differ
diff --git a/addons/rmsmartshape/documentation/imgs/grass-corner-inner.png b/addons/rmsmartshape/documentation/imgs/grass-corner-inner.png
new file mode 100644
index 0000000..59c4705
Binary files /dev/null and b/addons/rmsmartshape/documentation/imgs/grass-corner-inner.png differ
diff --git a/addons/rmsmartshape/documentation/imgs/grass-corner-outer.png b/addons/rmsmartshape/documentation/imgs/grass-corner-outer.png
new file mode 100644
index 0000000..5aa5b59
Binary files /dev/null and b/addons/rmsmartshape/documentation/imgs/grass-corner-outer.png differ
diff --git a/addons/rmsmartshape/documentation/imgs/grass-taper-left.png b/addons/rmsmartshape/documentation/imgs/grass-taper-left.png
new file mode 100644
index 0000000..c28f026
Binary files /dev/null and b/addons/rmsmartshape/documentation/imgs/grass-taper-left.png differ
diff --git a/addons/rmsmartshape/documentation/imgs/grass-taper-right.png b/addons/rmsmartshape/documentation/imgs/grass-taper-right.png
new file mode 100644
index 0000000..c3803fc
Binary files /dev/null and b/addons/rmsmartshape/documentation/imgs/grass-taper-right.png differ
diff --git a/addons/rmsmartshape/documentation/imgs/grass.png b/addons/rmsmartshape/documentation/imgs/grass.png
new file mode 100644
index 0000000..caa0420
Binary files /dev/null and b/addons/rmsmartshape/documentation/imgs/grass.png differ
diff --git a/addons/rmsmartshape/documentation/imgs/icon_edge.png b/addons/rmsmartshape/documentation/imgs/icon_edge.png
new file mode 100644
index 0000000..e2e08d6
Binary files /dev/null and b/addons/rmsmartshape/documentation/imgs/icon_edge.png differ
diff --git a/addons/rmsmartshape/documentation/imgs/sample.gif b/addons/rmsmartshape/documentation/imgs/sample.gif
new file mode 100644
index 0000000..0e9064e
Binary files /dev/null and b/addons/rmsmartshape/documentation/imgs/sample.gif differ
diff --git a/addons/rmsmartshape/documentation/imgs/sample.png b/addons/rmsmartshape/documentation/imgs/sample.png
new file mode 100644
index 0000000..bf47a3e
Binary files /dev/null and b/addons/rmsmartshape/documentation/imgs/sample.png differ
diff --git a/addons/rmsmartshape/documentation/imgs/smartshape_textures_breakdown.png b/addons/rmsmartshape/documentation/imgs/smartshape_textures_breakdown.png
new file mode 100644
index 0000000..36a7886
Binary files /dev/null and b/addons/rmsmartshape/documentation/imgs/smartshape_textures_breakdown.png differ
diff --git a/addons/rmsmartshape/editors/NormalRangeEditor/NormalRangeEditor.gd b/addons/rmsmartshape/editors/NormalRangeEditor/NormalRangeEditor.gd
new file mode 100644
index 0000000..89ef973
--- /dev/null
+++ b/addons/rmsmartshape/editors/NormalRangeEditor/NormalRangeEditor.gd
@@ -0,0 +1,64 @@
+@tool
+extends VBoxContainer
+class_name SS2D_NormalRangeEditor
+
+signal value_changed
+
+var start: float: get = _get_start, set = _set_start
+var end: float: get = _get_end, set = _set_end
+var zero_equals_full_circle := true
+
+var _progress_bar: TextureProgressBar:
+ get:
+ if _progress_bar == null:
+ _progress_bar = get_node_or_null("%TextureProgressBar")
+ return _progress_bar
+
+
+func _ready() -> void:
+ _set_initial_angles()
+
+
+func _enter_tree() -> void:
+ _set_initial_angles()
+
+
+func _set_initial_angles() -> void:
+ _set_start(_progress_bar.radial_initial_angle)
+ _set_end(_progress_bar.radial_fill_degrees)
+
+
+func _on_startSlider_value_changed(value: float) -> void:
+ _set_start(value)
+
+
+func _on_endSlider_value_changed(value: float) -> void:
+ _set_end(value)
+
+
+func _set_start(value: float) -> void:
+ var fill: float = _progress_bar.radial_fill_degrees
+ var init_angle: float = 360.0 - fill - value + 90.0
+ _progress_bar.radial_initial_angle = _mutate_angle_deg(init_angle)
+
+
+func _get_start() -> float:
+ return _progress_bar.radial_initial_angle
+
+
+func _set_end(value: float) -> void:
+ _progress_bar.radial_fill_degrees = _mutate_angle_deg(value)
+
+
+func _get_end() -> float:
+ return _progress_bar.radial_fill_degrees
+
+
+func _on_SS2D_StartEditorSpinSlider_value_changed(value: float) -> void:
+ _set_start(value)
+
+
+func _mutate_angle_deg(v: float) -> float:
+ if zero_equals_full_circle and v == 0.0:
+ return 360.0
+ return v
diff --git a/addons/rmsmartshape/editors/NormalRangeEditor/NormalRangeEditor.tscn b/addons/rmsmartshape/editors/NormalRangeEditor/NormalRangeEditor.tscn
new file mode 100644
index 0000000..37f4d4b
--- /dev/null
+++ b/addons/rmsmartshape/editors/NormalRangeEditor/NormalRangeEditor.tscn
@@ -0,0 +1,32 @@
+[gd_scene load_steps=5 format=3 uid="uid://cg1vy768oxwxr"]
+
+[ext_resource type="Texture2D" uid="uid://dajqljvjm2d2q" path="res://addons/rmsmartshape/editors/NormalRangeEditor/progress_texture.png" id="1"]
+[ext_resource type="Script" path="res://addons/rmsmartshape/editors/NormalRangeEditor/NormalRangeEditor.gd" id="2"]
+[ext_resource type="Texture2D" uid="uid://dgd4sbhsw1no3" path="res://addons/rmsmartshape/editors/NormalRangeEditor/over_texture.png" id="3"]
+[ext_resource type="Texture2D" uid="uid://cax163h05b60j" path="res://addons/rmsmartshape/editors/NormalRangeEditor/under_texture.png" id="4"]
+
+[node name="NormalRangeEditor" type="VBoxContainer"]
+anchors_preset = -1
+anchor_right = 0.087
+anchor_bottom = 0.027
+offset_right = 38.912
+offset_bottom = 203.8
+size_flags_horizontal = 3
+size_flags_vertical = 9
+script = ExtResource("2")
+
+[node name="CenterContainer" type="CenterContainer" parent="."]
+layout_mode = 2
+
+[node name="TextureProgressBar" type="TextureProgressBar" parent="CenterContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+max_value = 1.0
+step = 0.0
+value = 1.0
+fill_mode = 4
+texture_under = ExtResource("4")
+texture_over = ExtResource("3")
+texture_progress = ExtResource("1")
+radial_initial_angle = 79.3
+radial_fill_degrees = 93.8
diff --git a/addons/rmsmartshape/editors/NormalRangeEditor/NormalRangeEditorProperty.gd b/addons/rmsmartshape/editors/NormalRangeEditor/NormalRangeEditorProperty.gd
new file mode 100644
index 0000000..6e28eac
--- /dev/null
+++ b/addons/rmsmartshape/editors/NormalRangeEditor/NormalRangeEditorProperty.gd
@@ -0,0 +1,26 @@
+extends EditorProperty
+class_name SS2D_NormalRangeEditorProperty
+
+var control: SS2D_NormalRangeEditor = preload(
+ "res://addons/rmsmartshape/editors/NormalRangeEditor/NormalRangeEditor.tscn"
+ ).instantiate()
+
+
+func _init() -> void:
+ add_child(control)
+ add_focusable(control)
+
+
+func _enter_tree() -> void:
+ control.connect("value_changed", self._value_changed)
+ _value_changed()
+
+
+func _exit_tree() -> void:
+ control.disconnect("value_changed", self._value_changed)
+
+
+func _value_changed() -> void:
+ var obj: SS2D_NormalRange = get_edited_object()
+ control.end = obj.distance
+ control.start = obj.begin
diff --git a/addons/rmsmartshape/editors/NormalRangeEditor/over_texture.png b/addons/rmsmartshape/editors/NormalRangeEditor/over_texture.png
new file mode 100644
index 0000000..7b47da6
Binary files /dev/null and b/addons/rmsmartshape/editors/NormalRangeEditor/over_texture.png differ
diff --git a/addons/rmsmartshape/editors/NormalRangeEditor/over_texture.png.import b/addons/rmsmartshape/editors/NormalRangeEditor/over_texture.png.import
new file mode 100644
index 0000000..b8afe45
--- /dev/null
+++ b/addons/rmsmartshape/editors/NormalRangeEditor/over_texture.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dgd4sbhsw1no3"
+path="res://.godot/imported/over_texture.png-8aa46a605c51c9642f0609a4a8bbd43d.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/rmsmartshape/editors/NormalRangeEditor/over_texture.png"
+dest_files=["res://.godot/imported/over_texture.png-8aa46a605c51c9642f0609a4a8bbd43d.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/addons/rmsmartshape/editors/NormalRangeEditor/progress_texture.png b/addons/rmsmartshape/editors/NormalRangeEditor/progress_texture.png
new file mode 100644
index 0000000..1911c99
Binary files /dev/null and b/addons/rmsmartshape/editors/NormalRangeEditor/progress_texture.png differ
diff --git a/addons/rmsmartshape/editors/NormalRangeEditor/progress_texture.png.import b/addons/rmsmartshape/editors/NormalRangeEditor/progress_texture.png.import
new file mode 100644
index 0000000..6407be1
--- /dev/null
+++ b/addons/rmsmartshape/editors/NormalRangeEditor/progress_texture.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dajqljvjm2d2q"
+path="res://.godot/imported/progress_texture.png-5c3ce00c522dd3c8b7a6f676bd7a3382.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/rmsmartshape/editors/NormalRangeEditor/progress_texture.png"
+dest_files=["res://.godot/imported/progress_texture.png-5c3ce00c522dd3c8b7a6f676bd7a3382.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/addons/rmsmartshape/editors/NormalRangeEditor/under_texture.png b/addons/rmsmartshape/editors/NormalRangeEditor/under_texture.png
new file mode 100644
index 0000000..b838714
Binary files /dev/null and b/addons/rmsmartshape/editors/NormalRangeEditor/under_texture.png differ
diff --git a/addons/rmsmartshape/editors/NormalRangeEditor/under_texture.png.import b/addons/rmsmartshape/editors/NormalRangeEditor/under_texture.png.import
new file mode 100644
index 0000000..9914330
--- /dev/null
+++ b/addons/rmsmartshape/editors/NormalRangeEditor/under_texture.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cax163h05b60j"
+path="res://.godot/imported/under_texture.png-87a243e17179ba2b162ef41ea65a8e51.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/rmsmartshape/editors/NormalRangeEditor/under_texture.png"
+dest_files=["res://.godot/imported/under_texture.png-87a243e17179ba2b162ef41ea65a8e51.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/addons/rmsmartshape/editors/action_property_inspector_plugin.gd b/addons/rmsmartshape/editors/action_property_inspector_plugin.gd
new file mode 100644
index 0000000..cbb97b3
--- /dev/null
+++ b/addons/rmsmartshape/editors/action_property_inspector_plugin.gd
@@ -0,0 +1,60 @@
+extends EditorInspectorPlugin
+
+## This inspector plugin will show an Execute button for action properties in
+## SS2D_Shape.
+##
+## To add an action property export it with:
+##
+## @export_placeholder("ActionProperty") var ...
+##
+## Then, when an action is executed by user (by pressing an Execute button in inspector),
+## setter will be called with non-empty string:
+##
+## func _action_property_setter(value: String) -> void:
+## if value.size() > 0:
+## ## Action is executed
+##
+
+class ActionPropertyEditor:
+ extends EditorProperty
+
+ signal action_pressed
+
+ var button: Button
+
+ func _init() -> void:
+ button = Button.new()
+ button.text = "Execute"
+ add_child(button)
+ button.connect("pressed", func() -> void: emit_signal("action_pressed"))
+
+ func _ready() -> void:
+ button.icon = get_theme_icon("TextEditorPlay", "EditorIcons")
+
+
+func _can_handle(object: Object) -> bool:
+ if object is SS2D_Shape:
+ return true
+ return false
+
+
+func _parse_property(
+ object: Object,
+ _type: Variant.Type,
+ name: String,
+ _hint_type: PropertyHint,
+ hint_string: String,
+ _usage_flags,
+ _wide: bool
+) -> bool:
+ if hint_string == "ActionProperty":
+ var prop_editor := ActionPropertyEditor.new()
+ add_property_editor(name, prop_editor)
+ prop_editor.connect("action_pressed", self._on_action_pressed.bind(object, name))
+ return true
+ return false
+
+
+func _on_action_pressed(object: Object, prop_name: String) -> void:
+ prints("Action executed:", prop_name.capitalize())
+ object.set(prop_name, "executed")
diff --git a/addons/rmsmartshape/editors/normal_range_inspector_plugin.gd b/addons/rmsmartshape/editors/normal_range_inspector_plugin.gd
new file mode 100644
index 0000000..8678296
--- /dev/null
+++ b/addons/rmsmartshape/editors/normal_range_inspector_plugin.gd
@@ -0,0 +1,45 @@
+extends EditorInspectorPlugin
+
+var control: SS2D_NormalRangeEditorProperty = null
+
+
+func _can_handle(object: Object) -> bool:
+ #if object is SS2D_NormalRange:
+ # return true
+ if object is SS2D_NormalRange:
+ #Connect
+ if not object.is_connected("changed", self._changed):
+ object.connect("changed", self._changed.bind(object))
+ return true
+ else:
+ #Disconnect
+ if control != null:
+ control = null
+
+ if object.has_signal("changed"):
+ if object.is_connected("changed", self._changed):
+ object.disconnect("changed", self._changed)
+ pass
+
+ return false
+
+
+func _changed(_object: Object) -> void:
+ control._value_changed()
+
+
+func _parse_property(
+ _object: Object,
+ _type: Variant.Type,
+ name: String,
+ _hint_type: PropertyHint,
+ _hint_string: String,
+ _usage_flags: int,
+ _wide: bool
+) -> bool:
+ if name == "edgeRendering":
+ control = SS2D_NormalRangeEditorProperty.new()
+ add_property_editor(" ", control)
+ return true
+ return false
+
diff --git a/addons/rmsmartshape/lib/tesselation_vertex_mapping.gd b/addons/rmsmartshape/lib/tesselation_vertex_mapping.gd
new file mode 100644
index 0000000..8336b02
--- /dev/null
+++ b/addons/rmsmartshape/lib/tesselation_vertex_mapping.gd
@@ -0,0 +1,35 @@
+extends RefCounted
+class_name SS2D_TesselationVertexMapping
+
+## Provides mappings from tesselated point indices to their corresponding vertex indices and vice-versa.
+
+var _t_point_idx_to_point_idx := PackedInt32Array()
+var _point_idx_to_t_points_idx: Array[PackedInt32Array] = []
+
+
+## Rebuild the mapping using the given tesselated points and vertices.
+func build(tesselated_points: PackedVector2Array, vertices: PackedVector2Array) -> void:
+ _t_point_idx_to_point_idx.clear()
+ _point_idx_to_t_points_idx.clear()
+
+ var point_idx := -1
+
+ for t_point_idx in tesselated_points.size():
+ var next_point_idx := SS2D_PluginFunctionality.get_next_point_index_wrap_around(point_idx, vertices)
+
+ if tesselated_points[t_point_idx] == vertices[next_point_idx]:
+ point_idx = next_point_idx
+ _point_idx_to_t_points_idx.push_back(PackedInt32Array())
+
+ _t_point_idx_to_point_idx.push_back(point_idx)
+ _point_idx_to_t_points_idx[point_idx].push_back(t_point_idx)
+
+
+## Returns the vertex index corresponding to the given tesselated point index
+func tess_to_vertex_index(tesselated_idx: int) -> int:
+ return _t_point_idx_to_point_idx[tesselated_idx]
+
+
+## Returns a list of tesselated point indices corresponding to the given vertex index
+func vertex_to_tess_indices(vertex_idx: int) -> PackedInt32Array:
+ return _point_idx_to_t_points_idx[vertex_idx]
diff --git a/addons/rmsmartshape/lib/tuple.gd b/addons/rmsmartshape/lib/tuple.gd
new file mode 100644
index 0000000..aea5e3c
--- /dev/null
+++ b/addons/rmsmartshape/lib/tuple.gd
@@ -0,0 +1,142 @@
+@tool
+extends RefCounted
+class_name SS2D_IndexTuple
+
+## Provides utility functions for handling and storing indices of two related points using Vector2i.
+##
+## Index tuples are considered equal if their elements are equal, regardless of their order:
+## T(X, Y) <=> T(Y, X).
+##
+## For effectively working with containers, helper functions for arrays and dictionaries are
+## provided that implement the above behavior.
+
+## 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
+
+
+## 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)
+
+
+## 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
+
+
+## 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
+
+
+## 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) -> PackedInt32Array:
+ var out := PackedInt32Array()
+
+ for i in tuple_array.size():
+ if tuple_array[i].x == value or tuple_array[i].y == value:
+ out.push_back(i)
+
+ return out
+
+
+## Transform the tuple into a normalized representation (elements in ascending order).
+## Same as sort_ascending() at the moment.
+## Useful in more optimized use-cases where certain assumptions can be made if all tuples share a
+## normalized representation.
+static func normalize_tuple(tuple: Vector2i) -> Vector2i:
+ return sort_ascending(tuple)
+
+
+## Returns a tuple with elements in ascending order.
+static func sort_ascending(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 or key is PackedInt32Array or key is PackedInt64Array:
+ 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:
+ return dict.has(tuple) or 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)
+
+ return out
diff --git a/addons/rmsmartshape/materials/edge_material.gd b/addons/rmsmartshape/materials/edge_material.gd
new file mode 100644
index 0000000..81f5fc7
--- /dev/null
+++ b/addons/rmsmartshape/materials/edge_material.gd
@@ -0,0 +1,156 @@
+@tool
+extends Resource
+class_name SS2D_Material_Edge
+
+## This material represents the set of textures used for a single edge.
+##
+## This consists of: [br]
+## - textures [br]
+## - corner_textures [br]
+## - taper_textures [br]
+
+## All variations of the main edge texture.[br]
+## _textures[0] is considered the "main" texture for the EdgeMaterial.[br][br]
+## [b]Note:[/b] Will be used to generate an icon representing an edge texture.[br]
+@export var textures: Array[Texture2D] = [] : set = _set_textures
+
+# Textures for the final left and right quad of the edge when the angle is steep
+@export var textures_corner_outer: Array[Texture2D] = [] : set = _set_textures_corner_outer
+@export var textures_corner_inner: Array[Texture2D] = [] : set = _set_textures_corner_inner
+
+# Textures for the final left and right quad of the edge when the angle is shallow
+# Named as such because the desired look is that the texture "tapers-off"
+@export var textures_taper_left: Array[Texture2D] = [] : set = _set_textures_taper_left
+@export var textures_taper_right: Array[Texture2D] = [] : set = _set_textures_taper_right
+
+## Textures that will be used for the sharp_corner_tapering feature
+@export var textures_taper_corner_left: Array[Texture2D] = [] : set = _set_textures_taper_corner_left
+@export var textures_taper_corner_right: Array[Texture2D] = [] : set = _set_textures_taper_corner_right
+
+## If the texture choice should be randomized instead of the choice by point setup
+@export var randomize_texture: bool = false : set = _set_randomize_texture
+## If corner textures should be used
+@export var use_corner_texture: bool = true : set = _set_use_corner
+## If taper textures should be used
+@export var use_taper_texture: bool = true : set = _set_use_taper
+
+## Whether squishing can occur when texture doesn't fit nicely into total length.
+enum FITMODE {SQUISH_AND_STRETCH, CROP}
+@export var fit_mode: FITMODE = FITMODE.SQUISH_AND_STRETCH : set = _set_fit_texture
+
+@export var material: Material = null : set = _set_material
+
+
+###########
+# SETTERS #
+###########
+func _set_textures(ta: Array[Texture2D]) -> void:
+ textures = ta
+ emit_changed()
+
+
+func _set_textures_corner_outer(a: Array[Texture2D]) -> void:
+ textures_corner_outer = a
+ emit_changed()
+
+
+func _set_textures_corner_inner(a: Array[Texture2D]) -> void:
+ textures_corner_inner = a
+ emit_changed()
+
+
+func _set_textures_taper_left(a: Array[Texture2D]) -> void:
+ textures_taper_left = a
+ emit_changed()
+
+
+func _set_textures_taper_right(a: Array[Texture2D]) -> void:
+ textures_taper_right = a
+ emit_changed()
+
+func _set_textures_taper_corner_left(a: Array[Texture2D]) -> void:
+ textures_taper_corner_left = a
+ emit_changed()
+
+func _set_textures_taper_corner_right(a: Array[Texture2D]) -> void:
+ textures_taper_corner_right = a
+ emit_changed()
+
+func _set_randomize_texture(b: bool) -> void:
+ randomize_texture = b
+ emit_changed()
+
+
+func _set_use_corner(b: bool) -> void:
+ use_corner_texture = b
+ emit_changed()
+
+
+func _set_use_taper(b: bool) -> void:
+ use_taper_texture = b
+ emit_changed()
+
+
+func _set_fit_texture(fitmode: FITMODE) -> void:
+ fit_mode = fitmode
+ emit_changed()
+
+
+func _set_material(m: Material) -> void:
+ material = m
+ emit_changed()
+
+
+###########
+# GETTERS #
+###########
+func get_texture(idx: int) -> Texture2D:
+ return _get_element(idx, textures)
+
+
+func get_texture_corner_inner(idx: int) -> Texture2D:
+ return _get_element(idx, textures_corner_inner)
+
+
+func get_texture_corner_outer(idx: int) -> Texture2D:
+ return _get_element(idx, textures_corner_outer)
+
+
+func get_texture_taper_left(idx: int) -> Texture2D:
+ return _get_element(idx, textures_taper_left)
+
+
+func get_texture_taper_right(idx: int) -> Texture2D:
+ return _get_element(idx, textures_taper_right)
+
+
+func get_texture_taper_corner_left(idx: int) -> Texture2D:
+ return _get_element(idx, textures_taper_corner_left)
+
+
+func get_texture_taper_corner_right(idx: int) -> Texture2D:
+ return _get_element(idx, textures_taper_corner_right)
+
+
+#########
+# USAGE #
+#########
+
+## Returns main texture used to visually identify this edge material
+func get_icon_texture() -> Texture2D:
+ if not textures.is_empty():
+ return textures[0]
+ return null
+
+
+############
+# INTERNAL #
+############
+func _get_element(idx: int, a: Array) -> Variant:
+ if a.is_empty():
+ return null
+ return a[_adjust_idx(idx, a)]
+
+
+func _adjust_idx(idx: int, a: Array) -> int:
+ return idx % a.size()
diff --git a/addons/rmsmartshape/materials/edge_material_metadata.gd b/addons/rmsmartshape/materials/edge_material_metadata.gd
new file mode 100644
index 0000000..2573188
--- /dev/null
+++ b/addons/rmsmartshape/materials/edge_material_metadata.gd
@@ -0,0 +1,81 @@
+@tool
+extends Resource
+class_name SS2D_Material_Edge_Metadata
+
+## Represents the metadata for an edge material.
+##
+## Used by Shape Material.
+
+@export var edge_material: SS2D_Material_Edge = null : set = set_edge_material
+## What range of normals can this edge be used on.
+@export var normal_range := SS2D_NormalRange.new(0, 360) : set = set_normal_range
+## If edge should be welded to the edges surrounding it.
+@export var weld: bool = true : set = set_weld
+## Whether or not the edges should use the taper corner textures and not be welded, if they are
+## too sharp to be welded without significant distortion. NOTE this will not work properly
+## in curved shapes
+@export var taper_sharp_corners: bool = false : set = set_taper_sharp_corners
+## If this edge should be visible.
+@export var render: bool = true : set = set_render
+## z index for an edge.
+@export var z_index: int = 0 : set = set_z_index
+## z index for an edge.
+@export var z_as_relative: bool = true : set = set_z_as_relative
+## Distance from center.
+@export_range (-1.5, 1.5, 0.1) var offset: float = 0.0 : set = set_offset
+
+
+func _to_string() -> String:
+ return "%s | %s" % [str(edge_material), normal_range]
+
+
+func set_render(b: bool) -> void:
+ render = b
+ emit_changed()
+
+
+func set_edge_material(m: SS2D_Material_Edge) -> void:
+ if edge_material != null:
+ if edge_material.is_connected("changed", self._on_edge_changed):
+ edge_material.disconnect("changed", self._on_edge_changed)
+ edge_material = m
+ if edge_material != null:
+ edge_material.connect("changed", self._on_edge_changed)
+ emit_changed()
+
+
+func set_normal_range(nr: SS2D_NormalRange) -> void:
+ if nr == null:
+ return
+ if normal_range.is_connected("changed", self._on_edge_changed):
+ normal_range.disconnect("changed", self._on_edge_changed)
+ normal_range = nr
+ normal_range.connect("changed", self._on_edge_changed)
+ emit_changed()
+
+
+func set_weld(b: bool) -> void:
+ weld = b
+ emit_changed()
+
+func set_taper_sharp_corners(val: bool) -> void:
+ taper_sharp_corners = val
+ emit_changed()
+
+func set_z_index(z: int) -> void:
+ z_index = z
+ emit_changed()
+
+
+func set_z_as_relative(b: bool) -> void:
+ z_as_relative = b
+ emit_changed()
+
+
+func set_offset(f: float) -> void:
+ offset = f
+ emit_changed()
+
+
+func _on_edge_changed() -> void:
+ emit_changed()
diff --git a/addons/rmsmartshape/materials/shape_material.gd b/addons/rmsmartshape/materials/shape_material.gd
new file mode 100644
index 0000000..e77955c
--- /dev/null
+++ b/addons/rmsmartshape/materials/shape_material.gd
@@ -0,0 +1,139 @@
+@tool
+extends Resource
+class_name SS2D_Material_Shape
+
+## This material represents the set of edge materials used for a smart shape.
+##
+## Each edge represents a set of textures used to render an edge.
+
+## List of materials this shape can use.
+@export var _edge_meta_materials: Array[SS2D_Material_Edge_Metadata] = [] : set = set_edge_meta_materials
+@export var fill_textures: Array[Texture2D] = [] : set = set_fill_textures
+@export var fill_texture_z_index: int = -10 : set = set_fill_texture_z_index
+@export var fill_texture_show_behind_parent: bool = false : set = set_fill_texture_show_behind_parent
+
+## Scale the fill texture
+@export_range(0.1, 4, 0.01, "or_greater") var fill_texture_scale: float = 1.0 : set = set_fill_texture_scale
+
+## Whether the fill texture should start at the global 0/0 instead of the node's 0/0
+@export var fill_texture_absolute_position: bool = false : set = set_fill_texture_absolute_position
+
+## Whether the fill texture should ignore the node's rotation
+@export var fill_texture_absolute_rotation: bool = false : set = set_fill_texture_absolute_rotation
+
+## How many pixels the fill texture should be shifted in x and y direction
+@export var fill_texture_offset: Vector2 = Vector2.ZERO : set = set_fill_texture_offset
+
+## Added rotation of the texture in degrees
+@export_range(-180, 180, 0.1) var fill_texture_angle_offset: float = 0.0 : set = set_fill_texture_angle_offset
+
+@export var fill_mesh_offset: float = 0.0 : set = set_fill_mesh_offset
+@export var fill_mesh_material: Material = null : set = set_fill_mesh_material
+
+## How much to offset all edges
+@export_range (-1.5, 1.5, 0.1) var render_offset: float = 0.0 : set = set_render_offset
+
+
+func set_fill_mesh_material(m: Material) -> void:
+ fill_mesh_material = m
+ emit_changed()
+
+
+func set_fill_mesh_offset(f: float) -> void:
+ fill_mesh_offset = f
+ emit_changed()
+
+
+func set_render_offset(f: float) -> void:
+ render_offset = f
+ emit_changed()
+
+
+## Get all valid edge materials for this normal.
+func get_edge_meta_materials(normal: Vector2) -> Array[SS2D_Material_Edge_Metadata]:
+ var materials: Array[SS2D_Material_Edge_Metadata] = []
+ for e in _edge_meta_materials:
+ if e == null:
+ continue
+ if e.normal_range.is_in_range(normal):
+ materials.push_back(e)
+ return materials
+
+
+func get_all_edge_meta_materials() -> Array[SS2D_Material_Edge_Metadata]:
+ return _edge_meta_materials
+
+
+func get_all_edge_materials() -> Array[SS2D_Material_Edge]:
+ var materials: Array[SS2D_Material_Edge] = []
+ for meta in _edge_meta_materials:
+ if meta.edge_material != null:
+ materials.push_back(meta.edge_material)
+ return materials
+
+
+func add_edge_material(e: SS2D_Material_Edge_Metadata) -> void:
+ var new_array := _edge_meta_materials.duplicate()
+ new_array.push_back(e)
+ set_edge_meta_materials(new_array)
+
+
+func _on_edge_material_changed() -> void:
+ emit_changed()
+
+
+func set_fill_textures(a: Array[Texture2D]) -> void:
+ fill_textures = a
+ emit_changed()
+
+
+func set_fill_texture_z_index(i: int) -> void:
+ fill_texture_z_index = i
+ emit_changed()
+
+
+func set_fill_texture_show_behind_parent(value: bool) -> void:
+ fill_texture_show_behind_parent = value
+ emit_changed()
+
+
+func set_edge_meta_materials(a: Array[SS2D_Material_Edge_Metadata]) -> void:
+ for e in _edge_meta_materials:
+ if e == null:
+ continue
+ if not a.has(e):
+ e.disconnect("changed", self._on_edge_material_changed)
+
+ for e in a:
+ if e == null:
+ continue
+ if not e.is_connected("changed", self._on_edge_material_changed):
+ e.connect("changed", self._on_edge_material_changed)
+
+ _edge_meta_materials = a
+ emit_changed()
+
+
+func set_fill_texture_offset(value: Vector2) -> void:
+ fill_texture_offset = value
+ emit_changed()
+
+
+func set_fill_texture_scale(value:float) -> void:
+ fill_texture_scale = value
+ emit_changed()
+
+
+func set_fill_texture_absolute_rotation(value: bool) -> void:
+ fill_texture_absolute_rotation = value
+ emit_changed()
+
+
+func set_fill_texture_angle_offset(value: float) -> void:
+ fill_texture_angle_offset = value
+ emit_changed()
+
+
+func set_fill_texture_absolute_position(value: bool) -> void:
+ fill_texture_absolute_position = value
+ emit_changed()
\ No newline at end of file
diff --git a/addons/rmsmartshape/normal_range.gd b/addons/rmsmartshape/normal_range.gd
new file mode 100644
index 0000000..195d2f3
--- /dev/null
+++ b/addons/rmsmartshape/normal_range.gd
@@ -0,0 +1,99 @@
+@tool
+extends Resource
+class_name SS2D_NormalRange
+
+## This class will determine if the normal of a vector falls within the specifed angle ranges.
+##
+## - if begin and end are equal, any angle is considered to be within range [br]
+## - 360.0 and 0.0 degrees are considered equivilent [br]
+
+@export_range (0, 360, 1) var begin: float = 0.0 : set = set_begin
+@export_range (0, 360, 1) var distance: float = 0.0 : set = set_distance
+
+# This is a hack to support the custom editor, needed a property
+# to exist to lock the TextureProgressBar to. Makes it flow better
+# in the Inspector.
+@export var edgeRendering: Vector2
+
+
+func set_distance(f: float) -> void:
+ distance = f
+ emit_changed()
+
+
+func set_begin(f: float) -> void:
+ begin = f
+ emit_changed()
+
+
+func _to_string() -> String:
+ return "NormalRange: %s - %s" % [begin, begin + distance]
+
+
+static func get_angle_from_vector(vec: Vector2) -> float:
+ var normal: Vector2 = vec.normalized()
+ # With respect to the X-axis
+ # This is how Vector2.angle() is calculated, best to keep it consistent
+ var comparison_vector := Vector2(1, 0)
+
+ var ab: Vector2 = normal
+ var bc: Vector2 = comparison_vector
+ var dot_prod: float = ab.dot(bc)
+ var determinant: float = (ab.x * bc.y) - (ab.y * bc.x)
+ var angle: float = atan2(determinant, dot_prod)
+
+ # This angle has a range of 360 degrees
+ # Is between 180 and - 180
+ var deg: float = rad_to_deg(angle)
+
+ # Get range between 0.0 and 360.0
+ if deg < 0:
+ deg = 360.0 + deg
+ return deg
+
+
+# Get in range between 0.0 and 360.0.
+static func _get_positive_angle_deg(degrees: float) -> float:
+ while degrees < 0:
+ degrees += 360
+ return fmod(degrees, 360.0)
+
+# Get in range between -360.0 and 360.0
+static func _get_signed_angle_deg(degrees: float) -> float:
+ var new_degrees: float = degrees
+ while absf(new_degrees) > 360.0:
+ new_degrees += (360.0 * signf(degrees) * -1.0)
+ return new_degrees
+
+
+# Saving a scene with this resource requires a parameter-less init method
+func _init(_begin: float = 0.0, _distance: float = 0.0) -> void:
+ _begin = SS2D_NormalRange._get_signed_angle_deg(_begin)
+ _distance = SS2D_NormalRange._get_signed_angle_deg(_distance)
+
+ begin = _begin
+ distance = _distance
+
+
+func is_in_range(vec: Vector2) -> bool:
+ # A Distance of 0 or 360 is the entire circle
+ if distance == 0 or SS2D_NormalRange._get_positive_angle_deg(distance) == 360.0:
+ return true
+
+ var begin_positive: float = SS2D_NormalRange._get_positive_angle_deg(begin)
+ var end_positive: float = SS2D_NormalRange._get_positive_angle_deg(begin + distance)
+ # If positive, counter clockwise direction
+ # If negative, clockwise direction
+ var direction: float = signf(distance)
+ var angle: float = SS2D_NormalRange.get_angle_from_vector(vec)
+
+ # Swap begin and end if direction is negative
+ if direction == -1:
+ var t: float = begin_positive
+ begin_positive = end_positive
+ end_positive = t
+
+ if begin_positive < end_positive:
+ return ((angle >= begin_positive) and (angle <= end_positive))
+ else:
+ return ((angle >= begin_positive) or (angle <= end_positive))
diff --git a/addons/rmsmartshape/plugin.cfg b/addons/rmsmartshape/plugin.cfg
new file mode 100644
index 0000000..92088d1
--- /dev/null
+++ b/addons/rmsmartshape/plugin.cfg
@@ -0,0 +1,7 @@
+[plugin]
+
+name="SmartShape2D"
+description="Tool to design nicely textured 2D polygons"
+author="Ryan Lloyd, Robert Morse, Guy Unger, Serhii Snitsaruk, Marvin Ewald"
+version="3.2.0"
+script="plugin.gd"
diff --git a/addons/rmsmartshape/plugin.gd b/addons/rmsmartshape/plugin.gd
new file mode 100644
index 0000000..69208e1
--- /dev/null
+++ b/addons/rmsmartshape/plugin.gd
@@ -0,0 +1,1639 @@
+@tool
+extends EditorPlugin
+
+
+## Common Abbreviations
+## et = editor transform (viewport's canvas transform)
+##
+## - Snapping using the build in functionality isn't going to happen
+## - https://github.com/godotengine/godot/issues/11180
+## - https://godotengine.org/qa/18051/tool-script-in-3-0
+
+# Icons
+# TODO: Change to const and preload when this is resolved:
+# https://github.com/godotengine/godot/issues/17483
+var ICON_HANDLE: Texture2D = load("res://addons/rmsmartshape/assets/icon_editor_handle.svg")
+var ICON_HANDLE_SELECTED: Texture2D = load("res://addons/rmsmartshape/assets/icon_editor_handle_selected.svg")
+var ICON_HANDLE_BEZIER: Texture2D = load("res://addons/rmsmartshape/assets/icon_editor_handle_bezier.svg")
+var ICON_HANDLE_CONTROL: Texture2D = load("res://addons/rmsmartshape/assets/icon_editor_handle_control.svg")
+var ICON_FREEHAND_MODE: Texture2D = load("res://addons/rmsmartshape/assets/freehand.png")
+var ICON_CIRCLE_ERASE: Texture2D = load("res://addons/rmsmartshape/assets/icon_editor_snap.svg")
+var ICON_ADD_HANDLE: Texture2D = load("res://addons/rmsmartshape/assets/icon_editor_handle_add.svg")
+var ICON_CURVE_EDIT: Texture2D = load("res://addons/rmsmartshape/assets/icon_curve_edit.svg")
+var ICON_CURVE_CREATE: Texture2D = load("res://addons/rmsmartshape/assets/icon_curve_create.svg")
+var ICON_CURVE_DELETE: Texture2D = load("res://addons/rmsmartshape/assets/icon_curve_delete.svg")
+var ICON_PIVOT_POINT: Texture2D = load("res://addons/rmsmartshape/assets/icon_editor_position.svg")
+var ICON_CENTER_PIVOT: Texture2D = load("res://addons/rmsmartshape/assets/CenterView.svg")
+var ICON_COLLISION: Texture2D = load("res://addons/rmsmartshape/assets/icon_collision_polygon_2d.svg")
+var ICON_INTERP_LINEAR: Texture2D = load("res://addons/rmsmartshape/assets/InterpLinear.svg")
+var ICON_SNAP: Texture2D = load("res://addons/rmsmartshape/assets/icon_editor_snap.svg")
+var ICON_IMPORT_CLOSED: Texture2D = load("res://addons/rmsmartshape/assets/closed_shape.png")
+var ICON_IMPORT_OPEN: Texture2D = load("res://addons/rmsmartshape/assets/open_shape.png")
+
+const FUNC = preload("plugin_functionality.gd")
+const ActionAddCollisionNodes := preload("res://addons/rmsmartshape/actions/action_add_collision_nodes.gd")
+const ActionMoveVerticies := preload("res://addons/rmsmartshape/actions/action_move_verticies.gd")
+const ActionSetPivot := preload("res://addons/rmsmartshape/actions/action_set_pivot.gd")
+const ActionMoveControlPoints := preload("res://addons/rmsmartshape/actions/action_move_control_points.gd")
+const ActionDeleteControlPoint := preload("res://addons/rmsmartshape/actions/action_delete_control_point.gd")
+const ActionDeletePoint := preload("res://addons/rmsmartshape/actions/action_delete_point.gd")
+const ActionAddPoint := preload("res://addons/rmsmartshape/actions/action_add_point.gd")
+const ActionSplitCurve := preload("res://addons/rmsmartshape/actions/action_split_curve.gd")
+const ActionMakeShapeUnique := preload("res://addons/rmsmartshape/actions/action_make_shape_unique.gd")
+const ActionCutEdge := preload("res://addons/rmsmartshape/actions/action_cut_edge.gd")
+const ActionCloseShape := preload("res://addons/rmsmartshape/actions/action_close_shape.gd")
+const ActionSplitShape := preload("res://addons/rmsmartshape/actions/action_split_shape.gd")
+
+enum MODE { EDIT_VERT, EDIT_EDGE, CUT_EDGE, SET_PIVOT, CREATE_VERT, FREEHAND }
+
+enum SNAP_MENU { ID_USE_GRID_SNAP, ID_SNAP_RELATIVE, ID_CONFIGURE_SNAP }
+enum OPTIONS_MENU { ID_DEFER_MESH_UPDATES }
+
+enum ACTION_VERT {
+ NONE = 0,
+ MOVE_VERT = 1,
+ MOVE_CONTROL = 2,
+ MOVE_CONTROL_IN = 3,
+ MOVE_CONTROL_OUT = 4,
+ MOVE_WIDTH_HANDLE = 5
+}
+
+
+# Data related to an action being taken on points
+class ActionDataVert:
+ #Type of Action from the ACTION_VERT enum
+ var type: ACTION_VERT = ACTION_VERT.NONE
+ # The affected Verticies and their initial positions
+ var keys: PackedInt32Array
+ var starting_width: PackedFloat32Array
+ var starting_positions: PackedVector2Array = []
+ var starting_positions_control_in: PackedVector2Array = []
+ var starting_positions_control_out: PackedVector2Array = []
+
+ func _init(
+ _keys: PackedInt32Array,
+ positions: PackedVector2Array,
+ positions_in: PackedVector2Array,
+ positions_out: PackedVector2Array,
+ width: PackedFloat32Array,
+ t: ACTION_VERT
+ ) -> void:
+ type = t
+ keys = _keys
+ starting_positions = positions
+ starting_positions_control_in = positions_in
+ starting_positions_control_out = positions_out
+ starting_width = width
+
+ func are_verts_selected() -> bool:
+ return keys.size() > 0
+
+ func _to_string() -> String:
+ return "%s: %s = %s" % [type, keys, starting_positions]
+
+ func is_single_vert_selected() -> bool:
+ return keys.size() == 1
+
+ func current_point_key() -> int:
+ if not is_single_vert_selected():
+ return -1
+ return keys[0]
+
+ func current_point_index(s: SS2D_Shape) -> int:
+ if not is_single_vert_selected():
+ return -1
+ return s.get_point_index(keys[0])
+
+
+# PRELOADS
+var GUI_SNAP_POPUP := preload("scenes/SnapPopup.tscn")
+var GUI_POINT_INFO_PANEL := preload("scenes/GUI_InfoPanel.tscn")
+var GUI_EDGE_INFO_PANEL := preload("scenes/GUI_Edge_InfoPanel.tscn")
+var gui_point_info_panel: SS2D_PointInfoPanel = GUI_POINT_INFO_PANEL.instantiate()
+var gui_edge_info_panel: SS2D_EdgeInfoPanel = GUI_EDGE_INFO_PANEL.instantiate()
+var gui_snap_settings: SS2D_SnapPopup = GUI_SNAP_POPUP.instantiate()
+
+const GUI_POINT_INFO_PANEL_OFFSET := Vector2(256, 130)
+
+# This is the shape node being edited
+var shape: SS2D_Shape = null
+
+# Toolbar Stuff
+var tb_hb: HBoxContainer = null
+var tb_vert_create: Button = null
+var tb_vert_edit: Button = null
+var tb_edge_edit: Button = null
+var tb_edge_cut: Button = null
+var tb_pivot: Button = null
+var tb_center_pivot: Button = null
+var tb_collision: Button = null
+var tb_freehand: Button = null
+var tb_button_group: ButtonGroup = null
+
+var tb_snap: MenuButton = null
+# The PopupMenu that belongs to tb_snap
+var tb_snap_popup: PopupMenu = null
+
+var tb_options: MenuButton = null
+var tb_options_popup: PopupMenu = null
+
+var make_unique_dialog: AcceptDialog
+
+# Edge Stuff
+var on_edge: bool = false
+var edge_point: Vector2
+var edge_data: SS2D_Edge = null
+
+# Width Handle Stuff
+var on_width_handle: bool = false
+const WIDTH_HANDLE_OFFSET: float = 60.0
+var closest_key: int
+var closest_edge_keys := Vector2i(-1, -1)
+var width_scaling: float
+
+# Vertex paint mode stuff
+var last_point_position: Vector2
+var _mouse_lmb_pressed := false
+var _mouse_rmb_pressed := false
+var freehand_paint_size := 20.0
+var freehand_erase_size := 40.0
+
+# Track our mode of operation
+var current_mode: int = MODE.CREATE_VERT
+var previous_mode: int = MODE.CREATE_VERT
+
+var current_action := ActionDataVert.new([], [], [], [], [], ACTION_VERT.NONE)
+var cached_shape_global_transform: Transform2D
+
+# Action Move Variables
+var _mouse_motion_delta_starting_pos := Vector2(0, 0)
+
+# Defining the viewport to get the current zoom/scale
+var target_viewport: Viewport
+var current_zoom_level : float = 1.0
+
+# Track the property plugin
+var plugin: EditorInspectorPlugin
+
+var is_2d_screen_active := false
+
+var _defer_mesh_updates := false
+
+#######
+# GUI #
+#######
+
+
+func gui_display_snap_settings() -> void:
+ var pos := tb_snap.get_screen_position() + tb_snap.size
+ pos.x -= (gui_snap_settings.size.x + tb_snap.size.x) / 2.0
+ gui_snap_settings.position = pos
+ gui_snap_settings.popup()
+
+
+func _snapping_item_selected(id: int) -> void:
+ if id == SNAP_MENU.ID_USE_GRID_SNAP:
+ tb_snap_popup.set_item_checked(id, not tb_snap_popup.is_item_checked(id))
+ if id == SNAP_MENU.ID_SNAP_RELATIVE:
+ tb_snap_popup.set_item_checked(id, not tb_snap_popup.is_item_checked(id))
+ elif id == SNAP_MENU.ID_CONFIGURE_SNAP:
+ gui_display_snap_settings()
+
+
+func _options_item_selected(id: int) -> void:
+ if id == OPTIONS_MENU.ID_DEFER_MESH_UPDATES:
+ tb_options_popup.set_item_checked(id, not tb_options_popup.is_item_checked(id))
+ _defer_mesh_updates = tb_options_popup.is_item_checked(id)
+
+
+func _gui_build_toolbar() -> void:
+ tb_hb = HBoxContainer.new()
+ add_control_to_container(EditorPlugin.CONTAINER_CANVAS_EDITOR_MENU, tb_hb)
+
+ var sep := VSeparator.new()
+ tb_hb.add_child(sep)
+
+ tb_button_group = ButtonGroup.new()
+
+ tb_vert_create = create_tool_button(ICON_CURVE_CREATE, SS2D_Strings.EN_TOOLTIP_CREATE_VERT)
+ tb_vert_create.connect(&"pressed", self._enter_mode.bind(MODE.CREATE_VERT))
+ tb_vert_create.button_pressed = true
+
+ tb_vert_edit = create_tool_button(ICON_CURVE_EDIT, SS2D_Strings.EN_TOOLTIP_EDIT_VERT)
+ tb_vert_edit.connect(&"pressed", self._enter_mode.bind(MODE.EDIT_VERT))
+
+ tb_edge_edit = create_tool_button(ICON_INTERP_LINEAR, SS2D_Strings.EN_TOOLTIP_EDIT_EDGE)
+ tb_edge_edit.connect(&"pressed", self._enter_mode.bind(MODE.EDIT_EDGE))
+
+ var edge_cut_icon: Texture2D = EditorInterface.get_base_control().get_theme_icon(&"ActionCut", &"EditorIcons")
+ tb_edge_cut = create_tool_button(edge_cut_icon, SS2D_Strings.EN_TOOLTIP_CUT_EDGE)
+ tb_edge_cut.connect(&"pressed", _enter_mode.bind(MODE.CUT_EDGE))
+
+ tb_pivot = create_tool_button(ICON_PIVOT_POINT, SS2D_Strings.EN_TOOLTIP_PIVOT)
+ tb_pivot.connect(&"pressed", self._enter_mode.bind(MODE.SET_PIVOT))
+
+ tb_center_pivot = create_tool_button(ICON_CENTER_PIVOT, SS2D_Strings.EN_TOOLTIP_CENTER_PIVOT, false)
+ tb_center_pivot.connect(&"pressed", self._center_pivot)
+
+ tb_freehand = create_tool_button(ICON_FREEHAND_MODE, SS2D_Strings.EN_TOOLTIP_FREEHAND)
+ tb_freehand.connect(&"pressed", self._enter_mode.bind(MODE.FREEHAND))
+
+ tb_collision = create_tool_button(ICON_COLLISION, SS2D_Strings.EN_TOOLTIP_COLLISION, false)
+ tb_collision.connect(&"pressed", self._add_collision)
+
+ tb_snap = MenuButton.new()
+ tb_snap.tooltip_text = SS2D_Strings.EN_TOOLTIP_SNAP
+ tb_snap_popup = tb_snap.get_popup()
+ tb_snap.icon = ICON_SNAP
+ tb_snap_popup.add_check_item("Use Grid Snap", SNAP_MENU.ID_USE_GRID_SNAP)
+ tb_snap_popup.add_check_item("Snap Relative", SNAP_MENU.ID_SNAP_RELATIVE)
+ tb_snap_popup.add_separator()
+ tb_snap_popup.add_item("Configure Snap...", SNAP_MENU.ID_CONFIGURE_SNAP)
+ tb_snap_popup.hide_on_checkable_item_selection = false
+ tb_hb.add_child(tb_snap)
+ tb_snap_popup.connect("id_pressed", self._snapping_item_selected)
+
+ tb_options = MenuButton.new()
+ tb_options.tooltip_text = SS2D_Strings.EN_TOOLTIP_MORE_OPTIONS
+ tb_options.icon = EditorInterface.get_base_control().get_theme_icon("GuiTabMenuHl", "EditorIcons")
+ tb_options_popup = tb_options.get_popup()
+ tb_options_popup.add_check_item(SS2D_Strings.EN_OPTIONS_DEFER_MESH_UPDATES, OPTIONS_MENU.ID_DEFER_MESH_UPDATES)
+ tb_options_popup.hide_on_checkable_item_selection = false
+ tb_hb.add_child(tb_options)
+ tb_options_popup.connect("id_pressed", self._options_item_selected)
+
+
+func create_tool_button(icon: Texture2D, tooltip: String, toggle: bool = true) -> Button:
+ var tb := Button.new()
+ tb.toggle_mode = toggle
+ tb.button_group = tb_button_group
+ tb.theme_type_variation = "FlatButton"
+ tb.focus_mode = Control.FocusMode.FOCUS_NONE
+ tb.icon = icon
+ tb.tooltip_text = tooltip
+ tb_hb.add_child(tb)
+ return tb
+
+
+func _gui_update_vert_info_panel() -> void:
+ var idx: int = current_action.current_point_index(shape)
+ var key: int = current_action.current_point_key()
+ if not is_key_valid(key):
+ gui_point_info_panel.visible = false
+ return
+ gui_point_info_panel.visible = true
+ # Shrink panel
+ gui_point_info_panel.size = Vector2(1, 1)
+
+ var properties := shape.get_point_properties(key)
+ gui_point_info_panel.set_idx(idx)
+ gui_point_info_panel.set_texture_idx(properties.texture_idx)
+ gui_point_info_panel.set_width(properties.width)
+ gui_point_info_panel.set_flip(properties.flip)
+
+
+func _load_config() -> void:
+ var conf := ConfigFile.new()
+ conf.load(EditorInterface.get_editor_paths().get_project_settings_dir().path_join("ss2d.cfg"))
+ _defer_mesh_updates = conf.get_value("options", "defer_mesh_updates", false)
+ tb_options_popup.set_item_checked(OPTIONS_MENU.ID_DEFER_MESH_UPDATES, _defer_mesh_updates)
+ tb_snap_popup.set_item_checked(SNAP_MENU.ID_USE_GRID_SNAP, conf.get_value("options", "use_grid_snap", false))
+ tb_snap_popup.set_item_checked(SNAP_MENU.ID_SNAP_RELATIVE, conf.get_value("options", "snap_relative", false))
+
+
+func _save_config() -> void:
+ var conf := ConfigFile.new()
+ conf.set_value("options", "defer_mesh_updates", _defer_mesh_updates)
+ conf.set_value("options", "use_grid_snap", tb_snap_popup.is_item_checked(SNAP_MENU.ID_USE_GRID_SNAP))
+ conf.set_value("options", "snap_relative", tb_snap_popup.is_item_checked(SNAP_MENU.ID_SNAP_RELATIVE))
+ conf.save(EditorInterface.get_editor_paths().get_project_settings_dir().path_join("ss2d.cfg"))
+
+
+func _process(_delta: float) -> void:
+ if current_mode == MODE.FREEHAND:
+ current_zoom_level = get_canvas_scale()
+
+
+func get_canvas_scale() -> float:
+ get_current_viewport()
+ if target_viewport:
+ return target_viewport.global_canvas_transform.x.x
+ else:
+ return 1.0
+
+
+func get_current_viewport() -> void:
+ if !get_tree().get_edited_scene_root():
+ return
+ var editor_viewport: Node = get_tree().get_edited_scene_root().get_parent()
+
+ if editor_viewport is SubViewport:
+ target_viewport = editor_viewport
+ elif editor_viewport is SubViewportContainer:
+ target_viewport = get_tree().get_edited_scene_root()
+ else:
+ target_viewport = editor_viewport.get_parent()
+
+
+func _gui_update_edge_info_panel() -> void:
+ # Don't update if already visible
+ if gui_edge_info_panel.visible:
+ return
+ 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: 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)
+ if override != null:
+ gui_edge_info_panel.set_material_override(true)
+ gui_edge_info_panel.load_values_from_meta_material(override)
+ else:
+ gui_edge_info_panel.set_material_override(false)
+
+ # Shrink panel to minimum size
+ gui_edge_info_panel.size = Vector2(1, 1)
+
+
+func _gui_update_info_panels() -> void:
+ if not is_2d_screen_active:
+ _gui_hide_info_panels()
+ return
+ match current_mode:
+ MODE.EDIT_VERT:
+ _gui_update_vert_info_panel()
+ gui_edge_info_panel.visible = false
+ MODE.EDIT_EDGE:
+ _gui_update_edge_info_panel()
+ gui_point_info_panel.visible = false
+ _:
+ gui_point_info_panel.visible = false
+ gui_edge_info_panel.visible = false
+
+
+func _gui_hide_info_panels() -> void:
+ gui_edge_info_panel.visible = false
+ gui_point_info_panel.visible = false
+
+#########
+# GODOT #
+#########
+
+
+# Called when saving
+# https://docs.godotengine.org/en/3.2/classes/class_editorplugin.html?highlight=switch%20scene%20tab
+func _apply_changes() -> void:
+ gui_point_info_panel.visible = false
+ gui_edge_info_panel.visible = false
+
+
+func _init() -> void:
+ pass
+
+
+func _ready() -> void:
+ # Support the undo-redo actions
+ _gui_build_toolbar()
+ _load_config()
+ add_child(gui_point_info_panel)
+ gui_point_info_panel.visible = false
+ add_child(gui_edge_info_panel)
+ gui_edge_info_panel.visible = false
+ gui_edge_info_panel.connect("material_override_toggled", self._on_edge_material_override_toggled)
+ gui_edge_info_panel.connect("render_toggled", self._on_edge_material_override_render_toggled)
+ gui_edge_info_panel.connect("weld_toggled", self._on_edge_material_override_weld_toggled)
+ gui_edge_info_panel.connect("z_index_changed", self._on_edge_material_override_z_index_changed)
+ gui_edge_info_panel.connect("edge_material_changed", self._on_edge_material_changed)
+ add_child(gui_snap_settings)
+ gui_snap_settings.hide()
+
+ make_unique_dialog = AcceptDialog.new()
+ make_unique_dialog.title = "Make Shape Unique"
+ make_unique_dialog.get_label().text = "Make shape point geometry unique (not materials). Proceed?"
+ make_unique_dialog.get_ok_button().text = "Proceed"
+ make_unique_dialog.add_cancel_button("Cancel")
+ make_unique_dialog.theme = EditorInterface.get_base_control().theme
+ make_unique_dialog.connect("confirmed", self._shape_make_unique)
+ add_child(make_unique_dialog)
+
+ connect("main_screen_changed", self._on_main_screen_changed)
+
+
+func _enter_tree() -> void:
+ @warning_ignore("unsafe_method_access")
+ plugin = load("res://addons/rmsmartshape/editors/normal_range_inspector_plugin.gd").new()
+ if plugin != null:
+ add_inspector_plugin(plugin)
+
+ @warning_ignore("unsafe_method_access")
+ var action_plugin: EditorInspectorPlugin = load("res://addons/rmsmartshape/editors/action_property_inspector_plugin.gd").new()
+ if action_plugin != null:
+ add_inspector_plugin(action_plugin)
+
+
+func _exit_tree() -> void:
+ if (plugin != null):
+ remove_inspector_plugin(plugin)
+
+ _save_config()
+
+ gui_point_info_panel.visible = false
+ gui_edge_info_panel.visible = false
+ remove_control_from_container(EditorPlugin.CONTAINER_CANVAS_EDITOR_MENU, tb_hb)
+ tb_hb.queue_free()
+
+
+func _forward_canvas_gui_input(event: InputEvent) -> bool:
+ if not is_shape_valid():
+ return false
+
+ # Force update if global transforma has been changed
+ if cached_shape_global_transform != shape.get_global_transform():
+ shape.set_as_dirty()
+ cached_shape_global_transform = shape.get_global_transform()
+
+ var et: Transform2D = get_et()
+ var grab_threshold: float = EditorInterface.get_editor_settings().get(
+ "editors/polygon_editor/point_grab_radius"
+ )
+
+ var key_return_value := false
+ if event is InputEventKey:
+ key_return_value = _input_handle_keyboard_event(event)
+
+ var mb_return_value := false
+ if event is InputEventMouseButton:
+ mb_return_value = _input_handle_mouse_button_event(event, et, grab_threshold)
+
+ var mm_return_value := false
+ if event is InputEventMouseMotion:
+ mb_return_value = _input_handle_mouse_motion_event(event, et, grab_threshold)
+
+ var return_value := key_return_value == true or mb_return_value == true or mm_return_value == true
+ _gui_update_info_panels()
+ return return_value
+
+
+func _handles(object: Object) -> bool:
+ var hideToolbar: bool = true
+
+ update_overlays()
+ gui_point_info_panel.visible = false
+ gui_edge_info_panel.visible = false
+
+ var selection: EditorSelection = EditorInterface.get_selection()
+ if selection != null:
+ if selection.get_selected_nodes().size() == 1:
+ if selection.get_selected_nodes()[0] is SS2D_Shape:
+ hideToolbar = false
+
+ if hideToolbar == true:
+ tb_hb.hide()
+
+ if object is Resource:
+ return false
+
+ return object is SS2D_Shape
+
+
+func _edit(object: Object) -> void:
+ on_edge = false
+ deselect_verts()
+ if is_shape_valid():
+ disconnect_shape(shape)
+
+ shape = object
+
+ if not is_shape_valid():
+ gui_point_info_panel.visible = false
+ gui_edge_info_panel.visible = false
+ shape = null
+ else:
+ connect_shape(shape)
+
+ if shape.get_point_array().get_point_count() == 0:
+ _enter_mode(MODE.CREATE_VERT)
+ elif current_mode == MODE.CREATE_VERT:
+ _enter_mode(MODE.EDIT_VERT)
+
+ update_overlays()
+
+
+func _make_visible(visible: bool) -> void:
+ if visible:
+ tb_hb.show()
+ else:
+ tb_hb.hide()
+
+
+
+func _on_main_screen_changed(screen_name: String) -> void:
+ is_2d_screen_active = screen_name == "2D"
+ if not is_2d_screen_active:
+ _gui_hide_info_panels()
+
+
+############
+# SNAPPING #
+############
+func use_global_snap() -> bool:
+ return not tb_snap_popup.is_item_checked(SNAP_MENU.ID_SNAP_RELATIVE)
+
+
+func use_snap() -> bool:
+ return tb_snap_popup.is_item_checked(SNAP_MENU.ID_USE_GRID_SNAP)
+
+
+func get_snap_offset() -> Vector2:
+ return gui_snap_settings.get_snap_offset()
+
+
+func get_snap_step() -> Vector2:
+ return gui_snap_settings.get_snap_step()
+
+
+func snap(v: Vector2, force: bool = false) -> Vector2:
+ if not use_snap() and not force:
+ return v
+ var step: Vector2 = get_snap_step()
+ var offset: Vector2 = get_snap_offset()
+ var t := Transform2D.IDENTITY
+ if use_global_snap():
+ t = shape.get_global_transform()
+ return SS2D_PluginFunctionality.snap_position(v, offset, step, t)
+
+
+##########
+# PLUGIN #
+##########
+
+func disconnect_shape(s: SS2D_Shape) -> void:
+ if s.is_connected("make_unique_pressed", self._on_shape_make_unique):
+ s.disconnect("make_unique_pressed", self._on_shape_make_unique)
+
+
+func connect_shape(s: SS2D_Shape) -> void:
+ if not s.is_connected("make_unique_pressed", self._on_shape_make_unique):
+ s.connect("make_unique_pressed", self._on_shape_make_unique)
+
+
+func get_material_override_from_indicies() -> SS2D_Material_Edge_Metadata:
+ 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)
+
+
+func _on_edge_material_override_render_toggled(enabled: bool) -> void:
+ var override := get_material_override_from_indicies()
+ if override != null:
+ override.render = enabled
+
+
+func _on_edge_material_override_weld_toggled(enabled: bool) -> void:
+ var override := get_material_override_from_indicies()
+ if override != null:
+ override.weld = enabled
+
+
+func _on_edge_material_override_z_index_changed(z: int) -> void:
+ var override := get_material_override_from_indicies()
+ if override != null:
+ override.z_index = z
+
+
+func _on_edge_material_changed(m: SS2D_Material_Edge) -> void:
+ var override := get_material_override_from_indicies()
+ if override != null:
+ override.edge_material = m
+
+
+func _on_edge_material_override_toggled(enabled: bool) -> void:
+ var indices := gui_edge_info_panel.indicies
+
+ if SS2D_IndexTuple.has(indices, -1):
+ return
+
+ 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 = shape.get_point_array().get_material_override(keys)
+
+ if enabled:
+ if override == null:
+ override = SS2D_Material_Edge_Metadata.new()
+ override.edge_material = null
+ shape.get_point_array().set_material_override(keys, override)
+
+ # Load override data into the info panel
+ gui_edge_info_panel.load_values_from_meta_material(override)
+ else:
+ if override != null:
+ shape.get_point_array().remove_material_override(keys)
+
+
+func is_shape_valid() -> bool:
+ if shape == null:
+ return false
+ if not is_instance_valid(shape):
+ return false
+ if not shape.is_inside_tree():
+ return false
+ return true
+
+
+func _on_shape_make_unique(_shape: SS2D_Shape) -> void:
+ make_unique_dialog.popup_centered()
+
+
+func _shape_make_unique() -> void:
+ perform_action(ActionMakeShapeUnique.new(shape))
+
+
+func get_et() -> Transform2D:
+ return EditorInterface.get_edited_scene_root().get_viewport().global_canvas_transform
+
+
+func is_key_valid(key: int) -> bool:
+ if not is_shape_valid():
+ return false
+ return shape.has_point(key)
+
+
+func _enter_mode(mode: int) -> void:
+ if current_mode == mode:
+ return
+
+ for tb: Button in [tb_vert_edit, tb_edge_edit, tb_pivot, tb_center_pivot, tb_vert_create, tb_freehand]:
+ tb.button_pressed = false
+
+ previous_mode = current_mode
+ current_mode = mode
+ match mode:
+ MODE.CREATE_VERT:
+ tb_vert_create.button_pressed = true
+ MODE.EDIT_VERT:
+ tb_vert_edit.button_pressed = true
+ MODE.EDIT_EDGE:
+ tb_edge_edit.button_pressed = true
+ MODE.CUT_EDGE:
+ tb_edge_cut.button_pressed = true
+ MODE.SET_PIVOT:
+ tb_pivot.button_pressed = true
+ MODE.FREEHAND:
+ tb_freehand.button_pressed = true
+ _:
+ tb_vert_edit.button_pressed = true
+
+ update_overlays()
+
+
+func _add_collision() -> void:
+ call_deferred("_add_deferred_collision")
+
+
+func _add_deferred_collision() -> void:
+ if shape and not shape.get_parent() is PhysicsBody2D:
+ perform_action(ActionAddCollisionNodes.new(shape))
+
+
+func _center_pivot() -> void:
+ if shape and shape.is_shape_closed():
+ # Calculate centroid
+ var points: PackedVector2Array = shape.get_tessellated_points()
+ var point_count: int = points.size()
+ var total_area: float = 0.0
+ var center: Vector2 = Vector2.ZERO
+ for i in range(point_count):
+ var pt1: Vector2 = points[i]
+ var pt2: Vector2
+ if i == point_count - 1:
+ pt2 = points[0]
+ else:
+ pt2 = points[i + 1]
+
+ var triangle_area: float = pt1.cross(pt2)
+ total_area += triangle_area
+ center += (pt1 + pt2) * triangle_area
+
+ if total_area != 0.0:
+ center /= 3 * total_area
+
+ perform_action(ActionSetPivot.new(shape, shape.to_global(center)))
+
+#############
+# RENDERING #
+#############
+
+func _forward_canvas_draw_over_viewport(overlay: Control) -> void:
+ # Something might force a draw which we had no control over,
+ # in this case do some updating to be sure
+ if not is_shape_valid() or not is_inside_tree():
+ return
+
+ match current_mode:
+ MODE.CREATE_VERT:
+ draw_mode_edit_vert(overlay)
+ if Input.is_key_pressed(KEY_ALT) and Input.is_key_pressed(KEY_SHIFT):
+ draw_new_shape_preview(overlay)
+ elif Input.is_key_pressed(KEY_ALT):
+ draw_new_point_close_preview(overlay)
+ else:
+ draw_new_point_preview(overlay)
+ MODE.EDIT_VERT:
+ draw_mode_edit_vert(overlay)
+ if Input.is_key_pressed(KEY_ALT):
+ if Input.is_key_pressed(KEY_SHIFT):
+ draw_new_shape_preview(overlay)
+ elif not on_edge:
+ draw_new_point_close_preview(overlay)
+ MODE.EDIT_EDGE:
+ draw_mode_edit_edge(overlay, Color.WHITE, Color.YELLOW)
+ MODE.CUT_EDGE:
+ draw_mode_cut_edge(overlay)
+ MODE.FREEHAND:
+ if not _mouse_lmb_pressed:
+ draw_new_point_close_preview(overlay)
+ draw_freehand_circle(overlay)
+ draw_mode_edit_vert(overlay, false)
+
+
+func draw_freehand_circle(overlay: Control) -> void:
+ var mouse: Vector2 = overlay.get_local_mouse_position()
+ var size: float = freehand_paint_size
+ var color := Color.WHITE
+ if Input.is_key_pressed(KEY_CTRL):
+ color = Color.RED
+ size = freehand_erase_size
+ color.a = 0.5
+ overlay.draw_arc(mouse, size * 2 * current_zoom_level, 0, TAU, 64, color, 1, true)
+ color.a = 0.05
+ overlay.draw_circle(mouse, size * 2 * current_zoom_level, color)
+
+
+func draw_mode_edit_edge(overlay: Control, color_normal: Color, color_highlight: Color) -> void:
+ var t: Transform2D = get_et() * shape.get_global_transform()
+ var verts: PackedVector2Array = shape.get_vertices()
+
+ draw_shape_outline(overlay, t, verts, color_normal)
+ draw_vert_handles(overlay, t, verts, false)
+
+ if current_action.type == ACTION_VERT.MOVE_VERT:
+ 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 := _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)
+
+
+func draw_mode_cut_edge(overlay: Control) -> void:
+ draw_mode_edit_edge(overlay, Color(1.0, 0.25, 0.25, 0.8), Color(1.0, 0.75, 0.75, 1.0))
+
+ if on_edge:
+ # 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 := _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()
+ var num_crosses := remap(length, 0.0, 2000.0, 0.0, 10.0)
+ @warning_ignore("narrowing_conversion")
+ num_crosses = snappedi(num_crosses, 2.0) + 1
+ var fraction := 1.0 / (num_crosses + 1)
+ for i in num_crosses:
+ var pos: Vector2 = from + dir * length * fraction * (i + 1)
+ overlay.draw_line(Vector2(8.0, 8.0).rotated(angle) + pos,
+ Vector2(-8.0, -8.0).rotated(angle) + pos, Color.RED, 3.0, true)
+ overlay.draw_line(Vector2(-8.0, 8.0).rotated(angle) + pos,
+ Vector2(8.0, -8.0).rotated(angle) + pos, Color.RED, 3.0, true)
+
+
+func draw_mode_edit_vert(overlay: Control, show_vert_handles: bool = true) -> void:
+ var t: Transform2D = get_et() * shape.get_global_transform()
+ var verts: PackedVector2Array = shape.get_vertices()
+ var points: PackedVector2Array = shape.get_tessellated_points()
+ draw_shape_outline(overlay, t, points, shape.modulate)
+ if show_vert_handles:
+ draw_vert_handles(overlay, t, verts, true)
+ if on_edge:
+ overlay.draw_texture(ICON_ADD_HANDLE, edge_point - ICON_ADD_HANDLE.get_size() * 0.5)
+
+ # Draw Highlighted Handle
+ if current_action.is_single_vert_selected():
+ var tex: Texture2D = ICON_HANDLE_SELECTED
+ overlay.draw_texture(
+ tex, t * verts[current_action.current_point_index(shape)] - tex.get_size() * 0.5
+ )
+
+
+func draw_shape_outline(
+ overlay: Control, t: Transform2D, points: PackedVector2Array, color: Color, width: float = 2.0
+) -> void:
+ if points.size() >= 2:
+ overlay.draw_polyline(t * points, Color.BLACK, width * 1.5, true)
+ overlay.draw_polyline(t * points, color, width, true)
+
+
+func draw_vert_handles(
+ overlay: Control, t: Transform2D, verts: PackedVector2Array, control_points: bool
+) -> void:
+ var transformed_verts := t * verts
+ for i in verts.size():
+ # Draw Vert handles
+ var hp: Vector2 = transformed_verts[i]
+ var icon: Texture2D = ICON_HANDLE_BEZIER if (Input.is_key_pressed(KEY_SHIFT) and not current_mode == MODE.FREEHAND) else ICON_HANDLE
+ overlay.draw_texture(icon, hp - icon.get_size() * 0.5)
+
+ # Draw Width handle
+ var offset: float = WIDTH_HANDLE_OFFSET
+ var width_handle_key: int = closest_key
+ if (
+ Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)
+ and current_action.type == ACTION_VERT.MOVE_WIDTH_HANDLE
+ ):
+ offset *= width_scaling
+ width_handle_key = current_action.keys[0]
+
+ var point_index: int = shape.get_point_index(width_handle_key)
+ if point_index == -1:
+ return
+
+ var width_handle_normal: Vector2 = _get_vert_normal(t, verts, point_index)
+ var vertex_position: Vector2 = t * shape.get_point_position(width_handle_key)
+ var icon_position: Vector2 = vertex_position + width_handle_normal * offset
+ var size: Vector2 = Vector2.ONE * 10.0
+ var width_handle_color := Color("f53351")
+ overlay.draw_line(vertex_position, icon_position, width_handle_color, 1.0)
+ overlay.draw_set_transform(icon_position, width_handle_normal.angle(), Vector2.ONE)
+ overlay.draw_rect(Rect2(-size / 2.0, size), width_handle_color, true)
+ overlay.draw_set_transform(Vector2.ZERO, 0, Vector2.ONE)
+
+ # Draw Control point handles
+ if control_points:
+ for i in verts.size():
+ var key: int = shape.get_point_key_at_index(i)
+ var hp: Vector2 = transformed_verts[i]
+
+ # Drawing the point-out for the last point makes no sense, as there's no point ahead of it
+ if i < verts.size() - 1:
+ var pointout: Vector2 = t * (verts[i] + shape.get_point_out(key))
+ if hp != pointout:
+ _draw_control_point_line(overlay, hp, pointout, ICON_HANDLE_CONTROL)
+ # Drawing the point-in for point 0 makes no sense, as there's no point behind it
+ if i > 0:
+ var pointin: Vector2 = t * (verts[i] + shape.get_point_in(key))
+ if hp != pointin:
+ _draw_control_point_line(overlay, hp, pointin, ICON_HANDLE_CONTROL)
+
+
+func _draw_control_point_line(c: Control, vert: Vector2, cp: Vector2, tex: Texture2D) -> void:
+ # Draw the line with a dark and light color to be visible on all backgrounds
+ var color_dark := Color(0, 0, 0, 0.3)
+ var color_light := Color(1, 1, 1, .5)
+ var width := 2.0
+ var normal := (cp - vert).normalized()
+ c.draw_line(vert + normal * 4 + Vector2.DOWN, cp + Vector2.DOWN, color_dark, width)
+ c.draw_line(vert + normal * 4, cp, color_light, width)
+ c.draw_texture(tex, cp - tex.get_size() * 0.5)
+
+
+func draw_new_point_preview(overlay: Control) -> void:
+ # Draw lines to where a new point will be added
+ var verts: PackedVector2Array = shape.get_vertices()
+ var t: Transform2D = get_et() * shape.get_global_transform()
+ var color := Color(1, 1, 1, .5)
+ var width := 2.0
+ var mouse: Vector2 = overlay.get_local_mouse_position()
+
+ if verts.size() > 0:
+ var a: Vector2
+ if shape.is_shape_closed() and verts.size() > 1:
+ a = t * verts[verts.size() - 2]
+ overlay.draw_line(mouse, t * verts[0], color,width * .5)
+ else:
+ a = t * verts[verts.size() - 1]
+ overlay.draw_line(mouse, a, color, width)
+
+ overlay.draw_texture(ICON_ADD_HANDLE, mouse - ICON_ADD_HANDLE.get_size() * 0.5)
+
+
+func draw_new_point_close_preview(overlay: Control) -> void:
+ # Draw lines to where a new point will be added
+ var t: Transform2D = get_et() * shape.get_global_transform()
+ var color := Color(1, 1, 1, .5)
+ var width := 2.0
+
+ var mouse: Vector2 = overlay.get_local_mouse_position()
+ var a: Vector2 = t * shape.get_point_position(closest_edge_keys[0])
+ var b: Vector2 = t * shape.get_point_position(closest_edge_keys[1])
+ overlay.draw_line(mouse, a, color, width)
+ color.a = 0.1
+ overlay.draw_line(mouse, b, color, width)
+ overlay.draw_texture(ICON_ADD_HANDLE, mouse - ICON_ADD_HANDLE.get_size() * 0.5)
+
+
+func draw_new_shape_preview(overlay: Control) -> void:
+ # Draw a plus where a new shape will be added
+ var mouse: Vector2 = overlay.get_local_mouse_position()
+ overlay.draw_texture(ICON_ADD_HANDLE, mouse - ICON_ADD_HANDLE.get_size() * 0.5)
+
+
+##########
+# PLUGIN #
+##########
+func deselect_verts() -> void:
+ current_action = ActionDataVert.new([], [], [], [], [], ACTION_VERT.NONE)
+
+
+func select_verticies(keys: PackedInt32Array, action: ACTION_VERT) -> ActionDataVert:
+ var from_positions := PackedVector2Array()
+ var from_positions_c_in := PackedVector2Array()
+ var from_positions_c_out := PackedVector2Array()
+ var from_widths := PackedFloat32Array()
+ for key in keys:
+ from_positions.push_back(shape.get_point_position(key))
+ from_positions_c_in.push_back(shape.get_point_in(key))
+ from_positions_c_out.push_back(shape.get_point_out(key))
+ from_widths.push_back(shape.get_point_width(key))
+ return ActionDataVert.new(
+ keys, from_positions, from_positions_c_in, from_positions_c_out, from_widths, action
+ )
+
+
+func select_vertices_to_move(keys: PackedInt32Array, _mouse_starting_pos_viewport: Vector2) -> void:
+ _mouse_motion_delta_starting_pos = _mouse_starting_pos_viewport
+ current_action = select_verticies(keys, ACTION_VERT.MOVE_VERT)
+
+
+func select_control_points_to_move(
+ keys: PackedInt32Array, _mouse_starting_pos_viewport: Vector2, action: ACTION_VERT = ACTION_VERT.MOVE_CONTROL
+) -> void:
+ current_action = select_verticies(keys, action)
+ _mouse_motion_delta_starting_pos = _mouse_starting_pos_viewport
+
+
+func select_width_handle_to_move(keys: PackedInt32Array, _mouse_starting_pos_viewport: Vector2) -> void:
+ _mouse_motion_delta_starting_pos = _mouse_starting_pos_viewport
+ current_action = select_verticies(keys, ACTION_VERT.MOVE_WIDTH_HANDLE)
+
+
+func perform_action(action: SS2D_Action) -> void:
+ var undo := get_undo_redo()
+ undo.create_action(action.get_name(), UndoRedo.MERGE_DISABLE, shape.get_point_array())
+ undo.add_do_method(action, "do")
+ undo.add_do_method(self, "update_overlays")
+ undo.add_undo_method(action, "undo")
+ undo.add_undo_method(self, "update_overlays")
+ undo.commit_action()
+
+
+#########
+# INPUT #
+#########
+func _input_handle_right_click_press(mb_position: Vector2, grab_threshold: float) -> bool:
+ if not shape.can_edit:
+ return false
+ if current_mode == MODE.EDIT_VERT or current_mode == MODE.CREATE_VERT:
+ # Mouse over a single vertex?
+ if current_action.is_single_vert_selected():
+ perform_action(ActionDeletePoint.new(shape, current_action.keys[0]))
+ deselect_verts()
+ return true
+ else:
+ # Mouse over a control point?
+ var et: Transform2D = get_et()
+ var points_in: Array = FUNC.get_intersecting_control_point_in(
+ shape, et, mb_position, grab_threshold
+ )
+ var points_out: Array = FUNC.get_intersecting_control_point_out(
+ shape, et, mb_position, grab_threshold
+ )
+ if not points_in.is_empty():
+ perform_action(ActionDeleteControlPoint.new(shape, points_in[0],
+ ActionDeleteControlPoint.PointType.POINT_IN))
+ return true
+ elif not points_out.is_empty():
+ perform_action(ActionDeleteControlPoint.new(shape, points_out[0],
+ ActionDeleteControlPoint.PointType.POINT_OUT))
+ return true
+ elif current_mode == MODE.EDIT_EDGE:
+ if on_edge:
+ gui_edge_info_panel.visible = not gui_edge_info_panel.visible
+ gui_edge_info_panel.position = get_window().get_mouse_position()
+ return true
+ return false
+
+
+func _input_handle_left_click(
+ mb: InputEventMouseButton,
+ vp_m_pos: Vector2,
+ t: Transform2D,
+ et: Transform2D,
+ grab_threshold: float
+) -> bool:
+ # Set Pivot?
+ if current_mode == MODE.SET_PIVOT:
+ var local_position: Vector2 = et.affine_inverse() * mb.position
+ if use_snap():
+ local_position = snap(local_position)
+ perform_action(ActionSetPivot.new(shape, local_position))
+ return true
+
+ if current_mode == MODE.EDIT_VERT or current_mode == MODE.CREATE_VERT:
+ gui_edge_info_panel.visible = false
+ var can_add_point: bool = Input.is_key_pressed(KEY_ALT) or current_mode == MODE.CREATE_VERT
+ var is_first_selected: bool = current_action.is_single_vert_selected() and current_action.current_point_key() == shape.get_point_key_at_index(0)
+
+ if _defer_mesh_updates:
+ shape.begin_update()
+
+ # Close the shape if the first point is clicked
+ if can_add_point and is_first_selected and shape.can_close():
+ var close_action := ActionCloseShape.new(shape)
+ perform_action(close_action)
+ if Input.is_key_pressed(KEY_SHIFT):
+ select_control_points_to_move([close_action.get_key()], vp_m_pos)
+ else:
+ select_vertices_to_move([close_action.get_key()], vp_m_pos)
+ return true
+
+ # Any nearby control points to move?
+ if not Input.is_key_pressed(KEY_ALT):
+ if _input_move_control_points(mb, vp_m_pos, grab_threshold):
+ return true
+
+ # Highlighting a vert to move or add control points to
+ if current_action.is_single_vert_selected():
+ if on_width_handle:
+ select_width_handle_to_move([current_action.current_point_key()], vp_m_pos)
+ elif Input.is_key_pressed(KEY_SHIFT):
+ select_control_points_to_move([current_action.current_point_key()], vp_m_pos)
+ return true
+ else:
+ select_vertices_to_move([current_action.current_point_key()], vp_m_pos)
+ return true
+
+ # Split the Edge?
+ if _input_split_edge(mb, vp_m_pos, t):
+ return true
+
+ if not on_edge and can_add_point:
+ # Create new point
+ var local_position: Vector2 = t.affine_inverse() * mb.position
+ if use_snap():
+ local_position = snap(local_position)
+
+ var idx: int = -1
+ if Input.is_key_pressed(KEY_SHIFT) and Input.is_key_pressed(KEY_ALT):
+ # Copy shape with a new single point
+ var copy: SS2D_Shape = copy_shape(shape)
+ copy.set_point_array(SS2D_Point_Array.new())
+ _enter_mode(MODE.CREATE_VERT)
+ var selection := EditorInterface.get_selection()
+ selection.clear()
+ selection.add_node(copy)
+ shape = copy
+ elif Input.is_key_pressed(KEY_ALT):
+ # Add point between start and end points of the closest edge
+ idx = shape.get_point_index(closest_edge_keys[1])
+ var add_point := ActionAddPoint.new(shape, local_position, idx, not _defer_mesh_updates)
+ perform_action(add_point)
+ if Input.is_key_pressed(KEY_SHIFT) and not Input.is_key_pressed(KEY_ALT):
+ select_control_points_to_move([add_point.get_key()], vp_m_pos)
+ else:
+ select_vertices_to_move([add_point.get_key()], vp_m_pos)
+ return true
+ elif current_mode == MODE.EDIT_EDGE:
+ if gui_edge_info_panel.visible:
+ gui_edge_info_panel.visible = false
+ return true
+ if on_edge:
+ # Grab Edge (2 points)
+ var offset: float = shape.get_closest_offset_straight_edge(
+ t.affine_inverse() * edge_point
+ )
+ 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 := _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:
+ return true
+ return false
+
+
+func _input_handle_mouse_wheel(btn: int) -> bool:
+ if current_mode == MODE.FREEHAND:
+ if Input.is_key_pressed(KEY_CTRL) and Input.is_key_pressed(KEY_SHIFT):
+ var step_multiplier := 1.2 if btn == MOUSE_BUTTON_WHEEL_UP else 0.8
+ freehand_erase_size = roundf(clampf(freehand_erase_size * step_multiplier, 5, 400))
+ update_overlays()
+ return true
+ elif Input.is_key_pressed(KEY_SHIFT):
+ var step_multiplier := 1.2 if btn == MOUSE_BUTTON_WHEEL_UP else 0.8
+ freehand_paint_size = roundf(clampf(freehand_paint_size * step_multiplier, 5, 400))
+ update_overlays()
+ return true
+ elif current_action.is_single_vert_selected():
+ if not shape.can_edit:
+ return false
+ var key: int = current_action.current_point_key()
+ if Input.is_key_pressed(KEY_SHIFT):
+ var width: float = shape.get_point_width(key)
+ var width_step := 0.1
+ if btn == MOUSE_BUTTON_WHEEL_DOWN:
+ width_step *= -1
+ var new_width: float = width + width_step
+ shape.set_point_width(key, new_width)
+
+ else:
+ var texture_idx_step := 1
+ if btn == MOUSE_BUTTON_WHEEL_DOWN:
+ texture_idx_step *= -1
+
+ var tex_idx: int = shape.get_point_texture_index(key) + texture_idx_step
+ shape.set_point_texture_index(key, tex_idx)
+
+ update_overlays()
+ _gui_update_info_panels()
+ return true
+
+ return false
+
+
+func _input_handle_keyboard_event(event: InputEventKey) -> bool:
+ if not shape.can_edit:
+ return false
+ var kb: InputEventKey = event
+ if _is_valid_keyboard_scancode(kb):
+ if current_action.is_single_vert_selected():
+ if kb.pressed and kb.keycode == KEY_SPACE:
+ var key: int = current_action.current_point_key()
+ shape.set_point_texture_flip(key, not shape.get_point_texture_flip(key))
+ _gui_update_info_panels()
+
+ if kb.pressed and kb.keycode == KEY_ESCAPE:
+ # Hide edge_info_panel
+ if gui_edge_info_panel.visible:
+ gui_edge_info_panel.visible = false
+
+ if current_mode == MODE.CREATE_VERT:
+ _enter_mode(MODE.EDIT_VERT)
+
+ if kb.keycode == KEY_CTRL:
+ if kb.pressed and not kb.echo:
+ on_edge = false
+ if closest_key != -1:
+ current_action = select_verticies([closest_key], ACTION_VERT.NONE)
+ else:
+ deselect_verts()
+ update_overlays()
+
+ if kb.keycode == KEY_ALT:
+ update_overlays()
+
+ return true
+ return false
+
+
+func _is_valid_keyboard_scancode(kb: InputEventKey) -> bool:
+ match kb.keycode:
+ KEY_ESCAPE:
+ return true
+ KEY_ENTER:
+ return true
+ KEY_SPACE:
+ return true
+ KEY_SHIFT:
+ return true
+ KEY_ALT:
+ return true
+ KEY_CTRL:
+ return true
+ return false
+
+
+func _input_handle_mouse_button_event(
+ event: InputEventMouseButton, et: Transform2D, grab_threshold: float
+) -> bool:
+ if not shape.can_edit:
+ return false
+ var t: Transform2D = et * shape.get_global_transform()
+ var mb: InputEventMouseButton = event
+ var viewport_mouse_position: Vector2 = et.affine_inverse() * mb.position
+ var mouse_wheel_spun: bool = (
+ mb.pressed
+ and (mb.button_index == MOUSE_BUTTON_WHEEL_DOWN or mb.button_index == MOUSE_BUTTON_WHEEL_UP)
+ )
+
+ #######################################
+ # Left Mouse Button released
+ if not mb.pressed and mb.button_index == MOUSE_BUTTON_LEFT:
+ _mouse_lmb_pressed = false
+ var rslt: bool = false
+ var type: ACTION_VERT = current_action.type
+ var _in := type == ACTION_VERT.MOVE_CONTROL or type == ACTION_VERT.MOVE_CONTROL_IN
+ var _out := type == ACTION_VERT.MOVE_CONTROL or type == ACTION_VERT.MOVE_CONTROL_OUT
+ if type == ACTION_VERT.MOVE_VERT:
+ perform_action(ActionMoveVerticies.new(shape, current_action.keys,
+ current_action.starting_positions))
+ rslt = true
+ elif _in or _out:
+ perform_action(ActionMoveControlPoints.new(
+ shape,
+ current_action.keys,
+ current_action.starting_positions_control_in,
+ current_action.starting_positions_control_out
+ ))
+ rslt = true
+ elif current_mode == MODE.FREEHAND:
+ if _defer_mesh_updates:
+ shape.end_update()
+ deselect_verts()
+ return rslt
+
+ #######################################
+ # Right Mouse Button released
+ if not mb.pressed and mb.button_index == MOUSE_BUTTON_RIGHT:
+ _mouse_rmb_pressed = false
+
+ #########################################
+ # Mouse Wheel on valid point
+ elif mouse_wheel_spun:
+ return _input_handle_mouse_wheel(mb.button_index)
+
+ #########################################
+ # Mouse left click
+ elif mb.pressed and mb.button_index == MOUSE_BUTTON_LEFT:
+ _mouse_lmb_pressed = true
+ return _input_handle_left_click(mb, viewport_mouse_position, t, et, grab_threshold)
+
+ #########################################
+ # Mouse right click
+ elif mb.pressed and mb.button_index == MOUSE_BUTTON_RIGHT:
+ _mouse_rmb_pressed = true
+ return _input_handle_right_click_press(mb.position, grab_threshold)
+
+ return false
+
+
+func _input_split_edge(mb: InputEventMouseButton, vp_m_pos: Vector2, t: Transform2D) -> bool:
+ if not on_edge:
+ return false
+ var gpoint: Vector2 = mb.position
+ var insertion_point: int = -1
+ var mb_offset: float = shape.get_closest_offset(t.affine_inverse() * gpoint)
+
+ insertion_point = shape.get_point_index(_get_edge_point_keys_from_offset(mb_offset)[1])
+
+ if insertion_point == -1:
+ insertion_point = shape.get_point_count() - 1
+
+ var split_curve := ActionSplitCurve.new(shape, insertion_point, gpoint, t, not _defer_mesh_updates)
+ perform_action(split_curve)
+ select_vertices_to_move([split_curve.get_key()], vp_m_pos)
+ on_edge = false
+
+ if _defer_mesh_updates:
+ shape.begin_update()
+
+ return true
+
+
+func _input_move_control_points(mb: InputEventMouseButton, vp_m_pos: Vector2, grab_threshold: float) -> bool:
+ var points_in := SS2D_PluginFunctionality.get_intersecting_control_point_in(
+ shape, get_et(), mb.position, grab_threshold
+ )
+ var points_out := SS2D_PluginFunctionality.get_intersecting_control_point_out(
+ shape, get_et(), mb.position, grab_threshold
+ )
+ if not points_in.is_empty():
+ select_control_points_to_move([points_in[0]], vp_m_pos, ACTION_VERT.MOVE_CONTROL_IN)
+ return true
+ elif not points_out.is_empty():
+ select_control_points_to_move([points_out[0]], vp_m_pos, ACTION_VERT.MOVE_CONTROL_OUT)
+ return true
+ return false
+
+
+func _get_edge_point_keys_from_offset(
+ offset: float, straight: bool = false, _position := Vector2(0, 0)
+) -> 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)
+ var this_offset := 0.0
+ var next_offset := 0.0
+ if straight:
+ this_offset = shape.get_closest_offset_straight_edge(shape.get_point_position(key))
+ next_offset = shape.get_closest_offset_straight_edge(shape.get_point_position(key_next))
+ else:
+ this_offset = shape.get_closest_offset(shape.get_point_position(key))
+ next_offset = shape.get_closest_offset(shape.get_point_position(key_next))
+
+ if offset >= this_offset and offset <= next_offset:
+ 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 Vector2i(key, key_next)
+ return Vector2i(-1, -1)
+
+
+func _input_motion_is_on_edge(mm: InputEventMouseMotion, grab_threshold: float) -> bool:
+ var xform: Transform2D = get_et() * shape.get_global_transform()
+ if shape.get_point_count() < 2:
+ return false
+
+ # Find edge
+ var closest_point: Vector2
+ if current_mode == MODE.EDIT_EDGE or current_mode == MODE.CUT_EDGE:
+ closest_point = shape.get_closest_point_straight_edge(
+ xform.affine_inverse() * mm.position
+ )
+ else:
+ closest_point = shape.get_closest_point(xform.affine_inverse() * mm.position)
+ edge_point = xform * closest_point
+ if edge_point.distance_to(mm.position) <= grab_threshold:
+ return true
+ return false
+
+
+func _input_find_closest_edge_keys(mm: InputEventMouseMotion) -> void:
+ if shape.get_point_count() < 2:
+ return
+
+ # Find edge
+ var xform: Transform2D = get_et() * shape.get_global_transform()
+ var closest_point: Vector2 = shape.get_closest_point_straight_edge(xform.affine_inverse() * mm.position)
+ var edge_p: Vector2 = xform * closest_point
+ var offset: float = shape.get_closest_offset_straight_edge(xform.affine_inverse() * edge_p)
+ closest_edge_keys = _get_edge_point_keys_from_offset(offset, true, xform.affine_inverse() * mm.position)
+
+
+func get_mouse_over_vert_key(mm: InputEventMouseMotion, grab_threshold: float) -> int:
+ var xform: Transform2D = get_et() * shape.get_global_transform()
+ # However, if near a control point or one of its handles then we are not on the edge
+ for k in shape.get_all_point_keys():
+ var pp: Vector2 = shape.get_point_position(k)
+ var p: Vector2 = xform * pp
+ if p.distance_to(mm.position) <= grab_threshold:
+ return k
+ return -1
+
+
+func get_mouse_over_width_handle(mm: InputEventMouseMotion, grab_threshold: float) -> int:
+ var xform: Transform2D = get_et() * shape.get_global_transform()
+ for k in shape.get_all_point_keys():
+ var pp: Vector2 = shape.get_point_position(k)
+ var normal: Vector2 = _get_vert_normal(
+ xform, shape.get_vertices(), shape.get_point_index(k)
+ )
+ var p: Vector2 = xform * pp + normal * WIDTH_HANDLE_OFFSET
+ if p.distance_to(mm.position) <= grab_threshold:
+ return k
+ return -1
+
+
+func _input_motion_move_control_points(delta: Vector2, _in: bool, _out: bool) -> bool:
+ var rslt := false
+ for i in range(0, current_action.keys.size(), 1):
+ var key: int = current_action.keys[i]
+ var out_multiplier := 1
+ # Invert the delta for position_out if moving both at once
+ if _out and _in:
+ out_multiplier = -1
+
+ var from_in: Vector2 = shape.to_global(current_action.starting_positions_control_in[i])
+ var new_position_in: Vector2 = shape.global_transform.affine_inverse() * (
+ delta + from_in
+ )
+
+ var from_out: Vector2 = shape.to_global(current_action.starting_positions_control_out[i])
+ var new_position_out: Vector2 = shape.global_transform.affine_inverse() * (
+ (delta * out_multiplier)
+ + from_out
+ )
+
+ if use_snap():
+ new_position_in = snap(new_position_in)
+ new_position_out = snap(new_position_out)
+ if _in:
+ shape.set_point_in(key, new_position_in)
+ rslt = true
+ if _out:
+ shape.set_point_out(key, new_position_out)
+ rslt = true
+ update_overlays()
+
+ return rslt
+
+
+func _input_motion_move_verts(delta: Vector2) -> bool:
+ for i in range(0, current_action.keys.size(), 1):
+ var key: int = current_action.keys[i]
+ var from: Vector2 = shape.to_global(current_action.starting_positions[i])
+ var new_position: Vector2 = shape.global_transform.affine_inverse() * (from + delta)
+ if use_snap():
+ new_position = snap(new_position)
+ shape.set_point_position(key, new_position)
+ update_overlays()
+ return true
+
+
+func _input_motion_move_width_handle(mouse_position: Vector2, scale: Vector2) -> bool:
+ for i in range(0, current_action.keys.size(), 1):
+ var key: int = current_action.keys[i]
+ var from_width: float = current_action.starting_width[i]
+ var from_position: Vector2 = current_action.starting_positions[i]
+ width_scaling = from_position.distance_to(mouse_position) / WIDTH_HANDLE_OFFSET * scale.x
+ shape.set_point_width(key, roundf(from_width * width_scaling * 10.0) / 10.0)
+ update_overlays()
+ return true
+
+
+## Will return index of closest vert to point.
+func get_closest_vert_to_point(s: SS2D_Shape, p: Vector2) -> int:
+ var gt: Transform2D = shape.get_global_transform()
+ var verts: PackedVector2Array = s.get_vertices()
+ var transformed_point: Vector2 = gt.affine_inverse() * p
+ var idx: int = -1
+ var closest_distance: float = -1.0
+ for i in verts.size():
+ var distance: float = verts[i].distance_to(transformed_point)
+ if distance < closest_distance or closest_distance == -1.0:
+ idx = s.get_point_key_at_index(i)
+ closest_distance = distance
+ return idx
+
+
+func _input_handle_mouse_motion_event(
+ event: InputEventMouseMotion, et: Transform2D, grab_threshold: float
+) -> bool:
+ var t: Transform2D = et * shape.get_global_transform()
+ var mm: InputEventMouseMotion = event
+ var delta_current_pos: Vector2 = et.affine_inverse() * mm.position
+ gui_point_info_panel.position = mm.position + GUI_POINT_INFO_PANEL_OFFSET
+ var delta: Vector2 = delta_current_pos - _mouse_motion_delta_starting_pos
+
+ closest_key = get_closest_vert_to_point(shape, delta_current_pos)
+
+ if current_mode == MODE.EDIT_VERT or current_mode == MODE.CREATE_VERT:
+ var type: ACTION_VERT = current_action.type
+ var _in := type == ACTION_VERT.MOVE_CONTROL or type == ACTION_VERT.MOVE_CONTROL_IN
+ var _out := type == ACTION_VERT.MOVE_CONTROL or type == ACTION_VERT.MOVE_CONTROL_OUT
+
+ if type == ACTION_VERT.MOVE_VERT:
+ return _input_motion_move_verts(delta)
+ elif _in or _out:
+ return _input_motion_move_control_points(delta, _in, _out)
+ elif type == ACTION_VERT.MOVE_WIDTH_HANDLE:
+ return _input_motion_move_width_handle(
+ et.affine_inverse() * mm.position, et.get_scale()
+ )
+ var mouse_over_key: int = get_mouse_over_vert_key(event, grab_threshold)
+ var mouse_over_width_handle: int = get_mouse_over_width_handle(event, grab_threshold)
+
+ # Make the closest key grabable while holding down Control
+ if (
+ Input.is_key_pressed(KEY_CTRL)
+ and not Input.is_key_pressed(KEY_ALT)
+ and mouse_over_width_handle == -1
+ and mouse_over_key == -1
+ ):
+ mouse_over_key = closest_key
+
+ on_width_handle = false
+ if mouse_over_key != -1:
+ on_edge = false
+ current_action = select_verticies([mouse_over_key], ACTION_VERT.NONE)
+ elif mouse_over_width_handle != -1:
+ on_edge = false
+ on_width_handle = true
+ current_action = select_verticies([mouse_over_width_handle], ACTION_VERT.NONE)
+ elif Input.is_key_pressed(KEY_ALT):
+ _input_find_closest_edge_keys(mm)
+ else:
+ deselect_verts()
+ on_edge = _input_motion_is_on_edge(mm, grab_threshold)
+
+ elif current_mode == MODE.EDIT_EDGE or current_mode == MODE.CUT_EDGE:
+ # Don't update if edge panel is visible
+ if gui_edge_info_panel.visible:
+ return false
+ var type: ACTION_VERT = current_action.type
+ if type == ACTION_VERT.MOVE_VERT:
+ return _input_motion_move_verts(delta)
+ else:
+ deselect_verts()
+ on_edge = _input_motion_is_on_edge(mm, grab_threshold)
+
+ elif current_mode == MODE.FREEHAND:
+ if _mouse_lmb_pressed:
+ if not Input.is_key_pressed(KEY_CTRL):
+ var local_position: Vector2 = t.affine_inverse() * mm.position
+ if last_point_position.distance_to(local_position) >= freehand_paint_size * 2:
+ last_point_position = local_position
+ if use_snap():
+ local_position = snap(local_position)
+ var idx: int = shape.get_point_index(closest_edge_keys[1]) if shape.is_shape_closed() else -1
+ perform_action(ActionAddPoint.new(shape, local_position, idx, not _defer_mesh_updates))
+ update_overlays()
+ return true
+ else:
+ var xform: Transform2D = get_et() * shape.get_global_transform()
+ var closest_ss2d_point: SS2D_Point = (shape as SS2D_Shape).get_point(closest_key)
+ if closest_ss2d_point != null:
+ var closest_point: Vector2 = closest_ss2d_point.position
+ closest_point = xform * closest_point
+ if closest_point.distance_to(mm.position) / current_zoom_level <= freehand_erase_size * 2:
+ var delete_point: int = get_mouse_over_vert_key(event, grab_threshold)
+ delete_point = closest_key
+ on_width_handle = false
+ if delete_point != -1:
+ perform_action(ActionDeletePoint.new(shape, delete_point, not _defer_mesh_updates))
+ last_point_position = Vector2.ZERO
+ update_overlays()
+ return true
+ else:
+ _input_find_closest_edge_keys(mm)
+
+ update_overlays()
+ return false
+
+
+func _get_vert_normal(t: Transform2D, verts: PackedVector2Array, i: int) -> Vector2:
+ var point: Vector2 = t * verts[i]
+ var prev_point: Vector2 = t * (verts[(i - 1) % verts.size()])
+ var next_point: Vector2 = t * (verts[(i + 1) % verts.size()])
+ return ((prev_point - point).normalized().rotated(PI / 2) + (point - next_point).normalized().rotated(PI / 2)).normalized()
+
+
+func copy_shape(s: SS2D_Shape) -> SS2D_Shape:
+ var copy: SS2D_Shape = s.clone(false)
+
+ var undo := get_undo_redo()
+ undo.create_action("Add Shape Node")
+
+ undo.add_do_method(s.get_parent(), "add_child", copy, true)
+ undo.add_do_method(copy, "set_owner", get_tree().get_edited_scene_root())
+ undo.add_do_reference(copy)
+ undo.add_undo_method(copy, "set_owner", null)
+ undo.add_undo_method(s.get_parent(), "remove_child", copy)
+
+ var collision_polygon_original := s.get_collision_polygon_node()
+
+ if collision_polygon_original:
+ var collision_polygon_new := CollisionPolygon2D.new()
+ collision_polygon_new.visible = collision_polygon_original.visible
+
+ undo.add_do_method(collision_polygon_original.get_parent(), "add_child", collision_polygon_new, true)
+ undo.add_do_method(collision_polygon_new, "set_owner", get_tree().get_edited_scene_root())
+ undo.add_do_reference(collision_polygon_new)
+
+ undo.add_undo_method(collision_polygon_original.get_parent(), "remove_child", collision_polygon_new)
+
+ undo.commit_action()
+
+ copy.collision_polygon_node_path = copy.get_path_to(collision_polygon_new)
+ else:
+ undo.commit_action()
+
+ return copy
+
+
+#########
+# DEBUG #
+#########
+func _debug_mouse_positions(mm: InputEventMouseMotion, t: Transform2D) -> void:
+ print("========================================")
+ print("MouseDelta:%s" % str(_mouse_motion_delta_starting_pos))
+ print("= MousePositions =")
+ print("Position: %s" % str(mm.position))
+ print("Relative: %s" % str(mm.relative))
+ print("= Transforms =")
+ print("Transform3D: %s" % str(t))
+ print("Inverse: %s" % str(t.affine_inverse()))
+ print("= Transformed Mouse positions =")
+ print("Position: %s" % str(t.affine_inverse() * mm.position))
+ print("Relative: %s" % str(t.affine_inverse() * mm.relative))
+ print("MouseDelta:%s" % str(t.affine_inverse() * _mouse_motion_delta_starting_pos))
diff --git a/addons/rmsmartshape/plugin_functionality.gd b/addons/rmsmartshape/plugin_functionality.gd
new file mode 100644
index 0000000..3d12e53
--- /dev/null
+++ b/addons/rmsmartshape/plugin_functionality.gd
@@ -0,0 +1,132 @@
+@tool
+class_name SS2D_PluginFunctionality
+extends RefCounted
+
+## - Everything in this script should be static
+## - There is one reason to have code in this script
+## 1. To separate out code from the main plugin script to ease testing
+##
+## Common Abbreviations
+## et = editor transform (viewport's canvas transform)
+
+# --- VERTS
+
+static func get_intersecting_control_point_in(
+ s: SS2D_Shape, et: Transform2D, mouse_pos: Vector2, grab_threshold: float
+) -> PackedInt32Array:
+ return _get_intersecting_control_point(s, et, mouse_pos, grab_threshold, true)
+
+
+static func get_intersecting_control_point_out(
+ s: SS2D_Shape, et: Transform2D, mouse_pos: Vector2, grab_threshold: float
+) -> PackedInt32Array:
+ return _get_intersecting_control_point(s, et, mouse_pos, grab_threshold, false)
+
+
+static func _get_intersecting_control_point(
+ s: SS2D_Shape, et: Transform2D, mouse_pos: Vector2, grab_threshold: float, _in: bool
+) -> PackedInt32Array:
+ var points := PackedInt32Array()
+ var xform: Transform2D = et * s.get_global_transform()
+ for i in s.get_point_count():
+ var key: int = s.get_point_key_at_index(i)
+ var vec_pos: Vector2 = s.get_point_position(key)
+ var c_pos := Vector2.ZERO
+ if _in:
+ c_pos = s.get_point_in(key)
+ else:
+ c_pos = s.get_point_out(key)
+ if c_pos == Vector2.ZERO:
+ continue
+ var final_pos := vec_pos + c_pos
+ final_pos = xform * final_pos
+ if final_pos.distance_to(mouse_pos) <= grab_threshold:
+ points.push_back(key)
+
+ return points
+
+
+static func get_next_point_index(idx: int, points: PackedVector2Array, wrap_around: bool = false) -> int:
+ if wrap_around:
+ return get_next_point_index_wrap_around(idx, points)
+ return get_next_point_index_no_wrap_around(idx, points)
+
+
+static func get_previous_point_index(idx: int, points: PackedVector2Array, wrap_around: bool = false) -> int:
+ if wrap_around:
+ return get_previous_point_index_wrap_around(idx, points)
+ return get_previous_point_index_no_wrap_around(idx, points)
+
+
+static func get_next_point_index_no_wrap_around(idx: int, points: PackedVector2Array) -> int:
+ return mini(idx + 1, points.size() - 1)
+
+
+static func get_previous_point_index_no_wrap_around(idx: int, _points_: PackedVector2Array) -> int:
+ return maxi(idx - 1, 0)
+
+
+static func get_next_point_index_wrap_around(idx: int, points: PackedVector2Array) -> int:
+ return (idx + 1) % points.size()
+
+
+static func get_previous_point_index_wrap_around(idx: int, points: PackedVector2Array) -> int:
+ return posmod(idx - 1, points.size())
+
+
+## Get the next point that doesn't share the same position with the current point.[br]
+## In other words, get the next point in the array with a unique position.[br]
+static func get_next_unique_point_idx(idx: int, pts: PackedVector2Array, wrap_around: bool) -> int:
+ var next_idx: int = get_next_point_index(idx, pts, wrap_around)
+ if next_idx == idx:
+ return idx
+ var pt1: Vector2 = pts[idx]
+ var pt2: Vector2 = pts[next_idx]
+ if pt1 == pt2:
+ return get_next_unique_point_idx(next_idx, pts, wrap_around)
+ return next_idx
+
+
+static func get_previous_unique_point_idx(idx: int, pts: PackedVector2Array, wrap_around: bool) -> int:
+ var previous_idx: int = get_previous_point_index(idx, pts, wrap_around)
+ if previous_idx == idx:
+ return idx
+ var pt1: Vector2 = pts[idx]
+ var pt2: Vector2 = pts[previous_idx]
+ if pt1 == pt2:
+ return get_previous_unique_point_idx(previous_idx, pts, wrap_around)
+ return previous_idx
+
+
+static func snap_position(
+ pos_global: Vector2, snap_offset: Vector2, snap_step: Vector2, local_t: Transform2D
+) -> Vector2:
+ # Move global position to local position to snap in local space
+ var pos_local: Vector2 = local_t * pos_global
+
+ # Snap in local space
+ var x: float = pos_local.x
+ if snap_step.x != 0:
+ var delta: float = fmod(pos_local.x, snap_step.x)
+ # Round up
+ if delta >= (snap_step.x / 2.0):
+ x = pos_local.x + (snap_step.x - delta)
+ # Round down
+ else:
+ x = pos_local.x - delta
+ var y: float = pos_local.y
+ if snap_step.y != 0:
+ var delta: float = fmod(pos_local.y, snap_step.y)
+ # Round up
+ if delta >= (snap_step.y / 2.0):
+ y = pos_local.y + (snap_step.y - delta)
+ # Round down
+ else:
+ y = pos_local.y - delta
+
+ # Transform3D local position to global position
+ var pos_global_snapped := (local_t.affine_inverse() * Vector2(x, y)) + snap_offset
+ #print ("%s | %s | %s | %s" % [pos_global, pos_local, Vector2(x,y), pos_global_snapped])
+ return pos_global_snapped
+
+
diff --git a/addons/rmsmartshape/scenes/GUI_Edge_InfoPanel.gd b/addons/rmsmartshape/scenes/GUI_Edge_InfoPanel.gd
new file mode 100644
index 0000000..f748341
--- /dev/null
+++ b/addons/rmsmartshape/scenes/GUI_Edge_InfoPanel.gd
@@ -0,0 +1,146 @@
+@tool
+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 := Vector2i(-1, -1) : set = set_indicies
+var edge_material: SS2D_Material_Edge = null
+var edge_material_selector := FileDialog.new()
+
+@onready var idx_label: Label = %IDX
+@onready var material_override_button: Button = %MaterialOverride
+@onready var override_container: Container = %OverrideContainer
+@onready var render_checkbox: CheckBox = %Render
+@onready var weld_checkbox: CheckBox = %Weld
+@onready var z_index_spinbox: SpinBox = %ZIndex
+@onready var set_material_button: Button = %SetMaterial
+@onready var clear_material_button: Button = %ClearMaterial
+@onready var material_status: Label = %MaterialStatus
+
+
+func _ready() -> void:
+ material_override_button.connect("toggled", self._on_toggle_material_override)
+ render_checkbox.connect("toggled", self._on_toggle_render)
+ weld_checkbox.connect("toggled", self._on_toggle_weld)
+ z_index_spinbox.connect("value_changed", self._on_set_z_index)
+ set_material_button.connect("pressed", self._on_set_edge_material_pressed)
+ clear_material_button.connect("pressed", self._on_set_edge_material_clear_pressed)
+
+ override_container.hide()
+ clear_material_button.hide()
+
+ edge_material_selector.file_mode = FileDialog.FILE_MODE_OPEN_FILE
+ edge_material_selector.dialog_hide_on_ok = true
+ edge_material_selector.show_hidden_files = true
+ edge_material_selector.mode_overrides_title = false
+ edge_material_selector.title = "Select Edge Material"
+ edge_material_selector.filters = PackedStringArray(["*.tres"])
+ edge_material_selector.connect("file_selected", self._on_set_edge_material_file_selected)
+ add_child(edge_material_selector)
+
+
+func _on_set_edge_material_clear_pressed() -> void:
+ set_edge_material(null)
+
+
+func _on_set_edge_material_pressed() -> void:
+ # Update file list
+ edge_material_selector.invalidate()
+ edge_material_selector.popup_centered_ratio(0.8)
+
+
+func _on_set_edge_material_file_selected(f: String) -> void:
+ var rsc := load(f)
+ if not rsc is SS2D_Material_Edge:
+ push_error("Selected resource is not an Edge Material! (SS2D_Material_Edge)")
+ return
+ set_edge_material(rsc)
+
+
+func set_indicies(t: Vector2i) -> void:
+ indicies = t
+ idx_label.text = "IDX: %s" % indicies
+
+
+func set_material_override(enabled: bool) -> void:
+ material_override_button.button_pressed = enabled
+ _on_toggle_material_override(enabled)
+
+
+func set_render(enabled: bool, emit: bool = true) -> void:
+ render_checkbox.button_pressed = enabled
+ if emit:
+ _on_toggle_render(enabled)
+
+
+func set_weld(enabled: bool, emit: bool = true) -> void:
+ weld_checkbox.button_pressed = enabled
+ if emit:
+ _on_toggle_weld(enabled)
+
+
+func set_edge_material(v: SS2D_Material_Edge, emit: bool = true) -> void:
+ edge_material = v
+ if v == null:
+ material_status.text = "[No Material]"
+ clear_material_button.visible = false
+ else:
+ # Call string function 'get_file()' to get the filepath
+ material_status.text = "[%s]" % (v.resource_path).get_file()
+ clear_material_button.visible = true
+ if emit:
+ emit_signal("edge_material_changed", v)
+
+
+func set_edge_z_index(v: int, emit: bool = true) -> void:
+ z_index_spinbox.value = float(v)
+ if emit:
+ _on_set_z_index(float(v))
+
+
+func get_render() -> bool:
+ return render_checkbox.button_pressed
+
+
+func get_weld() -> bool:
+ return weld_checkbox.button_pressed
+
+
+func get_edge_z_index() -> int:
+ return int(z_index_spinbox.value)
+
+
+func _on_toggle_material_override(pressed: bool) -> void:
+ override_container.visible = pressed
+ emit_signal("material_override_toggled", pressed)
+
+
+func _on_toggle_render(pressed: bool) -> void:
+ emit_signal("render_toggled", pressed)
+
+
+func _on_toggle_weld(pressed: bool) -> void:
+ emit_signal("weld_toggled", pressed)
+
+
+func _on_set_z_index(v: float) -> void:
+ emit_signal("z_index_changed", int(v))
+
+
+func load_values_from_meta_material(meta_mat: SS2D_Material_Edge_Metadata) -> void:
+ set_render(meta_mat.render)
+ set_weld(meta_mat.weld)
+ set_z_index(meta_mat.z_index)
+ set_edge_material(meta_mat.edge_material)
+
+
+func save_values_to_meta_material(meta_mat: SS2D_Material_Edge_Metadata) -> void:
+ meta_mat.render = get_render()
+ meta_mat.weld = get_weld()
+ meta_mat.z_index = get_z_index()
+ meta_mat.edge_material = edge_material
diff --git a/addons/rmsmartshape/scenes/GUI_Edge_InfoPanel.tscn b/addons/rmsmartshape/scenes/GUI_Edge_InfoPanel.tscn
new file mode 100644
index 0000000..6ddae7e
--- /dev/null
+++ b/addons/rmsmartshape/scenes/GUI_Edge_InfoPanel.tscn
@@ -0,0 +1,100 @@
+[gd_scene load_steps=3 format=3 uid="uid://cy1l6tyadc4s3"]
+
+[ext_resource type="Theme" uid="uid://dud4fe6fsicvm" path="res://addons/rmsmartshape/assets/gui_theme.res" id="1_pwu3l"]
+[ext_resource type="Script" path="res://addons/rmsmartshape/scenes/GUI_Edge_InfoPanel.gd" id="2"]
+
+[node name="GUI_Edge_InfoPanel" type="PanelContainer"]
+anchors_preset = -1
+anchor_right = 0.203125
+anchor_bottom = 0.345679
+offset_bottom = -62.0
+size_flags_horizontal = 4
+size_flags_vertical = 4
+mouse_filter = 2
+theme = ExtResource("1_pwu3l")
+script = ExtResource("2")
+metadata/_edit_use_anchors_ = true
+
+[node name="Panel" type="Panel" parent="."]
+layout_mode = 2
+mouse_filter = 2
+
+[node name="Container" type="VBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="Properties" type="VBoxContainer" parent="Container"]
+layout_mode = 2
+mouse_filter = 2
+
+[node name="IDX" type="Label" parent="Container/Properties"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "IDX: [1,2]"
+
+[node name="Functions" type="VBoxContainer" parent="Container"]
+layout_mode = 2
+mouse_filter = 2
+
+[node name="Make Inner Curve" type="Button" parent="Container/Functions"]
+visible = false
+layout_mode = 2
+text = "Make Inner Curve"
+
+[node name="Make Outer Curve" type="Button" parent="Container/Functions"]
+visible = false
+layout_mode = 2
+text = "Make Outer Curve"
+
+[node name="MaterialOverride" type="Button" parent="Container/Functions"]
+unique_name_in_owner = true
+layout_mode = 2
+toggle_mode = true
+text = "Material Override"
+
+[node name="OverrideContainer" type="VBoxContainer" parent="Container"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+
+[node name="HSeparator" type="HSeparator" parent="Container/OverrideContainer"]
+layout_mode = 2
+
+[node name="Render" type="CheckBox" parent="Container/OverrideContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Render"
+
+[node name="Weld" type="CheckBox" parent="Container/OverrideContainer"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+text = "Weld"
+
+[node name="ZIndexSection" type="HBoxContainer" parent="Container/OverrideContainer"]
+visible = false
+layout_mode = 2
+
+[node name="lbl" type="Label" parent="Container/OverrideContainer/ZIndexSection"]
+layout_mode = 2
+text = "Z:"
+
+[node name="ZIndex" type="SpinBox" parent="Container/OverrideContainer/ZIndexSection"]
+unique_name_in_owner = true
+layout_mode = 2
+min_value = -100.0
+
+[node name="SetMaterial" type="Button" parent="Container/OverrideContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Set Material"
+
+[node name="ClearMaterial" type="Button" parent="Container/OverrideContainer"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+text = "Clear Material"
+
+[node name="MaterialStatus" type="Label" parent="Container/OverrideContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "[No Material]"
diff --git a/addons/rmsmartshape/scenes/GUI_InfoPanel.gd b/addons/rmsmartshape/scenes/GUI_InfoPanel.gd
new file mode 100644
index 0000000..1ff15f2
--- /dev/null
+++ b/addons/rmsmartshape/scenes/GUI_InfoPanel.gd
@@ -0,0 +1,25 @@
+@tool
+extends PanelContainer
+class_name SS2D_PointInfoPanel
+
+
+@onready var idx_label: Label = %IDX
+@onready var tex_label: Label = %Tex
+@onready var width_label: Label = %Width
+@onready var flip_label: Label = %Flip
+
+
+func set_idx(i: int) -> void:
+ idx_label.text = "IDX: %s" % i
+
+
+func set_texture_idx(i: int) -> void:
+ tex_label.text = "Texture2D: %s" % i
+
+
+func set_width(f: float) -> void:
+ width_label.text = "Width: %s" % f
+
+
+func set_flip(b: bool) -> void:
+ flip_label.text = "Flip: %s" % b
diff --git a/addons/rmsmartshape/scenes/GUI_InfoPanel.tscn b/addons/rmsmartshape/scenes/GUI_InfoPanel.tscn
new file mode 100644
index 0000000..9bb1c70
--- /dev/null
+++ b/addons/rmsmartshape/scenes/GUI_InfoPanel.tscn
@@ -0,0 +1,44 @@
+[gd_scene load_steps=3 format=3 uid="uid://cxu6258urdtf1"]
+
+[ext_resource type="Theme" uid="uid://dud4fe6fsicvm" path="res://addons/rmsmartshape/assets/gui_theme.res" id="1_byls6"]
+[ext_resource type="Script" path="res://addons/rmsmartshape/scenes/GUI_InfoPanel.gd" id="2"]
+
+[node name="GUI_InfoPanel" type="PanelContainer"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_right = -918.0
+offset_bottom = -486.0
+size_flags_horizontal = 4
+size_flags_vertical = 4
+mouse_filter = 2
+theme = ExtResource("1_byls6")
+script = ExtResource("2")
+
+[node name="Panel" type="Panel" parent="."]
+layout_mode = 2
+mouse_filter = 2
+
+[node name="Properties" type="VBoxContainer" parent="."]
+layout_mode = 2
+mouse_filter = 2
+
+[node name="IDX" type="Label" parent="Properties"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "IDX: 13"
+
+[node name="Tex" type="Label" parent="Properties"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Tex: 1"
+
+[node name="Width" type="Label" parent="Properties"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Width: 1.0"
+
+[node name="Flip" type="Label" parent="Properties"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Flip: N"
diff --git a/addons/rmsmartshape/scenes/SnapPopup.gd b/addons/rmsmartshape/scenes/SnapPopup.gd
new file mode 100644
index 0000000..cb87ea0
--- /dev/null
+++ b/addons/rmsmartshape/scenes/SnapPopup.gd
@@ -0,0 +1,16 @@
+@tool
+extends Popup
+class_name SS2D_SnapPopup
+
+@onready var snap_offset_x: SpinBox = %SnapOffsetX
+@onready var snap_offset_y: SpinBox = %SnapOffsetY
+@onready var snap_step_x: SpinBox = %SnapStepX
+@onready var snap_step_y: SpinBox = %SnapStepY
+
+
+func get_snap_offset() -> Vector2:
+ return Vector2(snap_offset_x.value, snap_offset_y.value)
+
+
+func get_snap_step() -> Vector2:
+ return Vector2(snap_step_x.value, snap_step_y.value)
diff --git a/addons/rmsmartshape/scenes/SnapPopup.tscn b/addons/rmsmartshape/scenes/SnapPopup.tscn
new file mode 100644
index 0000000..12af8f3
--- /dev/null
+++ b/addons/rmsmartshape/scenes/SnapPopup.tscn
@@ -0,0 +1,58 @@
+[gd_scene load_steps=3 format=3 uid="uid://d1acvw8yuubxo"]
+
+[ext_resource type="Script" path="res://addons/rmsmartshape/scenes/SnapPopup.gd" id="1"]
+[ext_resource type="Theme" uid="uid://dud4fe6fsicvm" path="res://addons/rmsmartshape/assets/gui_theme.res" id="1_6ft7o"]
+
+[node name="SnapPopup" type="PopupPanel"]
+size = Vector2i(301, 128)
+visible = true
+theme = ExtResource("1_6ft7o")
+script = ExtResource("1")
+
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
+offset_left = 12.0
+offset_top = 12.0
+offset_right = 289.0
+offset_bottom = 116.0
+
+[node name="Label" type="Label" parent="VBoxContainer"]
+layout_mode = 2
+text = "Configure Snap"
+
+[node name="SnapOffset" type="HBoxContainer" parent="VBoxContainer"]
+layout_mode = 2
+alignment = 2
+
+[node name="Label" type="Label" parent="VBoxContainer/SnapOffset"]
+layout_mode = 2
+text = "Grid Offset:"
+
+[node name="SnapOffsetX" type="SpinBox" parent="VBoxContainer/SnapOffset"]
+unique_name_in_owner = true
+layout_mode = 2
+suffix = "px"
+
+[node name="SnapOffsetY" type="SpinBox" parent="VBoxContainer/SnapOffset"]
+unique_name_in_owner = true
+layout_mode = 2
+suffix = "px"
+
+[node name="SnapStep" type="HBoxContainer" parent="VBoxContainer"]
+layout_mode = 2
+alignment = 2
+
+[node name="Label" type="Label" parent="VBoxContainer/SnapStep"]
+layout_mode = 2
+text = "Grid Step: "
+
+[node name="SnapStepX" type="SpinBox" parent="VBoxContainer/SnapStep"]
+unique_name_in_owner = true
+layout_mode = 2
+value = 8.0
+suffix = "px"
+
+[node name="SnapStepY" type="SpinBox" parent="VBoxContainer/SnapStep"]
+unique_name_in_owner = true
+layout_mode = 2
+value = 8.0
+suffix = "px"
diff --git a/addons/rmsmartshape/shapes/edge.gd b/addons/rmsmartshape/shapes/edge.gd
new file mode 100644
index 0000000..35ee62b
--- /dev/null
+++ b/addons/rmsmartshape/shapes/edge.gd
@@ -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)
diff --git a/addons/rmsmartshape/shapes/index_map.gd b/addons/rmsmartshape/shapes/index_map.gd
new file mode 100644
index 0000000..31f7eda
--- /dev/null
+++ b/addons/rmsmartshape/shapes/index_map.gd
@@ -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
diff --git a/addons/rmsmartshape/shapes/mesh.gd b/addons/rmsmartshape/shapes/mesh.gd
new file mode 100644
index 0000000..0c26f9b
--- /dev/null
+++ b/addons/rmsmartshape/shapes/mesh.gd
@@ -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)
diff --git a/addons/rmsmartshape/shapes/point.gd b/addons/rmsmartshape/shapes/point.gd
new file mode 100644
index 0000000..a7b58e4
--- /dev/null
+++ b/addons/rmsmartshape/shapes/point.gd
@@ -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 "" % [position]
+
+
+func _on_properties_changed() -> void:
+ emit_changed()
diff --git a/addons/rmsmartshape/shapes/point_array.gd b/addons/rmsmartshape/shapes/point_array.gd
new file mode 100644
index 0000000..0d74011
--- /dev/null
+++ b/addons/rmsmartshape/shapes/point_array.gd
@@ -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 "" % [_points.keys(), _point_order]
+
+
+func _on_material_override_changed(tuple: Vector2i) -> void:
+ material_override_changed.emit(tuple)
diff --git a/addons/rmsmartshape/shapes/quad.gd b/addons/rmsmartshape/shapes/quad.gd
new file mode 100644
index 0000000..a5d3375
--- /dev/null
+++ b/addons/rmsmartshape/shapes/quad.gd
@@ -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)
diff --git a/addons/rmsmartshape/shapes/shape.gd b/addons/rmsmartshape/shapes/shape.gd
new file mode 100644
index 0000000..8dacecf
--- /dev/null
+++ b/addons/rmsmartshape/shapes/shape.gd
@@ -0,0 +1,1894 @@
+@tool
+@icon("../assets/closed_shape.png")
+extends Node2D
+class_name SS2D_Shape
+
+## Represents the base functionality for all smart shapes.
+
+# Functions consist of the following categories:[br]
+# - Setters / Getters
+# - Curve
+# - Curve Wrapper
+# - Godot
+# - Misc
+#
+# To use search to jump between categories, use the regex: # .+ #
+
+################
+#-DECLARATIONS-#
+################
+
+var _dirty: bool = false
+var _edges: Array[SS2D_Edge] = []
+var _meshes: Array[SS2D_Mesh] = []
+var _collision_polygon_node: CollisionPolygon2D
+# Whether or not the plugin should allow editing this shape
+var can_edit: bool = true
+
+signal points_modified
+signal on_dirty_update
+signal make_unique_pressed(shape: SS2D_Shape)
+
+enum ORIENTATION { COLINEAR, CLOCKWISE, C_CLOCKWISE }
+
+enum CollisionGenerationMethod {
+ ## Uses the shape curve to generate a collision polygon. Usually this method is accurate enough.
+ ## For open shapes, a precise method will be used instead, as the fast method is not suitable.
+ Fast,
+ ## Uses the edge generation algorithm to create an accurate collision representation that
+ ## exactly matches the shape's visuals.
+ ## Depending on the shape's complexity, this method is very expensive.
+ Precise,
+}
+
+enum CollisionUpdateMode {
+ ## Only update collisions in editor. If the corresponding CollisionPolygon2D is part of the same
+ ## scene, it will be saved automatically by Godot, hence no additional regeneration at runtime
+ ## is necessary, which reduces the loading times.
+ ## Does not work if the CollisionPolygon2D is part of an instanced scene, as only the scene root
+ ## node will be saved by Godot.
+ Editor,
+ ## Only update collisions during runtime. Improves the shape-editing performance in editor but
+ ## increases loading times as collision generation is deferred to runtime.
+ Runtime,
+ ## Update collisions both in editor and during runtime. This is the default behavior in older
+ ## SS2D versions.
+ EditorAndRuntime,
+}
+
+###########
+#-EXPORTS-#
+###########
+
+# Execute to refresh shape rendered geometry and textures.
+@warning_ignore("unused_private_class_variable")
+@export_placeholder("ActionProperty") var _refresh: String = "" : set = _refresh_action
+# ActionProperty will add a button to inspector to execute this action.
+# When non-empty string is passed into setter, action is considerd executed.
+
+## Visualize generated quads and edges.
+@export var editor_debug: bool = false : set = _set_editor_debug
+
+## @deprecated
+@export_range(1, 512) var curve_bake_interval: float = 20.0 :
+ set(value): _points.curve_bake_interval = value
+ get: return _points.curve_bake_interval
+
+## How to treat color data. See [enum SS2D_Edge.COLOR_ENCODING].
+@export var color_encoding: SS2D_Edge.COLOR_ENCODING = SS2D_Edge.COLOR_ENCODING.COLOR : set = set_color_encoding
+
+@export_group("Geometry")
+
+# Execute to make shape point geometry unique (not materials).
+@warning_ignore("unused_private_class_variable")
+@export_placeholder("ActionProperty") var _make_unique: String = "" : set = _make_unique_action
+# ActionProperty will add a button to inspector to execute this action.
+# When non-empty string is passed into setter, action is considerd executed.
+
+## Resource that holds shape point geometry (aka point array).
+@export var _points: SS2D_Point_Array : set = set_point_array
+
+@export_group("Edges")
+
+@export var flip_edges: bool = false : set = set_flip_edges
+
+## Enable/disable rendering of the edges.
+@export var render_edges: bool = true : set = set_render_edges
+
+@export_group("Materials")
+
+## Contains textures and data on how to visualize the shape.
+@export var shape_material := SS2D_Material_Shape.new() : set = _set_material
+
+## Dictionary of (Array of 2 keys) to (SS2D_Material_Edge_Metadata)
+## Deprecated, exists for Support of older versions
+## @deprecated
+@export var material_overrides: Dictionary = {} : set = set_material_overrides
+
+@export_group("Tesselation")
+
+## Controls how many subdivisions a curve segment may face before it is considered
+## approximate enough.
+## @deprecated
+@export_range(0, 8, 1)
+var tessellation_stages: int = 3 :
+ set(value): _points.tessellation_stages = value
+ get: return _points.tessellation_stages
+
+## Controls how many degrees the midpoint of a segment may deviate from the real
+## curve, before the segment has to be subdivided.
+## @deprecated
+@export_range(0.1, 16.0, 0.1, "or_greater", "or_lesser")
+var tessellation_tolerence: float = 6.0 :
+ set(value): _points.tessellation_tolerance = value
+ get: return _points.tessellation_tolerance
+
+@export_group("Collision")
+
+## Controls which method should be used to generate the collision shape.
+@export var collision_generation_method := CollisionGenerationMethod.Fast : set = set_collision_generation_method
+
+## Controls when to update collisions.
+@export var collision_update_mode := CollisionUpdateMode.Editor : set = set_collision_update_mode
+
+## Controls size of generated polygon for CollisionPolygon2D.
+@export_range(0.0, 64.0, 1.0, "or_greater")
+var collision_size: float = 32 : set = set_collision_size
+
+## Controls offset of generated polygon for CollisionPolygon2D.
+@export_range(-64.0, 64.0, 1.0, "or_greater", "or_lesser")
+var collision_offset: float = 0.0 : set = set_collision_offset
+
+## NodePath to CollisionPolygon2D node for which polygon data will be generated.
+@export_node_path("CollisionPolygon2D") var collision_polygon_node_path: NodePath : set = set_collision_polygon_node_path
+
+#####################
+#-SETTERS / GETTERS-#
+#####################
+
+func set_collision_polygon_node_path(value: NodePath) -> void:
+ collision_polygon_node_path = value
+ set_as_dirty()
+
+ if not is_inside_tree():
+ return
+
+ if collision_polygon_node_path.is_empty():
+ _collision_polygon_node = null
+ return
+
+ _collision_polygon_node = get_node(collision_polygon_node_path) as CollisionPolygon2D
+
+ if not _collision_polygon_node:
+ push_error("collision_polygon_node_path should point to proper CollisionPolygon2D node.")
+
+
+func get_collision_polygon_node() -> CollisionPolygon2D:
+ return _collision_polygon_node
+
+
+func get_point_array() -> SS2D_Point_Array:
+ return _points
+
+
+func set_point_array(a: SS2D_Point_Array) -> void:
+ if _points != null:
+ if _points.is_connected("update_finished", self._points_modified):
+ _points.disconnect("update_finished", self._points_modified)
+ if _points.material_override_changed.is_connected(_handle_material_override_change):
+ _points.material_override_changed.disconnect(_handle_material_override_change)
+ if a == null:
+ a = SS2D_Point_Array.new()
+ _points = a
+ _points.connect("update_finished", self._points_modified)
+ _points.material_override_changed.connect(_handle_material_override_change)
+ clear_cached_data()
+ set_as_dirty()
+ notify_property_list_changed()
+
+
+func _refresh_action(value: String) -> void:
+ if value.length() > 0:
+ _points_modified()
+
+
+func _make_unique_action(value: String) -> void:
+ if value.length() > 0:
+ emit_signal("make_unique_pressed", self)
+
+
+func set_flip_edges(b: bool) -> void:
+ flip_edges = b
+ set_as_dirty()
+ notify_property_list_changed()
+
+
+func set_render_edges(b: bool) -> void:
+ render_edges = b
+ set_as_dirty()
+ notify_property_list_changed()
+
+
+func set_collision_generation_method(value: CollisionGenerationMethod) -> void:
+ collision_generation_method = value
+ set_as_dirty()
+
+
+func set_collision_update_mode(value: CollisionUpdateMode) -> void:
+ collision_update_mode = value
+ set_as_dirty()
+
+
+func set_collision_size(s: float) -> void:
+ collision_size = s
+ set_as_dirty()
+ notify_property_list_changed()
+
+
+func set_collision_offset(s: float) -> void:
+ collision_offset = s
+ set_as_dirty()
+ notify_property_list_changed()
+
+
+# FIXME: Only used by unit test.
+func set_curve(curve: Curve2D) -> void:
+ _points.begin_update()
+ _points.clear()
+
+ for i in curve.get_point_count():
+ _points.add_point(curve.get_point_position(i))
+
+ _points.end_update()
+
+
+## Deprecated. Use get_point_array().get_curve() instead.
+## @deprecated
+func get_curve() -> Curve2D:
+ return _points.get_curve()
+
+
+func _set_editor_debug(value: bool) -> void:
+ editor_debug = value
+ set_as_dirty()
+ notify_property_list_changed()
+
+
+func set_render_node_light_masks(value: int) -> void:
+ # TODO: This method should be called when user changes mask in the inspector.
+ var render_parent: SS2D_Shape_Render = _get_rendering_nodes_parent()
+ for c: CanvasItem in render_parent.get_children():
+ c.light_mask = value
+ render_parent.light_mask = value
+
+
+func set_render_node_owners(v: bool) -> void:
+ if Engine.is_editor_hint():
+ # Force scene tree update
+ var render_parent: SS2D_Shape_Render = _get_rendering_nodes_parent()
+ var new_owner: Node = null
+ if v:
+ new_owner = get_tree().edited_scene_root
+ render_parent.set_owner(new_owner)
+
+ # Set owner recurisvely
+ for c in render_parent.get_children():
+ c.set_owner(new_owner)
+
+ # Force update
+ var dummy_name := "__DUMMY__"
+ if has_node(dummy_name):
+ var n: Node = get_node(dummy_name)
+ remove_child(n)
+ n.queue_free()
+
+ var dummy := Node2D.new()
+ dummy.name = dummy_name
+ add_child(dummy)
+ dummy.set_owner(new_owner)
+
+
+func update_render_nodes() -> void:
+# set_render_node_owners(editor_debug)
+ set_render_node_light_masks(light_mask)
+
+
+## Deprecated. Use get_point_array().tessellation_stages instead.
+## @deprecated
+func set_tessellation_stages(value: int) -> void:
+ _points.tessellation_stages = value
+
+
+## Deprecated. Use get_point_array().tessellation_tolerance instead.
+## @deprecated
+func set_tessellation_tolerence(value: float) -> void:
+ _points.tessellation_tolerance = value
+
+
+## Deprecated. Use get_point_array().curve_bake_interval instead.
+## @deprecated
+func set_curve_bake_interval(f: float) -> void:
+ _points.curve_bake_interval = f
+
+
+func set_color_encoding(i: SS2D_Edge.COLOR_ENCODING) -> void:
+ color_encoding = i
+ notify_property_list_changed()
+ set_as_dirty()
+
+
+func _set_material(value: SS2D_Material_Shape) -> void:
+ if (
+ shape_material != null
+ and shape_material.is_connected("changed", self._handle_material_change)
+ ):
+ shape_material.disconnect("changed", self._handle_material_change)
+
+ shape_material = value
+ if shape_material != null:
+ shape_material.connect("changed", self._handle_material_change)
+ set_as_dirty()
+ notify_property_list_changed()
+
+
+func set_material_overrides(dict: Dictionary) -> void:
+ material_overrides = {}
+ if dict == null:
+ return
+ _points.set_material_overrides(dict)
+
+
+#########
+#-CURVE-#
+#########
+
+## Deprecated. Use get_point_array().get_vertices() instead.
+## @deprecated
+func get_vertices() -> PackedVector2Array:
+ return _points.get_vertices()
+
+
+## Deprecated. Use get_point_array().get_tessellated_points() instead.
+## @deprecated
+func get_tessellated_points() -> PackedVector2Array:
+ return _points.get_tessellated_points()
+
+
+## Deprecated. Use get_point_array().invert_point_order() instead.
+## @deprecated
+func invert_point_order() -> void:
+ _points.invert_point_order()
+
+
+## Deprecated. Use get_point_array().clear() instead.
+## @deprecated
+func clear_points() -> void:
+ _points.clear()
+
+
+func adjust_add_point_index(index: int) -> int:
+ # Don't allow a point to be added after the last point of the closed shape or before the first
+ if _has_closing_point():
+ if index < 0 or (index > get_point_count() - 1):
+ index = maxi(get_point_count() - 1, 0)
+ if index < 1:
+ index = 1
+ return index
+
+
+# FIXME: Only unit tests use this.
+func add_points(verts: PackedVector2Array, starting_index: int = -1, key: int = -1) -> PackedInt32Array:
+ starting_index = adjust_add_point_index(starting_index)
+ var keys := PackedInt32Array()
+ _points.begin_update()
+ for i in range(0, verts.size(), 1):
+ var v: Vector2 = verts[i]
+ if starting_index != -1:
+ keys.push_back(_points.add_point(v, starting_index + i, key))
+ else:
+ keys.push_back(_points.add_point(v, starting_index, key))
+ _points.end_update()
+ return keys
+
+
+## Deprecated. Use get_point_array().add_point() instead.
+## @deprecated
+func add_point(pos: Vector2, index: int = -1, key: int = -1) -> int:
+ return _points.add_point(pos, adjust_add_point_index(index), key)
+
+
+## Is this shape closed, i.e. last point is constrained to the first point.
+func is_shape_closed() -> bool:
+ if _points.get_point_count() < 4:
+ return false
+ return _has_closing_point()
+
+
+## Is this shape not yet closed but should be.[br]
+## Returns [code]false[/code] for open shapes.[br]
+func can_close() -> bool:
+ return _points.get_point_count() > 2 and _has_closing_point() == false
+
+
+## Will mutate the _points to ensure this is a closed_shape.[br]
+## Last point will be constrained to first point.[br]
+## Returns key of a point used to close the shape.[br]
+## [param key] suggests which key to use instead of auto-generated.[br]
+func close_shape(key: int = -1) -> int:
+ if not can_close():
+ return -1
+
+ var key_first: int = _points.get_point_key_at_index(0)
+ var key_last: int = _points.get_point_key_at_index(_points.get_point_count() - 1)
+
+ if get_point_position(key_first) != get_point_position(key_last):
+ key_last = _points.add_point(_points.get_point_position(key_first), -1, key)
+ _points.set_constraint(key_first, key_last, SS2D_Point_Array.CONSTRAINT.ALL)
+
+ return key_last
+
+
+## Open shape by removing edge that starts at specified point index.
+func open_shape_at_edge(edge_start_idx: int) -> void:
+ var last_idx: int = get_point_count() - 1
+ if is_shape_closed():
+ remove_point(get_point_key_at_index(last_idx))
+ if edge_start_idx < last_idx:
+ for i in range(edge_start_idx + 1):
+ _points.set_point_index(_points.get_point_key_at_index(0), last_idx)
+ else:
+ push_warning("Can't open a shape that is not a closed shape.")
+
+
+## Undo shape opening done by [method open_shape_at_edge].
+func undo_open_shape_at_edge(edge_start_idx: int, closing_index: int) -> void:
+ var last_idx := get_point_count() - 1
+ if edge_start_idx < last_idx:
+ for i in range(edge_start_idx + 1):
+ _points.set_point_index(_points.get_point_key_at_index(last_idx), 0)
+ if can_close():
+ close_shape(closing_index)
+
+
+func _has_closing_point() -> bool:
+ if _points.get_point_count() < 2:
+ return false
+ var key1: int = _points.get_point_key_at_index(0)
+ var key2: int = _points.get_point_key_at_index(_points.get_point_count() - 1)
+ return _points.get_point_constraint(key1, key2) == SS2D_Point_Array.CONSTRAINT.ALL
+
+
+## Deprecated. Use get_point_array().begin_update() instead.
+## @deprecated
+func begin_update() -> void:
+ _points.begin_update()
+
+
+## Deprecated. Use get_point_array().end_update() instead.
+## @deprecated
+func end_update() -> void:
+ _points.end_update()
+
+
+## Deprecated. Use get_point_array().is_updating() instead.
+## @deprecated
+func is_updating() -> bool:
+ return _points.is_updating()
+
+
+## Deprecated. Use get_point_array().get_next_key() instead.
+## @deprecated
+func get_next_key() -> int:
+ return _points.get_next_key()
+
+
+## Deprecated. Use get_point_array().reserve_key() instead.
+## @deprecated
+func reserve_key() -> int:
+ return _points.reserve_key()
+
+
+func _points_modified() -> void:
+ set_as_dirty()
+ points_modified.emit()
+
+
+func _is_array_index_in_range(a: Array, i: int) -> bool:
+ return a.size() > i and i >= 0;
+
+
+## Deprecated. Use respective function in get_point_array() instead.
+## @deprecated
+func is_index_in_range(idx: int) -> bool:
+ return _points.is_index_in_range(idx)
+
+
+## Deprecated. Use respective function in get_point_array() instead.
+## @deprecated
+func set_point_position(key: int, pos: Vector2) -> void:
+ _points.set_point_position(key, pos)
+
+
+## Deprecated. Use respective function in get_point_array() instead.
+## @deprecated
+func remove_point(key: int) -> void:
+ _points.remove_point(key)
+
+
+## Deprecated. Use respective function in get_point_array() instead.
+## @deprecated
+func remove_point_at_index(idx: int) -> void:
+ _points.remove_point_at_index(idx)
+
+
+func clone(clone_point_array: bool = true) -> SS2D_Shape:
+ var copy := SS2D_Shape.new()
+ copy.transform = transform
+ copy.modulate = modulate
+ copy.shape_material = shape_material
+ copy.editor_debug = editor_debug
+ copy.flip_edges = flip_edges
+ copy.editor_debug = editor_debug
+ copy.collision_size = collision_size
+ copy.collision_offset = collision_offset
+ #copy.material_overrides = s.material_overrides
+ copy.name = get_name().rstrip("0123456789")
+ if clone_point_array:
+ copy.set_point_array(_points.clone(true))
+ return copy
+
+
+#######################
+#-POINT ARRAY WRAPPER-#
+#######################
+
+## Deprecated. Use respective function in get_point_array() instead.
+## @deprecated
+func has_point(key: int) -> bool:
+ return _points.has_point(key)
+
+
+## Deprecated. Use respective function in get_point_array() instead.
+## @deprecated
+func get_all_point_keys() -> PackedInt32Array:
+ return _points.get_all_point_keys()
+
+
+## Deprecated. Use respective function in get_point_array() instead.
+## @deprecated
+func get_point_key_at_index(idx: int) -> int:
+ return _points.get_point_key_at_index(idx)
+
+
+## Deprecated. Use respective function in get_point_array() instead.
+## @deprecated
+func get_point_at_index(idx: int) -> SS2D_Point:
+ return _points.get_point_at_index(idx)
+
+
+## Deprecated. Use respective function in get_point_array() instead.
+## @deprecated
+func get_point_index(key: int) -> int:
+ return _points.get_point_index(key)
+
+
+## Deprecated. Use respective function in get_point_array() instead.
+## @deprecated
+func set_point_in(key: int, v: Vector2) -> void:
+ _points.set_point_in(key, v)
+
+
+## Deprecated. Use respective function in get_point_array() instead.
+## @deprecated
+func set_point_out(key: int, v: Vector2) -> void:
+ _points.set_point_out(key, v)
+
+
+## Deprecated. Use respective function in get_point_array() instead.
+## @deprecated
+func get_point_in(key: int) -> Vector2:
+ return _points.get_point_in(key)
+
+
+## Deprecated. Use respective function in get_point_array() instead.
+## @deprecated
+func get_point_out(key: int) -> Vector2:
+ return _points.get_point_out(key)
+
+
+func get_closest_point(to_point: Vector2) -> Vector2:
+ return _points.get_curve().get_closest_point(to_point)
+
+
+func get_closest_point_straight_edge(to_point: Vector2) -> Vector2:
+ return _points.get_curve_no_control_points().get_closest_point(to_point)
+
+
+func get_closest_offset_straight_edge(to_point: Vector2) -> float:
+ return _points.get_curve_no_control_points().get_closest_offset(to_point)
+
+
+func get_closest_offset(to_point: Vector2) -> float:
+ return _points.get_curve().get_closest_offset(to_point)
+
+
+## Deprecated. Use respective function in get_point_array() instead.
+## @deprecated
+func disable_constraints() -> void:
+ _points.disable_constraints()
+
+
+## Deprecated. Use respective function in get_point_array() instead.
+## @deprecated
+func enable_constraints() -> void:
+ _points.enable_constraints()
+
+
+## Deprecated. Use respective function in get_point_array() instead.
+## @deprecated
+func get_point_count() -> int:
+ return _points.get_point_count()
+
+
+func get_edges() -> Array[SS2D_Edge]:
+ return _edges
+
+
+## Deprecated. Use respective function in get_point_array() instead.
+## @deprecated
+func get_point_position(key: int) -> Vector2:
+ return _points.get_point_position(key)
+
+
+## Deprecated. Use respective function in get_point_array() instead.
+## @deprecated
+func get_point(key: int) -> SS2D_Point:
+ return _points.get_point(key)
+
+
+## Deprecated. Use respective function in get_point_array() instead.
+## @deprecated
+func get_point_constraints(key: int) -> Dictionary:
+ return _points.get_point_constraints(key)
+
+
+## Deprecated. Use respective function in get_point_array() instead.
+## @deprecated
+func get_point_constraint(key1: int, key2: int) -> SS2D_Point_Array.CONSTRAINT:
+ return _points.get_point_constraint(key1, key2)
+
+
+## Deprecated. Use respective function in get_point_array() instead.
+## @deprecated
+func set_constraint(key1: int, key2: int, c: SS2D_Point_Array.CONSTRAINT) -> void:
+ _points.set_constraint(key1, key2, c)
+
+
+## Deprecated. Use respective function in get_point_array() instead.
+## @deprecated
+func set_point(key: int, value: SS2D_Point) -> void:
+ _points.set_point(key, value)
+
+
+## Deprecated. Use respective property in get_point_array().get_point_properties() instead.
+## @deprecated
+func set_point_width(key: int, w: float) -> void:
+ _points.get_point_properties(key).width = w
+
+
+## Deprecated. Use respective property in get_point_array().get_point_properties() instead.
+## @deprecated
+func get_point_width(key: int) -> float:
+ return _points.get_point_properties(key).width
+
+
+## Deprecated. Use respective property in get_point_array().get_point_properties() instead.
+## @deprecated
+func set_point_texture_index(key: int, tex_idx: int) -> void:
+ _points.get_point_properties(key).texture_idx = tex_idx
+
+
+## Deprecated. Use respective property in get_point_array().get_point_properties() instead.
+## @deprecated
+func get_point_texture_index(key: int) -> int:
+ return _points.get_point_properties(key).texture_idx
+
+
+## Deprecated. Use respective property in get_point_array().get_point_properties() instead.
+## @deprecated
+func set_point_texture_flip(key: int, flip: bool) -> void:
+ _points.get_point_properties(key).flip = flip
+
+
+## Deprecated. Use respective property in get_point_array().get_point_properties() instead.
+## @deprecated
+func get_point_texture_flip(key: int) -> bool:
+ return _points.get_point_properties(key).flip
+
+
+## Deprecated. Use respective function in get_point_array() instead.
+## @deprecated
+func get_point_properties(key: int) -> SS2D_VertexProperties:
+ return _points.get_point_properties(key)
+
+
+## Deprecated. Use respective function in get_point_array() instead.
+## @deprecated
+func set_point_properties(key: int, properties: SS2D_VertexProperties) -> void:
+ _points.set_point_properties(key, properties)
+
+
+#########
+#-GODOT-#
+#########
+
+func _init() -> void:
+ set_point_array(SS2D_Point_Array.new())
+
+
+func _enter_tree() -> void:
+ # Call this again because get_node() only works when the node is inside the tree
+ set_collision_polygon_node_path(collision_polygon_node_path)
+
+ # Handle material changes if scene is (re-)entered (e.g. after switching to another)
+ if shape_material != null:
+ if not shape_material.is_connected("changed", self._handle_material_change):
+ shape_material.connect("changed", self._handle_material_change)
+
+
+func _get_rendering_nodes_parent() -> SS2D_Shape_Render:
+ var render_parent_name := "_SS2D_RENDER"
+ var render_parent: SS2D_Shape_Render = null
+ if not has_node(render_parent_name):
+ render_parent = SS2D_Shape_Render.new()
+ render_parent.name = render_parent_name
+ render_parent.light_mask = light_mask
+ add_child(render_parent)
+ if editor_debug and Engine.is_editor_hint():
+ render_parent.set_owner(get_tree().edited_scene_root)
+ else:
+ render_parent = get_node(render_parent_name)
+ return render_parent
+
+
+# Returns true if the children have changed.
+func _create_rendering_nodes(size: int) -> bool:
+ var render_parent: SS2D_Shape_Render = _get_rendering_nodes_parent()
+ var child_count := render_parent.get_child_count()
+ var delta := size - child_count
+ #print ("%s | %s | %s" % [child_count, size, delta])
+ # Size and child_count match
+ if delta == 0:
+ return false
+
+ # More children than needed
+ elif delta < 0:
+ var children := render_parent.get_children()
+ for i in range(0, abs(delta), 1):
+ var child: SS2D_Shape_Render = children[child_count - 1 - i]
+ render_parent.remove_child(child)
+ child.set_mesh(null)
+ child.queue_free()
+
+ # Fewer children than needed
+ elif delta > 0:
+ for i in range(0, delta, 1):
+ var child := SS2D_Shape_Render.new()
+ child.light_mask = light_mask
+ render_parent.add_child(child)
+ if editor_debug and Engine.is_editor_hint():
+ child.set_owner(get_tree().edited_scene_root)
+ return true
+
+
+# Takes an array of SS2D_Meshes and returns a flat array of SS2D_Meshes.
+# If a SS2D_Mesh has n meshes, will return an array contain n SS2D_Mesh.
+# The returned array will consist of SS2D_Meshes each with a SS2D_Mesh::meshes array of size 1.
+func _draw_flatten_meshes_array(meshes: Array[SS2D_Mesh]) -> Array[SS2D_Mesh]:
+ var flat_meshes: Array[SS2D_Mesh] = []
+ for ss2d_mesh in meshes:
+ for godot_mesh in ss2d_mesh.meshes:
+ var new_mesh: SS2D_Mesh = ss2d_mesh.duplicate(false)
+ var arr: Array[ArrayMesh] = [godot_mesh]
+ new_mesh.meshes = arr
+ flat_meshes.push_back(new_mesh)
+ return flat_meshes
+
+
+func _draw() -> void:
+ var flat_meshes: Array[SS2D_Mesh] = _draw_flatten_meshes_array(_meshes)
+ _create_rendering_nodes(flat_meshes.size())
+ var render_parent: SS2D_Shape_Render = _get_rendering_nodes_parent()
+ var render_nodes := render_parent.get_children()
+ #print ("RENDER | %s" % [render_nodes])
+ #print ("MESHES | %s" % [flat_meshes])
+ for i in range(0, flat_meshes.size(), 1):
+ var m: SS2D_Mesh = flat_meshes[i]
+ var render_node: SS2D_Shape_Render = render_nodes[i]
+ render_node.set_mesh(m)
+
+ if editor_debug and Engine.is_editor_hint():
+ _draw_debug(SS2D_Shape.sort_by_z_index(_edges))
+
+
+func _draw_debug(edges: Array[SS2D_Edge]) -> void:
+ for e in edges:
+ for q in e.quads:
+ q.render_lines(self)
+
+ var _range := range(0, e.quads.size(), 1)
+ for i: int in _range:
+ var q := e.quads[i]
+ if not (i % 3 == 0):
+ continue
+ q.render_points(3, 0.5, self)
+
+ for i: int in _range:
+ var q := e.quads[i]
+ if not ((i + 1) % 3 == 0):
+ continue
+ q.render_points(2, 0.75, self)
+
+ for i: int in _range:
+ var q := e.quads[i]
+ if not ((i + 2) % 3 == 0):
+ continue
+ q.render_points(1, 1.0, self)
+
+
+func _exit_tree() -> void:
+ if shape_material != null:
+ if shape_material.is_connected("changed", self._handle_material_change):
+ shape_material.disconnect("changed", self._handle_material_change)
+
+
+############
+#-GEOMETRY-#
+############
+
+
+func should_flip_edges() -> bool:
+ if is_shape_closed():
+ return (are_points_clockwise() == flip_edges)
+ else:
+ return flip_edges
+
+
+func _generate_collision_points_precise() -> PackedVector2Array:
+ var points := PackedVector2Array()
+ var num_points: int = _points.get_point_count()
+ if num_points < 2:
+ return points
+
+ var csize: float = 1.0 if is_shape_closed() else collision_size
+ var indices := PackedInt32Array(range(num_points))
+ var edge_data := SS2D_IndexMap.new(indices, null)
+ var edge: SS2D_Edge = _build_edge_with_material(edge_data, collision_offset - 1.0, csize)
+ _weld_quad_array(edge.quads, false)
+
+ if is_shape_closed():
+ var first_quad: SS2D_Quad = edge.quads[0]
+ var last_quad: SS2D_Quad = edge.quads.back()
+ SS2D_Shape.weld_quads(last_quad, first_quad)
+
+ if not edge.quads.is_empty():
+ # Top edge (typically point A unless corner quad)
+ for quad in edge.quads:
+ if quad.corner == SS2D_Quad.CORNER.NONE:
+ points.push_back(quad.pt_a)
+ elif quad.corner == SS2D_Quad.CORNER.OUTER:
+ points.push_back(quad.pt_d)
+ elif quad.corner == SS2D_Quad.CORNER.INNER:
+ pass
+
+ if not is_shape_closed():
+ # Right Edge (point d, the first or final quad will never be a corner)
+ points.push_back(edge.quads[edge.quads.size() - 1].pt_d)
+
+ # Bottom Edge (typically point c)
+ for quad_index in edge.quads.size():
+ var quad: SS2D_Quad = edge.quads[edge.quads.size() - 1 - quad_index]
+ if quad.corner == SS2D_Quad.CORNER.NONE:
+ points.push_back(quad.pt_c)
+ elif quad.corner == SS2D_Quad.CORNER.OUTER:
+ pass
+ elif quad.corner == SS2D_Quad.CORNER.INNER:
+ points.push_back(quad.pt_b)
+
+ # Left Edge (point b)
+ points.push_back(edge.quads[0].pt_b)
+ return points
+
+
+func _generate_collision_points_fast() -> PackedVector2Array:
+ return _points.get_tessellated_points()
+
+
+func bake_collision() -> void:
+ if not _collision_polygon_node:
+ return
+
+ if collision_update_mode == CollisionUpdateMode.Editor and not Engine.is_editor_hint() \
+ or collision_update_mode == CollisionUpdateMode.Runtime and Engine.is_editor_hint():
+ return
+
+ var generated_points: PackedVector2Array
+
+ if collision_generation_method == CollisionGenerationMethod.Fast and is_shape_closed():
+ generated_points = _generate_collision_points_fast()
+ else:
+ generated_points = _generate_collision_points_precise()
+
+ var xform := _collision_polygon_node.get_global_transform().affine_inverse() * get_global_transform()
+ _collision_polygon_node.polygon = xform * generated_points
+
+
+func cache_edges() -> void:
+ if shape_material != null and render_edges:
+ _edges = _build_edges(shape_material, _points.get_vertices())
+ else:
+ _edges = []
+
+
+func cache_meshes() -> void:
+ if shape_material != null:
+ _meshes = _build_meshes(SS2D_Shape.sort_by_z_index(_edges))
+
+
+func _build_meshes(edges: Array[SS2D_Edge]) -> Array[SS2D_Mesh]:
+ var meshes: Array[SS2D_Mesh] = []
+ if _points == null or _points.get_point_count() < 2:
+ return meshes
+
+ var produced_fill_mesh := false
+ for e in edges:
+ if not produced_fill_mesh and is_shape_closed():
+ if e.z_index > shape_material.fill_texture_z_index:
+ # Produce Fill Meshes
+ for m in _build_fill_mesh(_points.get_tessellated_points(), shape_material):
+ meshes.push_back(m)
+ produced_fill_mesh = true
+
+ # Produce edge Meshes
+ for m in e.get_meshes(color_encoding):
+ meshes.push_back(m)
+ if not produced_fill_mesh and is_shape_closed():
+ for m in _build_fill_mesh(_points.get_tessellated_points(), shape_material):
+ meshes.push_back(m)
+ produced_fill_mesh = true
+ return meshes
+
+
+func _build_fill_mesh(points: PackedVector2Array, s_mat: SS2D_Material_Shape) -> Array[SS2D_Mesh]:
+ var meshes: Array[SS2D_Mesh] = []
+ if s_mat == null:
+ return meshes
+ if s_mat.fill_textures.is_empty():
+ return meshes
+ if points.size() < 3:
+ return meshes
+
+ var tex: Texture2D = null
+ if s_mat.fill_textures.is_empty():
+ return meshes
+ tex = s_mat.fill_textures[0]
+ var tex_size: Vector2 = tex.get_size()
+
+ # Points to produce the fill mesh
+ var fill_points: PackedVector2Array = PackedVector2Array()
+ var polygons: Array[PackedVector2Array] = Geometry2D.offset_polygon(
+ PackedVector2Array(points), tex_size.x * s_mat.fill_mesh_offset
+ )
+ points = polygons[0]
+ fill_points.resize(points.size())
+ for i in range(points.size()):
+ fill_points[i] = points[i]
+
+ # Produce the fill mesh
+ var fill_tris: PackedInt32Array = Geometry2D.triangulate_polygon(fill_points)
+ if fill_tris.is_empty():
+ push_error("'%s': Couldn't Triangulate shape" % name)
+ return []
+
+ var st: SurfaceTool
+ st = SurfaceTool.new()
+ st.begin(Mesh.PRIMITIVE_TRIANGLES)
+
+ var uv_points := _get_uv_points(points, s_mat, tex_size)
+
+ for i in range(0, fill_tris.size() - 1, 3):
+ st.set_color(Color.WHITE)
+ _add_uv_to_surface_tool(st, uv_points[fill_tris[i]])
+ st.add_vertex(Vector3(points[fill_tris[i]].x, points[fill_tris[i]].y, 0))
+ st.set_color(Color.WHITE)
+ _add_uv_to_surface_tool(st, uv_points[fill_tris[i + 1]])
+ st.add_vertex(Vector3(points[fill_tris[i + 1]].x, points[fill_tris[i + 1]].y, 0))
+ st.set_color(Color.WHITE)
+ _add_uv_to_surface_tool(st, uv_points[fill_tris[i + 2]])
+ st.add_vertex(Vector3(points[fill_tris[i + 2]].x, points[fill_tris[i + 2]].y, 0))
+ st.index()
+ st.generate_normals()
+ st.generate_tangents()
+ var array_mesh := st.commit()
+ var flip := false
+ var trans := Transform2D()
+ var mesh_data := SS2D_Mesh.new(tex, flip, trans, [array_mesh])
+ mesh_data.material = s_mat.fill_mesh_material
+ mesh_data.z_index = s_mat.fill_texture_z_index
+ mesh_data.z_as_relative = true
+ mesh_data.show_behind_parent = s_mat.fill_texture_show_behind_parent
+ meshes.push_back(mesh_data)
+
+ return meshes
+
+
+func _get_uv_points(
+ points: PackedVector2Array,
+ s_material: SS2D_Material_Shape,
+ tex_size: Vector2
+) -> PackedVector2Array:
+ var transformation: Transform2D = global_transform
+
+ # If relative position ... undo translation from global_transform
+ if not s_material.fill_texture_absolute_position:
+ transformation = transformation.translated(-global_position)
+
+ # Scale
+ var tex_scale := 1.0 / s_material.fill_texture_scale
+ transformation = transformation.scaled(Vector2(tex_scale, tex_scale))
+
+ # If relative rotation ... undo rotation from global_transform
+ if not s_material.fill_texture_absolute_rotation:
+ transformation = transformation.rotated(-global_rotation)
+
+ # Rotate the desired extra amount
+ transformation = transformation.rotated(-deg_to_rad(s_material.fill_texture_angle_offset))
+
+ # Shift the desired amount (adjusted so it's scale independent)
+ transformation = transformation.translated(-s_material.fill_texture_offset / s_material.fill_texture_scale)
+
+ # Convert local space to UV
+ transformation = transformation.scaled(Vector2(1 / tex_size.x, 1 / tex_size.y))
+
+ return transformation * points
+
+
+## Given three colinear points p, q, r, the function checks if point q lies on line segment 'pr'.[br]
+## See: https://www.geeksforgeeks.org/check-if-two-given-line-segments-intersect/
+static 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)
+ )
+
+
+static func get_points_orientation(points: PackedVector2Array) -> ORIENTATION:
+ var point_count: int = points.size()
+ if point_count < 3:
+ return ORIENTATION.COLINEAR
+
+ var sum := 0.0
+ for i in point_count:
+ var pt := points[i]
+ var pt2 := points[(i + 1) % point_count]
+ sum += pt.cross(pt2)
+
+ # Colinear
+ if sum == 0.0:
+ return ORIENTATION.COLINEAR
+
+ # Clockwise
+ if sum > 0.0:
+ return ORIENTATION.CLOCKWISE
+ return ORIENTATION.C_CLOCKWISE
+
+
+func are_points_clockwise() -> bool:
+ var points: PackedVector2Array = _points.get_tessellated_points()
+ var orient: ORIENTATION = SS2D_Shape.get_points_orientation(points)
+ return orient == ORIENTATION.CLOCKWISE
+
+
+func _add_uv_to_surface_tool(surface_tool: SurfaceTool, uv: Vector2) -> void:
+ surface_tool.set_uv(uv)
+ surface_tool.set_uv2(uv)
+
+
+static func build_quad_from_two_points(
+ pt: Vector2,
+ pt_next: Vector2,
+ tex: Texture2D,
+ width: float,
+ flip_x: bool,
+ flip_y: bool,
+ first_point: bool,
+ last_point: bool,
+ custom_offset: float,
+ custom_extends: float,
+ fit_texture: SS2D_Material_Edge.FITMODE
+) -> SS2D_Quad:
+ # Create new quad
+ var quad := SS2D_Quad.new()
+ quad.texture = tex
+ quad.color = Color(1.0, 1.0, 1.0, 1.0)
+ quad.flip_texture = flip_x
+ quad.fit_texture = fit_texture
+
+ # Calculate the normal
+ var delta: Vector2 = pt_next - pt
+ var delta_normal := delta.normalized()
+ var normal_direction := Vector2(delta.y, -delta.x).normalized()
+ var normal_length: float = width
+ var normal_with_magnitude: Vector2 = normal_direction * (normal_length * 0.5)
+ if flip_y:
+ normal_with_magnitude *= -1
+ var offset: Vector2 = normal_with_magnitude * custom_offset
+
+ # If is first or last point, extend past the normal boundary by 'custom_extends' pixels
+ if first_point:
+ pt -= (delta_normal * custom_extends)
+ if last_point:
+ pt_next += (delta_normal * custom_extends)
+
+ ##############################################
+ # QUAD POINT ILLUSTRATION # #
+ ##############################################
+ # LENGTH #
+ # <--------------> #
+ # pt_a -> O--------O <- pt_d â–² #
+ # | | | #
+ # | pt | | WIDTH #
+ # | | | #
+ # pt_b -> O--------O <- pt_c â–¼ #
+ ##############################################
+ ##############################################
+
+ quad.pt_a = pt + normal_with_magnitude + offset
+ quad.pt_b = pt - normal_with_magnitude + offset
+ quad.pt_c = pt_next - normal_with_magnitude + offset
+ quad.pt_d = pt_next + normal_with_magnitude + offset
+
+ return quad
+
+
+## Builds a corner quad. [br]
+## - [param pt] is the center of this corner quad. [br]
+## - [param width] will scale the quad in line with the next point (one dimension). [br]
+## - [param prev_width] will scale the quad in line with the prev point (hte other dimension). [br]
+## - [param custom_scale] will scale the quad in both dimensions. [br]
+static func build_quad_corner(
+ pt_next: Vector2,
+ pt: Vector2,
+ pt_prev: Vector2,
+ pt_width: float,
+ pt_prev_width: float,
+ flip_edges_: bool,
+ corner_status: int,
+ texture: Texture2D,
+ size: Vector2,
+ custom_scale: float,
+ custom_offset: float
+) -> SS2D_Quad:
+ var new_quad := SS2D_Quad.new()
+
+ # :BUILD PLAN:
+ # OUTER CORNER INNER CORNER
+ #
+ # 0------A-----D 0-----0
+ # | 1 : 2 | | 3 :
+ # 0......B.....C | :
+ # : | 0-------D-----A
+ # : 3 | | 1 | 2 :
+ # 0-----0 0.......C.....B
+ #
+ # 1-previous, 2-current, 3-next (points)
+
+ var quad_size: Vector2 = size * 0.5
+ var dir_12: Vector2 = (pt - pt_prev).normalized()
+ var dir_23: Vector2 = (pt_next - pt).normalized()
+ var offset_12: Vector2 = dir_12 * custom_scale * pt_width * quad_size
+ var offset_23: Vector2 = dir_23 * custom_scale * pt_prev_width * quad_size
+ var custom_offset_13: Vector2 = (dir_12 - dir_23) * custom_offset * quad_size
+
+ if flip_edges_:
+ offset_12 *= -1
+ offset_23 *= -1
+ custom_offset_13 *= -1
+
+ # Should we mirror internal ABCD vertices relative to quad center.
+ # - Historically, quad internal vertices are flipped for inner corner quads (see illustration).
+ # - Value: 1.0 for outer, -1.0 for inner (mirrored).
+ var mirror: float = -1.0 if corner_status == SS2D_Quad.CORNER.INNER else 1.0
+
+ new_quad.pt_a = pt + (-offset_12 - offset_23 + custom_offset_13) * mirror
+ new_quad.pt_b = pt + (-offset_12 + offset_23 + custom_offset_13) * mirror
+ new_quad.pt_c = pt + (offset_12 + offset_23 + custom_offset_13) * mirror
+ new_quad.pt_d = pt + (offset_12 - offset_23 + custom_offset_13) * mirror
+
+ new_quad.corner = corner_status
+ new_quad.texture = texture
+
+ return new_quad
+
+
+func _get_width_for_tessellated_point(
+ points: PackedVector2Array,
+ t_idx: int
+) -> float:
+ var v_idx := _points.get_tesselation_vertex_mapping().tess_to_vertex_index(t_idx)
+ var v_idx_next := SS2D_PluginFunctionality.get_next_point_index(v_idx, points)
+ var w1: float = _points.get_point_properties(_points.get_point_key_at_index(v_idx)).width
+ var w2: float = _points.get_point_properties(_points.get_point_key_at_index(v_idx_next)).width
+ var ratio: float = get_ratio_from_tessellated_point_to_vertex(t_idx)
+ return lerp(w1, w2, ratio)
+
+
+## Mutates two quads to be welded.[br]
+## Returns the midpoint of the weld.[br]
+static func weld_quads(a: SS2D_Quad, b: SS2D_Quad, custom_scale: float = 1.0) -> Vector2:
+ var midpoint := Vector2(0, 0)
+ # If both quads are not a corner
+ if a.corner == SS2D_Quad.CORNER.NONE and b.corner == SS2D_Quad.CORNER.NONE:
+ var needed_height: float = (a.get_height_average() + b.get_height_average()) / 2.0
+
+ var pt1: Vector2 = (a.pt_d + b.pt_a) * 0.5
+ var pt2: Vector2 = (a.pt_c + b.pt_b) * 0.5
+
+ midpoint = Vector2(pt1 + pt2) / 2.0
+ var half_line: Vector2 = (pt2 - midpoint).normalized() * needed_height * custom_scale / 2.0
+
+ if half_line != Vector2.ZERO:
+ pt2 = midpoint + half_line
+ pt1 = midpoint - half_line
+
+ a.pt_d = pt1
+ a.pt_c = pt2
+ b.pt_a = pt1
+ b.pt_b = pt2
+
+ # If either quad is a corner
+ else:
+ if a.corner == SS2D_Quad.CORNER.OUTER:
+ b.pt_a = a.pt_c
+ b.pt_b = a.pt_b
+ midpoint = (b.pt_a + b.pt_b) / 2.0
+
+ elif a.corner == SS2D_Quad.CORNER.INNER:
+ b.pt_a = a.pt_d
+ b.pt_b = a.pt_a
+ midpoint = (b.pt_a + b.pt_b) / 2.0
+
+ elif b.corner == SS2D_Quad.CORNER.OUTER:
+ a.pt_d = b.pt_a
+ a.pt_c = b.pt_b
+ midpoint = (a.pt_d + a.pt_c) / 2.0
+
+ elif b.corner == SS2D_Quad.CORNER.INNER:
+ a.pt_d = b.pt_d
+ a.pt_c = b.pt_c
+ midpoint = (a.pt_d + a.pt_c) / 2.0
+
+ return midpoint
+
+
+func _weld_quad_array(
+ quads: Array[SS2D_Quad], weld_first_and_last: bool, start_idx: int = 0
+) -> void:
+ if quads.is_empty():
+ return
+
+ for index in range(start_idx, quads.size() - 1, 1):
+ var this_quad: SS2D_Quad = quads[index]
+ var next_quad: SS2D_Quad = quads[index + 1]
+ if not this_quad.ignore_weld_next:
+ SS2D_Shape.weld_quads(this_quad, next_quad)
+ # If this quad self_intersects after welding, it's likely very small and can be removed
+ # Usually happens when welding a very large and very small quad together
+ # Generally looks better when simply being removed
+ #
+ # 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/
+ if this_quad.self_intersects():
+ quads.remove_at(index)
+ if index < quads.size():
+ var new_index: int = maxi(index - 1, 0)
+ _weld_quad_array(quads, weld_first_and_last, new_index)
+ return
+
+ if weld_first_and_last:
+ if not quads[-1].ignore_weld_next:
+ SS2D_Shape.weld_quads(quads[-1], quads[0])
+
+
+func _merge_index_maps(imaps: Array[SS2D_IndexMap], verts: PackedVector2Array) -> Array[SS2D_IndexMap]:
+ if not is_shape_closed():
+ return imaps
+ # See if any edges have both the first (0) and last idx (size)
+ # Merge them into one if so
+ var final_edges: Array[SS2D_IndexMap] = imaps.duplicate()
+ var edges_by_material: Dictionary = SS2D_IndexMap.index_map_array_sort_by_object(final_edges)
+ # Erase any with null material
+ edges_by_material.erase(null)
+ for mat: Variant in edges_by_material:
+ var edge_first_idx: SS2D_IndexMap = null
+ var edge_last_idx: SS2D_IndexMap = null
+ for e: SS2D_IndexMap in edges_by_material[mat]:
+ if e.indicies.has(0):
+ edge_first_idx = e
+ if e.indicies.has(verts.size()-1):
+ edge_last_idx = e
+ if edge_first_idx != null and edge_last_idx != null:
+ break
+ if edge_first_idx != null and edge_last_idx != null:
+ if edge_first_idx == edge_last_idx:
+ pass
+ else:
+ final_edges.erase(edge_last_idx)
+ final_edges.erase(edge_first_idx)
+ var indicies := edge_last_idx.indicies + edge_first_idx.indicies
+ var merged_edge := SS2D_IndexMap.new(indicies, mat)
+ final_edges.push_back(merged_edge)
+ return final_edges
+
+
+func _build_edges(s_mat: SS2D_Material_Shape, verts: PackedVector2Array) -> Array[SS2D_Edge]:
+ var edges: Array[SS2D_Edge] = []
+ if s_mat == null:
+ return edges
+
+ var index_maps: Array[SS2D_IndexMap] = _get_meta_material_index_mapping(s_mat, verts)
+ var overrides: Array[SS2D_IndexMap] = SS2D_Shape.get_meta_material_index_mapping_for_overrides(s_mat, _points)
+
+ # Remove the override indicies from the default index_maps
+ for override in overrides:
+ var old_to_new_imaps := {}
+ for index_map in index_maps:
+ var new_imaps: Array[SS2D_IndexMap] = index_map.remove_edges(override.indicies)
+ old_to_new_imaps[index_map] = new_imaps
+ for k: SS2D_IndexMap in old_to_new_imaps:
+ index_maps.erase(k)
+ for new_imap: SS2D_IndexMap in old_to_new_imaps[k]:
+ index_maps.push_back(new_imap)
+
+ # Merge index maps
+ index_maps = _merge_index_maps(index_maps, verts)
+
+ # Add the overrides to the mappings to be rendered
+ for override in overrides:
+ index_maps.push_back(override)
+
+ # Edge case for web so it doesn't use thread
+ if OS.get_name() != "Web":
+ var threads: Array[Thread] = []
+ for index_map in index_maps:
+ var thread := Thread.new()
+ var args := [index_map, s_mat.render_offset, 0.0]
+ var priority := 2
+ thread.start(self._build_edge_with_material_thread_wrapper.bind(args), priority)
+ threads.push_back(thread)
+ for thread in threads:
+ var new_edge: SS2D_Edge = thread.wait_to_finish()
+ edges.push_back(new_edge)
+
+ else:
+ # Process index_maps sequentially for web exports (probably slower than thread)
+ for index_map in index_maps:
+ var args = [index_map, s_mat.render_offset, 0.0]
+ var new_edge: SS2D_Edge = _build_edge_with_material_thread_wrapper(args)
+ edges.push_back(new_edge)
+
+ return edges
+
+
+## Will return an array of SS2D_IndexMaps.[br]
+## Each index map will map a set of indicies to a meta_material.[br]
+static func get_meta_material_index_mapping_for_overrides(
+ _s_material: SS2D_Material_Shape, pa: SS2D_Point_Array
+) -> Array[SS2D_IndexMap]:
+ var mappings: Array[SS2D_IndexMap] = []
+ for key_tuple in pa.get_material_overrides():
+ var indices := SS2D_IndexTuple.sort_ascending(Vector2i(pa.get_point_index(key_tuple.x), pa.get_point_index(key_tuple.y)))
+ var m: SS2D_Material_Edge_Metadata = pa.get_material_override(key_tuple)
+ var new_mapping := SS2D_IndexMap.new(PackedInt32Array([ indices.x, indices.y ]), m)
+ mappings.push_back(new_mapping)
+
+ return mappings
+
+
+## Will return a dictionary containing array of SS2D_IndexMap.[br]
+## Each element in the array is a contiguous sequence of indicies that fit inside
+## the meta_material's normalrange.[br]
+func _get_meta_material_index_mapping(
+ s_material: SS2D_Material_Shape, verts: PackedVector2Array
+) -> Array[SS2D_IndexMap]:
+ return SS2D_Shape.get_meta_material_index_mapping(s_material, verts, is_shape_closed())
+
+
+static func get_meta_material_index_mapping(
+ s_material: SS2D_Material_Shape, verts: PackedVector2Array, wrap_around: bool
+) -> Array[SS2D_IndexMap]:
+ var final_edges: Array[SS2D_IndexMap] = []
+ var edge_building: Dictionary = {} # Dict[SS2D_Material_Edge_Metadata, SS2D_IndexMap]
+ for idx in range(0, verts.size() - 1, 1):
+ var idx_next: int = SS2D_PluginFunctionality.get_next_point_index(idx, verts, wrap_around)
+ var pt: Vector2 = verts[idx]
+ var pt_next: Vector2 = verts[idx_next]
+ var delta: Vector2 = pt_next - pt
+ var normal := Vector2(delta.y, -delta.x).normalized()
+
+ # Get all valid edge_meta_materials for this normal value
+ var edge_meta_materials := s_material.get_edge_meta_materials(normal)
+
+ # Append to existing edges being built. Add new ones if needed
+ for e in edge_meta_materials:
+ var imap: SS2D_IndexMap = edge_building.get(e)
+
+ # Is exsiting, append
+ if imap:
+ if not idx_next in imap.indicies:
+ imap.indicies.push_back(idx_next)
+ # Isn't existing, make a new mapping
+ else:
+ edge_building[e] = SS2D_IndexMap.new([idx, idx_next], e)
+
+ # Closeout and stop building edges that are no longer viable
+ for e: SS2D_Material_Edge_Metadata in edge_building.keys():
+ if not edge_meta_materials.has(e):
+ final_edges.push_back(edge_building[e])
+ edge_building.erase(e)
+
+ # Closeout all edge building
+ for e: SS2D_Material_Edge_Metadata in edge_building.keys():
+ final_edges.push_back(edge_building[e])
+
+ return final_edges
+
+########
+#-MISC-#
+########
+func _handle_material_change() -> void:
+ set_as_dirty()
+
+
+func _handle_material_override_change(_tuple: Vector2i) -> void:
+ set_as_dirty()
+
+
+func set_as_dirty() -> void:
+ if not _dirty:
+ call_deferred("_on_dirty_update")
+ _dirty = true
+
+
+static func sort_by_z_index(a: Array) -> Array:
+ a.sort_custom(Callable(SS2D_Common_Functions, "sort_z"))
+ return a
+
+
+static func sort_by_int_ascending(a: Array) -> Array:
+ a.sort_custom(Callable(SS2D_Common_Functions, "sort_int_ascending"))
+ return a
+
+
+func clear_cached_data() -> void:
+ _edges = []
+ _meshes = []
+
+
+func _on_dirty_update() -> void:
+ if _dirty:
+ force_update()
+
+
+func force_update() -> void:
+ update_render_nodes()
+ clear_cached_data()
+
+ bake_collision()
+ if get_point_count() >= 2:
+ cache_edges()
+ cache_meshes()
+ queue_redraw()
+ _dirty = false
+
+
+## Returns a float between 0.0 and 1.0.[br]
+## 0.0 means that this tessellated point is at the same position as the vertex.[br]
+## 0.5 means that this tessellated point is half-way between this vertex and the next.[br]
+## 0.999 means that this tessellated point is basically at the next vertex.[br]
+## 1.0 isn't going to happen; If a tess point is at the same position as a vert, it gets a ratio of 0.0.[br]
+func get_ratio_from_tessellated_point_to_vertex(t_point_idx: int) -> float:
+ # Index of the starting vertex
+ var point_idx := _points.get_tesselation_vertex_mapping().tess_to_vertex_index(t_point_idx)
+ # Index of the first tesselated point with the same vertex
+ var tess_point_first_idx: int = _points.get_tesselation_vertex_mapping().vertex_to_tess_indices(point_idx)[0]
+ # The total tessellated points with the same vertex
+ var tess_point_count := _points.get_tesselation_vertex_mapping().vertex_to_tess_indices(point_idx).size()
+ # The index of the passed t_point_idx relative to the starting vert
+ var tess_index_count := t_point_idx - tess_point_first_idx
+ return tess_index_count / float(tess_point_count)
+
+
+func debug_print_points() -> void:
+ _points.debug_print()
+
+
+###################
+#-EDGE GENERATION-#
+###################
+
+## Get Number of TessPoints from the start and end indicies of the index_map parameter.
+func _edge_data_get_tess_point_count(index_map: SS2D_IndexMap) -> int:
+ ## TODO Test this function
+ var count: int = 0
+ for i in range(index_map.indicies.size() - 1):
+ var this_idx := index_map.indicies[i]
+ var next_idx := index_map.indicies[i + 1]
+ if this_idx > next_idx:
+ count += 1
+ continue
+ var this_t_idx: int = _points.get_tesselation_vertex_mapping().vertex_to_tess_indices(this_idx)[0]
+ var next_t_idx: int = _points.get_tesselation_vertex_mapping().vertex_to_tess_indices(next_idx)[0]
+ var delta: int = next_t_idx - this_t_idx
+ count += delta
+ return count
+
+
+## This function determines if a corner quad should be generated.[br]
+## if so, OUTER or INNER? [br]
+## - The conditions deg < 0 and flip_edges are used to determine this.[br]
+## - These conditions works correctly so long as the points are in Clockwise order.[br]
+static func edge_should_generate_corner(pt_prev: Vector2, pt: Vector2, pt_next: Vector2, flip_edges_: bool) -> SS2D_Quad.CORNER:
+ var generate_corner := SS2D_Quad.CORNER.NONE
+ var ab: Vector2 = pt - pt_prev
+ var bc: Vector2 = pt_next - pt
+ var dot_prod: float = ab.dot(bc)
+ var determinant: float = (ab.x * bc.y) - (ab.y * bc.x)
+ var angle := atan2(determinant, dot_prod)
+ # This angle has a range of 360 degrees
+ # Is between 180 and - 180
+ var deg := rad_to_deg(angle)
+ var corner_range := 10.0
+ var corner_angle := 90.0
+ if absf(deg) >= corner_angle - corner_range and absf(deg) <= corner_angle + corner_range:
+ var inner := false
+ if deg < 0:
+ inner = true
+ if flip_edges_:
+ inner = not inner
+ if inner:
+ generate_corner = SS2D_Quad.CORNER.INNER
+ else:
+ generate_corner = SS2D_Quad.CORNER.OUTER
+ return generate_corner
+
+
+func _edge_generate_corner(
+ pt_prev: Vector2,
+ pt: Vector2,
+ pt_next: Vector2,
+ width_prev: float,
+ width: float,
+ size: float,
+ edge_material: SS2D_Material_Edge,
+ texture_idx: int,
+ c_scale: float,
+ c_offset: float
+) -> SS2D_Quad:
+ var generate_corner := SS2D_Shape.edge_should_generate_corner(pt_prev, pt, pt_next, flip_edges)
+ if generate_corner == SS2D_Quad.CORNER.NONE:
+ return null
+ var corner_texture: Texture2D = null
+ if edge_material != null:
+ if generate_corner == SS2D_Quad.CORNER.OUTER:
+ corner_texture = edge_material.get_texture_corner_outer(texture_idx)
+ elif generate_corner == SS2D_Quad.CORNER.INNER:
+ corner_texture = edge_material.get_texture_corner_inner(texture_idx)
+ var corner_quad: SS2D_Quad = SS2D_Shape.build_quad_corner(
+ pt_next,
+ pt,
+ pt_prev,
+ width,
+ width_prev,
+ flip_edges,
+ generate_corner,
+ corner_texture,
+ Vector2(size, size),
+ c_scale,
+ c_offset
+ )
+ return corner_quad
+
+
+func _imap_contains_all_points(imap: SS2D_IndexMap, verts: PackedVector2Array) -> bool:
+ return imap.indicies[0] == 0 and imap.indicies[-1] == verts.size()-1
+
+
+func _is_edge_contiguous(imap: SS2D_IndexMap, verts: PackedVector2Array) -> bool:
+ if not is_shape_closed():
+ return false
+ return _imap_contains_all_points(imap, verts)
+
+
+# Will construct an SS2D_Edge from the passed parameters.
+# index_map must be a SS2D_IndexMap with a SS2D_Material_Edge_Metadata for an object
+# the indicies used by index_map should match up with the get_verticies() indicies
+#
+# default_quad_width is the quad width used if a texture isn't available
+#
+# c_offset is the magnitude to offset all of the points
+# the direction of the offset is the surface_normal
+func _build_edge_with_material(
+ index_map: SS2D_IndexMap, c_offset: float, default_quad_width: float
+) -> SS2D_Edge:
+ var verts_t: PackedVector2Array = _points.get_tessellated_points()
+ var verts: PackedVector2Array = _points.get_vertices()
+ var edge := SS2D_Edge.new()
+ var is_edge_contiguous: bool = _is_edge_contiguous(index_map, verts)
+ edge.wrap_around = is_edge_contiguous
+ if not index_map.is_valid():
+ return edge
+ var c_scale := 1.0
+ var c_extends := 0.0
+
+ var edge_material_meta: SS2D_Material_Edge_Metadata = null
+ var edge_material: SS2D_Material_Edge = null
+ if index_map.object != null:
+ edge_material_meta = index_map.object
+ if edge_material_meta == null:
+ return edge
+ if not edge_material_meta.render:
+ return edge
+ edge_material = edge_material_meta.edge_material
+ if edge_material == null:
+ return edge
+ c_offset += edge_material_meta.offset
+
+ edge.z_index = edge_material_meta.z_index
+ edge.z_as_relative = edge_material_meta.z_as_relative
+ edge.material = edge_material_meta.edge_material.material
+
+ var first_idx: int = index_map.indicies[0]
+ var last_idx: int = index_map.indicies[-1]
+ var first_idx_t: int = _points.get_tesselation_vertex_mapping().vertex_to_tess_indices(first_idx)[0]
+ var last_idx_t: int = _points.get_tesselation_vertex_mapping().vertex_to_tess_indices(last_idx)[-1]
+ edge.first_point_key = _points.get_point_key_at_index(first_idx)
+ edge.last_point_key = _points.get_point_key_at_index(last_idx)
+
+ var should_flip := should_flip_edges()
+
+ # How many tessellated points are contained within this index map?
+ var tess_point_count: int = _edge_data_get_tess_point_count(index_map)
+
+ var i := 0
+ var texture_idx := 0
+ var sharp_taper_next: SS2D_Quad = null
+ var is_not_corner: bool = true
+ var taper_sharp: bool = edge_material_meta != null and edge_material_meta.taper_sharp_corners
+ while i < tess_point_count:
+ var tess_idx: int = (first_idx_t + i) % verts_t.size()
+ var tess_idx_next: int = SS2D_PluginFunctionality.get_next_unique_point_idx(tess_idx, verts_t, true)
+ var tess_idx_prev: int = SS2D_PluginFunctionality.get_previous_unique_point_idx(tess_idx, verts_t, true)
+
+ # set next_point_delta
+ # next_point_delta is the number of tess_pts from
+ # the current tess_pt to the next unique tess_pt
+ # unique meaning it has a different position from the current tess_pt
+ var next_point_delta := 0
+ for j in range(verts_t.size()):
+ if ((tess_idx + j) % verts_t.size()) == tess_idx_next:
+ next_point_delta = j
+ break
+
+ var vert_idx: int = _points.get_tesselation_vertex_mapping().tess_to_vertex_index(tess_idx)
+ var vert_key: int = get_point_key_at_index(vert_idx)
+ var pt: Vector2 = verts_t[tess_idx]
+ var pt_next: Vector2 = verts_t[tess_idx_next]
+ var pt_prev: Vector2 = verts_t[tess_idx_prev]
+ var flip_x: bool = get_point_texture_flip(vert_key)
+
+ var width_scale: float = _get_width_for_tessellated_point(verts, tess_idx)
+ var is_first_point: bool = (vert_idx == first_idx) and not is_edge_contiguous
+ var is_last_point: bool = (vert_idx == last_idx - 1) and not is_edge_contiguous
+ var is_first_tess_point: bool = (tess_idx == first_idx_t) and not is_edge_contiguous
+ var is_last_tess_point: bool = (tess_idx == last_idx_t - 1) and not is_edge_contiguous
+
+ var tex: Texture2D = null
+ var tex_size := Vector2(default_quad_width, default_quad_width)
+ var fitmode := SS2D_Material_Edge.FITMODE.SQUISH_AND_STRETCH
+ if edge_material != null:
+ if edge_material.randomize_texture:
+ texture_idx = randi() % edge_material.textures.size()
+ else :
+ texture_idx = get_point_texture_index(vert_key)
+ tex = edge_material.get_texture(texture_idx)
+ tex_size = tex.get_size()
+ fitmode = edge_material.fit_mode
+ # Exit if we have an edge material defined but no texture to render
+ if tex == null:
+ i += next_point_delta
+ continue
+
+ var new_quad: SS2D_Quad = SS2D_Shape.build_quad_from_two_points(
+ pt,
+ pt_next,
+ tex,
+ width_scale * c_scale * tex_size.y,
+ flip_x,
+ should_flip,
+ is_first_point,
+ is_last_point,
+ c_offset,
+ c_extends,
+ fitmode
+ )
+ var new_quads: Array[SS2D_Quad] = []
+ new_quads.push_back(new_quad)
+
+ # Corner Quad
+ if edge_material != null and edge_material.use_corner_texture:
+ if tess_idx != first_idx_t or is_edge_contiguous:
+ var prev_width: float = _get_width_for_tessellated_point(verts, tess_idx_prev)
+ var q: SS2D_Quad = _edge_generate_corner(
+ pt_prev,
+ pt,
+ pt_next,
+ prev_width,
+ width_scale,
+ tex_size.y,
+ edge_material,
+ texture_idx,
+ c_scale,
+ c_offset
+ )
+ if q != null:
+ new_quads.push_front(q)
+ is_not_corner = false
+ else:
+ is_not_corner = true
+
+ # Taper Quad
+ # Bear in mind, a point can be both first AND last
+ # Consider an edge that consists of two points (one edge)
+ # This first point is used to generate the quad; it is both first and last
+ var did_taper_left: bool = false
+ var did_taper_right: bool = false
+ if is_first_tess_point and edge_material != null and edge_material.use_taper_texture:
+ did_taper_left = true
+ var taper_quad := _taper_quad(new_quad, edge_material, texture_idx, false, false)
+ if taper_quad != null:
+ new_quads.push_front(taper_quad)
+ if is_last_tess_point and edge_material != null and edge_material.use_taper_texture:
+ did_taper_right = true
+ var taper_quad := _taper_quad(new_quad, edge_material, texture_idx, true, false)
+ if taper_quad != null:
+ new_quads.push_back(taper_quad)
+
+ # Taper sharp corners
+ if taper_sharp:
+ var ang_threshold := PI * 0.5
+ if sharp_taper_next != null and is_not_corner:
+ var taper := _taper_quad(sharp_taper_next, edge_material, texture_idx, true, true)
+ if taper != null:
+ taper.ignore_weld_next = true
+ edge.quads.push_back(taper)
+ else:
+ sharp_taper_next.ignore_weld_next = true
+ sharp_taper_next = null
+ var vert := verts[vert_idx]
+ var prev_vert := verts[wrapi(vert_idx - 1, 0, verts.size() - 1)]
+ var next_vert := verts[wrapi(vert_idx + 1, 0, verts.size() - 1)]
+ if not did_taper_left and is_not_corner:
+ var ang_from := prev_vert.angle_to_point(vert)
+ var ang_to := vert.angle_to_point(next_vert)
+ var ang_dif := angle_difference(ang_from, ang_to)
+ if absf(ang_dif) > ang_threshold:
+ var taper := _taper_quad(new_quad, edge_material, texture_idx, false, true)
+ if taper != null:
+ new_quads.push_front(taper)
+ if not did_taper_right:
+ var next_next_vert := verts[wrapi(vert_idx + 2, 0, verts.size() - 1)]
+ var ang_from := vert.angle_to_point(next_vert)
+ var ang_to := next_vert.angle_to_point(next_next_vert)
+ var ang_dif := angle_difference(ang_from, ang_to)
+ if absf(ang_dif) > ang_threshold:
+ sharp_taper_next = new_quad
+
+ # Final point for closed shapes fix
+ # Corner quads aren't always correctly when the corner is between final and first pt
+ if is_last_point and is_edge_contiguous:
+ var idx_mid: int = verts_t.size() - 1
+ var idx_next: int = SS2D_PluginFunctionality.get_next_unique_point_idx(idx_mid, verts_t, true)
+ var idx_prev: int = SS2D_PluginFunctionality.get_previous_unique_point_idx(idx_mid, verts_t, true)
+ var p_p: Vector2 = verts_t[idx_prev]
+ var p_m: Vector2 = verts_t[idx_mid]
+ var p_n: Vector2 = verts_t[idx_next]
+ var w_p: float = _get_width_for_tessellated_point(verts, idx_prev)
+ var w_m: float = _get_width_for_tessellated_point(verts, idx_mid)
+ var q: SS2D_Quad = _edge_generate_corner(
+ p_p, p_m, p_n, w_p, w_m, tex_size.y, edge_material, texture_idx, c_scale, c_offset
+ )
+ if q != null:
+ new_quads.push_back(q)
+
+ # Add new quads to edge
+ for q in new_quads:
+ edge.quads.push_back(q)
+ i += next_point_delta
+
+ # leftover final taper for the last sharp corner if required
+ if taper_sharp:
+ if sharp_taper_next != null and edge.quads[0].corner == SS2D_Quad.CORNER.NONE:
+ var taper := _taper_quad(sharp_taper_next, edge_material, texture_idx, true, true)
+ if taper != null:
+ taper.ignore_weld_next = true
+ edge.quads.push_back(taper)
+ else:
+ sharp_taper_next.ignore_weld_next = true
+ sharp_taper_next = null
+
+ if edge_material_meta != null:
+ if edge_material_meta.weld:
+ _weld_quad_array(edge.quads, edge.wrap_around)
+
+ return edge
+
+
+# get the appropriate tapering texture based on direction and whether the current taper is a sharp
+# corner taper or normal material edge taper
+func get_taper_tex(edge_mat: SS2D_Material_Edge, tex_idx: int, facing_right: bool, corner_taper: bool) -> Texture2D:
+ if facing_right:
+ if corner_taper:
+ return edge_mat.get_texture_taper_corner_right(tex_idx)
+ else:
+ return edge_mat.get_texture_taper_right(tex_idx)
+ else:
+ if corner_taper:
+ return edge_mat.get_texture_taper_corner_left(tex_idx)
+ else:
+ return edge_mat.get_texture_taper_left(tex_idx)
+
+
+func _taper_quad(
+ quad: SS2D_Quad,
+ edge_mat: SS2D_Material_Edge,
+ tex_idx: int,
+ facing_right: bool,
+ corner_taper: bool
+) -> SS2D_Quad:
+ var taper_texture: Texture2D = get_taper_tex(edge_mat, tex_idx, facing_right, corner_taper)
+ if taper_texture != null:
+ var taper_size: Vector2 = taper_texture.get_size()
+ var fit: bool = absf(taper_size.x) <= quad.get_length_average()
+ if fit:
+ var taper_quad := quad.duplicate()
+ taper_quad.corner = SS2D_Quad.CORNER.NONE
+ taper_quad.texture = taper_texture
+ var delta_normal: Vector2 = (taper_quad.pt_d - taper_quad.pt_a).normalized()
+ var offset: Vector2 = delta_normal * taper_size
+ if facing_right:
+ taper_quad.pt_a = taper_quad.pt_d - offset
+ taper_quad.pt_b = taper_quad.pt_c - offset
+ quad.pt_d = taper_quad.pt_a
+ quad.pt_c = taper_quad.pt_b
+ else:
+ taper_quad.pt_d = taper_quad.pt_a + offset
+ taper_quad.pt_c = taper_quad.pt_b + offset
+ quad.pt_a = taper_quad.pt_d
+ quad.pt_b = taper_quad.pt_c
+
+ taper_quad.is_tapered = true
+ return taper_quad
+ # If a new taper quad doesn't fit, re-texture the new_quad
+ else:
+ quad.is_tapered = true
+ quad.texture = taper_texture
+ return null
+
+
+func _build_edge_with_material_thread_wrapper(args: Array) -> SS2D_Edge:
+ return _build_edge_with_material(args[0], args[1], args[2])
diff --git a/addons/rmsmartshape/shapes/shape_anchor.gd b/addons/rmsmartshape/shapes/shape_anchor.gd
new file mode 100644
index 0000000..f6492b8
--- /dev/null
+++ b/addons/rmsmartshape/shapes/shape_anchor.gd
@@ -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)
diff --git a/addons/rmsmartshape/shapes/shape_closed.gd b/addons/rmsmartshape/shapes/shape_closed.gd
new file mode 100644
index 0000000..1fa3ae7
--- /dev/null
+++ b/addons/rmsmartshape/shapes/shape_closed.gd
@@ -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))
+
+
diff --git a/addons/rmsmartshape/shapes/shape_open.gd b/addons/rmsmartshape/shapes/shape_open.gd
new file mode 100644
index 0000000..76d11d3
--- /dev/null
+++ b/addons/rmsmartshape/shapes/shape_open.gd
@@ -0,0 +1,7 @@
+@tool
+@icon("../assets/open_shape.png")
+extends SS2D_Shape
+class_name SS2D_Shape_Open
+
+## DEPRECATED: Use [SS2D_Shape] instead.
+## @deprecated
diff --git a/addons/rmsmartshape/shapes/shape_render.gd b/addons/rmsmartshape/shapes/shape_render.gd
new file mode 100644
index 0000000..dfa43b7
--- /dev/null
+++ b/addons/rmsmartshape/shapes/shape_render.gd
@@ -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)
diff --git a/addons/rmsmartshape/strings.gd b/addons/rmsmartshape/strings.gd
new file mode 100644
index 0000000..9195b79
--- /dev/null
+++ b/addons/rmsmartshape/strings.gd
@@ -0,0 +1,16 @@
+@tool
+extends Resource
+class_name SS2D_Strings
+
+const EN_TOOLTIP_CREATE_VERT := "Create Vertices Tool\nLMB: Add vertex\nShift+LMB: Create Bezier curve using Control Points\nControl+LMB: Set Pivot Point\nLMB+Drag: Move Point\nLMB: Click on edge to split\nRMB: Delete Point"
+const EN_TOOLTIP_EDIT_VERT := "Edit Vertices Tool\nShift+LMB: Create Bezier curve using Control Points\nAlt+LMB: Add vertex\nControl+LMB: Set Pivot Point\nLMB+Drag: Move Point\nLMB: Click on edge to split\nRMB: Delete Point"
+const EN_TOOLTIP_EDIT_EDGE := "Edit Edge Tool\nSelect each edge's properties"
+const EN_TOOLTIP_CUT_EDGE := "Cut Edge Tool\nRemoves the edge between vertices, opening the shape.\nCan be used to split the shape into two separate shapes."
+const EN_TOOLTIP_FREEHAND := "Freehand Tool\nHold LMB: Add vertices along the drag line.\nControl+LMB: remove vertices inside the circle while dragging.\nShift+Mousewheel: Change circle size for drawing.\nShift+Control+Mousewheel: Change circle size for eraser."
+const EN_TOOLTIP_PIVOT := "Set Pivot Tool\nSets the origin of the shape"
+const EN_TOOLTIP_CENTER_PIVOT := "Center Pivot\nSets the origin to the centroid of the shape"
+const EN_TOOLTIP_COLLISION := "Collision Tool\nAdds a static body parent and collision polygon sibling\nUse this to auto-generate collision nodes"
+const EN_TOOLTIP_SNAP := "Snapping Options"
+const EN_TOOLTIP_MORE_OPTIONS := "More Options"
+
+const EN_OPTIONS_DEFER_MESH_UPDATES := "Defer Mesh Updates"
diff --git a/addons/rmsmartshape/vertex_properties.gd b/addons/rmsmartshape/vertex_properties.gd
new file mode 100644
index 0000000..ab53688
--- /dev/null
+++ b/addons/rmsmartshape/vertex_properties.gd
@@ -0,0 +1,38 @@
+@tool
+extends Resource
+class_name SS2D_VertexProperties
+
+@export var texture_idx: int : set = set_texture_idx
+@export var flip: bool : set = set_flip
+@export var width: float : set = set_width
+
+
+func set_texture_idx(i: int) -> void:
+ texture_idx = i
+ emit_changed()
+
+
+func set_flip(b: bool) -> void:
+ flip = b
+ emit_changed()
+
+
+func set_width(w: float) -> void:
+ width = w
+ emit_changed()
+
+
+func _init() -> void:
+ texture_idx = 0
+ flip = false
+ width = 1.0
+
+
+func equals(other: SS2D_VertexProperties) -> bool:
+ if other.flip != flip:
+ return false
+ if other.texture_idx != texture_idx:
+ return false
+ if other.width != width:
+ return false
+ return true
diff --git a/examples/edge_shader/edge_shader.gdshader b/examples/edge_shader/edge_shader.gdshader
new file mode 100644
index 0000000..a2def85
--- /dev/null
+++ b/examples/edge_shader/edge_shader.gdshader
@@ -0,0 +1,16 @@
+shader_type canvas_item;
+/**
+ * This shader fixes normal textures looking wrong when used with SmartShape2D.
+ * See: https://github.com/SirRamEsq/SmartShape2D/blob/master/addons/rmsmartshape/documentation/Normals.md
+ */
+
+varying mat2 NORMAL_MATRIX;
+
+void vertex() {
+ NORMAL_MATRIX = mat2(COLOR.rg, COLOR.ba) * 2.0 - mat2(vec2(1.0), vec2(1.0));
+}
+
+void fragment() {
+ NORMAL.xy = NORMAL_MATRIX * NORMAL.xy;
+ COLOR = texture(TEXTURE, UV);
+}
diff --git a/examples/edge_shader/material_edge_shader.tres b/examples/edge_shader/material_edge_shader.tres
new file mode 100644
index 0000000..0bda5c4
--- /dev/null
+++ b/examples/edge_shader/material_edge_shader.tres
@@ -0,0 +1,6 @@
+[gd_resource type="ShaderMaterial" load_steps=2 format=3 uid="uid://dpec5hjpflmri"]
+
+[ext_resource type="Shader" path="res://examples/edge_shader/edge_shader.gdshader" id="1_akl0v"]
+
+[resource]
+shader = ExtResource("1_akl0v")
diff --git a/examples/sharp_corner_tapering/assets/mat_edge_metal.tres b/examples/sharp_corner_tapering/assets/mat_edge_metal.tres
new file mode 100644
index 0000000..aacc59b
--- /dev/null
+++ b/examples/sharp_corner_tapering/assets/mat_edge_metal.tres
@@ -0,0 +1,22 @@
+[gd_resource type="Resource" script_class="SS2D_Material_Edge" load_steps=7 format=3 uid="uid://c8q2nrxnbopbb"]
+
+[ext_resource type="Script" path="res://addons/rmsmartshape/materials/edge_material.gd" id="1_lbfns"]
+[ext_resource type="Texture2D" uid="uid://b3aqa3bj1osvp" path="res://examples/sharp_corner_tapering/assets/tex_metal_edge.png" id="2_bmjh3"]
+[ext_resource type="Texture2D" uid="uid://bobwi3r6aiiqg" path="res://examples/sharp_corner_tapering/assets/tex_metal_corner_inner.png" id="3_n1i1s"]
+[ext_resource type="Texture2D" uid="uid://bc34cuc50as8f" path="res://examples/sharp_corner_tapering/assets/tex_metal_corner_outer.png" id="4_ml2u7"]
+[ext_resource type="Texture2D" uid="uid://jc3g5qsmnpdd" path="res://examples/sharp_corner_tapering/assets/tex_metal_taper_corner_left.png" id="5_244mn"]
+[ext_resource type="Texture2D" uid="uid://cdnfaf3bslk38" path="res://examples/sharp_corner_tapering/assets/tex_metal_taper_corner_right.png" id="6_1c2na"]
+
+[resource]
+script = ExtResource("1_lbfns")
+textures = Array[Texture2D]([ExtResource("2_bmjh3")])
+textures_corner_outer = Array[Texture2D]([ExtResource("4_ml2u7")])
+textures_corner_inner = Array[Texture2D]([ExtResource("3_n1i1s")])
+textures_taper_left = Array[Texture2D]([])
+textures_taper_right = Array[Texture2D]([])
+textures_taper_corner_left = Array[Texture2D]([ExtResource("5_244mn")])
+textures_taper_corner_right = Array[Texture2D]([ExtResource("6_1c2na")])
+randomize_texture = false
+use_corner_texture = true
+use_taper_texture = false
+fit_mode = 0
diff --git a/examples/sharp_corner_tapering/assets/tex_metal_corner_inner.png b/examples/sharp_corner_tapering/assets/tex_metal_corner_inner.png
new file mode 100644
index 0000000..6adcf60
Binary files /dev/null and b/examples/sharp_corner_tapering/assets/tex_metal_corner_inner.png differ
diff --git a/examples/sharp_corner_tapering/assets/tex_metal_corner_inner.png.import b/examples/sharp_corner_tapering/assets/tex_metal_corner_inner.png.import
new file mode 100644
index 0000000..835818b
--- /dev/null
+++ b/examples/sharp_corner_tapering/assets/tex_metal_corner_inner.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bobwi3r6aiiqg"
+path="res://.godot/imported/tex_metal_corner_inner.png-cdfd7b81c523bc5bb5c7863d7307007e.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://examples/sharp_corner_tapering/assets/tex_metal_corner_inner.png"
+dest_files=["res://.godot/imported/tex_metal_corner_inner.png-cdfd7b81c523bc5bb5c7863d7307007e.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/examples/sharp_corner_tapering/assets/tex_metal_corner_outer.png b/examples/sharp_corner_tapering/assets/tex_metal_corner_outer.png
new file mode 100644
index 0000000..1dcc88a
Binary files /dev/null and b/examples/sharp_corner_tapering/assets/tex_metal_corner_outer.png differ
diff --git a/examples/sharp_corner_tapering/assets/tex_metal_corner_outer.png.import b/examples/sharp_corner_tapering/assets/tex_metal_corner_outer.png.import
new file mode 100644
index 0000000..10587db
--- /dev/null
+++ b/examples/sharp_corner_tapering/assets/tex_metal_corner_outer.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bc34cuc50as8f"
+path="res://.godot/imported/tex_metal_corner_outer.png-6ea6aecab21ef2cdfa9c20ae8389ee06.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://examples/sharp_corner_tapering/assets/tex_metal_corner_outer.png"
+dest_files=["res://.godot/imported/tex_metal_corner_outer.png-6ea6aecab21ef2cdfa9c20ae8389ee06.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/examples/sharp_corner_tapering/assets/tex_metal_edge.png b/examples/sharp_corner_tapering/assets/tex_metal_edge.png
new file mode 100644
index 0000000..cab72a4
Binary files /dev/null and b/examples/sharp_corner_tapering/assets/tex_metal_edge.png differ
diff --git a/examples/sharp_corner_tapering/assets/tex_metal_edge.png.import b/examples/sharp_corner_tapering/assets/tex_metal_edge.png.import
new file mode 100644
index 0000000..4e4b107
--- /dev/null
+++ b/examples/sharp_corner_tapering/assets/tex_metal_edge.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b3aqa3bj1osvp"
+path="res://.godot/imported/tex_metal_edge.png-5aaf6e458197a953a7afeee0e270fea4.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://examples/sharp_corner_tapering/assets/tex_metal_edge.png"
+dest_files=["res://.godot/imported/tex_metal_edge.png-5aaf6e458197a953a7afeee0e270fea4.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/examples/sharp_corner_tapering/assets/tex_metal_fill.png b/examples/sharp_corner_tapering/assets/tex_metal_fill.png
new file mode 100644
index 0000000..d8a5619
Binary files /dev/null and b/examples/sharp_corner_tapering/assets/tex_metal_fill.png differ
diff --git a/examples/sharp_corner_tapering/assets/tex_metal_fill.png.import b/examples/sharp_corner_tapering/assets/tex_metal_fill.png.import
new file mode 100644
index 0000000..2b1eb9c
--- /dev/null
+++ b/examples/sharp_corner_tapering/assets/tex_metal_fill.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bj658oli0klj3"
+path="res://.godot/imported/tex_metal_fill.png-307d5ebcedc9e8c4f154a2c09687cb41.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://examples/sharp_corner_tapering/assets/tex_metal_fill.png"
+dest_files=["res://.godot/imported/tex_metal_fill.png-307d5ebcedc9e8c4f154a2c09687cb41.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/examples/sharp_corner_tapering/assets/tex_metal_taper_corner_left.png b/examples/sharp_corner_tapering/assets/tex_metal_taper_corner_left.png
new file mode 100644
index 0000000..17ab69d
Binary files /dev/null and b/examples/sharp_corner_tapering/assets/tex_metal_taper_corner_left.png differ
diff --git a/examples/sharp_corner_tapering/assets/tex_metal_taper_corner_left.png.import b/examples/sharp_corner_tapering/assets/tex_metal_taper_corner_left.png.import
new file mode 100644
index 0000000..5315f40
--- /dev/null
+++ b/examples/sharp_corner_tapering/assets/tex_metal_taper_corner_left.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://jc3g5qsmnpdd"
+path="res://.godot/imported/tex_metal_taper_corner_left.png-735fbf01b6fc0725b32a91fb24aae5d5.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://examples/sharp_corner_tapering/assets/tex_metal_taper_corner_left.png"
+dest_files=["res://.godot/imported/tex_metal_taper_corner_left.png-735fbf01b6fc0725b32a91fb24aae5d5.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/examples/sharp_corner_tapering/assets/tex_metal_taper_corner_right.png b/examples/sharp_corner_tapering/assets/tex_metal_taper_corner_right.png
new file mode 100644
index 0000000..3d06df9
Binary files /dev/null and b/examples/sharp_corner_tapering/assets/tex_metal_taper_corner_right.png differ
diff --git a/examples/sharp_corner_tapering/assets/tex_metal_taper_corner_right.png.import b/examples/sharp_corner_tapering/assets/tex_metal_taper_corner_right.png.import
new file mode 100644
index 0000000..7adf36e
--- /dev/null
+++ b/examples/sharp_corner_tapering/assets/tex_metal_taper_corner_right.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cdnfaf3bslk38"
+path="res://.godot/imported/tex_metal_taper_corner_right.png-75d13fe99bb8e540489390ee64c63ea5.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://examples/sharp_corner_tapering/assets/tex_metal_taper_corner_right.png"
+dest_files=["res://.godot/imported/tex_metal_taper_corner_right.png-75d13fe99bb8e540489390ee64c63ea5.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/examples/sharp_corner_tapering/sharp_corner_tapering.tscn b/examples/sharp_corner_tapering/sharp_corner_tapering.tscn
new file mode 100644
index 0000000..ecbfeb0
--- /dev/null
+++ b/examples/sharp_corner_tapering/sharp_corner_tapering.tscn
@@ -0,0 +1,315 @@
+[gd_scene load_steps=46 format=3 uid="uid://cl71flny1scun"]
+
+[ext_resource type="Script" path="res://addons/rmsmartshape/shapes/shape.gd" id="1_5hlge"]
+[ext_resource type="Script" path="res://addons/rmsmartshape/vertex_properties.gd" id="2_yp1d2"]
+[ext_resource type="Script" path="res://addons/rmsmartshape/shapes/point.gd" id="3_edpr6"]
+[ext_resource type="Script" path="res://addons/rmsmartshape/shapes/point_array.gd" id="4_udfgv"]
+[ext_resource type="Texture2D" uid="uid://bj658oli0klj3" path="res://examples/sharp_corner_tapering/assets/tex_metal_fill.png" id="6_ry0x0"]
+[ext_resource type="Script" path="res://addons/rmsmartshape/materials/shape_material.gd" id="7_ncdby"]
+[ext_resource type="Resource" uid="uid://c8q2nrxnbopbb" path="res://examples/sharp_corner_tapering/assets/mat_edge_metal.tres" id="8_uwntg"]
+[ext_resource type="Script" path="res://addons/rmsmartshape/normal_range.gd" id="9_tqu3h"]
+[ext_resource type="Script" path="res://addons/rmsmartshape/materials/edge_material.gd" id="10_55l7o"]
+[ext_resource type="Script" path="res://addons/rmsmartshape/materials/edge_material_metadata.gd" id="10_umoay"]
+[ext_resource type="Texture2D" uid="uid://b3aqa3bj1osvp" path="res://examples/sharp_corner_tapering/assets/tex_metal_edge.png" id="11_jc4po"]
+[ext_resource type="Texture2D" uid="uid://bobwi3r6aiiqg" path="res://examples/sharp_corner_tapering/assets/tex_metal_corner_inner.png" id="12_va43u"]
+[ext_resource type="Texture2D" uid="uid://bc34cuc50as8f" path="res://examples/sharp_corner_tapering/assets/tex_metal_corner_outer.png" id="13_f7yxr"]
+
+[sub_resource type="Resource" id="Resource_2r7x3"]
+script = ExtResource("2_yp1d2")
+texture_idx = 0
+flip = false
+width = 1.0
+
+[sub_resource type="Resource" id="Resource_vpbfx"]
+script = ExtResource("3_edpr6")
+position = Vector2(83.365, 45.3257)
+point_in = Vector2(0, 0)
+point_out = Vector2(0, 0)
+properties = SubResource("Resource_2r7x3")
+
+[sub_resource type="Resource" id="Resource_a0g7n"]
+script = ExtResource("2_yp1d2")
+texture_idx = 0
+flip = false
+width = 1.0
+
+[sub_resource type="Resource" id="Resource_w5cbt"]
+script = ExtResource("3_edpr6")
+position = Vector2(263.601, 615.758)
+point_in = Vector2(0, 0)
+point_out = Vector2(0, 0)
+properties = SubResource("Resource_a0g7n")
+
+[sub_resource type="Resource" id="Resource_h14mo"]
+script = ExtResource("2_yp1d2")
+texture_idx = -4
+flip = false
+width = 1.0
+
+[sub_resource type="Resource" id="Resource_j5l1j"]
+script = ExtResource("3_edpr6")
+position = Vector2(1026.77, 577.75)
+point_in = Vector2(0, 0)
+point_out = Vector2(0, 0)
+properties = SubResource("Resource_h14mo")
+
+[sub_resource type="Resource" id="Resource_ukuig"]
+script = ExtResource("2_yp1d2")
+texture_idx = 0
+flip = false
+width = 1.0
+
+[sub_resource type="Resource" id="Resource_324cb"]
+script = ExtResource("3_edpr6")
+position = Vector2(731.167, 13.1754)
+point_in = Vector2(0, 0)
+point_out = Vector2(0, 0)
+properties = SubResource("Resource_ukuig")
+
+[sub_resource type="Resource" id="Resource_wasbm"]
+script = ExtResource("2_yp1d2")
+texture_idx = 0
+flip = false
+width = 1.0
+
+[sub_resource type="Resource" id="Resource_p3ja8"]
+script = ExtResource("3_edpr6")
+position = Vector2(83.365, 45.3257)
+point_in = Vector2(0, 0)
+point_out = Vector2(0, 0)
+properties = SubResource("Resource_wasbm")
+
+[sub_resource type="Resource" id="Resource_1fh62"]
+script = ExtResource("2_yp1d2")
+texture_idx = 0
+flip = false
+width = 1.0
+
+[sub_resource type="Resource" id="Resource_qjb65"]
+script = ExtResource("3_edpr6")
+position = Vector2(742.381, 447.381)
+point_in = Vector2(0, 0)
+point_out = Vector2(0, 0)
+properties = SubResource("Resource_1fh62")
+
+[sub_resource type="Resource" id="Resource_o8bfq"]
+script = ExtResource("2_yp1d2")
+texture_idx = 0
+flip = false
+width = 1.0
+
+[sub_resource type="Resource" id="Resource_w1ohc"]
+script = ExtResource("3_edpr6")
+position = Vector2(641.411, 322.256)
+point_in = Vector2(0, 0)
+point_out = Vector2(0, 0)
+properties = SubResource("Resource_o8bfq")
+
+[sub_resource type="Resource" id="Resource_tp3up"]
+script = ExtResource("2_yp1d2")
+texture_idx = 0
+flip = false
+width = 1.0
+
+[sub_resource type="Resource" id="Resource_tlf23"]
+script = ExtResource("3_edpr6")
+position = Vector2(1133.09, 291.15)
+point_in = Vector2(0, 0)
+point_out = Vector2(0, 0)
+properties = SubResource("Resource_tp3up")
+
+[sub_resource type="Resource" id="Resource_6u83i"]
+script = ExtResource("2_yp1d2")
+texture_idx = 0
+flip = false
+width = 1.0
+
+[sub_resource type="Resource" id="Resource_86kx1"]
+script = ExtResource("3_edpr6")
+position = Vector2(546.956, 94.6283)
+point_in = Vector2(0, 0)
+point_out = Vector2(0, 0)
+properties = SubResource("Resource_6u83i")
+
+[sub_resource type="Resource" id="Resource_kysxg"]
+script = ExtResource("2_yp1d2")
+texture_idx = 0
+flip = false
+width = 1.0
+
+[sub_resource type="Resource" id="Resource_eqyck"]
+script = ExtResource("3_edpr6")
+position = Vector2(293.278, 51.6277)
+point_in = Vector2(0, 0)
+point_out = Vector2(0, 0)
+properties = SubResource("Resource_kysxg")
+
+[sub_resource type="Resource" id="Resource_egpc8"]
+script = ExtResource("2_yp1d2")
+texture_idx = 0
+flip = false
+width = 1.0
+
+[sub_resource type="Resource" id="Resource_1wyqg"]
+script = ExtResource("3_edpr6")
+position = Vector2(871.022, 351.231)
+point_in = Vector2(0, 0)
+point_out = Vector2(0, 0)
+properties = SubResource("Resource_egpc8")
+
+[sub_resource type="Resource" id="Resource_baip5"]
+script = ExtResource("2_yp1d2")
+texture_idx = 0
+flip = false
+width = 1.0
+
+[sub_resource type="Resource" id="Resource_2owaw"]
+script = ExtResource("3_edpr6")
+position = Vector2(239.115, 334.66)
+point_in = Vector2(0, 0)
+point_out = Vector2(0, 0)
+properties = SubResource("Resource_baip5")
+
+[sub_resource type="Resource" id="Resource_o2w6y"]
+script = ExtResource("4_udfgv")
+_points = {
+0: SubResource("Resource_vpbfx"),
+1: SubResource("Resource_w5cbt"),
+2: SubResource("Resource_j5l1j"),
+3: SubResource("Resource_324cb"),
+4: SubResource("Resource_p3ja8"),
+6: SubResource("Resource_qjb65"),
+8: SubResource("Resource_w1ohc"),
+9: SubResource("Resource_tlf23"),
+10: SubResource("Resource_86kx1"),
+11: SubResource("Resource_eqyck"),
+12: SubResource("Resource_1wyqg"),
+13: SubResource("Resource_2owaw")
+}
+_point_order = PackedInt32Array(4, 11, 8, 10, 3, 12, 9, 2, 6, 1, 13, 0)
+_constraints = {
+Vector2i(0, 4): 15
+}
+_next_key = 14
+_material_overrides = {}
+tessellation_stages = 5
+tessellation_tolerance = 4.0
+curve_bake_interval = 20.0
+
+[sub_resource type="Resource" id="Resource_42qh7"]
+script = ExtResource("9_tqu3h")
+begin = 0.0
+distance = 360.0
+edgeRendering = Vector2(0, 0)
+
+[sub_resource type="Resource" id="Resource_t7ed3"]
+script = ExtResource("10_umoay")
+edge_material = ExtResource("8_uwntg")
+normal_range = SubResource("Resource_42qh7")
+weld = true
+taper_sharp_corners = true
+render = true
+z_index = 0
+z_as_relative = true
+offset = 0.0
+
+[sub_resource type="Resource" id="Resource_07ymf"]
+script = ExtResource("7_ncdby")
+_edge_meta_materials = Array[ExtResource("10_umoay")]([SubResource("Resource_t7ed3")])
+fill_textures = Array[Texture2D]([ExtResource("6_ry0x0")])
+fill_texture_z_index = -10
+fill_texture_show_behind_parent = false
+fill_texture_scale = 1.0
+fill_texture_absolute_position = false
+fill_texture_absolute_rotation = false
+fill_texture_offset = Vector2(0, 0)
+fill_texture_angle_offset = 0.0
+fill_mesh_offset = 0.0
+render_offset = 0.0
+
+[sub_resource type="Resource" id="Resource_ma2v2"]
+script = ExtResource("10_55l7o")
+textures = Array[Texture2D]([ExtResource("11_jc4po")])
+textures_corner_outer = Array[Texture2D]([ExtResource("13_f7yxr")])
+textures_corner_inner = Array[Texture2D]([ExtResource("12_va43u")])
+textures_taper_left = Array[Texture2D]([])
+textures_taper_right = Array[Texture2D]([])
+textures_taper_corner_left = Array[Texture2D]([])
+textures_taper_corner_right = Array[Texture2D]([])
+randomize_texture = false
+use_corner_texture = true
+use_taper_texture = true
+fit_mode = 0
+
+[sub_resource type="Resource" id="Resource_atqmo"]
+script = ExtResource("9_tqu3h")
+begin = 0.0
+distance = 360.0
+edgeRendering = Vector2(0, 0)
+
+[sub_resource type="Resource" id="Resource_vboqn"]
+script = ExtResource("10_umoay")
+edge_material = SubResource("Resource_ma2v2")
+normal_range = SubResource("Resource_atqmo")
+weld = true
+taper_sharp_corners = false
+render = true
+z_index = 0
+z_as_relative = true
+offset = 0.0
+
+[sub_resource type="Resource" id="Resource_xsfev"]
+script = ExtResource("7_ncdby")
+_edge_meta_materials = Array[ExtResource("10_umoay")]([SubResource("Resource_vboqn")])
+fill_textures = Array[Texture2D]([ExtResource("6_ry0x0")])
+fill_texture_z_index = -10
+fill_texture_show_behind_parent = false
+fill_texture_scale = 1.0
+fill_texture_absolute_position = false
+fill_texture_absolute_rotation = false
+fill_texture_offset = Vector2(0, 0)
+fill_texture_angle_offset = 0.0
+fill_mesh_offset = 0.0
+render_offset = 0.0
+
+[node name="Node2D" type="Node2D"]
+
+[node name="WithTapering" type="Node2D" parent="."]
+texture_repeat = 2
+position = Vector2(-30, 228)
+scale = Vector2(0.65, 0.65)
+script = ExtResource("1_5hlge")
+_points = SubResource("Resource_o2w6y")
+shape_material = SubResource("Resource_07ymf")
+
+[node name="WithoutTapering" type="Node2D" parent="."]
+texture_repeat = 2
+position = Vector2(400, 10)
+scale = Vector2(0.65, 0.65)
+script = ExtResource("1_5hlge")
+_points = SubResource("Resource_o2w6y")
+shape_material = SubResource("Resource_xsfev")
+
+[node name="Message" type="Label" parent="."]
+offset_left = 33.0
+offset_top = 82.0
+offset_right = 434.0
+offset_bottom = 190.0
+theme_override_font_sizes/font_size = 24
+text = "NOTE: Sharp corner tapering can
+cause issues when used on shapes
+with curved geometry"
+
+[node name="With" type="Label" parent="."]
+offset_left = 275.0
+offset_top = 454.0
+offset_right = 503.0
+offset_bottom = 477.0
+text = "Sharp Corner Tapering"
+
+[node name="Without" type="Label" parent="."]
+offset_left = 666.0
+offset_top = 237.0
+offset_right = 907.0
+offset_bottom = 260.0
+text = "No Sharp Corner Tapering"
diff --git a/objects/big_coin.tscn b/objects/big_coin.tscn
new file mode 100644
index 0000000..4cff7f9
--- /dev/null
+++ b/objects/big_coin.tscn
@@ -0,0 +1,35 @@
+[gd_scene load_steps=6 format=3 uid="uid://bargnp4twtmxg"]
+
+[ext_resource type="Texture2D" uid="uid://djifxc5x0dyrw" path="res://sprites/ppc_tileset.png" id="1_aya2w"]
+[ext_resource type="Script" path="res://scripts/components/collectable.gd" id="2_7cph7"]
+[ext_resource type="Resource" uid="uid://bsnr5v2b2mfsl" path="res://resources/collectables/big_coin.tres" id="3_lk3av"]
+[ext_resource type="Script" path="res://scripts/components/fade_away.gd" id="4_wkrj0"]
+
+[sub_resource type="CircleShape2D" id="CircleShape2D_3ask2"]
+radius = 9.0
+
+[node name="Big Coin" type="Area2D"]
+scale = Vector2(2, 2)
+collision_layer = 2
+collision_mask = 4
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
+shape = SubResource("CircleShape2D_3ask2")
+
+[node name="Sprite2D" type="Sprite2D" parent="."]
+texture = ExtResource("1_aya2w")
+hframes = 12
+vframes = 12
+frame = 51
+
+[node name="Collectable" type="Node" parent="." node_paths=PackedStringArray("area2d") groups=["coins"]]
+script = ExtResource("2_7cph7")
+area2d = NodePath("..")
+collectable_data = ExtResource("3_lk3av")
+
+[node name="FadeAwayComponent" type="Node" parent="." node_paths=PackedStringArray("sprite2d", "root", "area2d")]
+script = ExtResource("4_wkrj0")
+sprite2d = NodePath("../Sprite2D")
+fade_duration = 0.4
+root = NodePath("..")
+area2d = NodePath("..")
diff --git a/objects/big_treasure.tscn b/objects/big_treasure.tscn
new file mode 100644
index 0000000..5346e97
--- /dev/null
+++ b/objects/big_treasure.tscn
@@ -0,0 +1,34 @@
+[gd_scene load_steps=6 format=3 uid="uid://d08dfqmirnd66"]
+
+[ext_resource type="Texture2D" uid="uid://djifxc5x0dyrw" path="res://sprites/ppc_tileset.png" id="1_1co1x"]
+[ext_resource type="Script" path="res://scripts/components/collectable.gd" id="2_cujcq"]
+[ext_resource type="Resource" uid="uid://b6xqotmke54x" path="res://resources/collectables/big_treasure.tres" id="3_k64cr"]
+[ext_resource type="Script" path="res://scripts/components/fade_away.gd" id="4_nw7tw"]
+
+[sub_resource type="CircleShape2D" id="CircleShape2D_3ask2"]
+radius = 9.0
+
+[node name="Big Treasure" type="Area2D"]
+collision_layer = 2
+collision_mask = 4
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
+shape = SubResource("CircleShape2D_3ask2")
+
+[node name="Sprite2D" type="Sprite2D" parent="."]
+texture = ExtResource("1_1co1x")
+hframes = 12
+vframes = 12
+frame = 64
+
+[node name="Collectable" type="Node" parent="." node_paths=PackedStringArray("area2d") groups=["coins"]]
+script = ExtResource("2_cujcq")
+area2d = NodePath("..")
+collectable_data = ExtResource("3_k64cr")
+
+[node name="FadeAwayComponent" type="Node" parent="." node_paths=PackedStringArray("sprite2d", "root", "area2d")]
+script = ExtResource("4_nw7tw")
+sprite2d = NodePath("../Sprite2D")
+fade_duration = 0.4
+root = NodePath("..")
+area2d = NodePath("..")
diff --git a/objects/brick_player.tscn b/objects/brick_player.tscn
index 7f9fedb..25ad50e 100644
--- a/objects/brick_player.tscn
+++ b/objects/brick_player.tscn
@@ -1,4 +1,4 @@
-[gd_scene load_steps=19 format=3 uid="uid://bqi5s710xb1ju"]
+[gd_scene load_steps=21 format=3 uid="uid://bqi5s710xb1ju"]
[ext_resource type="Script" path="res://scripts/player.gd" id="1_8j4h4"]
[ext_resource type="Texture2D" uid="uid://b7gp0gqvkv8j4" path="res://sprites/MrBrick_base.png" id="2_bc55y"]
@@ -15,6 +15,8 @@
[ext_resource type="Script" path="res://scripts/components/flashing_component.gd" id="13_hrtyn"]
[ext_resource type="Script" path="res://scripts/components/invulnerability_component.gd" id="14_jopig"]
[ext_resource type="Script" path="res://scripts/components/magnetic_skill.gd" id="15_4df3h"]
+[ext_resource type="Script" path="res://scripts/components/can_be_launched_component.gd" id="16_kemlv"]
+[ext_resource type="Script" path="res://scripts/components/trigger_lever_component.gd" id="17_hglfj"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_hdsg1"]
size = Vector2(16, 31)
@@ -27,7 +29,7 @@ radius = 48.0
[node name="Brick Player" type="CharacterBody2D"]
collision_layer = 4
-collision_mask = 11
+collision_mask = 43
script = ExtResource("1_8j4h4")
[node name="Root" type="Node2D" parent="."]
@@ -132,5 +134,11 @@ collision_mask = 2
[node name="CollisionShape2D" type="CollisionShape2D" parent="MagneticArea"]
shape = SubResource("CircleShape2D_ps31c")
+[node name="CanBeLaunchedComponent" type="Node" parent="."]
+script = ExtResource("16_kemlv")
+
+[node name="TriggerLeverComponent" type="Node" parent="."]
+script = ExtResource("17_hglfj")
+
[connection signal="on_death" from="HealthComponent" to="PlayerDeathComponent" method="_on_health_component_on_death"]
[connection signal="on_health_change" from="HealthComponent" to="KnockbackComponent" method="_on_health_component_on_health_change"]
diff --git a/objects/cage.tscn b/objects/cage.tscn
new file mode 100644
index 0000000..93845f0
--- /dev/null
+++ b/objects/cage.tscn
@@ -0,0 +1,28 @@
+[gd_scene load_steps=5 format=3 uid="uid://to2xnqev0pu1"]
+
+[ext_resource type="Texture2D" uid="uid://djifxc5x0dyrw" path="res://sprites/ppc_tileset.png" id="1_5poh3"]
+[ext_resource type="Script" path="res://scripts/components/cage_component.gd" id="2_unomj"]
+
+[sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_aivtb"]
+texture = ExtResource("1_5poh3")
+2:9/size_in_atlas = Vector2i(2, 2)
+2:9/0 = 0
+2:9/0/physics_layer_0/polygon_0/points = PackedVector2Array(-16, -16, 16, -16, 16, 16, -16, 16)
+
+[sub_resource type="TileSet" id="TileSet_67qt2"]
+tile_size = Vector2i(32, 32)
+physics_layer_0/collision_layer = 1
+sources/0 = SubResource("TileSetAtlasSource_aivtb")
+
+[node name="Cage" type="Node2D"]
+scale = Vector2(2, 2)
+
+[node name="TileMapLayer" type="TileMapLayer" parent="."]
+position = Vector2(20, 4)
+tile_map_data = PackedByteArray(0, 0, 255, 255, 255, 255, 0, 0, 2, 0, 9, 0, 0, 0)
+tile_set = SubResource("TileSet_67qt2")
+
+[node name="CageComponent" type="Node" parent="." node_paths=PackedStringArray("root")]
+script = ExtResource("2_unomj")
+root = NodePath("..")
+should_free = false
diff --git a/objects/enemy.tscn b/objects/enemy.tscn
index 281b452..fa8efc2 100644
--- a/objects/enemy.tscn
+++ b/objects/enemy.tscn
@@ -75,9 +75,11 @@ debug_color = Color(0.913521, 0.265052, 0.323172, 0.42)
[node name="Left Ray" type="RayCast2D" parent="."]
position = Vector2(-16, 13)
+target_position = Vector2(0, 8)
[node name="Right Ray" type="RayCast2D" parent="."]
position = Vector2(16, 13)
+target_position = Vector2(0, 8)
[node name="FlashingComponent" type="Node" parent="." node_paths=PackedStringArray("sprite", "health_component")]
process_mode = 3
diff --git a/objects/jump_pad.tscn b/objects/jump_pad.tscn
new file mode 100644
index 0000000..2a9bab8
--- /dev/null
+++ b/objects/jump_pad.tscn
@@ -0,0 +1,28 @@
+[gd_scene load_steps=4 format=3 uid="uid://cm3rixnnev1pg"]
+
+[ext_resource type="Texture2D" uid="uid://djifxc5x0dyrw" path="res://sprites/ppc_tileset.png" id="1_ctugi"]
+[ext_resource type="Script" path="res://scripts/components/jump_pad_component.gd" id="2_2ypfs"]
+
+[sub_resource type="RectangleShape2D" id="RectangleShape2D_ci3ij"]
+size = Vector2(16, 6)
+
+[node name="Jump pad" type="Area2D"]
+collision_layer = 0
+collision_mask = 4
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
+position = Vector2(0, 5)
+shape = SubResource("RectangleShape2D_ci3ij")
+
+[node name="Sprite2D" type="Sprite2D" parent="."]
+texture = ExtResource("1_ctugi")
+hframes = 12
+vframes = 12
+frame = 120
+
+[node name="JumpPadComponent" type="Node" parent="." node_paths=PackedStringArray("area2d", "sprite2d")]
+script = ExtResource("2_2ypfs")
+jump_force = 1110.0
+area2d = NodePath("..")
+sprite2d = NodePath("../Sprite2D")
+start_animation_index = 120
diff --git a/objects/lever.tscn b/objects/lever.tscn
new file mode 100644
index 0000000..bb6abae
--- /dev/null
+++ b/objects/lever.tscn
@@ -0,0 +1,27 @@
+[gd_scene load_steps=4 format=3 uid="uid://bd51frym6mm7v"]
+
+[ext_resource type="Texture2D" uid="uid://djifxc5x0dyrw" path="res://sprites/ppc_tileset.png" id="1_psg62"]
+[ext_resource type="Script" path="res://scripts/components/lever_component.gd" id="2_0p0wb"]
+
+[sub_resource type="RectangleShape2D" id="RectangleShape2D_ke5tv"]
+size = Vector2(12, 13)
+
+[node name="Lever" type="Area2D" groups=["levers"]]
+collision_layer = 0
+collision_mask = 4
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
+position = Vector2(0, 1.5)
+shape = SubResource("RectangleShape2D_ke5tv")
+
+[node name="Sprite2D" type="Sprite2D" parent="."]
+texture = ExtResource("1_psg62")
+hframes = 12
+vframes = 12
+frame = 75
+
+[node name="LeverComponent" type="Node" parent="." node_paths=PackedStringArray("area2d", "sprite2d")]
+script = ExtResource("2_0p0wb")
+area2d = NodePath("..")
+sprite2d = NodePath("../Sprite2D")
+start_animation_index = 75
diff --git a/objects/treasure.tscn b/objects/treasure.tscn
new file mode 100644
index 0000000..7a3575d
--- /dev/null
+++ b/objects/treasure.tscn
@@ -0,0 +1,34 @@
+[gd_scene load_steps=6 format=3 uid="uid://073ts5cxtwbl"]
+
+[ext_resource type="Texture2D" uid="uid://djifxc5x0dyrw" path="res://sprites/ppc_tileset.png" id="1_uh3ex"]
+[ext_resource type="Script" path="res://scripts/components/collectable.gd" id="2_i1ssp"]
+[ext_resource type="Resource" uid="uid://bws2xldndlre1" path="res://resources/collectables/treasure.tres" id="3_uknsr"]
+[ext_resource type="Script" path="res://scripts/components/fade_away.gd" id="4_ccbcr"]
+
+[sub_resource type="CircleShape2D" id="CircleShape2D_3ask2"]
+radius = 9.0
+
+[node name="Treasure" type="Area2D"]
+collision_layer = 2
+collision_mask = 4
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
+shape = SubResource("CircleShape2D_3ask2")
+
+[node name="Sprite2D" type="Sprite2D" parent="."]
+texture = ExtResource("1_uh3ex")
+hframes = 12
+vframes = 12
+frame = 66
+
+[node name="Collectable" type="Node" parent="." node_paths=PackedStringArray("area2d") groups=["coins"]]
+script = ExtResource("2_i1ssp")
+area2d = NodePath("..")
+collectable_data = ExtResource("3_uknsr")
+
+[node name="FadeAwayComponent" type="Node" parent="." node_paths=PackedStringArray("sprite2d", "root", "area2d")]
+script = ExtResource("4_ccbcr")
+sprite2d = NodePath("../Sprite2D")
+fade_duration = 0.4
+root = NodePath("..")
+area2d = NodePath("..")
diff --git a/project.godot b/project.godot
index 829bbe7..8895a83 100644
--- a/project.godot
+++ b/project.godot
@@ -46,11 +46,12 @@ project/assembly_name="Mr. Brick Adventures"
[editor_plugins]
-enabled=PackedStringArray("res://addons/phantom_camera/plugin.cfg")
+enabled=PackedStringArray("res://addons/phantom_camera/plugin.cfg", "res://addons/rmsmartshape/plugin.cfg")
[global_group]
coins=""
+levers=""
[input]
@@ -88,6 +89,7 @@ attack={
2d_physics/layer_3="Player"
2d_physics/layer_4="Enemy"
2d_physics/layer_5="player projectiles"
+2d_physics/layer_6="Environment"
[physics]
diff --git a/resources/collectables/big_coin.tres b/resources/collectables/big_coin.tres
new file mode 100644
index 0000000..e7549c4
--- /dev/null
+++ b/resources/collectables/big_coin.tres
@@ -0,0 +1,7 @@
+[gd_resource type="Resource" script_class="CollectableResource" load_steps=2 format=3 uid="uid://bsnr5v2b2mfsl"]
+
+[ext_resource type="Script" path="res://scripts/resources/collectable_resource.gd" id="1_fudbo"]
+
+[resource]
+script = ExtResource("1_fudbo")
+amount = 5
diff --git a/resources/collectables/big_treasure.tres b/resources/collectables/big_treasure.tres
new file mode 100644
index 0000000..a6f9604
--- /dev/null
+++ b/resources/collectables/big_treasure.tres
@@ -0,0 +1,7 @@
+[gd_resource type="Resource" script_class="CollectableResource" load_steps=2 format=3 uid="uid://b6xqotmke54x"]
+
+[ext_resource type="Script" path="res://scripts/resources/collectable_resource.gd" id="1_037vi"]
+
+[resource]
+script = ExtResource("1_037vi")
+amount = 100
diff --git a/resources/collectables/treasure.tres b/resources/collectables/treasure.tres
new file mode 100644
index 0000000..c423854
--- /dev/null
+++ b/resources/collectables/treasure.tres
@@ -0,0 +1,7 @@
+[gd_resource type="Resource" script_class="CollectableResource" load_steps=2 format=3 uid="uid://bws2xldndlre1"]
+
+[ext_resource type="Script" path="res://scripts/resources/collectable_resource.gd" id="1_w50p5"]
+
+[resource]
+script = ExtResource("1_w50p5")
+amount = 50
diff --git a/scenes/test.tscn b/scenes/test.tscn
index a8e7607..486741d 100644
--- a/scenes/test.tscn
+++ b/scenes/test.tscn
@@ -1,16 +1,22 @@
-[gd_scene load_steps=27 format=4 uid="uid://h60obxmju6mo"]
+[gd_scene load_steps=33 format=4 uid="uid://h60obxmju6mo"]
[ext_resource type="Texture2D" uid="uid://djifxc5x0dyrw" path="res://sprites/ppc_tileset.png" id="1_5lb42"]
[ext_resource type="TileSet" uid="uid://cl4bn8lofqvky" path="res://tileset/village/tileset_village.tres" id="1_d680t"]
[ext_resource type="Texture2D" uid="uid://dxvevrm15uus1" path="res://sprites/flowers_tileset.png" id="3_235u2"]
[ext_resource type="PackedScene" uid="uid://54w4wisfj8v8" path="res://objects/coin.tscn" id="3_ygehw"]
[ext_resource type="PackedScene" uid="uid://bqi5s710xb1ju" path="res://objects/brick_player.tscn" id="4_hetw8"]
+[ext_resource type="PackedScene" uid="uid://bargnp4twtmxg" path="res://objects/big_coin.tscn" id="5_bpga1"]
[ext_resource type="PackedScene" uid="uid://ct8fim6mduyl3" path="res://objects/collapsing_bridge.tscn" id="6_84ckv"]
+[ext_resource type="PackedScene" uid="uid://d08dfqmirnd66" path="res://objects/big_treasure.tscn" id="6_a3vrq"]
+[ext_resource type="PackedScene" uid="uid://073ts5cxtwbl" path="res://objects/treasure.tscn" id="7_4mhb5"]
[ext_resource type="Script" path="res://addons/phantom_camera/scripts/phantom_camera_host/phantom_camera_host.gd" id="7_jgh7o"]
[ext_resource type="PackedScene" uid="uid://bwdlmualj6xbw" path="res://objects/enemy.tscn" id="7_qgddg"]
[ext_resource type="PackedScene" uid="uid://bhc7y4xugu4q7" path="res://objects/bullet.tscn" id="8_c68mx"]
+[ext_resource type="PackedScene" uid="uid://cm3rixnnev1pg" path="res://objects/jump_pad.tscn" id="8_dt3jb"]
+[ext_resource type="PackedScene" uid="uid://to2xnqev0pu1" path="res://objects/cage.tscn" id="9_oiafb"]
[ext_resource type="PackedScene" uid="uid://dfwpha0d18dmn" path="res://objects/cannon_ray_down.tscn" id="9_ysd8p"]
[ext_resource type="PackedScene" uid="uid://d3lt4rhxduv44" path="res://objects/cannon_ray_left.tscn" id="10_7v2ff"]
+[ext_resource type="PackedScene" uid="uid://bd51frym6mm7v" path="res://objects/lever.tscn" id="10_gxrpi"]
[ext_resource type="PackedScene" uid="uid://dstko446qydsc" path="res://objects/cannon.tscn" id="11_qv64j"]
[ext_resource type="Script" path="res://addons/phantom_camera/scripts/phantom_camera/phantom_camera_2d.gd" id="13_rsy5s"]
[ext_resource type="Script" path="res://addons/phantom_camera/scripts/resources/tween_resource.gd" id="14_mjvn7"]
@@ -260,6 +266,12 @@ texture = ExtResource("1_5lb42")
[sub_resource type="TileSetScenesCollectionSource" id="TileSetScenesCollectionSource_v3eon"]
scenes/1/scene = ExtResource("3_ygehw")
scenes/2/scene = ExtResource("6_84ckv")
+scenes/3/scene = ExtResource("5_bpga1")
+scenes/4/scene = ExtResource("6_a3vrq")
+scenes/5/scene = ExtResource("7_4mhb5")
+scenes/6/scene = ExtResource("8_dt3jb")
+scenes/7/scene = ExtResource("9_oiafb")
+scenes/8/scene = ExtResource("10_gxrpi")
[sub_resource type="TileSet" id="TileSet_yf4x4"]
physics_layer_0/collision_layer = 1
@@ -404,19 +416,19 @@ scale = Vector2(4.05664, 753)
texture = SubResource("GradientTexture1D_uhg81")
[node name="Background Layer" type="TileMapLayer" parent="."]
-tile_map_data = PackedByteArray("AAAKAP7/AAAAAAYAAAAKAP//AAAAAAcAAAALAP7/AAABAAYAAAALAP//AAABAAcAAAALAAAAAAABAAgAAAALAAEAAAABAAkAAAAMAP7/AAACAAYAAAAMAP//AAACAAcAAAAMAAAAAAACAAgAAAATAAAAAAAAAAYAAAATAAEAAAAAAAcAAAAUAAAAAAABAAYAAAAUAAEAAAABAAcAAAAUAAIAAAABAAgAAAAUAAMAAAABAAkAAAAVAAAAAAACAAYAAAAVAAEAAAACAAcAAAAVAAIAAAACAAgAAAAbAPb/AAAAAAYAAAAbAPf/AAAAAAcAAAAcAPb/AAABAAYAAAAcAPf/AAABAAcAAAAcAPj/AAABAAgAAAAcAPn/AAABAAkAAAAdAPb/AAACAAYAAAAdAPf/AAACAAcAAAAdAPj/AAACAAgAAAAPAPb/AAAAAAYAAAAPAPf/AAAAAAcAAAAQAPb/AAABAAYAAAAQAPf/AAABAAcAAAAQAPj/AAABAAgAAAAQAPn/AAABAAkAAAARAPb/AAACAAYAAAARAPf/AAACAAcAAAARAPj/AAACAAgAAAD3//z/AAALAAUAAAA=")
+tile_map_data = PackedByteArray("AAAKAP7/AAAAAAYAAAAKAP//AAAAAAcAAAALAP7/AAABAAYAAAALAP//AAABAAcAAAALAAAAAAABAAgAAAALAAEAAAABAAkAAAAMAP7/AAACAAYAAAAMAP//AAACAAcAAAAMAAAAAAACAAgAAAATAAAAAAAAAAYAAAATAAEAAAAAAAcAAAAUAAAAAAABAAYAAAAUAAEAAAABAAcAAAAUAAIAAAABAAgAAAAUAAMAAAABAAkAAAAVAAAAAAACAAYAAAAVAAEAAAACAAcAAAAVAAIAAAACAAgAAAAbAPb/AAAAAAYAAAAbAPf/AAAAAAcAAAAcAPb/AAABAAYAAAAcAPf/AAABAAcAAAAcAPj/AAABAAgAAAAcAPn/AAABAAkAAAAdAPb/AAACAAYAAAAdAPf/AAACAAcAAAAdAPj/AAACAAgAAAAPAPb/AAAAAAYAAAAPAPf/AAAAAAcAAAAQAPb/AAABAAYAAAAQAPf/AAABAAcAAAAQAPj/AAABAAgAAAAQAPn/AAABAAkAAAARAPb/AAACAAYAAAARAPf/AAACAAcAAAARAPj/AAACAAgAAAD3//z/AAALAAUAAABjAAUAAAAKAAUAAABkAAUAAAAKAAUAAABlAAUAAAAKAAUAAABmAAUAAAAKAAUAAABnAAUAAAAKAAUAAABoAAUAAAAKAAUAAABpAAUAAAAKAAUAAABqAAUAAAAKAAUAAABrAAUAAAAKAAUAAABsAAUAAAAKAAUAAABjAAQAAAAJAAUAAABkAAQAAAAJAAUAAABlAAQAAAAJAAUAAABmAAQAAAAJAAUAAABnAAQAAAAJAAUAAABoAAQAAAAJAAUAAABpAAQAAAAJAAUAAABqAAQAAAAJAAUAAABrAAQAAAAJAAUAAABsAAQAAAAJAAUAAABtAAQAAAAJAAUAAABuAAQAAAAJAAUAAABvAAQAAAAJAAUAAABwAAQAAAAJAAUAAABxAAQAAAAJAAUAAAByAAQAAAAJAAUAAABzAAQAAAAJAAUAAAB0AAQAAAAJAAUAAAB1AAQAAAAJAAUAAAB2AAQAAAAJAAUAAAB3AAQAAAAJAAUAAAB4AAQAAAAJAAUAAAB5AAQAAAAJAAUAAAB6AAQAAAAJAAUAAAB7AAQAAAAJAAUAAAB8AAQAAAAJAAUAAAB8AAUAAAAKAAUAAAB7AAUAAAAKAAUAAAB6AAUAAAAKAAUAAAB5AAUAAAAKAAUAAAB4AAUAAAAKAAUAAAB3AAUAAAAKAAUAAAB2AAUAAAAKAAUAAAB1AAUAAAAKAAUAAAB0AAUAAAAKAAUAAABzAAUAAAAKAAUAAAByAAUAAAAKAAUAAABxAAUAAAAKAAUAAABwAAUAAAAKAAUAAABvAAUAAAAKAAUAAABuAAUAAAAKAAUAAABtAAUAAAAKAAUAAABjAAYAAAAKAAUAAABjAAcAAAAKAAUAAABkAAcAAAAKAAUAAABlAAcAAAAKAAUAAABmAAcAAAAKAAUAAABnAAcAAAAKAAUAAABoAAcAAAAKAAUAAABpAAcAAAAKAAUAAABqAAcAAAAKAAUAAABrAAcAAAAKAAUAAABsAAcAAAAKAAUAAABtAAcAAAAKAAUAAABuAAcAAAAKAAUAAABvAAcAAAAKAAUAAABwAAcAAAAKAAUAAABxAAcAAAAKAAUAAAByAAcAAAAKAAUAAABzAAcAAAAKAAUAAAB0AAcAAAAKAAUAAAB1AAcAAAAKAAUAAAB2AAcAAAAKAAUAAAB3AAcAAAAKAAUAAAB4AAcAAAAKAAUAAAB5AAcAAAAKAAUAAAB6AAcAAAAKAAUAAAB7AAcAAAAKAAUAAAB7AAYAAAAKAAUAAAB8AAYAAAAKAAUAAAB8AAcAAAAKAAUAAAB6AAYAAAAKAAUAAAB5AAYAAAAKAAUAAAB4AAYAAAAKAAUAAAB3AAYAAAAKAAUAAAB2AAYAAAAKAAUAAAB1AAYAAAAKAAUAAAB0AAYAAAAKAAUAAABzAAYAAAAKAAUAAAByAAYAAAAKAAUAAABxAAYAAAAKAAUAAABwAAYAAAAKAAUAAABvAAYAAAAKAAUAAABuAAYAAAAKAAUAAABtAAYAAAAKAAUAAABsAAYAAAAKAAUAAABrAAYAAAAKAAUAAABqAAYAAAAKAAUAAABpAAYAAAAKAAUAAABoAAYAAAAKAAUAAABnAAYAAAAKAAUAAABmAAYAAAAKAAUAAABlAAYAAAAKAAUAAABkAAYAAAAKAAUAAAA=")
tile_set = SubResource("TileSet_7ri0q")
[node name="Terrain Layer" type="TileMapLayer" parent="."]
-tile_map_data = PackedByteArray("AAD8////AAABAAAAAAD9////AAABAAAAAAD+////AAABAAAAAAD/////AAABAAAAAAAAAP//AAABAAAAAAABAP//AAABAAAAAAABAAAAAAABAAEAAAABAAEAAAABAAEAAAAAAAEAAAABAAEAAAD//wEAAAABAAEAAAD+/wEAAAABAAEAAAD9/wEAAAABAAEAAAD8/wEAAAABAAEAAAD8/wAAAAABAAEAAAD9/wAAAAABAAEAAAD+/wAAAAABAAEAAAD//wAAAAABAAEAAAAAAAAAAAABAAEAAAACAP//AAABAAAAAAACAAAAAAABAAEAAAACAAEAAAABAAEAAAADAAEAAAABAAEAAAAEAAEAAAABAAEAAAAEAAAAAAABAAEAAAAEAP//AAABAAEAAAADAP//AAAGAAIAAAADAAAAAAABAAEAAAAFAAAAAAABAAEAAAAFAP//AAABAAEAAAAFAAEAAAABAAEAAAADAP7/AAAAAAEAAAADAP3/AAAAAAEAAAADAPz/AAAAAAEAAAADAPv/AAAAAAAAAAAEAPv/AAABAAAAAAAFAPv/AAACAAAAAAAFAPz/AAAFAAIAAAAFAP3/AAABAAEAAAAFAP7/AAABAAEAAAAEAP3/AAABAAEAAAAGAP3/AAABAAEAAAAGAPz/AAABAAAAAAAHAPz/AAACAAAAAAAHAP3/AAAFAAIAAAAIAP3/AAACAAAAAAAIAP7/AAACAAEAAAAIAP//AAACAAEAAAAIAAAAAAAFAAIAAAAJAAAAAAACAAAAAAAJAAEAAAACAAEAAAAJAAIAAAAFAAIAAAAKAAIAAAABAAAAAAALAAIAAAABAAAAAAAMAAIAAAABAAAAAAANAAIAAAAGAAIAAAANAAEAAAAAAAAAAAAOAAEAAAABAAAAAAAPAAEAAAABAAAAAAAQAAEAAAABAAAAAAARAAEAAAABAAAAAAASAAEAAAACAAAAAAASAAIAAAACAAEAAAASAAMAAAACAAEAAAARAAMAAAABAAEAAAAQAAMAAAABAAEAAAAPAAMAAAABAAEAAAAOAAMAAAABAAEAAAANAAMAAAABAAEAAAAMAAMAAAABAAEAAAALAAMAAAABAAEAAAAKAAMAAAABAAEAAAAJAAMAAAABAAEAAAAIAAMAAAABAAEAAAAHAAMAAAABAAEAAAAGAAMAAAABAAEAAAAGAAIAAAABAAEAAAAFAAIAAAABAAEAAAAEAAIAAAABAAEAAAADAAIAAAABAAEAAAACAAIAAAABAAEAAAABAAIAAAABAAEAAAAAAAIAAAABAAEAAAD//wIAAAABAAEAAAD+/wIAAAABAAEAAAD9/wIAAAABAAEAAAD8/wIAAAABAAEAAAD8/wMAAAABAAEAAAD9/wMAAAABAAEAAAD+/wMAAAABAAEAAAD+/wQAAAABAAEAAAD//wQAAAABAAEAAAAAAAQAAAABAAEAAAABAAQAAAABAAEAAAACAAQAAAABAAEAAAADAAQAAAABAAEAAAAEAAQAAAABAAEAAAAFAAQAAAABAAEAAAAGAAQAAAABAAEAAAAHAAQAAAABAAEAAAAIAAQAAAABAAEAAAAJAAQAAAABAAEAAAAKAAQAAAABAAEAAAALAAQAAAABAAEAAAAMAAQAAAABAAEAAAANAAQAAAABAAEAAAAOAAQAAAABAAEAAAAPAAQAAAABAAEAAAARAAIAAAABAAEAAAAQAAIAAAABAAEAAAAPAAIAAAABAAEAAAAOAAIAAAABAAEAAAAQAAQAAAABAAEAAAARAAQAAAABAAEAAAASAAQAAAAFAAIAAAAIAAIAAAABAAEAAAAHAAIAAAABAAEAAAAFAAMAAAABAAEAAAAEAAMAAAABAAEAAAADAAMAAAABAAEAAAACAAMAAAABAAEAAAABAAMAAAABAAEAAAAAAAMAAAABAAEAAAD//wMAAAABAAEAAAD9/wQAAAABAAEAAAD8/wQAAAABAAEAAAAIAAEAAAABAAEAAAAHAAEAAAABAAEAAAAHAAAAAAABAAEAAAAHAP//AAABAAEAAAAHAP7/AAABAAEAAAAGAAEAAAABAAEAAAAGAAAAAAABAAEAAAAGAP//AAABAAEAAAAGAP7/AAABAAEAAAAEAP7/AAABAAEAAAAEAPz/AAABAAEAAAAVAPz/AAABAAIAAAAUAPz/AAABAAIAAAATAP3/AAACAAIAAAASAP3/AAABAAIAAAARAP3/AAABAAIAAAAQAP3/AAABAAIAAAAPAP7/AAACAAIAAAAOAP7/AAAAAAIAAAAdAAQAAAABAAEAAAAcAAMAAAAAAAEAAAAbAAIAAAAAAAIAAAAaAAEAAAAAAAIAAAAZAAAAAAAAAAIAAAAZAP//AAAAAAEAAAAYAP7/AAAAAAIAAAAXAP3/AAAAAAIAAAAWAPz/AAABAAIAAAAcAAQAAAAGAAIAAAAbAAQAAAABAAAAAAAaAAQAAAABAAAAAAAZAAQAAAABAAAAAAAYAAQAAAABAAAAAAAXAAQAAAABAAAAAAAWAAQAAAABAAAAAAAVAAQAAAABAAAAAAAUAAQAAAABAAAAAAATAAQAAAABAAAAAAAdAAMAAAABAAEAAAAdAAIAAAABAAEAAAAcAAIAAAAGAAEAAAAcAAEAAAABAAEAAAAbAAEAAAAGAAEAAAAdAAEAAAABAAEAAAAdAAAAAAABAAEAAAAcAAAAAAABAAEAAAAbAAAAAAABAAEAAAAaAAAAAAAGAAEAAAAaAP//AAABAAEAAAAaAP7/AAABAAEAAAAZAP7/AAAGAAEAAAAZAP3/AAABAAEAAAAYAP3/AAAGAAEAAAAYAPz/AAABAAEAAAAXAPz/AAAGAAEAAAAZAPz/AAABAAEAAAAaAPz/AAABAAEAAAAaAP3/AAABAAEAAAAbAP3/AAABAAEAAAAbAP7/AAABAAEAAAAbAP//AAABAAEAAAAcAP//AAABAAEAAAAdAP//AAABAAEAAAAdAP7/AAABAAEAAAAcAP3/AAABAAEAAAAcAPz/AAABAAEAAAAbAPz/AAABAAEAAAAcAP7/AAABAAEAAAAdAP3/AAABAAEAAAAdAPz/AAABAAEAAAAdAPv/AAABAAEAAAAcAPv/AAABAAEAAAAbAPv/AAABAAEAAAAaAPv/AAABAAEAAAAZAPv/AAABAAEAAAAYAPv/AAABAAEAAAAXAPv/AAABAAEAAAAWAPv/AAABAAEAAAAWAPr/AAABAAAAAAAVAPr/AAABAAAAAAAUAPr/AAABAAAAAAAUAPv/AAABAAEAAAAVAPv/AAABAAEAAAAXAPr/AAABAAAAAAAYAPr/AAABAAAAAAAZAPr/AAABAAAAAAAaAPr/AAABAAAAAAAbAPr/AAABAAAAAAAcAPr/AAABAAAAAAAdAPr/AAABAAAAAAATAPz/AAAFAAEAAAASAPz/AAABAAEAAAARAPz/AAABAAEAAAARAPv/AAABAAEAAAASAPv/AAABAAEAAAASAPr/AAABAAAAAAATAPr/AAABAAAAAAATAPv/AAABAAEAAAARAPr/AAABAAAAAAAQAPr/AAABAAAAAAAQAPv/AAABAAEAAAAQAPz/AAABAAEAAAAPAPz/AAABAAEAAAAPAP3/AAAFAAEAAAAOAP3/AAAAAAEAAAAOAPz/AAAAAAEAAAAOAPv/AAAAAAEAAAAPAPv/AAABAAEAAAAPAPr/AAABAAAAAAAOAPr/AAAAAAAAAAAdAAUAAAABAAIAAAAcAAUAAAABAAIAAAAbAAUAAAABAAIAAAAaAAUAAAABAAIAAAAZAAUAAAABAAIAAAAYAAUAAAABAAIAAAASAAUAAAABAAIAAAATAAUAAAABAAIAAAAUAAUAAAABAAIAAAAVAAUAAAABAAIAAAAWAAUAAAABAAIAAAAXAAUAAAABAAIAAAARAAUAAAABAAIAAAAQAAUAAAABAAIAAAAPAAUAAAABAAIAAAAOAAUAAAABAAIAAAANAAUAAAABAAIAAAAMAAUAAAABAAIAAAALAAUAAAABAAIAAAAKAAUAAAABAAIAAAAJAAUAAAABAAIAAAAIAAUAAAABAAIAAAAHAAUAAAABAAIAAAAGAAUAAAABAAIAAAAFAAUAAAABAAIAAAAEAAUAAAABAAIAAAADAAUAAAABAAIAAAACAAUAAAABAAIAAAABAAUAAAABAAIAAAAAAAUAAAABAAIAAAD//wUAAAABAAIAAAD+/wUAAAABAAIAAAD9/wUAAAABAAIAAAD8/wUAAAABAAIAAAAnAAUAAAACAAIAAAAnAAQAAAACAAEAAAAnAAMAAAACAAEAAAAnAAIAAAACAAEAAAAnAAEAAAACAAEAAAAnAAAAAAACAAEAAAAnAP//AAACAAEAAAAnAP7/AAACAAEAAAAnAP3/AAACAAEAAAAnAPz/AAACAAEAAAAnAPv/AAACAAEAAAAnAPr/AAACAAEAAAAmAAUAAAABAAIAAAAmAAQAAAABAAEAAAAmAAMAAAABAAEAAAAmAAIAAAABAAEAAAAmAAEAAAABAAEAAAAmAAAAAAABAAEAAAAmAP//AAABAAEAAAAmAP7/AAABAAEAAAAmAP3/AAABAAEAAAAmAPz/AAABAAEAAAAmAPv/AAABAAEAAAAmAPr/AAABAAEAAAAlAAUAAAABAAIAAAAlAAQAAAABAAEAAAAlAAMAAAABAAEAAAAlAAIAAAABAAEAAAAlAAEAAAABAAEAAAAlAAAAAAABAAEAAAAlAP//AAABAAEAAAAlAP7/AAABAAEAAAAlAP3/AAABAAEAAAAlAPz/AAABAAEAAAAlAPv/AAABAAEAAAAlAPr/AAABAAEAAAAkAAUAAAABAAIAAAAkAAQAAAABAAEAAAAkAAMAAAABAAEAAAAkAAIAAAABAAEAAAAkAAEAAAABAAEAAAAkAAAAAAABAAEAAAAkAP//AAABAAEAAAAkAP7/AAABAAEAAAAkAP3/AAABAAEAAAAkAPz/AAABAAEAAAAkAPv/AAABAAEAAAAkAPr/AAABAAEAAAAjAAUAAAABAAIAAAAjAAQAAAABAAEAAAAjAAMAAAABAAEAAAAjAAIAAAABAAEAAAAjAAEAAAABAAEAAAAjAAAAAAABAAEAAAAjAP//AAABAAEAAAAjAP7/AAABAAEAAAAjAP3/AAABAAEAAAAjAPz/AAABAAEAAAAjAPv/AAABAAEAAAAjAPr/AAABAAEAAAAiAAUAAAABAAIAAAAiAAQAAAABAAEAAAAiAAMAAAABAAEAAAAiAAIAAAABAAEAAAAiAAEAAAABAAEAAAAiAAAAAAABAAEAAAAiAP//AAABAAEAAAAiAP7/AAABAAEAAAAiAP3/AAABAAEAAAAiAPz/AAABAAEAAAAiAPv/AAABAAEAAAAiAPr/AAABAAEAAAAhAAUAAAABAAIAAAAhAAQAAAABAAEAAAAhAAMAAAABAAEAAAAhAAIAAAABAAEAAAAhAAEAAAABAAEAAAAhAAAAAAABAAEAAAAhAP//AAABAAEAAAAhAP7/AAABAAEAAAAhAP3/AAABAAEAAAAhAPz/AAABAAEAAAAhAPv/AAABAAEAAAAhAPr/AAABAAEAAAAgAAUAAAABAAIAAAAgAAQAAAABAAEAAAAgAAMAAAABAAEAAAAgAAIAAAABAAEAAAAgAAEAAAABAAEAAAAgAAAAAAABAAEAAAAgAP//AAABAAEAAAAgAP7/AAABAAEAAAAgAP3/AAABAAEAAAAgAPz/AAABAAEAAAAgAPv/AAABAAEAAAAgAPr/AAAGAAIAAAAfAAUAAAABAAIAAAAfAAQAAAABAAEAAAAfAAMAAAABAAEAAAAfAAIAAAABAAEAAAAfAAEAAAABAAEAAAAfAAAAAAABAAEAAAAfAP//AAABAAEAAAAfAP7/AAABAAEAAAAfAP3/AAABAAEAAAAfAPz/AAABAAEAAAAfAPv/AAABAAEAAAAfAPr/AAABAAAAAAAeAAUAAAABAAIAAAAeAAQAAAABAAEAAAAeAAMAAAABAAEAAAAeAAIAAAABAAEAAAAeAAEAAAABAAEAAAAeAAAAAAABAAEAAAAeAP//AAABAAEAAAAeAP7/AAABAAEAAAAeAP3/AAABAAEAAAAeAPz/AAABAAEAAAAeAPv/AAABAAEAAAAeAPr/AAABAAAAAAAnAPn/AAACAAEAAAAnAPj/AAACAAEAAAAnAPf/AAACAAEAAAAnAPb/AAACAAEAAAAnAPX/AAACAAEAAAAnAPT/AAAFAAEAAAAnAPP/AAABAAEAAAAnAPL/AAABAAEAAAAnAPH/AAABAAEAAAAnAPD/AAABAAEAAAAmAPn/AAABAAEAAAAmAPj/AAABAAEAAAAmAPf/AAABAAEAAAAmAPb/AAABAAEAAAAmAPX/AAABAAEAAAAmAPT/AAABAAEAAAAmAPP/AAABAAEAAAAmAPL/AAABAAEAAAAmAPH/AAABAAEAAAAmAPD/AAABAAEAAAAlAPn/AAABAAEAAAAlAPj/AAABAAEAAAAlAPf/AAABAAEAAAAlAPb/AAABAAEAAAAlAPX/AAABAAEAAAAlAPT/AAABAAEAAAAlAPP/AAABAAEAAAAlAPL/AAABAAEAAAAlAPH/AAABAAEAAAAlAPD/AAABAAEAAAAkAPn/AAABAAEAAAAkAPj/AAABAAEAAAAkAPf/AAABAAEAAAAkAPb/AAABAAEAAAAkAPX/AAABAAEAAAAkAPT/AAABAAEAAAAkAPP/AAABAAEAAAAkAPL/AAABAAEAAAAkAPH/AAABAAEAAAAkAPD/AAABAAEAAAAjAPn/AAABAAEAAAAjAPj/AAABAAEAAAAjAPf/AAABAAEAAAAjAPb/AAABAAEAAAAjAPX/AAABAAEAAAAjAPT/AAABAAEAAAAjAPP/AAABAAEAAAAjAPL/AAABAAEAAAAjAPH/AAABAAEAAAAjAPD/AAABAAEAAAAiAPn/AAABAAEAAAAiAPj/AAABAAEAAAAiAPf/AAABAAEAAAAiAPb/AAABAAEAAAAiAPX/AAABAAEAAAAiAPT/AAABAAEAAAAiAPP/AAABAAEAAAAiAPL/AAABAAEAAAAiAPH/AAABAAEAAAAiAPD/AAABAAEAAAAhAPn/AAABAAEAAAAhAPj/AAABAAEAAAAhAPf/AAABAAEAAAAhAPb/AAABAAEAAAAhAPX/AAABAAEAAAAhAPT/AAABAAEAAAAhAPP/AAABAAEAAAAhAPL/AAABAAEAAAAhAPH/AAABAAEAAAAhAPD/AAABAAEAAAAgAPn/AAAAAAEAAAAgAPj/AAAAAAEAAAAgAPf/AAAAAAEAAAAgAPb/AAAAAAEAAAAgAPX/AAAAAAEAAAAgAPT/AAAAAAEAAAAgAPP/AAAAAAEAAAAgAPL/AAAAAAEAAAAgAPH/AAAAAAEAAAAgAPD/AAAAAAEAAAApAO//AAACAAEAAAApAO7/AAACAAEAAAApAO3/AAACAAEAAAApAOz/AAACAAEAAAApAOv/AAACAAEAAAApAOr/AAACAAAAAAAoAO//AAABAAEAAAAoAO7/AAABAAEAAAAoAO3/AAABAAEAAAAoAOz/AAABAAEAAAAoAOv/AAABAAEAAAAoAOr/AAABAAAAAAAnAO//AAABAAEAAAAnAO7/AAABAAEAAAAnAO3/AAABAAEAAAAnAOz/AAABAAEAAAAnAOv/AAABAAEAAAAnAOr/AAABAAAAAAAmAO//AAABAAEAAAAmAO7/AAABAAEAAAAmAO3/AAABAAEAAAAmAOz/AAABAAEAAAAmAOv/AAABAAEAAAAmAOr/AAABAAAAAAAlAO//AAABAAEAAAAlAO7/AAABAAEAAAAlAO3/AAABAAEAAAAlAOz/AAABAAEAAAAlAOv/AAABAAEAAAAlAOr/AAABAAAAAAAkAO//AAABAAEAAAAkAO7/AAABAAEAAAAkAO3/AAABAAEAAAAkAOz/AAABAAEAAAAkAOv/AAABAAEAAAAkAOr/AAABAAAAAAAjAO//AAABAAEAAAAjAO7/AAABAAEAAAAjAO3/AAABAAEAAAAjAOz/AAABAAEAAAAjAOv/AAABAAEAAAAjAOr/AAABAAAAAAAiAO//AAABAAEAAAAiAO7/AAABAAEAAAAiAO3/AAABAAEAAAAiAOz/AAABAAEAAAAiAOv/AAABAAEAAAAiAOr/AAABAAAAAAAhAO//AAABAAEAAAAhAO7/AAABAAEAAAAhAO3/AAABAAEAAAAhAOz/AAABAAEAAAAhAOv/AAABAAEAAAAhAOr/AAABAAAAAAAgAO//AAAGAAEAAAAgAO7/AAABAAEAAAAgAO3/AAABAAEAAAAgAOz/AAABAAEAAAAgAOv/AAABAAEAAAAgAOr/AAABAAAAAAApAPT/AAACAAIAAAApAPP/AAACAAEAAAApAPL/AAACAAEAAAApAPH/AAACAAEAAAApAPD/AAACAAEAAAAoAPT/AAABAAIAAAAoAPP/AAABAAEAAAAoAPL/AAABAAEAAAAoAPH/AAABAAEAAAAoAPD/AAABAAEAAAAfAO//AAABAAIAAAAfAO7/AAABAAEAAAAfAO3/AAABAAEAAAAfAOz/AAABAAEAAAAfAOv/AAABAAEAAAAfAOr/AAABAAAAAAAeAO//AAABAAIAAAAeAO7/AAABAAEAAAAeAO3/AAABAAEAAAAeAOz/AAABAAEAAAAeAOv/AAABAAEAAAAeAOr/AAABAAAAAAAdAO//AAABAAIAAAAdAO7/AAABAAEAAAAdAO3/AAABAAEAAAAdAOz/AAABAAEAAAAdAOv/AAABAAEAAAAdAOr/AAABAAAAAAAcAO//AAABAAIAAAAcAO7/AAABAAEAAAAcAO3/AAABAAEAAAAcAOz/AAABAAEAAAAcAOv/AAABAAEAAAAcAOr/AAABAAAAAAAbAO//AAABAAIAAAAbAO7/AAABAAEAAAAbAO3/AAABAAEAAAAbAOz/AAABAAEAAAAbAOv/AAABAAEAAAAbAOr/AAABAAAAAAAaAO//AAABAAIAAAAaAO7/AAABAAEAAAAaAO3/AAABAAEAAAAaAOz/AAABAAEAAAAaAOv/AAABAAEAAAAaAOr/AAABAAAAAAAZAO//AAABAAIAAAAZAO7/AAABAAEAAAAZAO3/AAABAAEAAAAZAOz/AAABAAEAAAAZAOv/AAABAAEAAAAZAOr/AAABAAAAAAAYAO//AAABAAIAAAAYAO7/AAABAAEAAAAYAO3/AAABAAEAAAAYAOz/AAABAAEAAAAYAOv/AAABAAEAAAAYAOr/AAABAAAAAAAXAO//AAABAAIAAAAXAO7/AAABAAEAAAAXAO3/AAABAAEAAAAXAOz/AAABAAEAAAAXAOv/AAABAAEAAAAXAOr/AAABAAAAAAAWAO//AAABAAIAAAAWAO7/AAABAAEAAAAWAO3/AAABAAEAAAAWAOz/AAABAAEAAAAWAOv/AAABAAEAAAAWAOr/AAABAAAAAAAVAO//AAABAAIAAAAVAO7/AAABAAEAAAAVAO3/AAABAAEAAAAVAOz/AAABAAEAAAAVAOv/AAABAAEAAAAVAOr/AAABAAAAAAAUAO//AAABAAIAAAAUAO7/AAABAAEAAAAUAO3/AAABAAEAAAAUAOz/AAABAAEAAAAUAOv/AAABAAEAAAAUAOr/AAABAAAAAAATAO//AAABAAIAAAATAO7/AAABAAEAAAATAO3/AAABAAEAAAATAOz/AAABAAEAAAATAOv/AAABAAEAAAATAOr/AAABAAAAAAASAO//AAABAAIAAAASAO7/AAABAAEAAAASAO3/AAABAAEAAAASAOz/AAABAAEAAAASAOv/AAABAAEAAAASAOr/AAABAAAAAAARAO//AAABAAIAAAARAO7/AAABAAEAAAARAO3/AAABAAEAAAARAOz/AAABAAEAAAARAOv/AAABAAEAAAARAOr/AAABAAAAAAAQAO//AAABAAIAAAAQAO7/AAABAAEAAAAQAO3/AAABAAEAAAAQAOz/AAABAAEAAAAQAOv/AAABAAEAAAAQAOr/AAABAAAAAAAPAO//AAABAAIAAAAPAO7/AAABAAEAAAAPAO3/AAABAAEAAAAPAOz/AAABAAEAAAAPAOv/AAABAAEAAAAPAOr/AAABAAAAAAAOAO//AAABAAIAAAAOAO7/AAABAAEAAAAOAO3/AAABAAEAAAAOAOz/AAABAAEAAAAOAOv/AAABAAEAAAAOAOr/AAABAAAAAAANAO//AAABAAIAAAANAO7/AAABAAEAAAANAO3/AAABAAEAAAANAOz/AAABAAEAAAANAOv/AAABAAEAAAANAOr/AAABAAAAAAAMAO//AAABAAIAAAAMAO7/AAABAAEAAAAMAO3/AAABAAEAAAAMAOz/AAABAAEAAAAMAOv/AAABAAEAAAAMAOr/AAABAAAAAAALAO//AAABAAIAAAALAO7/AAABAAEAAAALAO3/AAABAAEAAAALAOz/AAABAAEAAAALAOv/AAABAAEAAAALAOr/AAABAAAAAAAKAO//AAABAAIAAAAKAO7/AAABAAEAAAAKAO3/AAABAAEAAAAKAOz/AAABAAEAAAAKAOv/AAABAAEAAAAKAOr/AAABAAAAAAAJAO//AAABAAIAAAAJAO7/AAABAAEAAAAJAO3/AAABAAEAAAAJAOz/AAABAAEAAAAJAOv/AAABAAEAAAAJAOr/AAABAAAAAAAIAO//AAABAAIAAAAIAO7/AAABAAEAAAAIAO3/AAABAAEAAAAIAOz/AAABAAEAAAAIAOv/AAABAAEAAAAIAOr/AAABAAAAAAAHAO//AAABAAIAAAAHAO7/AAABAAEAAAAHAO3/AAABAAEAAAAHAOz/AAABAAEAAAAHAOv/AAABAAEAAAAHAOr/AAABAAAAAAAGAO//AAABAAIAAAAGAO7/AAABAAEAAAAGAO3/AAABAAEAAAAGAOz/AAABAAEAAAAGAOv/AAABAAEAAAAGAOr/AAABAAAAAAAFAO//AAABAAIAAAAFAO7/AAABAAEAAAAFAO3/AAABAAEAAAAFAOz/AAABAAEAAAAFAOv/AAABAAEAAAAFAOr/AAABAAAAAAAEAO//AAABAAIAAAAEAO7/AAABAAEAAAAEAO3/AAABAAEAAAAEAOz/AAABAAEAAAAEAOv/AAABAAEAAAAEAOr/AAABAAAAAAADAO//AAABAAIAAAADAO7/AAABAAEAAAADAO3/AAABAAEAAAADAOz/AAABAAEAAAADAOv/AAABAAEAAAADAOr/AAABAAAAAAACAO//AAABAAIAAAACAO7/AAABAAEAAAACAO3/AAABAAEAAAACAOz/AAABAAEAAAACAOv/AAABAAEAAAACAOr/AAABAAAAAAABAO//AAABAAIAAAABAO7/AAABAAEAAAABAO3/AAABAAEAAAABAOz/AAABAAEAAAABAOv/AAABAAEAAAABAOr/AAABAAAAAAAAAO//AAABAAIAAAAAAO7/AAABAAEAAAAAAO3/AAABAAEAAAAAAOz/AAABAAEAAAAAAOv/AAABAAEAAAAAAOr/AAABAAAAAAD//+//AAABAAIAAAD//+7/AAABAAEAAAD//+3/AAABAAEAAAD//+z/AAABAAEAAAD//+v/AAABAAEAAAD//+r/AAABAAAAAAD+/+//AAABAAIAAAD+/+7/AAABAAEAAAD+/+3/AAABAAEAAAD+/+z/AAABAAEAAAD+/+v/AAABAAEAAAD+/+r/AAABAAAAAAD9/+//AAABAAIAAAD9/+7/AAABAAEAAAD9/+3/AAABAAEAAAD9/+z/AAABAAEAAAD9/+v/AAABAAEAAAD9/+r/AAAFAAIAAAD9/+n/AAACAAAAAAD8/+//AAABAAIAAAD8/+7/AAABAAEAAAD8/+3/AAABAAEAAAD8/+z/AAABAAEAAAD8/+v/AAABAAEAAAD8/+r/AAABAAEAAAD8/+n/AAABAAAAAAD7/+//AAAFAAEAAAD7/+7/AAABAAEAAAD7/+3/AAABAAEAAAD7/+z/AAABAAEAAAD7/+v/AAABAAEAAAD7/+r/AAABAAEAAAD7/+n/AAABAAAAAAD6/+//AAABAAEAAAD6/+7/AAABAAEAAAD6/+3/AAABAAEAAAD6/+z/AAABAAEAAAD6/+v/AAABAAEAAAD6/+r/AAABAAEAAAD6/+n/AAABAAAAAAD5/+//AAABAAEAAAD5/+7/AAABAAEAAAD5/+3/AAABAAEAAAD5/+z/AAABAAEAAAD5/+v/AAABAAEAAAD5/+r/AAABAAEAAAD5/+n/AAABAAAAAAD4/+//AAABAAEAAAD4/+7/AAABAAEAAAD4/+3/AAABAAEAAAD4/+z/AAABAAEAAAD4/+v/AAABAAEAAAD4/+r/AAABAAEAAAD4/+n/AAABAAAAAAD3/+//AAABAAEAAAD3/+7/AAABAAEAAAD3/+3/AAABAAEAAAD3/+z/AAABAAEAAAD3/+v/AAABAAEAAAD3/+r/AAABAAEAAAD3/+n/AAABAAAAAAD2/+//AAABAAEAAAD2/+7/AAABAAEAAAD2/+3/AAABAAEAAAD2/+z/AAABAAEAAAD2/+v/AAABAAEAAAD2/+r/AAABAAEAAAD2/+n/AAABAAAAAAD1/+//AAAAAAEAAAD1/+7/AAAAAAEAAAD1/+3/AAAAAAEAAAD1/+z/AAAAAAEAAAD1/+v/AAAAAAEAAAD1/+r/AAAAAAEAAAD1/+n/AAAAAAAAAAD7/wUAAAABAAIAAAD7/wQAAAABAAEAAAD7/wMAAAABAAEAAAD7/wIAAAABAAEAAAD7/wEAAAABAAEAAAD7/wAAAAABAAEAAAD7////AAAFAAIAAAD7//7/AAACAAAAAAD7//j/AAACAAIAAAD7//f/AAACAAEAAAD7//b/AAACAAEAAAD7//X/AAACAAEAAAD7//T/AAACAAEAAAD7//P/AAACAAEAAAD7//L/AAACAAEAAAD7//H/AAACAAEAAAD7//D/AAACAAEAAAD6/wUAAAABAAIAAAD6/wQAAAABAAEAAAD6/wMAAAABAAEAAAD6/wIAAAABAAEAAAD6/wEAAAABAAEAAAD6/wAAAAABAAEAAAD6////AAABAAEAAAD6//7/AAABAAAAAAD6//j/AAABAAIAAAD6//f/AAABAAEAAAD6//b/AAABAAEAAAD6//X/AAABAAEAAAD6//T/AAABAAEAAAD6//P/AAABAAEAAAD6//L/AAABAAEAAAD6//H/AAABAAEAAAD6//D/AAABAAEAAAD5/wUAAAABAAIAAAD5/wQAAAABAAEAAAD5/wMAAAABAAEAAAD5/wIAAAABAAEAAAD5/wEAAAABAAEAAAD5/wAAAAABAAEAAAD5////AAABAAEAAAD5//7/AAABAAAAAAD5//j/AAABAAIAAAD5//f/AAABAAEAAAD5//b/AAABAAEAAAD5//X/AAABAAEAAAD5//T/AAABAAEAAAD5//P/AAABAAEAAAD5//L/AAABAAEAAAD5//H/AAABAAEAAAD5//D/AAABAAEAAAD4/wUAAAABAAIAAAD4/wQAAAABAAEAAAD4/wMAAAABAAEAAAD4/wIAAAABAAEAAAD4/wEAAAABAAEAAAD4/wAAAAABAAEAAAD4////AAABAAEAAAD4//7/AAAFAAIAAAD4//j/AAABAAIAAAD4//f/AAABAAEAAAD4//b/AAABAAEAAAD4//X/AAABAAEAAAD4//T/AAABAAEAAAD4//P/AAABAAEAAAD4//L/AAABAAEAAAD4//H/AAABAAEAAAD4//D/AAABAAEAAAD3/wUAAAABAAIAAAD3/wQAAAABAAEAAAD3/wMAAAABAAEAAAD3/wIAAAABAAEAAAD3/wEAAAABAAEAAAD3/wAAAAABAAEAAAD3////AAABAAEAAAD3//7/AAABAAEAAAD3//j/AAABAAIAAAD3//f/AAABAAEAAAD3//b/AAABAAEAAAD3//X/AAABAAEAAAD3//T/AAABAAEAAAD3//P/AAABAAEAAAD3//L/AAABAAEAAAD3//H/AAABAAEAAAD3//D/AAABAAEAAAD2/wUAAAABAAIAAAD2/wQAAAABAAEAAAD2/wMAAAABAAEAAAD2/wIAAAABAAEAAAD2/wEAAAABAAEAAAD2/wAAAAABAAEAAAD2////AAABAAEAAAD2//7/AAABAAEAAAD2//j/AAAFAAEAAAD2//f/AAABAAEAAAD2//b/AAABAAEAAAD2//X/AAABAAEAAAD2//T/AAABAAEAAAD2//P/AAABAAEAAAD2//L/AAABAAEAAAD2//H/AAABAAEAAAD2//D/AAABAAEAAAD1/wUAAAAAAAIAAAD1/wQAAAAAAAEAAAD1/wMAAAAAAAEAAAD1/wIAAAAAAAEAAAD1/wEAAAAAAAEAAAD1/wAAAAAAAAEAAAD1////AAAAAAEAAAD1//7/AAAAAAEAAAD1//j/AAAAAAEAAAD1//f/AAAAAAEAAAD1//b/AAAAAAEAAAD1//X/AAAAAAEAAAD1//T/AAAAAAEAAAD1//P/AAAAAAEAAAD1//L/AAAAAAEAAAD1//H/AAAAAAEAAAD1//D/AAAAAAEAAAD4//3/AAACAAAAAAD3//3/AAABAAAAAAD2//3/AAAFAAIAAAD1//3/AAAAAAEAAAD1//z/AAAAAAEAAAD1//v/AAAAAAEAAAD1//r/AAAAAAEAAAD1//n/AAAAAAEAAAD2//n/AAACAAEAAAD2//r/AAACAAEAAAD2//v/AAACAAEAAAD2//z/AAACAAEAAAA=")
+tile_map_data = PackedByteArray("AAD8////AAABAAAAAAD9////AAABAAAAAAD+////AAABAAAAAAD/////AAABAAAAAAAAAP//AAABAAAAAAABAP//AAABAAAAAAABAAAAAAABAAEAAAABAAEAAAABAAEAAAAAAAEAAAABAAEAAAD//wEAAAABAAEAAAD+/wEAAAABAAEAAAD9/wEAAAABAAEAAAD8/wEAAAABAAEAAAD8/wAAAAABAAEAAAD9/wAAAAABAAEAAAD+/wAAAAABAAEAAAD//wAAAAABAAEAAAAAAAAAAAABAAEAAAACAP//AAABAAAAAAACAAAAAAABAAEAAAACAAEAAAABAAEAAAADAAEAAAABAAEAAAAEAAEAAAABAAEAAAAEAAAAAAABAAEAAAAEAP//AAABAAEAAAADAP//AAAGAAIAAAADAAAAAAABAAEAAAAFAAAAAAABAAEAAAAFAP//AAABAAEAAAAFAAEAAAABAAEAAAADAP7/AAAAAAEAAAADAP3/AAAAAAEAAAADAPz/AAAAAAEAAAADAPv/AAAAAAAAAAAEAPv/AAABAAAAAAAFAPv/AAACAAAAAAAFAPz/AAAFAAIAAAAFAP3/AAABAAEAAAAFAP7/AAABAAEAAAAEAP3/AAABAAEAAAAGAP3/AAABAAEAAAAGAPz/AAABAAAAAAAHAPz/AAACAAAAAAAHAP3/AAAFAAIAAAAIAP3/AAACAAAAAAAIAP7/AAACAAEAAAAIAP//AAACAAEAAAAIAAAAAAAFAAIAAAAJAAAAAAACAAAAAAAJAAEAAAACAAEAAAAJAAIAAAAFAAIAAAAKAAIAAAABAAAAAAALAAIAAAABAAAAAAAMAAIAAAABAAAAAAANAAIAAAAGAAIAAAANAAEAAAAAAAAAAAAOAAEAAAABAAAAAAAPAAEAAAABAAAAAAAQAAEAAAABAAAAAAARAAEAAAABAAAAAAASAAEAAAACAAAAAAASAAIAAAACAAEAAAASAAMAAAACAAEAAAARAAMAAAABAAEAAAAQAAMAAAABAAEAAAAPAAMAAAABAAEAAAAOAAMAAAABAAEAAAANAAMAAAABAAEAAAAMAAMAAAABAAEAAAALAAMAAAABAAEAAAAKAAMAAAABAAEAAAAJAAMAAAABAAEAAAAIAAMAAAABAAEAAAAHAAMAAAABAAEAAAAGAAMAAAABAAEAAAAGAAIAAAABAAEAAAAFAAIAAAABAAEAAAAEAAIAAAABAAEAAAADAAIAAAABAAEAAAACAAIAAAABAAEAAAABAAIAAAABAAEAAAAAAAIAAAABAAEAAAD//wIAAAABAAEAAAD+/wIAAAABAAEAAAD9/wIAAAABAAEAAAD8/wIAAAABAAEAAAD8/wMAAAABAAEAAAD9/wMAAAABAAEAAAD+/wMAAAABAAEAAAD+/wQAAAABAAEAAAD//wQAAAABAAEAAAAAAAQAAAABAAEAAAABAAQAAAABAAEAAAACAAQAAAABAAEAAAADAAQAAAABAAEAAAAEAAQAAAABAAEAAAAFAAQAAAABAAEAAAAGAAQAAAABAAEAAAAHAAQAAAABAAEAAAAIAAQAAAABAAEAAAAJAAQAAAABAAEAAAAKAAQAAAABAAEAAAALAAQAAAABAAEAAAAMAAQAAAABAAEAAAANAAQAAAABAAEAAAAOAAQAAAABAAEAAAAPAAQAAAABAAEAAAARAAIAAAABAAEAAAAQAAIAAAABAAEAAAAPAAIAAAABAAEAAAAOAAIAAAABAAEAAAAQAAQAAAABAAEAAAARAAQAAAABAAEAAAASAAQAAAAFAAIAAAAIAAIAAAABAAEAAAAHAAIAAAABAAEAAAAFAAMAAAABAAEAAAAEAAMAAAABAAEAAAADAAMAAAABAAEAAAACAAMAAAABAAEAAAABAAMAAAABAAEAAAAAAAMAAAABAAEAAAD//wMAAAABAAEAAAD9/wQAAAABAAEAAAD8/wQAAAABAAEAAAAIAAEAAAABAAEAAAAHAAEAAAABAAEAAAAHAAAAAAABAAEAAAAHAP//AAABAAEAAAAHAP7/AAABAAEAAAAGAAEAAAABAAEAAAAGAAAAAAABAAEAAAAGAP//AAABAAEAAAAGAP7/AAABAAEAAAAEAP7/AAABAAEAAAAEAPz/AAABAAEAAAAVAPz/AAABAAIAAAAUAPz/AAABAAIAAAATAP3/AAACAAIAAAASAP3/AAABAAIAAAARAP3/AAABAAIAAAAQAP3/AAABAAIAAAAPAP7/AAACAAIAAAAOAP7/AAAAAAIAAAAdAAQAAAABAAEAAAAcAAMAAAAAAAEAAAAbAAIAAAAAAAIAAAAaAAEAAAAAAAIAAAAZAAAAAAAAAAIAAAAZAP//AAAAAAEAAAAYAP7/AAAAAAIAAAAXAP3/AAAAAAIAAAAWAPz/AAABAAIAAAAcAAQAAAAGAAIAAAAbAAQAAAABAAAAAAAaAAQAAAABAAAAAAAZAAQAAAABAAAAAAAYAAQAAAABAAAAAAAXAAQAAAABAAAAAAAWAAQAAAABAAAAAAAVAAQAAAABAAAAAAAUAAQAAAABAAAAAAATAAQAAAABAAAAAAAdAAMAAAABAAEAAAAdAAIAAAABAAEAAAAcAAIAAAAGAAEAAAAcAAEAAAABAAEAAAAbAAEAAAAGAAEAAAAdAAEAAAABAAEAAAAdAAAAAAABAAEAAAAcAAAAAAABAAEAAAAbAAAAAAABAAEAAAAaAAAAAAAGAAEAAAAaAP//AAABAAEAAAAaAP7/AAABAAEAAAAZAP7/AAAGAAEAAAAZAP3/AAABAAEAAAAYAP3/AAAGAAEAAAAYAPz/AAABAAEAAAAXAPz/AAAGAAEAAAAZAPz/AAABAAEAAAAaAPz/AAABAAEAAAAaAP3/AAABAAEAAAAbAP3/AAABAAEAAAAbAP7/AAABAAEAAAAbAP//AAABAAEAAAAcAP//AAABAAEAAAAdAP//AAABAAEAAAAdAP7/AAABAAEAAAAcAP3/AAABAAEAAAAcAPz/AAABAAEAAAAbAPz/AAABAAEAAAAcAP7/AAABAAEAAAAdAP3/AAABAAEAAAAdAPz/AAABAAEAAAAdAPv/AAABAAEAAAAcAPv/AAABAAEAAAAbAPv/AAABAAEAAAAaAPv/AAABAAEAAAAZAPv/AAABAAEAAAAYAPv/AAABAAEAAAAXAPv/AAABAAEAAAAWAPv/AAABAAEAAAAWAPr/AAABAAAAAAAVAPr/AAABAAAAAAAUAPr/AAABAAAAAAAUAPv/AAABAAEAAAAVAPv/AAABAAEAAAAXAPr/AAABAAAAAAAYAPr/AAABAAAAAAAZAPr/AAABAAAAAAAaAPr/AAABAAAAAAAbAPr/AAABAAAAAAAcAPr/AAABAAAAAAAdAPr/AAABAAAAAAATAPz/AAAFAAEAAAASAPz/AAABAAEAAAARAPz/AAABAAEAAAARAPv/AAABAAEAAAASAPv/AAABAAEAAAASAPr/AAABAAAAAAATAPr/AAABAAAAAAATAPv/AAABAAEAAAARAPr/AAABAAAAAAAQAPr/AAABAAAAAAAQAPv/AAABAAEAAAAQAPz/AAABAAEAAAAPAPz/AAABAAEAAAAPAP3/AAAFAAEAAAAOAP3/AAAAAAEAAAAOAPz/AAAAAAEAAAAOAPv/AAAAAAEAAAAPAPv/AAABAAEAAAAPAPr/AAABAAAAAAAOAPr/AAAAAAAAAAAdAAUAAAABAAIAAAAcAAUAAAABAAIAAAAbAAUAAAABAAIAAAAaAAUAAAABAAIAAAAZAAUAAAABAAIAAAAYAAUAAAABAAIAAAASAAUAAAABAAIAAAATAAUAAAABAAIAAAAUAAUAAAABAAIAAAAVAAUAAAABAAIAAAAWAAUAAAABAAIAAAAXAAUAAAABAAIAAAARAAUAAAABAAIAAAAQAAUAAAABAAIAAAAPAAUAAAABAAIAAAAOAAUAAAABAAIAAAANAAUAAAABAAIAAAAMAAUAAAABAAIAAAALAAUAAAABAAIAAAAKAAUAAAABAAIAAAAJAAUAAAABAAIAAAAIAAUAAAABAAIAAAAHAAUAAAABAAIAAAAGAAUAAAABAAIAAAAFAAUAAAABAAIAAAAEAAUAAAABAAIAAAADAAUAAAABAAIAAAACAAUAAAABAAIAAAABAAUAAAABAAIAAAAAAAUAAAABAAIAAAD//wUAAAABAAIAAAD+/wUAAAABAAIAAAD9/wUAAAABAAIAAAD8/wUAAAABAAIAAAAnAAUAAAACAAIAAAAnAAQAAAACAAEAAAAnAAMAAAACAAEAAAAnAAIAAAACAAEAAAAnAAEAAAACAAEAAAAnAAAAAAACAAEAAAAnAP//AAACAAEAAAAnAP7/AAACAAEAAAAnAP3/AAACAAEAAAAnAPz/AAACAAEAAAAnAPv/AAACAAEAAAAnAPr/AAACAAEAAAAmAAUAAAABAAIAAAAmAAQAAAABAAEAAAAmAAMAAAABAAEAAAAmAAIAAAABAAEAAAAmAAEAAAABAAEAAAAmAAAAAAABAAEAAAAmAP//AAABAAEAAAAmAP7/AAABAAEAAAAmAP3/AAABAAEAAAAmAPz/AAABAAEAAAAmAPv/AAABAAEAAAAmAPr/AAABAAEAAAAlAAUAAAABAAIAAAAlAAQAAAABAAEAAAAlAAMAAAABAAEAAAAlAAIAAAABAAEAAAAlAAEAAAABAAEAAAAlAAAAAAABAAEAAAAlAP//AAABAAEAAAAlAP7/AAABAAEAAAAlAP3/AAABAAEAAAAlAPz/AAABAAEAAAAlAPv/AAABAAEAAAAlAPr/AAABAAEAAAAkAAUAAAABAAIAAAAkAAQAAAABAAEAAAAkAAMAAAABAAEAAAAkAAIAAAABAAEAAAAkAAEAAAABAAEAAAAkAAAAAAABAAEAAAAkAP//AAABAAEAAAAkAP7/AAABAAEAAAAkAP3/AAABAAEAAAAkAPz/AAABAAEAAAAkAPv/AAABAAEAAAAkAPr/AAABAAEAAAAjAAUAAAABAAIAAAAjAAQAAAABAAEAAAAjAAMAAAABAAEAAAAjAAIAAAABAAEAAAAjAAEAAAABAAEAAAAjAAAAAAABAAEAAAAjAP//AAABAAEAAAAjAP7/AAABAAEAAAAjAP3/AAABAAEAAAAjAPz/AAABAAEAAAAjAPv/AAABAAEAAAAjAPr/AAABAAEAAAAiAAUAAAABAAIAAAAiAAQAAAABAAEAAAAiAAMAAAABAAEAAAAiAAIAAAABAAEAAAAiAAEAAAABAAEAAAAiAAAAAAABAAEAAAAiAP//AAABAAEAAAAiAP7/AAABAAEAAAAiAP3/AAABAAEAAAAiAPz/AAABAAEAAAAiAPv/AAABAAEAAAAiAPr/AAABAAEAAAAhAAUAAAABAAIAAAAhAAQAAAABAAEAAAAhAAMAAAABAAEAAAAhAAIAAAABAAEAAAAhAAEAAAABAAEAAAAhAAAAAAABAAEAAAAhAP//AAABAAEAAAAhAP7/AAABAAEAAAAhAP3/AAABAAEAAAAhAPz/AAABAAEAAAAhAPv/AAABAAEAAAAhAPr/AAABAAEAAAAgAAUAAAABAAIAAAAgAAQAAAABAAEAAAAgAAMAAAABAAEAAAAgAAIAAAABAAEAAAAgAAEAAAABAAEAAAAgAAAAAAABAAEAAAAgAP//AAABAAEAAAAgAP7/AAABAAEAAAAgAP3/AAABAAEAAAAgAPz/AAABAAEAAAAgAPv/AAABAAEAAAAgAPr/AAAGAAIAAAAfAAUAAAABAAIAAAAfAAQAAAABAAEAAAAfAAMAAAABAAEAAAAfAAIAAAABAAEAAAAfAAEAAAABAAEAAAAfAAAAAAABAAEAAAAfAP//AAABAAEAAAAfAP7/AAABAAEAAAAfAP3/AAABAAEAAAAfAPz/AAABAAEAAAAfAPv/AAABAAEAAAAfAPr/AAABAAAAAAAeAAUAAAABAAIAAAAeAAQAAAABAAEAAAAeAAMAAAABAAEAAAAeAAIAAAABAAEAAAAeAAEAAAABAAEAAAAeAAAAAAABAAEAAAAeAP//AAABAAEAAAAeAP7/AAABAAEAAAAeAP3/AAABAAEAAAAeAPz/AAABAAEAAAAeAPv/AAABAAEAAAAeAPr/AAABAAAAAAAnAPn/AAACAAEAAAAnAPj/AAACAAEAAAAnAPf/AAACAAEAAAAnAPb/AAACAAEAAAAnAPX/AAACAAEAAAAnAPT/AAAFAAEAAAAnAPP/AAABAAEAAAAnAPL/AAABAAEAAAAnAPH/AAABAAEAAAAnAPD/AAABAAEAAAAmAPn/AAABAAEAAAAmAPj/AAABAAEAAAAmAPf/AAABAAEAAAAmAPb/AAABAAEAAAAmAPX/AAABAAEAAAAmAPT/AAABAAEAAAAmAPP/AAABAAEAAAAmAPL/AAABAAEAAAAmAPH/AAABAAEAAAAmAPD/AAABAAEAAAAlAPn/AAABAAEAAAAlAPj/AAABAAEAAAAlAPf/AAABAAEAAAAlAPb/AAABAAEAAAAlAPX/AAABAAEAAAAlAPT/AAABAAEAAAAlAPP/AAABAAEAAAAlAPL/AAABAAEAAAAlAPH/AAABAAEAAAAlAPD/AAABAAEAAAAkAPn/AAABAAEAAAAkAPj/AAABAAEAAAAkAPf/AAABAAEAAAAkAPb/AAABAAEAAAAkAPX/AAABAAEAAAAkAPT/AAABAAEAAAAkAPP/AAABAAEAAAAkAPL/AAABAAEAAAAkAPH/AAABAAEAAAAkAPD/AAABAAEAAAAjAPn/AAABAAEAAAAjAPj/AAABAAEAAAAjAPf/AAABAAEAAAAjAPb/AAABAAEAAAAjAPX/AAABAAEAAAAjAPT/AAABAAEAAAAjAPP/AAABAAEAAAAjAPL/AAABAAEAAAAjAPH/AAABAAEAAAAjAPD/AAABAAEAAAAiAPn/AAABAAEAAAAiAPj/AAABAAEAAAAiAPf/AAABAAEAAAAiAPb/AAABAAEAAAAiAPX/AAABAAEAAAAiAPT/AAABAAEAAAAiAPP/AAABAAEAAAAiAPL/AAABAAEAAAAiAPH/AAABAAEAAAAiAPD/AAABAAEAAAAhAPn/AAABAAEAAAAhAPj/AAABAAEAAAAhAPf/AAABAAEAAAAhAPb/AAABAAEAAAAhAPX/AAABAAEAAAAhAPT/AAABAAEAAAAhAPP/AAABAAEAAAAhAPL/AAABAAEAAAAhAPH/AAABAAEAAAAhAPD/AAABAAEAAAAgAPn/AAAAAAEAAAAgAPj/AAAAAAEAAAAgAPf/AAAAAAEAAAAgAPb/AAAAAAEAAAAgAPX/AAAAAAEAAAAgAPT/AAAAAAEAAAAgAPP/AAAAAAEAAAAgAPL/AAAAAAEAAAAgAPH/AAAAAAEAAAAgAPD/AAAAAAEAAAApAO//AAACAAEAAAApAO7/AAACAAEAAAApAO3/AAACAAEAAAApAOz/AAACAAEAAAApAOv/AAACAAEAAAApAOr/AAACAAAAAAAoAO//AAABAAEAAAAoAO7/AAABAAEAAAAoAO3/AAABAAEAAAAoAOz/AAABAAEAAAAoAOv/AAABAAEAAAAoAOr/AAABAAAAAAAnAO//AAABAAEAAAAnAO7/AAABAAEAAAAnAO3/AAABAAEAAAAnAOz/AAABAAEAAAAnAOv/AAABAAEAAAAnAOr/AAABAAAAAAAmAO//AAABAAEAAAAmAO7/AAABAAEAAAAmAO3/AAABAAEAAAAmAOz/AAABAAEAAAAmAOv/AAABAAEAAAAmAOr/AAABAAAAAAAlAO//AAABAAEAAAAlAO7/AAABAAEAAAAlAO3/AAABAAEAAAAlAOz/AAABAAEAAAAlAOv/AAABAAEAAAAlAOr/AAABAAAAAAAkAO//AAABAAEAAAAkAO7/AAABAAEAAAAkAO3/AAABAAEAAAAkAOz/AAABAAEAAAAkAOv/AAABAAEAAAAkAOr/AAABAAAAAAAjAO//AAABAAEAAAAjAO7/AAABAAEAAAAjAO3/AAABAAEAAAAjAOz/AAABAAEAAAAjAOv/AAABAAEAAAAjAOr/AAABAAAAAAAiAO//AAABAAEAAAAiAO7/AAABAAEAAAAiAO3/AAABAAEAAAAiAOz/AAABAAEAAAAiAOv/AAABAAEAAAAiAOr/AAABAAAAAAAhAO//AAABAAEAAAAhAO7/AAABAAEAAAAhAO3/AAABAAEAAAAhAOz/AAABAAEAAAAhAOv/AAABAAEAAAAhAOr/AAABAAAAAAAgAO//AAAGAAEAAAAgAO7/AAABAAEAAAAgAO3/AAABAAEAAAAgAOz/AAABAAEAAAAgAOv/AAABAAEAAAAgAOr/AAABAAAAAAApAPT/AAACAAIAAAApAPP/AAACAAEAAAApAPL/AAACAAEAAAApAPH/AAACAAEAAAApAPD/AAACAAEAAAAoAPT/AAABAAIAAAAoAPP/AAABAAEAAAAoAPL/AAABAAEAAAAoAPH/AAABAAEAAAAoAPD/AAABAAEAAAAfAO//AAABAAIAAAAfAO7/AAABAAEAAAAfAO3/AAABAAEAAAAfAOz/AAABAAEAAAAfAOv/AAABAAEAAAAfAOr/AAABAAAAAAAeAO//AAABAAIAAAAeAO7/AAABAAEAAAAeAO3/AAABAAEAAAAeAOz/AAABAAEAAAAeAOv/AAABAAEAAAAeAOr/AAABAAAAAAAdAO//AAABAAIAAAAdAO7/AAABAAEAAAAdAO3/AAABAAEAAAAdAOz/AAABAAEAAAAdAOv/AAABAAEAAAAdAOr/AAABAAAAAAAcAO//AAABAAIAAAAcAO7/AAABAAEAAAAcAO3/AAABAAEAAAAcAOz/AAABAAEAAAAcAOv/AAABAAEAAAAcAOr/AAABAAAAAAAbAO//AAABAAIAAAAbAO7/AAABAAEAAAAbAO3/AAABAAEAAAAbAOz/AAABAAEAAAAbAOv/AAABAAEAAAAbAOr/AAABAAAAAAAaAO//AAABAAIAAAAaAO7/AAABAAEAAAAaAO3/AAABAAEAAAAaAOz/AAABAAEAAAAaAOv/AAABAAEAAAAaAOr/AAABAAAAAAAZAO//AAABAAIAAAAZAO7/AAABAAEAAAAZAO3/AAABAAEAAAAZAOz/AAABAAEAAAAZAOv/AAABAAEAAAAZAOr/AAABAAAAAAAYAO//AAABAAIAAAAYAO7/AAABAAEAAAAYAO3/AAABAAEAAAAYAOz/AAABAAEAAAAYAOv/AAABAAEAAAAYAOr/AAABAAAAAAAXAO//AAABAAIAAAAXAO7/AAABAAEAAAAXAO3/AAABAAEAAAAXAOz/AAABAAEAAAAXAOv/AAABAAEAAAAXAOr/AAABAAAAAAAWAO//AAABAAIAAAAWAO7/AAABAAEAAAAWAO3/AAABAAEAAAAWAOz/AAABAAEAAAAWAOv/AAABAAEAAAAWAOr/AAABAAAAAAAVAO//AAABAAIAAAAVAO7/AAABAAEAAAAVAO3/AAABAAEAAAAVAOz/AAABAAEAAAAVAOv/AAABAAEAAAAVAOr/AAABAAAAAAAUAO//AAABAAIAAAAUAO7/AAABAAEAAAAUAO3/AAABAAEAAAAUAOz/AAABAAEAAAAUAOv/AAABAAEAAAAUAOr/AAABAAAAAAATAO//AAABAAIAAAATAO7/AAABAAEAAAATAO3/AAABAAEAAAATAOz/AAABAAEAAAATAOv/AAABAAEAAAATAOr/AAABAAAAAAASAO//AAABAAIAAAASAO7/AAABAAEAAAASAO3/AAABAAEAAAASAOz/AAABAAEAAAASAOv/AAABAAEAAAASAOr/AAABAAAAAAARAO//AAABAAIAAAARAO7/AAABAAEAAAARAO3/AAABAAEAAAARAOz/AAABAAEAAAARAOv/AAABAAEAAAARAOr/AAABAAAAAAAQAO//AAABAAIAAAAQAO7/AAABAAEAAAAQAO3/AAABAAEAAAAQAOz/AAABAAEAAAAQAOv/AAABAAEAAAAQAOr/AAABAAAAAAAPAO//AAABAAIAAAAPAO7/AAABAAEAAAAPAO3/AAABAAEAAAAPAOz/AAABAAEAAAAPAOv/AAABAAEAAAAPAOr/AAABAAAAAAAOAO//AAABAAIAAAAOAO7/AAABAAEAAAAOAO3/AAABAAEAAAAOAOz/AAABAAEAAAAOAOv/AAABAAEAAAAOAOr/AAABAAAAAAANAO//AAABAAIAAAANAO7/AAABAAEAAAANAO3/AAABAAEAAAANAOz/AAABAAEAAAANAOv/AAABAAEAAAANAOr/AAABAAAAAAAMAO//AAABAAIAAAAMAO7/AAABAAEAAAAMAO3/AAABAAEAAAAMAOz/AAABAAEAAAAMAOv/AAABAAEAAAAMAOr/AAABAAAAAAALAO//AAABAAIAAAALAO7/AAABAAEAAAALAO3/AAABAAEAAAALAOz/AAABAAEAAAALAOv/AAABAAEAAAALAOr/AAABAAAAAAAKAO//AAABAAIAAAAKAO7/AAABAAEAAAAKAO3/AAABAAEAAAAKAOz/AAABAAEAAAAKAOv/AAABAAEAAAAKAOr/AAABAAAAAAAJAO//AAABAAIAAAAJAO7/AAABAAEAAAAJAO3/AAABAAEAAAAJAOz/AAABAAEAAAAJAOv/AAABAAEAAAAJAOr/AAABAAAAAAAIAO//AAABAAIAAAAIAO7/AAABAAEAAAAIAO3/AAABAAEAAAAIAOz/AAABAAEAAAAIAOv/AAABAAEAAAAIAOr/AAABAAAAAAAHAO//AAABAAIAAAAHAO7/AAABAAEAAAAHAO3/AAABAAEAAAAHAOz/AAABAAEAAAAHAOv/AAABAAEAAAAHAOr/AAABAAAAAAAGAO//AAABAAIAAAAGAO7/AAABAAEAAAAGAO3/AAABAAEAAAAGAOz/AAABAAEAAAAGAOv/AAABAAEAAAAGAOr/AAABAAAAAAAFAO//AAABAAIAAAAFAO7/AAABAAEAAAAFAO3/AAABAAEAAAAFAOz/AAABAAEAAAAFAOv/AAABAAEAAAAFAOr/AAABAAAAAAAEAO//AAABAAIAAAAEAO7/AAABAAEAAAAEAO3/AAABAAEAAAAEAOz/AAABAAEAAAAEAOv/AAABAAEAAAAEAOr/AAABAAAAAAADAO//AAABAAIAAAADAO7/AAABAAEAAAADAO3/AAABAAEAAAADAOz/AAABAAEAAAADAOv/AAABAAEAAAADAOr/AAABAAAAAAACAO//AAABAAIAAAACAO7/AAABAAEAAAACAO3/AAABAAEAAAACAOz/AAABAAEAAAACAOv/AAABAAEAAAACAOr/AAABAAAAAAABAO//AAABAAIAAAABAO7/AAABAAEAAAABAO3/AAABAAEAAAABAOz/AAABAAEAAAABAOv/AAABAAEAAAABAOr/AAABAAAAAAAAAO//AAABAAIAAAAAAO7/AAABAAEAAAAAAO3/AAABAAEAAAAAAOz/AAABAAEAAAAAAOv/AAABAAEAAAAAAOr/AAABAAAAAAD//+//AAABAAIAAAD//+7/AAABAAEAAAD//+3/AAABAAEAAAD//+z/AAABAAEAAAD//+v/AAABAAEAAAD//+r/AAABAAAAAAD+/+//AAABAAIAAAD+/+7/AAABAAEAAAD+/+3/AAABAAEAAAD+/+z/AAABAAEAAAD+/+v/AAABAAEAAAD+/+r/AAABAAAAAAD9/+//AAABAAIAAAD9/+7/AAABAAEAAAD9/+3/AAABAAEAAAD9/+z/AAABAAEAAAD9/+v/AAABAAEAAAD9/+r/AAAFAAIAAAD9/+n/AAACAAAAAAD8/+//AAABAAIAAAD8/+7/AAABAAEAAAD8/+3/AAABAAEAAAD8/+z/AAABAAEAAAD8/+v/AAABAAEAAAD8/+r/AAABAAEAAAD8/+n/AAABAAAAAAD7/+//AAAFAAEAAAD7/+7/AAABAAEAAAD7/+3/AAABAAEAAAD7/+z/AAABAAEAAAD7/+v/AAABAAEAAAD7/+r/AAABAAEAAAD7/+n/AAABAAAAAAD6/+//AAABAAEAAAD6/+7/AAABAAEAAAD6/+3/AAABAAEAAAD6/+z/AAABAAEAAAD6/+v/AAABAAEAAAD6/+r/AAABAAEAAAD6/+n/AAABAAAAAAD5/+//AAABAAEAAAD5/+7/AAABAAEAAAD5/+3/AAABAAEAAAD5/+z/AAABAAEAAAD5/+v/AAABAAEAAAD5/+r/AAABAAEAAAD5/+n/AAABAAAAAAD4/+//AAABAAEAAAD4/+7/AAABAAEAAAD4/+3/AAABAAEAAAD4/+z/AAABAAEAAAD4/+v/AAABAAEAAAD4/+r/AAABAAEAAAD4/+n/AAABAAAAAAD3/+//AAABAAEAAAD3/+7/AAABAAEAAAD3/+3/AAABAAEAAAD3/+z/AAABAAEAAAD3/+v/AAABAAEAAAD3/+r/AAABAAEAAAD3/+n/AAABAAAAAAD2/+//AAABAAEAAAD2/+7/AAABAAEAAAD2/+3/AAABAAEAAAD2/+z/AAABAAEAAAD2/+v/AAABAAEAAAD2/+r/AAABAAEAAAD2/+n/AAABAAAAAAD1/+//AAAAAAEAAAD1/+7/AAAAAAEAAAD1/+3/AAAAAAEAAAD1/+z/AAAAAAEAAAD1/+v/AAAAAAEAAAD1/+r/AAAAAAEAAAD1/+n/AAAAAAAAAAD7/wUAAAABAAIAAAD7/wQAAAABAAEAAAD7/wMAAAABAAEAAAD7/wIAAAABAAEAAAD7/wEAAAABAAEAAAD7/wAAAAABAAEAAAD7////AAAFAAIAAAD7//7/AAACAAAAAAD7//j/AAACAAIAAAD7//f/AAACAAEAAAD7//b/AAACAAEAAAD7//X/AAACAAEAAAD7//T/AAACAAEAAAD7//P/AAACAAEAAAD7//L/AAACAAEAAAD7//H/AAACAAEAAAD7//D/AAACAAEAAAD6/wUAAAABAAIAAAD6/wQAAAABAAEAAAD6/wMAAAABAAEAAAD6/wIAAAABAAEAAAD6/wEAAAABAAEAAAD6/wAAAAABAAEAAAD6////AAABAAEAAAD6//7/AAABAAAAAAD6//j/AAABAAIAAAD6//f/AAABAAEAAAD6//b/AAABAAEAAAD6//X/AAABAAEAAAD6//T/AAABAAEAAAD6//P/AAABAAEAAAD6//L/AAABAAEAAAD6//H/AAABAAEAAAD6//D/AAABAAEAAAD5/wUAAAABAAIAAAD5/wQAAAABAAEAAAD5/wMAAAABAAEAAAD5/wIAAAABAAEAAAD5/wEAAAABAAEAAAD5/wAAAAABAAEAAAD5////AAABAAEAAAD5//7/AAABAAAAAAD5//j/AAABAAIAAAD5//f/AAABAAEAAAD5//b/AAABAAEAAAD5//X/AAABAAEAAAD5//T/AAABAAEAAAD5//P/AAABAAEAAAD5//L/AAABAAEAAAD5//H/AAABAAEAAAD5//D/AAABAAEAAAD4/wUAAAABAAIAAAD4/wQAAAABAAEAAAD4/wMAAAABAAEAAAD4/wIAAAABAAEAAAD4/wEAAAABAAEAAAD4/wAAAAABAAEAAAD4////AAABAAEAAAD4//7/AAAFAAIAAAD4//j/AAABAAIAAAD4//f/AAABAAEAAAD4//b/AAABAAEAAAD4//X/AAABAAEAAAD4//T/AAABAAEAAAD4//P/AAABAAEAAAD4//L/AAABAAEAAAD4//H/AAABAAEAAAD4//D/AAABAAEAAAD3/wUAAAABAAIAAAD3/wQAAAABAAEAAAD3/wMAAAABAAEAAAD3/wIAAAABAAEAAAD3/wEAAAABAAEAAAD3/wAAAAABAAEAAAD3////AAABAAEAAAD3//7/AAABAAEAAAD3//j/AAABAAIAAAD3//f/AAABAAEAAAD3//b/AAABAAEAAAD3//X/AAABAAEAAAD3//T/AAABAAEAAAD3//P/AAABAAEAAAD3//L/AAABAAEAAAD3//H/AAABAAEAAAD3//D/AAABAAEAAAD2/wUAAAABAAIAAAD2/wQAAAABAAEAAAD2/wMAAAABAAEAAAD2/wIAAAABAAEAAAD2/wEAAAABAAEAAAD2/wAAAAABAAEAAAD2////AAABAAEAAAD2//7/AAABAAEAAAD2//j/AAAFAAEAAAD2//f/AAABAAEAAAD2//b/AAABAAEAAAD2//X/AAABAAEAAAD2//T/AAABAAEAAAD2//P/AAABAAEAAAD2//L/AAABAAEAAAD2//H/AAABAAEAAAD2//D/AAABAAEAAAD1/wUAAAAAAAIAAAD1/wQAAAAAAAEAAAD1/wMAAAAAAAEAAAD1/wIAAAAAAAEAAAD1/wEAAAAAAAEAAAD1/wAAAAAAAAEAAAD1////AAAAAAEAAAD1//7/AAAAAAEAAAD1//j/AAAAAAEAAAD1//f/AAAAAAEAAAD1//b/AAAAAAEAAAD1//X/AAAAAAEAAAD1//T/AAAAAAEAAAD1//P/AAAAAAEAAAD1//L/AAAAAAEAAAD1//H/AAAAAAEAAAD1//D/AAAAAAEAAAD4//3/AAACAAAAAAD3//3/AAABAAAAAAD2//3/AAAFAAIAAAD1//3/AAAAAAEAAAD1//z/AAAAAAEAAAD1//v/AAAAAAEAAAD1//r/AAAAAAEAAAD1//n/AAAAAAEAAAD2//n/AAACAAEAAAD2//r/AAACAAEAAAD2//v/AAACAAEAAAD2//z/AAACAAEAAABiAAcAAAACAAIAAABiAAYAAAACAAEAAABiAAUAAAACAAEAAABiAAQAAAACAAEAAABiAAMAAAACAAEAAABiAAIAAAACAAEAAABiAAEAAAACAAEAAABiAAAAAAACAAEAAABhAAcAAAABAAIAAABhAAYAAAABAAEAAABhAAUAAAABAAEAAABhAAQAAAABAAEAAABhAAMAAAABAAEAAABhAAIAAAABAAEAAABhAAEAAAABAAEAAABhAAAAAAABAAEAAABgAAcAAAABAAIAAABgAAYAAAABAAEAAABgAAUAAAABAAEAAABgAAQAAAABAAEAAABgAAMAAAABAAEAAABgAAIAAAABAAEAAABgAAEAAAABAAEAAABgAAAAAAABAAEAAABfAAcAAAABAAIAAABfAAYAAAABAAEAAABfAAUAAAABAAEAAABfAAQAAAABAAEAAABfAAMAAAABAAEAAABfAAIAAAABAAEAAABfAAEAAAABAAEAAABfAAAAAAABAAEAAABeAAcAAAABAAIAAABeAAYAAAABAAEAAABeAAUAAAABAAEAAABeAAQAAAABAAEAAABeAAMAAAABAAEAAABeAAIAAAABAAEAAABeAAEAAAABAAEAAABeAAAAAAABAAEAAABdAAcAAAABAAIAAABdAAYAAAABAAEAAABdAAUAAAABAAEAAABdAAQAAAABAAEAAABdAAMAAAABAAEAAABdAAIAAAABAAEAAABdAAEAAAABAAEAAABdAAAAAAABAAEAAABcAAcAAAABAAIAAABcAAYAAAABAAEAAABcAAUAAAABAAEAAABcAAQAAAABAAEAAABcAAMAAAABAAEAAABcAAIAAAABAAEAAABcAAEAAAABAAEAAABcAAAAAAABAAEAAABbAAcAAAABAAIAAABbAAYAAAABAAEAAABbAAUAAAABAAEAAABbAAQAAAABAAEAAABbAAMAAAABAAEAAABbAAIAAAABAAEAAABbAAEAAAABAAEAAABbAAAAAAABAAEAAABaAAcAAAABAAIAAABaAAYAAAABAAEAAABaAAUAAAABAAEAAABaAAQAAAABAAEAAABaAAMAAAABAAEAAABaAAIAAAABAAEAAABaAAEAAAABAAEAAABaAAAAAAABAAEAAABZAAcAAAABAAIAAABZAAYAAAABAAEAAABZAAUAAAABAAEAAABZAAQAAAABAAEAAABZAAMAAAABAAEAAABZAAIAAAABAAEAAABZAAEAAAABAAEAAABZAAAAAAABAAEAAABYAAcAAAABAAIAAABYAAYAAAABAAEAAABYAAUAAAABAAEAAABYAAQAAAABAAEAAABYAAMAAAABAAEAAABYAAIAAAABAAEAAABYAAEAAAABAAEAAABYAAAAAAABAAEAAABXAAcAAAABAAIAAABXAAYAAAABAAEAAABXAAUAAAABAAEAAABXAAQAAAABAAEAAABXAAMAAAABAAEAAABXAAIAAAABAAEAAABXAAEAAAABAAEAAABXAAAAAAABAAEAAABWAAcAAAABAAIAAABWAAYAAAABAAEAAABWAAUAAAABAAEAAABWAAQAAAABAAEAAABWAAMAAAABAAEAAABWAAIAAAABAAEAAABWAAEAAAABAAEAAABWAAAAAAABAAEAAABVAAcAAAABAAIAAABVAAYAAAABAAEAAABVAAUAAAABAAEAAABVAAQAAAABAAEAAABVAAMAAAABAAEAAABVAAIAAAABAAEAAABVAAEAAAABAAEAAABVAAAAAAABAAEAAABUAAcAAAABAAIAAABUAAYAAAABAAEAAABUAAUAAAABAAEAAABUAAQAAAABAAEAAABUAAMAAAABAAEAAABUAAIAAAABAAEAAABUAAEAAAABAAEAAABUAAAAAAABAAEAAABTAAcAAAABAAIAAABTAAYAAAABAAEAAABTAAUAAAABAAEAAABTAAQAAAABAAEAAABTAAMAAAABAAEAAABTAAIAAAABAAEAAABTAAEAAAABAAEAAABTAAAAAAABAAEAAABSAAcAAAABAAIAAABSAAYAAAABAAEAAABSAAUAAAABAAEAAABSAAQAAAABAAEAAABSAAMAAAABAAEAAABSAAIAAAABAAEAAABSAAEAAAABAAEAAABSAAAAAAABAAEAAABRAAcAAAABAAIAAABRAAYAAAABAAEAAABRAAUAAAABAAEAAABRAAQAAAABAAEAAABRAAMAAAABAAEAAABRAAIAAAABAAEAAABRAAEAAAABAAEAAABRAAAAAAABAAEAAABQAAcAAAABAAIAAABQAAYAAAABAAEAAABQAAUAAAABAAEAAABQAAQAAAABAAEAAABQAAMAAAABAAEAAABQAAIAAAABAAEAAABQAAEAAAABAAEAAABQAAAAAAABAAEAAABPAAcAAAABAAIAAABPAAYAAAABAAEAAABPAAUAAAABAAEAAABPAAQAAAABAAEAAABPAAMAAAABAAEAAABPAAIAAAABAAEAAABPAAEAAAABAAEAAABPAAAAAAABAAEAAABOAAcAAAABAAIAAABOAAYAAAABAAEAAABOAAUAAAABAAEAAABOAAQAAAABAAEAAABOAAMAAAABAAEAAABOAAIAAAABAAEAAABOAAEAAAABAAEAAABOAAAAAAABAAEAAABNAAcAAAABAAIAAABNAAYAAAABAAEAAABNAAUAAAABAAEAAABNAAQAAAABAAEAAABNAAMAAAABAAEAAABNAAIAAAABAAEAAABNAAEAAAABAAEAAABNAAAAAAABAAEAAABMAAcAAAABAAIAAABMAAYAAAABAAEAAABMAAUAAAABAAEAAABMAAQAAAABAAEAAABMAAMAAAABAAEAAABMAAIAAAABAAEAAABMAAEAAAABAAEAAABMAAAAAAABAAEAAABLAAcAAAABAAIAAABLAAYAAAABAAEAAABLAAUAAAABAAEAAABLAAQAAAABAAEAAABLAAMAAAABAAEAAABLAAIAAAABAAEAAABLAAEAAAABAAEAAABLAAAAAAAGAAIAAABKAAcAAAABAAIAAABKAAYAAAABAAEAAABKAAUAAAABAAEAAABKAAQAAAABAAEAAABKAAMAAAABAAEAAABKAAIAAAABAAEAAABKAAEAAAABAAEAAABKAAAAAAABAAAAAABJAAcAAAABAAIAAABJAAYAAAABAAEAAABJAAUAAAABAAEAAABJAAQAAAABAAEAAABJAAMAAAABAAEAAABJAAIAAAABAAEAAABJAAEAAAABAAEAAABJAAAAAAABAAAAAABIAAcAAAABAAIAAABIAAYAAAABAAEAAABIAAUAAAABAAEAAABIAAQAAAABAAEAAABIAAMAAAABAAEAAABIAAIAAAABAAEAAABIAAEAAAABAAEAAABIAAAAAAABAAAAAABHAAcAAAABAAIAAABHAAYAAAABAAEAAABHAAUAAAABAAEAAABHAAQAAAABAAEAAABHAAMAAAABAAEAAABHAAIAAAABAAEAAABHAAEAAAABAAEAAABHAAAAAAABAAAAAABGAAcAAAAAAAIAAABGAAYAAAAAAAEAAABGAAUAAAAAAAEAAABGAAQAAAAAAAEAAABGAAMAAAAAAAEAAABGAAIAAAAAAAEAAABGAAEAAAAAAAEAAABGAAAAAAAAAAAAAAA/AAcAAAACAAIAAAA/AAYAAAACAAEAAAA/AAUAAAACAAEAAAA/AAQAAAACAAEAAAA/AAMAAAACAAEAAAA/AAIAAAACAAEAAAA/AAEAAAACAAEAAAA/AAAAAAACAAEAAAA+AAcAAAABAAIAAAA+AAYAAAABAAEAAAA+AAUAAAABAAEAAAA+AAQAAAABAAEAAAA+AAMAAAABAAEAAAA+AAIAAAABAAEAAAA+AAEAAAABAAEAAAA+AAAAAAABAAEAAAA9AAcAAAABAAIAAAA9AAYAAAABAAEAAAA9AAUAAAABAAEAAAA9AAQAAAABAAEAAAA9AAMAAAABAAEAAAA9AAIAAAABAAEAAAA9AAEAAAABAAEAAAA9AAAAAAABAAEAAAA8AAcAAAABAAIAAAA8AAYAAAABAAEAAAA8AAUAAAABAAEAAAA8AAQAAAABAAEAAAA8AAMAAAABAAEAAAA8AAIAAAABAAEAAAA8AAEAAAABAAEAAAA8AAAAAAABAAEAAAA7AAcAAAABAAIAAAA7AAYAAAABAAEAAAA7AAUAAAABAAEAAAA7AAQAAAABAAEAAAA7AAMAAAABAAEAAAA7AAIAAAABAAEAAAA7AAEAAAABAAEAAAA7AAAAAAABAAEAAAA6AAcAAAABAAIAAAA6AAYAAAABAAEAAAA6AAUAAAABAAEAAAA6AAQAAAABAAEAAAA6AAMAAAABAAEAAAA6AAIAAAABAAEAAAA6AAEAAAABAAEAAAA6AAAAAAABAAEAAAA5AAcAAAABAAIAAAA5AAYAAAABAAEAAAA5AAUAAAABAAEAAAA5AAQAAAABAAEAAAA5AAMAAAABAAEAAAA5AAIAAAABAAEAAAA5AAEAAAABAAEAAAA5AAAAAAABAAEAAAA4AAcAAAABAAIAAAA4AAYAAAABAAEAAAA4AAUAAAABAAEAAAA4AAQAAAABAAEAAAA4AAMAAAABAAEAAAA4AAIAAAABAAEAAAA4AAEAAAABAAEAAAA4AAAAAAABAAEAAAA3AAcAAAAAAAIAAAA3AAYAAAAAAAEAAAA3AAUAAAAAAAEAAAA3AAQAAAAAAAEAAAA3AAMAAAAAAAEAAAA3AAIAAAAAAAEAAAA3AAEAAAAAAAEAAAA3AAAAAAAAAAEAAAA/AP//AAACAAEAAAA/AP7/AAACAAEAAAA/AP3/AAACAAEAAAA/APz/AAACAAEAAAA/APv/AAACAAEAAAA/APr/AAACAAAAAAA+AP//AAABAAEAAAA+AP7/AAABAAEAAAA+AP3/AAABAAEAAAA+APz/AAABAAEAAAA+APv/AAABAAEAAAA+APr/AAABAAAAAAA9AP//AAABAAEAAAA9AP7/AAABAAEAAAA9AP3/AAABAAEAAAA9APz/AAABAAEAAAA9APv/AAABAAEAAAA9APr/AAABAAAAAAA8AP//AAABAAEAAAA8AP7/AAABAAEAAAA8AP3/AAABAAEAAAA8APz/AAABAAEAAAA8APv/AAABAAEAAAA8APr/AAABAAAAAAA7AP//AAABAAEAAAA7AP7/AAABAAEAAAA7AP3/AAABAAEAAAA7APz/AAABAAEAAAA7APv/AAABAAEAAAA7APr/AAABAAAAAAA6AP//AAABAAEAAAA6AP7/AAABAAEAAAA6AP3/AAABAAEAAAA6APz/AAABAAEAAAA6APv/AAABAAEAAAA6APr/AAABAAAAAAA5AP//AAABAAEAAAA5AP7/AAABAAEAAAA5AP3/AAABAAEAAAA5APz/AAABAAEAAAA5APv/AAABAAEAAAA5APr/AAABAAAAAAA4AP//AAABAAEAAAA4AP7/AAABAAEAAAA4AP3/AAABAAEAAAA4APz/AAABAAEAAAA4APv/AAABAAEAAAA4APr/AAABAAAAAAA3AP//AAAAAAEAAAA3AP7/AAAAAAEAAAA3AP3/AAAAAAEAAAA3APz/AAAAAAEAAAA3APv/AAAAAAEAAAA3APr/AAAAAAAAAABiAP//AAACAAEAAABiAP7/AAACAAAAAABhAP//AAABAAEAAABhAP7/AAABAAAAAABgAP//AAABAAEAAABgAP7/AAABAAAAAABfAP//AAABAAEAAABfAP7/AAABAAAAAABeAP//AAABAAEAAABeAP7/AAABAAAAAABdAP//AAABAAEAAABdAP7/AAABAAAAAABcAP//AAABAAEAAABcAP7/AAABAAAAAABbAP//AAABAAEAAABbAP7/AAABAAAAAABaAP//AAABAAEAAABaAP7/AAABAAAAAABZAP//AAABAAEAAABZAP7/AAABAAAAAABYAP//AAABAAEAAABYAP7/AAABAAAAAABXAP//AAABAAEAAABXAP7/AAABAAAAAABWAP//AAABAAEAAABWAP7/AAABAAAAAABVAP//AAABAAEAAABVAP7/AAABAAAAAABUAP//AAABAAEAAABUAP7/AAABAAAAAABTAP//AAABAAEAAABTAP7/AAABAAAAAABSAP//AAABAAEAAABSAP7/AAABAAAAAABRAP//AAABAAEAAABRAP7/AAABAAAAAABQAP//AAABAAEAAABQAP7/AAABAAAAAABPAP//AAABAAEAAABPAP7/AAABAAAAAABOAP//AAABAAEAAABOAP7/AAABAAAAAABNAP//AAABAAEAAABNAP7/AAABAAAAAABMAP//AAABAAEAAABMAP7/AAABAAAAAABLAP//AAAAAAEAAABLAP7/AAAAAAAAAABeAPr/AAABAAIAAABeAPn/AAABAAAAAABdAPr/AAABAAIAAABdAPn/AAABAAAAAABcAPr/AAABAAIAAABcAPn/AAABAAAAAABbAPr/AAABAAIAAABbAPn/AAABAAAAAABaAPr/AAABAAIAAABaAPn/AAABAAAAAABZAPr/AAABAAIAAABZAPn/AAABAAAAAABYAPr/AAABAAIAAABYAPn/AAABAAAAAABXAPr/AAABAAIAAABXAPn/AAABAAAAAABWAPr/AAABAAIAAABWAPn/AAABAAAAAABVAPr/AAABAAIAAABVAPn/AAABAAAAAABUAPr/AAABAAIAAABUAPn/AAABAAAAAABTAPr/AAABAAIAAABTAPn/AAABAAAAAABSAPr/AAABAAIAAABSAPn/AAABAAAAAABRAPr/AAABAAIAAABRAPn/AAABAAAAAABQAPr/AAABAAIAAABQAPn/AAABAAAAAABPAPr/AAABAAIAAABPAPn/AAABAAAAAABOAPr/AAABAAIAAABOAPn/AAABAAAAAABNAPr/AAABAAIAAABNAPn/AAABAAAAAABMAPr/AAAAAAIAAABMAPn/AAAAAAAAAABiAPr/AAACAAIAAABiAPn/AAACAAAAAABhAPr/AAABAAIAAABhAPn/AAABAAAAAABgAPr/AAABAAIAAABgAPn/AAABAAAAAABfAPr/AAABAAIAAABfAPn/AAABAAAAAACMAAcAAAABAAIAAACMAAYAAAABAAEAAACMAAUAAAABAAEAAACMAAQAAAABAAEAAACMAAMAAAABAAEAAACMAAIAAAABAAEAAACMAAEAAAABAAEAAACMAAAAAAABAAEAAACMAP//AAABAAEAAACMAP7/AAABAAEAAACMAP3/AAABAAAAAACLAAcAAAABAAIAAACLAAYAAAABAAEAAACLAAUAAAABAAEAAACLAAQAAAABAAEAAACLAAMAAAABAAEAAACLAAIAAAABAAEAAACLAAEAAAABAAEAAACLAAAAAAABAAEAAACLAP//AAABAAEAAACLAP7/AAABAAEAAACLAP3/AAABAAAAAACKAAcAAAABAAIAAACKAAYAAAABAAEAAACKAAUAAAABAAEAAACKAAQAAAABAAEAAACKAAMAAAABAAEAAACKAAIAAAABAAEAAACKAAEAAAABAAEAAACKAAAAAAABAAEAAACKAP//AAABAAEAAACKAP7/AAABAAEAAACKAP3/AAABAAAAAACJAAcAAAABAAIAAACJAAYAAAABAAEAAACJAAUAAAABAAEAAACJAAQAAAABAAEAAACJAAMAAAABAAEAAACJAAIAAAABAAEAAACJAAEAAAABAAEAAACJAAAAAAABAAEAAACJAP//AAABAAEAAACJAP7/AAABAAEAAACJAP3/AAABAAAAAACIAAcAAAABAAIAAACIAAYAAAABAAEAAACIAAUAAAABAAEAAACIAAQAAAABAAEAAACIAAMAAAABAAEAAACIAAIAAAABAAEAAACIAAEAAAABAAEAAACIAAAAAAABAAEAAACIAP//AAABAAEAAACIAP7/AAABAAEAAACIAP3/AAABAAAAAACHAAcAAAABAAIAAACHAAYAAAABAAEAAACHAAUAAAABAAEAAACHAAQAAAABAAEAAACHAAMAAAABAAEAAACHAAIAAAABAAEAAACHAAEAAAABAAEAAACHAAAAAAABAAEAAACHAP//AAABAAEAAACHAP7/AAABAAEAAACHAP3/AAABAAAAAACGAAcAAAABAAIAAACGAAYAAAABAAEAAACGAAUAAAABAAEAAACGAAQAAAABAAEAAACGAAMAAAABAAEAAACGAAIAAAABAAEAAACGAAEAAAABAAEAAACGAAAAAAABAAEAAACGAP//AAABAAEAAACGAP7/AAABAAEAAACGAP3/AAABAAAAAACFAAcAAAABAAIAAACFAAYAAAABAAEAAACFAAUAAAABAAEAAACFAAQAAAABAAEAAACFAAMAAAABAAEAAACFAAIAAAABAAEAAACFAAEAAAABAAEAAACFAAAAAAABAAEAAACFAP//AAABAAEAAACFAP7/AAABAAEAAACFAP3/AAABAAAAAACEAAcAAAABAAIAAACEAAYAAAABAAEAAACEAAUAAAABAAEAAACEAAQAAAABAAEAAACEAAMAAAABAAEAAACEAAIAAAABAAEAAACEAAEAAAABAAEAAACEAAAAAAABAAEAAACEAP//AAABAAEAAACEAP7/AAABAAEAAACEAP3/AAABAAAAAACDAAcAAAABAAIAAACDAAYAAAABAAEAAACDAAUAAAABAAEAAACDAAQAAAABAAEAAACDAAMAAAABAAEAAACDAAIAAAABAAEAAACDAAEAAAABAAEAAACDAAAAAAABAAEAAACDAP//AAABAAEAAACDAP7/AAABAAEAAACDAP3/AAABAAAAAACCAAcAAAABAAIAAACCAAYAAAABAAEAAACCAAUAAAABAAEAAACCAAQAAAABAAEAAACCAAMAAAABAAEAAACCAAIAAAABAAEAAACCAAEAAAABAAEAAACCAAAAAAABAAEAAACCAP//AAABAAEAAACCAP7/AAABAAEAAACCAP3/AAABAAAAAACBAAcAAAABAAIAAACBAAYAAAABAAEAAACBAAUAAAABAAEAAACBAAQAAAABAAEAAACBAAMAAAABAAEAAACBAAIAAAABAAEAAACBAAEAAAABAAEAAACBAAAAAAABAAEAAACBAP//AAABAAEAAACBAP7/AAABAAEAAACBAP3/AAABAAAAAACAAAcAAAABAAIAAACAAAYAAAABAAEAAACAAAUAAAABAAEAAACAAAQAAAABAAEAAACAAAMAAAABAAEAAACAAAIAAAABAAEAAACAAAEAAAABAAEAAACAAAAAAAABAAEAAACAAP//AAABAAEAAACAAP7/AAABAAEAAACAAP3/AAABAAAAAAB/AAcAAAABAAIAAAB/AAYAAAABAAEAAAB/AAUAAAABAAEAAAB/AAQAAAABAAEAAAB/AAMAAAABAAEAAAB/AAIAAAABAAEAAAB/AAEAAAABAAEAAAB/AAAAAAABAAEAAAB/AP//AAABAAEAAAB/AP7/AAABAAEAAAB/AP3/AAABAAAAAAB+AAcAAAABAAIAAAB+AAYAAAABAAEAAAB+AAUAAAABAAEAAAB+AAQAAAABAAEAAAB+AAMAAAABAAEAAAB+AAIAAAABAAEAAAB+AAEAAAABAAEAAAB+AAAAAAABAAEAAAB+AP//AAABAAEAAAB+AP7/AAABAAEAAAB+AP3/AAABAAAAAAB9AAcAAAAAAAIAAAB9AAYAAAAAAAEAAAB9AAUAAAAAAAEAAAB9AAQAAAAAAAEAAAB9AAMAAAAAAAEAAAB9AAIAAAAAAAEAAAB9AAEAAAAAAAEAAAB9AAAAAAAAAAEAAAB9AP//AAAAAAEAAAB9AP7/AAAAAAEAAAB9AP3/AAAAAAAAAACaAAcAAAACAAIAAACaAAYAAAACAAEAAACaAAUAAAACAAEAAACaAAQAAAACAAEAAACaAAMAAAACAAEAAACaAAIAAAACAAEAAACaAAEAAAACAAEAAACaAAAAAAACAAEAAACaAP//AAACAAEAAACaAP7/AAACAAEAAACaAP3/AAACAAAAAACZAAcAAAABAAIAAACZAAYAAAABAAEAAACZAAUAAAABAAEAAACZAAQAAAABAAEAAACZAAMAAAABAAEAAACZAAIAAAABAAEAAACZAAEAAAABAAEAAACZAAAAAAABAAEAAACZAP//AAABAAEAAACZAP7/AAABAAEAAACZAP3/AAABAAAAAACYAAcAAAABAAIAAACYAAYAAAABAAEAAACYAAUAAAABAAEAAACYAAQAAAABAAEAAACYAAMAAAABAAEAAACYAAIAAAABAAEAAACYAAEAAAABAAEAAACYAAAAAAABAAEAAACYAP//AAABAAEAAACYAP7/AAABAAEAAACYAP3/AAABAAAAAACXAAcAAAABAAIAAACXAAYAAAABAAEAAACXAAUAAAABAAEAAACXAAQAAAABAAEAAACXAAMAAAABAAEAAACXAAIAAAABAAEAAACXAAEAAAABAAEAAACXAAAAAAABAAEAAACXAP//AAABAAEAAACXAP7/AAABAAEAAACXAP3/AAABAAAAAACWAAcAAAABAAIAAACWAAYAAAABAAEAAACWAAUAAAABAAEAAACWAAQAAAABAAEAAACWAAMAAAABAAEAAACWAAIAAAABAAEAAACWAAEAAAABAAEAAACWAAAAAAABAAEAAACWAP//AAABAAEAAACWAP7/AAABAAEAAACWAP3/AAABAAAAAACVAAcAAAABAAIAAACVAAYAAAABAAEAAACVAAUAAAABAAEAAACVAAQAAAABAAEAAACVAAMAAAABAAEAAACVAAIAAAABAAEAAACVAAEAAAABAAEAAACVAAAAAAABAAEAAACVAP//AAABAAEAAACVAP7/AAABAAEAAACVAP3/AAABAAAAAACUAAcAAAABAAIAAACUAAYAAAABAAEAAACUAAUAAAABAAEAAACUAAQAAAABAAEAAACUAAMAAAABAAEAAACUAAIAAAABAAEAAACUAAEAAAABAAEAAACUAAAAAAABAAEAAACUAP//AAABAAEAAACUAP7/AAABAAEAAACUAP3/AAABAAAAAACTAAcAAAABAAIAAACTAAYAAAABAAEAAACTAAUAAAABAAEAAACTAAQAAAABAAEAAACTAAMAAAABAAEAAACTAAIAAAABAAEAAACTAAEAAAABAAEAAACTAAAAAAABAAEAAACTAP//AAABAAEAAACTAP7/AAABAAEAAACTAP3/AAABAAAAAACSAAcAAAABAAIAAACSAAYAAAABAAEAAACSAAUAAAABAAEAAACSAAQAAAABAAEAAACSAAMAAAABAAEAAACSAAIAAAABAAEAAACSAAEAAAABAAEAAACSAAAAAAABAAEAAACSAP//AAABAAEAAACSAP7/AAABAAEAAACSAP3/AAABAAAAAACRAAcAAAABAAIAAACRAAYAAAABAAEAAACRAAUAAAABAAEAAACRAAQAAAABAAEAAACRAAMAAAABAAEAAACRAAIAAAABAAEAAACRAAEAAAABAAEAAACRAAAAAAABAAEAAACRAP//AAABAAEAAACRAP7/AAABAAEAAACRAP3/AAABAAAAAACQAAcAAAABAAIAAACQAAYAAAABAAEAAACQAAUAAAABAAEAAACQAAQAAAABAAEAAACQAAMAAAABAAEAAACQAAIAAAABAAEAAACQAAEAAAABAAEAAACQAAAAAAABAAEAAACQAP//AAABAAEAAACQAP7/AAABAAEAAACQAP3/AAABAAAAAACPAAcAAAABAAIAAACPAAYAAAABAAEAAACPAAUAAAABAAEAAACPAAQAAAABAAEAAACPAAMAAAABAAEAAACPAAIAAAABAAEAAACPAAEAAAABAAEAAACPAAAAAAABAAEAAACPAP//AAABAAEAAACPAP7/AAABAAEAAACPAP3/AAABAAAAAACOAAcAAAABAAIAAACOAAYAAAABAAEAAACOAAUAAAABAAEAAACOAAQAAAABAAEAAACOAAMAAAABAAEAAACOAAIAAAABAAEAAACOAAEAAAABAAEAAACOAAAAAAABAAEAAACOAP//AAABAAEAAACOAP7/AAABAAEAAACOAP3/AAABAAAAAACNAAcAAAABAAIAAACNAAYAAAABAAEAAACNAAUAAAABAAEAAACNAAQAAAABAAEAAACNAAMAAAABAAEAAACNAAIAAAABAAEAAACNAAEAAAABAAEAAACNAAAAAAABAAEAAACNAP//AAABAAEAAACNAP7/AAABAAEAAACNAP3/AAABAAAAAACgAAcAAAACAAIAAACgAAYAAAACAAEAAACgAAUAAAACAAEAAACgAAQAAAACAAEAAACgAAMAAAACAAEAAACgAAIAAAACAAAAAACfAAcAAAABAAIAAACfAAYAAAABAAEAAACfAAUAAAABAAEAAACfAAQAAAABAAEAAACfAAMAAAABAAEAAACfAAIAAAABAAAAAACeAAcAAAABAAIAAACeAAYAAAABAAEAAACeAAUAAAABAAEAAACeAAQAAAABAAEAAACeAAMAAAABAAEAAACeAAIAAAABAAAAAACdAAcAAAAAAAIAAACdAAYAAAAAAAEAAACdAAUAAAAAAAEAAACdAAQAAAAAAAEAAACdAAMAAAAAAAEAAACdAAIAAAAAAAAAAACnAAcAAAABAAIAAACnAAYAAAABAAEAAACnAAUAAAABAAEAAACnAAQAAAABAAEAAACnAAMAAAABAAEAAACnAAIAAAABAAEAAACmAAcAAAABAAIAAACmAAYAAAABAAEAAACmAAUAAAABAAEAAACmAAQAAAABAAEAAACmAAMAAAABAAEAAACmAAIAAAABAAEAAAClAAcAAAABAAIAAAClAAYAAAABAAEAAAClAAUAAAABAAEAAAClAAQAAAABAAEAAAClAAMAAAABAAEAAAClAAIAAAABAAEAAACnAAEAAAAFAAIAAACnAAAAAAACAAAAAACmAAEAAAABAAEAAACmAAAAAAABAAAAAAClAAEAAAAGAAIAAAClAAAAAAAAAAAAAACoAAcAAAACAAIAAACoAAYAAAACAAEAAACoAAUAAAACAAEAAACoAAQAAAACAAEAAACoAAMAAAACAAEAAACoAAIAAAACAAEAAACoAAEAAAACAAAAAACkAAcAAAAAAAIAAACkAAYAAAAAAAEAAACkAAUAAAAAAAEAAACkAAQAAAAAAAEAAACkAAMAAAAAAAEAAACkAAIAAAAAAAEAAACkAAEAAAAAAAAAAACxAAcAAAACAAIAAACxAAYAAAACAAEAAACxAAUAAAACAAEAAACxAAQAAAACAAEAAACxAAMAAAACAAEAAACxAAIAAAACAAAAAACwAAcAAAABAAIAAACwAAYAAAABAAEAAACwAAUAAAABAAEAAACwAAQAAAABAAEAAACwAAMAAAABAAEAAACwAAIAAAABAAAAAACvAAcAAAABAAIAAACvAAYAAAABAAEAAACvAAUAAAABAAEAAACvAAQAAAAGAAIAAACvAAMAAAAAAAEAAACvAAIAAAAAAAAAAACuAAcAAAABAAIAAACuAAYAAAABAAEAAACuAAUAAAABAAEAAACuAAQAAAABAAAAAACtAAcAAAABAAIAAACtAAYAAAABAAEAAACtAAUAAAAGAAIAAACtAAQAAAAAAAAAAACsAAcAAAAAAAIAAACsAAYAAAAAAAEAAACsAAUAAAAAAAAAAAC2ADEAAAACAAIAAAC2ADAAAAACAAEAAAC2AC8AAAACAAEAAAC2AC4AAAACAAEAAAC2AC0AAAAHAAIAAAC2ACwAAAADAAEAAAC2ACsAAAADAAEAAAC2ACoAAAADAAEAAAC2ACkAAAADAAEAAAC2ACgAAAADAAEAAAC2ACcAAAADAAEAAAC2ACYAAAADAAEAAAC2ACUAAAADAAEAAAC1ADEAAAABAAIAAAC1ADAAAAABAAEAAAC1AC8AAAABAAEAAAC1AC4AAAABAAEAAAC1AC0AAAABAAAAAAC0ADEAAAABAAIAAAC0ADAAAAABAAEAAAC0AC8AAAABAAEAAAC0AC4AAAABAAEAAAC0AC0AAAABAAAAAACzADEAAAABAAIAAACzADAAAAABAAEAAACzAC8AAAABAAEAAACzAC4AAAABAAEAAACzAC0AAAABAAAAAACyADEAAAABAAIAAACyADAAAAABAAEAAACxADEAAAABAAIAAACxADAAAAABAAEAAACwADEAAAABAAIAAACwADAAAAABAAEAAACvADEAAAABAAIAAACvADAAAAABAAEAAACuADEAAAABAAIAAACuADAAAAABAAEAAACtADEAAAABAAIAAACtADAAAAABAAEAAACsADEAAAABAAIAAACsADAAAAABAAEAAACrADEAAAABAAIAAACrADAAAAABAAEAAACqADEAAAABAAIAAACqADAAAAABAAEAAACpADEAAAABAAIAAACpADAAAAABAAEAAACoADEAAAABAAIAAACoADAAAAABAAEAAACnADEAAAABAAIAAACnADAAAAABAAEAAACmADEAAAABAAIAAACmADAAAAABAAEAAAClADEAAAABAAIAAAClADAAAAABAAEAAAClAC8AAAABAAEAAAClAC4AAAABAAEAAAClAC0AAAABAAAAAACkADEAAAABAAIAAACkADAAAAABAAEAAACkAC8AAAABAAEAAACkAC4AAAABAAEAAACkAC0AAAABAAAAAACjADEAAAABAAIAAACjADAAAAABAAEAAACjAC8AAAABAAEAAACjAC4AAAABAAEAAACjAC0AAAABAAAAAACiADEAAAABAAIAAACiADAAAAABAAEAAACiAC8AAAABAAEAAACiAC4AAAABAAEAAACiAC0AAAABAAAAAAChADEAAAABAAIAAAChADAAAAABAAEAAAChAC8AAAABAAEAAAChAC4AAAABAAEAAAChAC0AAAABAAAAAACgADEAAAABAAIAAACgADAAAAABAAEAAACgAC8AAAABAAEAAACgAC4AAAABAAEAAACgAC0AAAABAAAAAACfADEAAAABAAIAAACfADAAAAABAAEAAACfAC8AAAABAAEAAACfAC4AAAABAAEAAACfAC0AAAABAAAAAACeADEAAAABAAIAAACeADAAAAABAAEAAACeAC8AAAABAAEAAACeAC4AAAABAAEAAACeAC0AAAABAAAAAACdADEAAAABAAIAAACdADAAAAABAAEAAACdAC8AAAABAAEAAACdAC4AAAABAAEAAACdAC0AAAABAAAAAACcADEAAAABAAIAAACcADAAAAABAAEAAACcAC8AAAABAAEAAACcAC4AAAABAAEAAACcAC0AAAABAAAAAACbADEAAAABAAIAAACbADAAAAABAAEAAACbAC8AAAABAAEAAACbAC4AAAABAAEAAACbAC0AAAABAAAAAACaADEAAAABAAIAAACaADAAAAABAAEAAACaAC8AAAABAAEAAACaAC4AAAABAAEAAACaAC0AAAABAAAAAACZADEAAAAAAAIAAACZADAAAAAAAAEAAACZAC8AAAAAAAEAAACZAC4AAAAAAAEAAACZAC0AAAAEAAIAAACZACwAAAADAAEAAACZACsAAAADAAEAAACZACoAAAADAAEAAACZACkAAAADAAEAAACZACgAAAADAAEAAACZACcAAAADAAEAAACZACYAAAADAAEAAACZACUAAAADAAEAAACyAC8AAAABAAEAAACyAC4AAAABAAEAAACyAC0AAAABAAAAAACxAC8AAAABAAEAAACxAC4AAAABAAEAAACxAC0AAAABAAAAAACwAC8AAAABAAEAAACwAC4AAAABAAEAAACwAC0AAAABAAAAAACvAC8AAAABAAEAAACvAC4AAAABAAEAAACvAC0AAAABAAAAAACuAC8AAAABAAEAAACuAC4AAAABAAEAAACuAC0AAAABAAAAAACtAC8AAAABAAEAAACtAC4AAAABAAEAAACtAC0AAAABAAAAAACsAC8AAAABAAEAAACsAC4AAAABAAEAAACsAC0AAAABAAAAAACrAC8AAAABAAEAAACrAC4AAAABAAEAAACrAC0AAAABAAAAAACqAC8AAAABAAEAAACqAC4AAAABAAEAAACqAC0AAAABAAAAAACpAC8AAAABAAEAAACpAC4AAAABAAEAAACpAC0AAAABAAAAAACoAC8AAAABAAEAAACoAC4AAAABAAEAAACoAC0AAAABAAAAAACnAC8AAAABAAEAAACnAC4AAAABAAEAAACnAC0AAAABAAAAAACmAC8AAAABAAEAAACmAC4AAAABAAEAAACmAC0AAAABAAAAAACeACQAAAABAAMAAACdACQAAAABAAMAAACcACQAAAABAAMAAACbACQAAAABAAMAAACaACQAAAABAAMAAACZACQAAAAEAAAAAAC2ACQAAAAHAAAAAAC1ACQAAAABAAMAAAC0ACQAAAABAAMAAACzACQAAAABAAMAAACyACQAAAABAAMAAACxACQAAAABAAMAAACwACQAAAAAAAMAAACgACQAAAACAAMAAACfACQAAAABAAMAAADcAAcAAAABAAIAAADcAAYAAAABAAEAAADcAAUAAAABAAEAAADcAAQAAAABAAEAAADcAAMAAAABAAEAAADcAAIAAAABAAEAAADcAAEAAAAFAAIAAADcAAAAAAACAAEAAADcAP//AAACAAAAAADbAAcAAAABAAIAAADbAAYAAAABAAEAAADbAAUAAAABAAEAAADbAAQAAAABAAEAAADbAAMAAAABAAEAAADbAAIAAAABAAEAAADbAAEAAAABAAEAAADbAAAAAAABAAEAAADbAP//AAABAAAAAADaAAcAAAABAAIAAADaAAYAAAABAAEAAADaAAUAAAABAAEAAADaAAQAAAABAAEAAADaAAMAAAABAAEAAADaAAIAAAABAAEAAADaAAEAAAABAAEAAADaAAAAAAABAAEAAADaAP//AAABAAAAAADZAAcAAAABAAIAAADZAAYAAAABAAEAAADZAAUAAAABAAEAAADZAAQAAAABAAEAAADZAAMAAAABAAEAAADZAAIAAAABAAEAAADZAAEAAAABAAEAAADZAAAAAAABAAEAAADZAP//AAABAAAAAADYAAcAAAABAAIAAADYAAYAAAABAAEAAADYAAUAAAABAAEAAADYAAQAAAABAAEAAADYAAMAAAABAAEAAADYAAIAAAABAAEAAADYAAEAAAABAAEAAADYAAAAAAABAAEAAADYAP//AAABAAAAAADXAAcAAAABAAIAAADXAAYAAAABAAEAAADXAAUAAAABAAEAAADXAAQAAAABAAEAAADXAAMAAAABAAEAAADXAAIAAAABAAEAAADXAAEAAAABAAEAAADXAAAAAAABAAEAAADXAP//AAABAAAAAADWAAcAAAABAAIAAADWAAYAAAABAAEAAADWAAUAAAABAAEAAADWAAQAAAABAAEAAADWAAMAAAABAAEAAADWAAIAAAABAAEAAADWAAEAAAABAAEAAADWAAAAAAABAAEAAADWAP//AAABAAAAAADVAAcAAAABAAIAAADVAAYAAAABAAEAAADVAAUAAAABAAEAAADVAAQAAAABAAEAAADVAAMAAAABAAEAAADVAAIAAAABAAEAAADVAAEAAAABAAEAAADVAAAAAAABAAEAAADVAP//AAABAAAAAADUAAcAAAABAAIAAADUAAYAAAABAAEAAADUAAUAAAABAAEAAADUAAQAAAABAAEAAADUAAMAAAABAAEAAADUAAIAAAABAAEAAADUAAEAAAABAAEAAADUAAAAAAABAAEAAADUAP//AAABAAAAAADTAAcAAAABAAIAAADTAAYAAAABAAEAAADTAAUAAAABAAEAAADTAAQAAAABAAEAAADTAAMAAAABAAEAAADTAAIAAAABAAEAAADTAAEAAAABAAEAAADTAAAAAAABAAEAAADTAP//AAABAAAAAADSAAcAAAABAAIAAADSAAYAAAABAAEAAADSAAUAAAABAAEAAADSAAQAAAABAAEAAADSAAMAAAABAAEAAADSAAIAAAABAAEAAADSAAEAAAABAAEAAADSAAAAAAABAAEAAADSAP//AAABAAAAAADRAAcAAAABAAIAAADRAAYAAAABAAEAAADRAAUAAAABAAEAAADRAAQAAAABAAEAAADRAAMAAAABAAEAAADRAAIAAAABAAEAAADRAAEAAAABAAEAAADRAAAAAAABAAEAAADRAP//AAABAAAAAADQAAcAAAABAAIAAADQAAYAAAABAAEAAADQAAUAAAABAAEAAADQAAQAAAABAAEAAADQAAMAAAABAAEAAADQAAIAAAABAAEAAADQAAEAAAABAAEAAADQAAAAAAABAAEAAADQAP//AAABAAAAAADPAAcAAAABAAIAAADPAAYAAAABAAEAAADPAAUAAAABAAEAAADPAAQAAAABAAEAAADPAAMAAAABAAEAAADPAAIAAAABAAEAAADPAAEAAAABAAEAAADPAAAAAAABAAEAAADPAP//AAABAAAAAADOAAcAAAABAAIAAADOAAYAAAABAAEAAADOAAUAAAABAAEAAADOAAQAAAABAAEAAADOAAMAAAABAAEAAADOAAIAAAABAAEAAADOAAEAAAABAAEAAADOAAAAAAABAAEAAADOAP//AAABAAAAAADNAAcAAAABAAIAAADNAAYAAAABAAEAAADNAAUAAAABAAEAAADNAAQAAAABAAEAAADNAAMAAAABAAEAAADNAAIAAAABAAEAAADNAAEAAAABAAEAAADNAAAAAAABAAEAAADNAP//AAABAAAAAADMAAcAAAABAAIAAADMAAYAAAABAAEAAADMAAUAAAABAAEAAADMAAQAAAABAAEAAADMAAMAAAABAAEAAADMAAIAAAABAAEAAADMAAEAAAABAAEAAADMAAAAAAABAAEAAADMAP//AAABAAAAAADLAAcAAAABAAIAAADLAAYAAAABAAEAAADLAAUAAAABAAEAAADLAAQAAAABAAEAAADLAAMAAAABAAEAAADLAAIAAAABAAEAAADLAAEAAAABAAEAAADLAAAAAAABAAEAAADLAP//AAABAAAAAADKAAcAAAABAAIAAADKAAYAAAABAAEAAADKAAUAAAABAAEAAADKAAQAAAABAAEAAADKAAMAAAABAAEAAADKAAIAAAABAAEAAADKAAEAAAABAAEAAADKAAAAAAABAAEAAADKAP//AAABAAAAAADJAAcAAAABAAIAAADJAAYAAAABAAEAAADJAAUAAAABAAEAAADJAAQAAAABAAEAAADJAAMAAAABAAEAAADJAAIAAAABAAEAAADJAAEAAAABAAEAAADJAAAAAAABAAEAAADJAP//AAABAAAAAADIAAcAAAABAAIAAADIAAYAAAABAAEAAADIAAUAAAABAAEAAADIAAQAAAABAAEAAADIAAMAAAABAAEAAADIAAIAAAABAAEAAADIAAEAAAABAAEAAADIAAAAAAABAAEAAADIAP//AAABAAAAAADHAAcAAAABAAIAAADHAAYAAAABAAEAAADHAAUAAAABAAEAAADHAAQAAAABAAEAAADHAAMAAAABAAEAAADHAAIAAAABAAEAAADHAAEAAAABAAEAAADHAAAAAAABAAEAAADHAP//AAABAAAAAADGAAcAAAABAAIAAADGAAYAAAABAAEAAADGAAUAAAABAAEAAADGAAQAAAABAAEAAADGAAMAAAABAAEAAADGAAIAAAABAAEAAADGAAEAAAABAAEAAADGAAAAAAABAAEAAADGAP//AAABAAAAAADFAAcAAAABAAIAAADFAAYAAAABAAEAAADFAAUAAAABAAEAAADFAAQAAAABAAEAAADFAAMAAAABAAEAAADFAAIAAAABAAEAAADFAAEAAAABAAEAAADFAAAAAAABAAEAAADFAP//AAABAAAAAADEAAcAAAABAAIAAADEAAYAAAABAAEAAADEAAUAAAABAAEAAADEAAQAAAABAAEAAADEAAMAAAABAAEAAADEAAIAAAABAAEAAADEAAEAAAABAAEAAADEAAAAAAABAAEAAADEAP//AAABAAAAAADDAAcAAAABAAIAAADDAAYAAAABAAEAAADDAAUAAAABAAEAAADDAAQAAAABAAEAAADDAAMAAAABAAEAAADDAAIAAAABAAEAAADDAAEAAAABAAEAAADDAAAAAAABAAEAAADDAP//AAABAAAAAADCAAcAAAABAAIAAADCAAYAAAABAAEAAADCAAUAAAABAAEAAADCAAQAAAABAAEAAADCAAMAAAABAAEAAADCAAIAAAABAAEAAADCAAEAAAABAAEAAADCAAAAAAABAAEAAADCAP//AAABAAAAAADBAAcAAAABAAIAAADBAAYAAAABAAEAAADBAAUAAAABAAEAAADBAAQAAAABAAEAAADBAAMAAAABAAEAAADBAAIAAAABAAEAAADBAAEAAAABAAEAAADBAAAAAAABAAEAAADBAP//AAABAAAAAADAAAcAAAABAAIAAADAAAYAAAABAAEAAADAAAUAAAABAAEAAADAAAQAAAABAAEAAADAAAMAAAABAAEAAADAAAIAAAABAAEAAADAAAEAAAABAAEAAADAAAAAAAABAAEAAADAAP//AAABAAAAAAC/AAcAAAABAAIAAAC/AAYAAAABAAEAAAC/AAUAAAABAAEAAAC/AAQAAAABAAEAAAC/AAMAAAABAAEAAAC/AAIAAAABAAEAAAC/AAEAAAABAAEAAAC/AAAAAAABAAEAAAC/AP//AAABAAAAAAC+AAcAAAABAAIAAAC+AAYAAAABAAEAAAC+AAUAAAABAAEAAAC+AAQAAAABAAEAAAC+AAMAAAABAAEAAAC+AAIAAAABAAEAAAC+AAEAAAABAAEAAAC+AAAAAAABAAEAAAC+AP//AAABAAAAAAC9AAcAAAABAAIAAAC9AAYAAAABAAEAAAC9AAUAAAABAAEAAAC9AAQAAAABAAEAAAC9AAMAAAABAAEAAAC9AAIAAAABAAEAAAC9AAEAAAABAAEAAAC9AAAAAAABAAEAAAC9AP//AAABAAAAAAC8AAcAAAABAAIAAAC8AAYAAAABAAEAAAC8AAUAAAABAAEAAAC8AAQAAAABAAEAAAC8AAMAAAABAAEAAAC8AAIAAAABAAEAAAC8AAEAAAABAAEAAAC8AAAAAAABAAEAAAC8AP//AAABAAAAAAC7AAcAAAABAAIAAAC7AAYAAAABAAEAAAC7AAUAAAABAAEAAAC7AAQAAAABAAEAAAC7AAMAAAABAAEAAAC7AAIAAAABAAEAAAC7AAEAAAABAAEAAAC7AAAAAAABAAEAAAC7AP//AAABAAAAAAC6AAcAAAABAAIAAAC6AAYAAAABAAEAAAC6AAUAAAABAAEAAAC6AAQAAAABAAEAAAC6AAMAAAABAAEAAAC6AAIAAAABAAEAAAC6AAEAAAABAAEAAAC6AAAAAAABAAEAAAC6AP//AAABAAAAAAC5AAcAAAABAAIAAAC5AAYAAAABAAEAAAC5AAUAAAABAAEAAAC5AAQAAAABAAEAAAC5AAMAAAABAAEAAAC5AAIAAAABAAEAAAC5AAEAAAABAAEAAAC5AAAAAAABAAEAAAC5AP//AAABAAAAAAC4AAcAAAAAAAIAAAC4AAYAAAAAAAEAAAC4AAUAAAAAAAEAAAC4AAQAAAAAAAEAAAC4AAMAAAAAAAEAAAC4AAIAAAAAAAEAAAC4AAEAAAAAAAEAAAC4AAAAAAAAAAEAAAC4AP//AAAAAAAAAADyAAcAAAABAAIAAADyAAYAAAABAAEAAADyAAUAAAABAAEAAADyAAQAAAABAAEAAADyAAMAAAAFAAIAAADyAAIAAAACAAEAAADyAAEAAAACAAAAAADxAAcAAAABAAIAAADxAAYAAAABAAEAAADxAAUAAAABAAEAAADxAAQAAAABAAEAAADxAAMAAAABAAEAAADxAAIAAAABAAEAAADxAAEAAAABAAAAAADwAAcAAAABAAIAAADwAAYAAAABAAEAAADwAAUAAAABAAEAAADwAAQAAAABAAEAAADwAAMAAAABAAEAAADwAAIAAAABAAEAAADwAAEAAAABAAAAAADvAAcAAAABAAIAAADvAAYAAAABAAEAAADvAAUAAAABAAEAAADvAAQAAAABAAEAAADvAAMAAAABAAEAAADvAAIAAAABAAEAAADvAAEAAAABAAAAAADuAAcAAAABAAIAAADuAAYAAAABAAEAAADuAAUAAAABAAEAAADuAAQAAAABAAEAAADuAAMAAAABAAEAAADuAAIAAAABAAEAAADuAAEAAAABAAAAAADtAAcAAAABAAIAAADtAAYAAAABAAEAAADtAAUAAAABAAEAAADtAAQAAAABAAEAAADtAAMAAAABAAEAAADtAAIAAAABAAEAAADtAAEAAAABAAAAAADsAAcAAAABAAIAAADsAAYAAAABAAEAAADsAAUAAAABAAEAAADsAAQAAAABAAEAAADsAAMAAAABAAEAAADsAAIAAAABAAEAAADsAAEAAAABAAAAAADrAAcAAAABAAIAAADrAAYAAAABAAEAAADrAAUAAAABAAEAAADrAAQAAAABAAEAAADrAAMAAAABAAEAAADrAAIAAAABAAEAAADrAAEAAAABAAAAAADqAAcAAAABAAIAAADqAAYAAAABAAEAAADqAAUAAAABAAEAAADqAAQAAAABAAEAAADqAAMAAAABAAEAAADqAAIAAAABAAEAAADqAAEAAAABAAAAAADpAAcAAAABAAIAAADpAAYAAAABAAEAAADpAAUAAAABAAEAAADpAAQAAAABAAEAAADpAAMAAAABAAEAAADpAAIAAAABAAEAAADpAAEAAAABAAAAAADoAAcAAAABAAIAAADoAAYAAAABAAEAAADoAAUAAAABAAEAAADoAAQAAAABAAEAAADoAAMAAAABAAEAAADoAAIAAAABAAEAAADoAAEAAAABAAAAAADnAAcAAAABAAIAAADnAAYAAAABAAEAAADnAAUAAAABAAEAAADnAAQAAAABAAEAAADnAAMAAAABAAEAAADnAAIAAAABAAEAAADnAAEAAAABAAAAAADmAAcAAAABAAIAAADmAAYAAAABAAEAAADmAAUAAAABAAEAAADmAAQAAAABAAEAAADmAAMAAAABAAEAAADmAAIAAAABAAEAAADmAAEAAAABAAAAAADlAAcAAAABAAIAAADlAAYAAAABAAEAAADlAAUAAAABAAEAAADlAAQAAAABAAEAAADlAAMAAAABAAEAAADlAAIAAAABAAEAAADlAAEAAAABAAAAAADkAAcAAAABAAIAAADkAAYAAAABAAEAAADkAAUAAAABAAEAAADkAAQAAAABAAEAAADkAAMAAAABAAEAAADkAAIAAAABAAEAAADkAAEAAAABAAAAAADjAAcAAAABAAIAAADjAAYAAAABAAEAAADjAAUAAAABAAEAAADjAAQAAAABAAEAAADjAAMAAAABAAEAAADjAAIAAAABAAEAAADjAAEAAAABAAAAAADiAAcAAAABAAIAAADiAAYAAAABAAEAAADiAAUAAAABAAEAAADiAAQAAAABAAEAAADiAAMAAAABAAEAAADiAAIAAAABAAEAAADiAAEAAAABAAAAAADhAAcAAAABAAIAAADhAAYAAAABAAEAAADhAAUAAAABAAEAAADhAAQAAAABAAEAAADhAAMAAAABAAEAAADhAAIAAAABAAEAAADhAAEAAAABAAAAAADgAAcAAAABAAIAAADgAAYAAAABAAEAAADgAAUAAAABAAEAAADgAAQAAAABAAEAAADgAAMAAAABAAEAAADgAAIAAAABAAEAAADgAAEAAAABAAAAAADfAAcAAAABAAIAAADfAAYAAAABAAEAAADfAAUAAAABAAEAAADfAAQAAAABAAEAAADfAAMAAAABAAEAAADfAAIAAAABAAEAAADfAAEAAAABAAAAAADeAAcAAAABAAIAAADeAAYAAAABAAEAAADeAAUAAAABAAEAAADeAAQAAAABAAEAAADeAAMAAAABAAEAAADeAAIAAAABAAEAAADeAAEAAAABAAAAAADdAAcAAAABAAIAAADdAAYAAAABAAEAAADdAAUAAAABAAEAAADdAAQAAAABAAEAAADdAAMAAAABAAEAAADdAAIAAAABAAEAAADdAAEAAAABAAAAAAABAQcAAAACAAIAAAABAQYAAAACAAEAAAABAQUAAAACAAEAAAABAQQAAAACAAEAAAABAQMAAAACAAAAAAAAAQcAAAABAAIAAAAAAQYAAAABAAEAAAAAAQUAAAABAAEAAAAAAQQAAAABAAEAAAAAAQMAAAABAAAAAAD/AAcAAAABAAIAAAD/AAYAAAABAAEAAAD/AAUAAAABAAEAAAD/AAQAAAABAAEAAAD/AAMAAAABAAAAAAD+AAcAAAABAAIAAAD+AAYAAAABAAEAAAD+AAUAAAABAAEAAAD+AAQAAAABAAEAAAD+AAMAAAABAAAAAAD9AAcAAAABAAIAAAD9AAYAAAABAAEAAAD9AAUAAAABAAEAAAD9AAQAAAABAAEAAAD9AAMAAAABAAAAAAD8AAcAAAABAAIAAAD8AAYAAAABAAEAAAD8AAUAAAABAAEAAAD8AAQAAAABAAEAAAD8AAMAAAABAAAAAAD7AAcAAAABAAIAAAD7AAYAAAABAAEAAAD7AAUAAAABAAEAAAD7AAQAAAABAAEAAAD7AAMAAAABAAAAAAD6AAcAAAABAAIAAAD6AAYAAAABAAEAAAD6AAUAAAABAAEAAAD6AAQAAAABAAEAAAD6AAMAAAABAAAAAAD5AAcAAAABAAIAAAD5AAYAAAABAAEAAAD5AAUAAAABAAEAAAD5AAQAAAABAAEAAAD5AAMAAAABAAAAAAD4AAcAAAABAAIAAAD4AAYAAAABAAEAAAD4AAUAAAABAAEAAAD4AAQAAAABAAEAAAD4AAMAAAABAAAAAAD3AAcAAAABAAIAAAD3AAYAAAABAAEAAAD3AAUAAAABAAEAAAD3AAQAAAABAAEAAAD3AAMAAAABAAAAAAD2AAcAAAABAAIAAAD2AAYAAAABAAEAAAD2AAUAAAABAAEAAAD2AAQAAAABAAEAAAD2AAMAAAABAAAAAAD1AAcAAAABAAIAAAD1AAYAAAABAAEAAAD1AAUAAAABAAEAAAD1AAQAAAABAAEAAAD1AAMAAAABAAAAAAD0AAcAAAABAAIAAAD0AAYAAAABAAEAAAD0AAUAAAABAAEAAAD0AAQAAAABAAEAAAD0AAMAAAABAAAAAADzAAcAAAABAAIAAADzAAYAAAABAAEAAADzAAUAAAABAAEAAADzAAQAAAABAAEAAADzAAMAAAABAAAAAABAAOv/AAACAAIAAABAAOr/AAACAAEAAABAAOn/AAACAAEAAABAAOj/AAACAAEAAABAAOf/AAACAAEAAABAAOb/AAACAAEAAABAAOX/AAACAAEAAABAAOT/AAACAAEAAABAAOP/AAACAAEAAABAAOL/AAACAAEAAABAAOH/AAACAAEAAABAAOD/AAACAAEAAABAAN//AAACAAEAAABAAN7/AAACAAEAAABAAN3/AAACAAAAAAA/AOv/AAABAAIAAAA/AOr/AAABAAEAAAA/AOn/AAABAAEAAAA/AOj/AAABAAEAAAA/AOf/AAABAAEAAAA/AOb/AAABAAEAAAA/AOX/AAABAAEAAAA/AOT/AAABAAEAAAA/AOP/AAABAAEAAAA/AOL/AAABAAEAAAA/AOH/AAABAAEAAAA/AOD/AAABAAEAAAA/AN//AAABAAEAAAA/AN7/AAABAAEAAAA/AN3/AAABAAAAAAA+AOv/AAABAAIAAAA+AOr/AAABAAEAAAA+AOn/AAABAAEAAAA+AOj/AAABAAEAAAA+AOf/AAABAAEAAAA+AOb/AAABAAEAAAA+AOX/AAABAAEAAAA+AOT/AAABAAEAAAA+AOP/AAABAAEAAAA+AOL/AAABAAEAAAA+AOH/AAABAAEAAAA+AOD/AAABAAEAAAA+AN//AAABAAEAAAA+AN7/AAABAAEAAAA+AN3/AAABAAAAAAA9AOv/AAABAAIAAAA9AOr/AAABAAEAAAA9AOn/AAABAAEAAAA9AOj/AAABAAEAAAA9AOf/AAABAAEAAAA9AOb/AAABAAEAAAA9AOX/AAABAAEAAAA9AOT/AAABAAEAAAA9AOP/AAABAAEAAAA9AOL/AAABAAEAAAA9AOH/AAABAAEAAAA9AOD/AAABAAEAAAA9AN//AAABAAEAAAA9AN7/AAABAAEAAAA9AN3/AAABAAAAAAA8AOv/AAABAAIAAAA8AOr/AAABAAEAAAA8AOn/AAABAAEAAAA8AOj/AAABAAEAAAA8AOf/AAABAAEAAAA8AOb/AAABAAEAAAA8AOX/AAABAAEAAAA8AOT/AAABAAEAAAA8AOP/AAABAAEAAAA8AOL/AAABAAEAAAA8AOH/AAABAAEAAAA8AOD/AAABAAEAAAA8AN//AAABAAEAAAA8AN7/AAABAAEAAAA8AN3/AAABAAAAAAA7AOv/AAABAAIAAAA7AOr/AAABAAEAAAA7AOn/AAABAAEAAAA7AOj/AAABAAEAAAA7AOf/AAABAAEAAAA7AOb/AAABAAEAAAA7AOX/AAABAAEAAAA7AOT/AAABAAEAAAA7AOP/AAABAAEAAAA7AOL/AAABAAEAAAA7AOH/AAABAAEAAAA7AOD/AAABAAEAAAA7AN//AAABAAEAAAA7AN7/AAABAAEAAAA7AN3/AAABAAAAAAA6AOv/AAABAAIAAAA6AOr/AAABAAEAAAA6AOn/AAABAAEAAAA6AOj/AAABAAEAAAA6AOf/AAABAAEAAAA6AOb/AAABAAEAAAA6AOX/AAABAAEAAAA6AOT/AAABAAEAAAA6AOP/AAABAAEAAAA6AOL/AAABAAEAAAA6AOH/AAABAAEAAAA6AOD/AAABAAEAAAA6AN//AAABAAEAAAA6AN7/AAABAAEAAAA6AN3/AAABAAAAAAA5AOv/AAABAAIAAAA5AOr/AAABAAEAAAA5AOn/AAABAAEAAAA5AOj/AAABAAEAAAA5AOf/AAABAAEAAAA5AOb/AAABAAEAAAA5AOX/AAABAAEAAAA5AOT/AAABAAEAAAA5AOP/AAABAAEAAAA5AOL/AAABAAEAAAA5AOH/AAABAAEAAAA5AOD/AAABAAEAAAA5AN//AAABAAEAAAA5AN7/AAABAAEAAAA5AN3/AAABAAAAAAA4AOv/AAABAAIAAAA4AOr/AAABAAEAAAA4AOn/AAABAAEAAAA4AOj/AAABAAEAAAA4AOf/AAABAAEAAAA4AOb/AAABAAEAAAA4AOX/AAABAAEAAAA4AOT/AAABAAEAAAA4AOP/AAABAAEAAAA4AOL/AAABAAEAAAA4AOH/AAABAAEAAAA4AOD/AAABAAEAAAA4AN//AAABAAEAAAA4AN7/AAABAAEAAAA4AN3/AAABAAAAAAA3AOv/AAABAAIAAAA3AOr/AAABAAEAAAA3AOn/AAABAAEAAAA3AOj/AAABAAEAAAA3AOf/AAABAAEAAAA3AOb/AAABAAEAAAA3AOX/AAABAAEAAAA3AOT/AAABAAEAAAA3AOP/AAABAAEAAAA3AOL/AAABAAEAAAA3AOH/AAABAAEAAAA3AOD/AAABAAEAAAA3AN//AAABAAEAAAA3AN7/AAABAAEAAAA3AN3/AAABAAAAAAA2AOv/AAABAAIAAAA2AOr/AAABAAEAAAA2AOn/AAABAAEAAAA2AOj/AAABAAEAAAA2AOf/AAABAAEAAAA2AOb/AAABAAEAAAA2AOX/AAABAAEAAAA2AOT/AAABAAEAAAA2AOP/AAABAAEAAAA2AOL/AAABAAEAAAA2AOH/AAABAAEAAAA2AOD/AAABAAEAAAA2AN//AAABAAEAAAA2AN7/AAABAAEAAAA2AN3/AAABAAAAAAA1AOv/AAABAAIAAAA1AOr/AAABAAEAAAA1AOn/AAABAAEAAAA1AOj/AAABAAEAAAA1AOf/AAABAAEAAAA1AOb/AAABAAEAAAA1AOX/AAABAAEAAAA1AOT/AAABAAEAAAA1AOP/AAABAAEAAAA1AOL/AAABAAEAAAA1AOH/AAABAAEAAAA1AOD/AAABAAEAAAA1AN//AAABAAEAAAA1AN7/AAABAAEAAAA1AN3/AAABAAAAAAA0AOv/AAAAAAIAAAA0AOr/AAAAAAEAAAA0AOn/AAAAAAEAAAA0AOj/AAAAAAEAAAA0AOf/AAAAAAEAAAA0AOb/AAAAAAEAAAA0AOX/AAAAAAEAAAA0AOT/AAAAAAEAAAA0AOP/AAAAAAEAAAA0AOL/AAAAAAEAAAA0AOH/AAAAAAEAAAA0AOD/AAAAAAEAAAA0AN//AAAAAAEAAAA0AN7/AAAAAAEAAAA0AN3/AAAAAAAAAABzAMj/AAACAAMAAAByAMj/AAABAAMAAABxAMj/AAABAAMAAABwAMj/AAABAAMAAABvAMj/AAABAAMAAABuAMj/AAABAAMAAABtAMj/AAABAAMAAABsAMj/AAABAAMAAABrAMj/AAABAAMAAABqAMj/AAABAAMAAABpAMj/AAABAAMAAABoAMj/AAABAAMAAABnAMj/AAABAAMAAABmAMj/AAABAAMAAABlAMj/AAABAAMAAABkAMj/AAABAAMAAABjAMj/AAABAAMAAABiAMj/AAABAAMAAABhAMj/AAABAAMAAABgAMj/AAABAAMAAABfAMj/AAABAAMAAABeAMj/AAABAAMAAABdAMj/AAABAAMAAABcAMj/AAABAAMAAABbAMj/AAABAAMAAABaAMj/AAABAAMAAABZAMj/AAABAAMAAABYAMj/AAABAAMAAABXAMj/AAABAAMAAABWAMj/AAABAAMAAABVAMj/AAABAAMAAABUAMj/AAABAAMAAABTAMj/AAABAAMAAABSAMj/AAABAAMAAABRAMj/AAABAAMAAABQAMj/AAABAAMAAABPAMj/AAABAAMAAABOAMj/AAABAAMAAABNAMj/AAABAAMAAABMAMj/AAABAAMAAABLAMj/AAABAAMAAABKAMj/AAABAAMAAABJAMj/AAABAAMAAABIAMj/AAABAAMAAABHAMj/AAABAAMAAABGAMj/AAABAAMAAABFAMj/AAABAAMAAABEAMj/AAABAAMAAABDAMj/AAABAAMAAABCAMj/AAABAAMAAABBAMj/AAABAAMAAABAAMj/AAABAAMAAAA/AMj/AAABAAMAAAA+AMj/AAABAAMAAAA9AMj/AAABAAMAAAA8AMj/AAABAAMAAAA7AMj/AAABAAMAAAA6AMj/AAABAAMAAAA5AMj/AAABAAMAAAA4AMj/AAABAAMAAAA3AMj/AAABAAMAAAA2AMj/AAABAAMAAAA1AMj/AAABAAMAAAA0AMj/AAABAAMAAAAzAMj/AAABAAMAAAAyAMj/AAABAAMAAAAAAMj/AAAAAAMAAAABAMj/AAABAAMAAAACAMj/AAABAAMAAAADAMj/AAABAAMAAAAEAMj/AAABAAMAAAAFAMj/AAABAAMAAAAGAMj/AAABAAMAAAAHAMj/AAABAAMAAAAIAMj/AAABAAMAAAAJAMj/AAABAAMAAAAKAMj/AAABAAMAAAALAMj/AAABAAMAAAAMAMj/AAABAAMAAAANAMj/AAABAAMAAAAOAMj/AAABAAMAAAAPAMj/AAABAAMAAAAQAMj/AAABAAMAAAARAMj/AAABAAMAAAASAMj/AAABAAMAAAATAMj/AAABAAMAAAAUAMj/AAABAAMAAAAVAMj/AAABAAMAAAAWAMj/AAABAAMAAAAXAMj/AAABAAMAAAAYAMj/AAABAAMAAAAZAMj/AAABAAMAAAAaAMj/AAABAAMAAAAbAMj/AAABAAMAAAAcAMj/AAABAAMAAAAdAMj/AAABAAMAAAAeAMj/AAABAAMAAAAfAMj/AAABAAMAAAAgAMj/AAABAAMAAAAhAMj/AAABAAMAAAAiAMj/AAABAAMAAAAjAMj/AAABAAMAAAAkAMj/AAABAAMAAAAlAMj/AAABAAMAAAAmAMj/AAABAAMAAAAnAMj/AAABAAMAAAAoAMj/AAABAAMAAAApAMj/AAABAAMAAAAqAMj/AAABAAMAAAArAMj/AAABAAMAAAAsAMj/AAABAAMAAAAtAMj/AAABAAMAAAAuAMj/AAABAAMAAAAvAMj/AAABAAMAAAAwAMj/AAABAAMAAAAxAMj/AAABAAMAAAA=")
tile_set = ExtResource("1_d680t")
[node name="Entities layer" type="TileMapLayer" parent="."]
-tile_map_data = PackedByteArray("AAAJAPz/AAAHAAMAAAAJAP3/AAAHAAQAAAAKAPz/AAAIAAMAAAALAPz/AAAIAAMAAAAMAPz/AAAJAAMAAAANAPz/AAAKAAMAAAANAP3/AAAKAAQAAAAUAPz/AAAGAAMAADAVAPz/AAAGAAMAADAWAPz/AAAGAAMAADAcAAMAAAAGAAMAAGAPAPn/AAADAAYAAAAaAAMAAAAGAAQAAAAWAPn/AQAAAAAAAQAXAPn/AQAAAAAAAQAYAPn/AQAAAAAAAQAZAPn/AQAAAAAAAQAaAPn/AQAAAAAAAQAZAPj/AQAAAAAAAQAYAPj/AQAAAAAAAQAXAPj/AQAAAAAAAQAYAPf/AQAAAAAAAQAKAP3/AQAAAAAAAgALAP3/AQAAAAAAAgAMAP3/AQAAAAAAAgA=")
+tile_map_data = PackedByteArray("AAAJAPz/AAAHAAMAAAAJAP3/AAAHAAQAAAAKAPz/AAAIAAMAAAALAPz/AAAIAAMAAAAMAPz/AAAJAAMAAAANAPz/AAAKAAMAAAANAP3/AAAKAAQAAAAUAPz/AAAGAAMAADAVAPz/AAAGAAMAADAWAPz/AAAGAAMAADAcAAMAAAAGAAMAAGAPAPn/AAADAAYAAAAaAAMAAAAGAAQAAAAWAPn/AQAAAAAAAQAXAPn/AQAAAAAAAQAYAPn/AQAAAAAAAQAZAPn/AQAAAAAAAQAaAPn/AQAAAAAAAQAZAPj/AQAAAAAAAQAYAPj/AQAAAAAAAQAXAPj/AQAAAAAAAQAYAPf/AQAAAAAAAQAKAP3/AQAAAAAAAgALAP3/AQAAAAAAAgAMAP3/AQAAAAAAAgBAAAAAAAAJAAYAAABFAAAAAAALAAYAAABEAAAAAAAKAAYAAABDAAAAAAAKAAYAAABCAAAAAAAKAAYAAABBAAAAAAAKAAYAAABAAPr/AAAJAAYAAABBAPr/AAAKAAYAAABCAPr/AAAKAAYAAABDAPr/AAAKAAYAAABEAPr/AAAKAAYAAABFAPr/AAAKAAYAAABGAPr/AAAKAAYAAABHAPr/AAAKAAYAAABIAPr/AAAKAAYAAABJAPr/AAAKAAYAAABKAPr/AAAKAAYAAABLAPr/AAALAAYAAABjAP7/AAAHAAQAAAB8AP7/AAAKAAQAAABkAP7/AQAAAAAAAgBlAP7/AQAAAAAAAgBmAP7/AQAAAAAAAgBnAP7/AQAAAAAAAgBoAP7/AQAAAAAAAgBpAP7/AQAAAAAAAgBsAP7/AQAAAAAAAgBtAP7/AQAAAAAAAgBrAP7/AQAAAAAAAgBqAP7/AQAAAAAAAgBuAP7/AQAAAAAAAgBvAP7/AQAAAAAAAgBwAP7/AQAAAAAAAgBxAP7/AQAAAAAAAgByAP7/AQAAAAAAAgBzAP7/AQAAAAAAAgB0AP7/AQAAAAAAAgB1AP7/AQAAAAAAAgB2AP7/AQAAAAAAAgB3AP7/AQAAAAAAAgB4AP7/AQAAAAAAAgB5AP7/AQAAAAAAAgB6AP7/AQAAAAAAAgB7AP7/AQAAAAAAAgCJAPr/AAAJAAYAAACKAPr/AAAKAAYAAACLAPr/AAALAAYAAADuAP7/AAAJAAYAAADvAP7/AAAKAAYAAADwAP7/AAALAAYAAADpAPz/AAAJAAYAAADqAPz/AAAKAAYAAADrAPz/AAALAAYAAADkAPv/AAAJAAYAAADlAPv/AAAKAAYAAADmAPv/AAALAAYAAADsAPn/AAAJAAYAAADtAPn/AAAKAAYAAADuAPn/AAALAAYAAADyAPf/AAAJAAYAAADzAPf/AAAKAAYAAAD0APf/AAALAAYAAAD3APb/AAAJAAYAAAD4APb/AAAKAAYAAAD5APb/AAALAAYAAAABAQIAAAAGAAQAAABSAPP/AAAKAAYAAABTAPP/AAAKAAYAAABUAPP/AAAKAAYAAABVAPP/AAAKAAYAAABWAPP/AAAKAAYAAABXAPP/AAAKAAYAAABYAPP/AAAKAAYAAABZAPP/AAAKAAYAAABaAPP/AAAKAAYAAABbAPP/AAAKAAYAAABcAPP/AAAKAAYAAABdAPP/AAAKAAYAAABeAPP/AAAKAAYAAABfAPP/AAAKAAYAAABgAPP/AAAKAAYAAABRAPP/AAAJAAYAAABhAPP/AAALAAYAAABkAO7/AAAJAAYAAABlAO7/AAAKAAYAAABmAO7/AAALAAYAAABMAO3/AAAJAAYAAABOAO3/AAALAAYAAABNAO3/AAAKAAYAAABBAOj/AAAJAAYAAABCAOj/AAAKAAYAAABDAOj/AAALAAYAAABGAOT/AAAJAAYAAABHAOT/AAAKAAYAAABIAOT/AAALAAYAAABBAN//AAAJAAYAAABCAN//AAAKAAYAAABDAN//AAALAAYAAABqAOz/AAAJAAYAAABrAOz/AAAKAAYAAABsAOz/AAALAAYAAAB+AOz/AAAJAAYAAAB/AOz/AAAKAAYAAACAAOz/AAALAAYAAACRAOz/AAAJAAYAAACSAOz/AAAKAAYAAACTAOz/AAALAAYAAABOAOz/AQAAAAAAAQBNAOz/AQAAAAAAAQBMAOz/AQAAAAAAAQBNAOv/AQAAAAAAAQBDAOf/AQAAAAAAAQBCAOf/AQAAAAAAAQBBAOf/AQAAAAAAAQBCAOb/AQAAAAAAAQBIAOP/AQAAAAAAAQBHAOP/AQAAAAAAAQBGAOP/AQAAAAAAAQBHAOL/AQAAAAAAAQBBAN7/AQAAAAAAAQBCAN7/AQAAAAAAAQBDAN7/AQAAAAAAAQBCAN3/AQAAAAAAAQBDAOb/AQAAAAAAAQBGAOL/AQAAAAAAAQBDAN3/AQAAAAAAAQBMAOv/AQAAAAAAAQCRAOv/AQAAAAAAAQCRAOr/AQAAAAAAAQCRAOn/AQAAAAAAAQCSAOn/AQAAAAAAAQCTAOn/AQAAAAAAAQCTAOr/AQAAAAAAAQCTAOv/AQAAAAAAAQCSAOv/AQAAAAAAAQCSAOr/AQAAAAAAAQCAAOv/AQAAAAAAAQB/AOv/AQAAAAAAAQB+AOv/AQAAAAAAAQB+AOr/AQAAAAAAAQB/AOr/AQAAAAAAAQBAAP//AQAAAAAAAQBBAP//AQAAAAAAAQBCAP//AQAAAAAAAQBDAP//AQAAAAAAAQBEAP//AQAAAAAAAQClAP//AQAAAAAAAQCmAP//AQAAAAAAAQCnAP//AQAAAAAAAQCnAP7/AQAAAAAAAQClAP7/AQAAAAAAAQD4APT/AQAAAAAAAwC5AP3/AQAAAAAAAwCnACwAAQAAAAAABQChACwAAQAAAAAABgCvACwAAQAAAAAABgA2ANz/AQAAAAAABwD5APX/AQAAAAAACAA=")
tile_set = SubResource("TileSet_yf4x4")
[node name="Foreground layer" type="TileMapLayer" parent="."]
-tile_map_data = PackedByteArray("AAD9/wAAAAAEAAMAAAD+/wIAAAAEAAMAAAD+/wQAAAAEAAMAAAD//wAAAAAEAAMAAAD//wMAAAAEAAMAAAAAAAAAAAAEAAMAAAAAAAEAAAAEAAMAAAADAAAAAAAEAAMAAAAFAAAAAAAEAAMAAAAFAAEAAAAEAAMAAAAFAAIAAAAEAAMAAAAEAP3/AAAEAAMAAAALAAQAAAAEAAMAAAAcAP//AAAEAAMAAAAYAPv/AAAEAAMAAAAYAPz/AAAEAAMAAAAaAPv/AAAEAAMAAAAcAPv/AAAEAAMAAAAbAP3/AAAEAAMAAAAcAP7/AAAEAAMAAAAQAPz/AAAEAAMAAAARAPv/AAAEAAMAAAARAPz/AAAEAAMAAAAQAAIAAAAEAAMAAAAQAAQAAAAEAAMAAAAAAP7/AAAEAAQAAAAVAPn/AAAFAAQAAAD2/+v/AAAEAAMAAAD2/+z/AAAEAAMAAAD3/+r/AAAEAAMAAAD3/+v/AAAEAAMAAAD3/+7/AAAEAAMAAAD5/+r/AAAEAAMAAAD5/+3/AAAEAAMAAAD6/+z/AAAEAAMAAAD7/+r/AAAEAAMAAAD8/+r/AAAEAAMAAAD8/+v/AAAEAAMAAAD9/+v/AAAEAAMAAAD9/+z/AAAEAAMAAAD9/+3/AAAEAAMAAAD//+3/AAAEAAMAAAAAAOz/AAAEAAMAAAAAAO3/AAAEAAMAAAABAOv/AAAEAAMAAAACAOv/AAAEAAMAAAACAOz/AAAEAAMAAAACAO7/AAAEAAMAAAADAOz/AAAEAAMAAAAEAOz/AAAEAAMAAAAGAOz/AAAEAAMAAAAGAO7/AAAEAAMAAAAHAOv/AAAEAAMAAAAHAOz/AAAEAAMAAAAHAO7/AAAEAAMAAAAIAOz/AAAEAAMAAAAIAO3/AAAEAAMAAAAJAOz/AAAEAAMAAAAJAO3/AAAEAAMAAAAKAOv/AAAEAAMAAAAKAOz/AAAEAAMAAAAKAO3/AAAEAAMAAAAKAO7/AAAEAAMAAAALAO3/AAAEAAMAAAAMAO3/AAAEAAMAAAANAOv/AAAEAAMAAAANAO7/AAAEAAMAAAAPAOv/AAAEAAMAAAAQAOv/AAAEAAMAAAASAOz/AAAEAAMAAAASAO7/AAAEAAMAAAAUAOv/AAAEAAMAAAAUAO3/AAAEAAMAAAAVAO7/AAAEAAMAAAAWAOv/AAAEAAMAAAAWAO3/AAAEAAMAAAAXAOz/AAAEAAMAAAAXAO7/AAAEAAMAAAAYAO7/AAAEAAMAAAAZAOz/AAAEAAMAAAAdAO7/AAAEAAMAAAAeAO3/AAAEAAMAAAAfAOv/AAAEAAMAAAAfAOz/AAAEAAMAAAAgAO7/AAAEAAMAAAAjAOv/AAAEAAMAAAAjAO3/AAAEAAMAAAAkAOv/AAAEAAMAAAAmAOz/AAAEAAMAAAAmAO3/AAAEAAMAAAAnAO7/AAAEAAMAAAAoAOv/AAAEAAMAAAAhAO//AAAEAAMAAAAhAPD/AAAEAAMAAAAhAPL/AAAEAAMAAAAhAPT/AAAEAAMAAAAhAPX/AAAEAAMAAAAhAPf/AAAEAAMAAAAhAPn/AAAEAAMAAAAhAP3/AAAEAAMAAAAhAAAAAAAEAAMAAAAhAAEAAAAEAAMAAAAiAPD/AAAEAAMAAAAiAPH/AAAEAAMAAAAiAPP/AAAEAAMAAAAiAPf/AAAEAAMAAAAiAPj/AAAEAAMAAAAiAP//AAAEAAMAAAAiAAIAAAAEAAMAAAAiAAMAAAAEAAMAAAAjAPH/AAAEAAMAAAAjAPL/AAAEAAMAAAAjAPX/AAAEAAMAAAAjAPb/AAAEAAMAAAAjAPj/AAAEAAMAAAAjAP7/AAAEAAMAAAAkAO//AAAEAAMAAAAkAPH/AAAEAAMAAAAkAPX/AAAEAAMAAAAkAPb/AAAEAAMAAAAkAPf/AAAEAAMAAAAkAPj/AAAEAAMAAAAkAPn/AAAEAAMAAAAkAP//AAAEAAMAAAAlAPH/AAAEAAMAAAAlAPb/AAAEAAMAAAAlAPr/AAAEAAMAAAAlAPv/AAAEAAMAAAAlAP3/AAAEAAMAAAAlAAEAAAAEAAMAAAAlAAQAAAAEAAMAAAAmAPD/AAAEAAMAAAAmAPP/AAAEAAMAAAAmAPr/AAAEAAMAAAAmAPz/AAAEAAMAAAAmAP//AAAEAAMAAAAmAAIAAAAEAAMAAAD2/+//AAAEAAMAAAD2//H/AAAEAAMAAAD2//X/AAAEAAMAAAD2//b/AAAEAAMAAAD2/wEAAAAEAAMAAAD2/wMAAAAEAAMAAAD3/+//AAAEAAMAAAD3//H/AAAEAAMAAAD3//P/AAAEAAMAAAD3//7/AAAEAAMAAAD3/wAAAAAEAAMAAAD3/wIAAAAEAAMAAAD3/wMAAAAEAAMAAAD4/+//AAAEAAMAAAD4//L/AAAEAAMAAAD4//X/AAAEAAMAAAD4////AAAEAAMAAAD5//D/AAAEAAMAAAD5//P/AAAEAAMAAAD5//f/AAAEAAMAAAD5////AAAEAAMAAAD5/wAAAAAEAAMAAAD6//X/AAAEAAMAAAD6//b/AAAEAAMAAAD6////AAAEAAMAAAA=")
+tile_map_data = PackedByteArray("AAD9/wAAAAAEAAMAAAD+/wIAAAAEAAMAAAD+/wQAAAAEAAMAAAD//wAAAAAEAAMAAAD//wMAAAAEAAMAAAAAAAAAAAAEAAMAAAAAAAEAAAAEAAMAAAADAAAAAAAEAAMAAAAFAAAAAAAEAAMAAAAFAAEAAAAEAAMAAAAFAAIAAAAEAAMAAAAEAP3/AAAEAAMAAAALAAQAAAAEAAMAAAAcAP//AAAEAAMAAAAYAPv/AAAEAAMAAAAYAPz/AAAEAAMAAAAaAPv/AAAEAAMAAAAcAPv/AAAEAAMAAAAbAP3/AAAEAAMAAAAcAP7/AAAEAAMAAAAQAPz/AAAEAAMAAAARAPv/AAAEAAMAAAARAPz/AAAEAAMAAAAQAAIAAAAEAAMAAAAQAAQAAAAEAAMAAAAAAP7/AAAEAAQAAAAVAPn/AAAFAAQAAAD2/+v/AAAEAAMAAAD2/+z/AAAEAAMAAAD3/+r/AAAEAAMAAAD3/+v/AAAEAAMAAAD3/+7/AAAEAAMAAAD5/+r/AAAEAAMAAAD5/+3/AAAEAAMAAAD6/+z/AAAEAAMAAAD7/+r/AAAEAAMAAAD8/+r/AAAEAAMAAAD8/+v/AAAEAAMAAAD9/+v/AAAEAAMAAAD9/+z/AAAEAAMAAAD9/+3/AAAEAAMAAAD//+3/AAAEAAMAAAAAAOz/AAAEAAMAAAAAAO3/AAAEAAMAAAABAOv/AAAEAAMAAAACAOv/AAAEAAMAAAACAOz/AAAEAAMAAAACAO7/AAAEAAMAAAADAOz/AAAEAAMAAAAEAOz/AAAEAAMAAAAGAOz/AAAEAAMAAAAGAO7/AAAEAAMAAAAHAOv/AAAEAAMAAAAHAOz/AAAEAAMAAAAHAO7/AAAEAAMAAAAIAOz/AAAEAAMAAAAIAO3/AAAEAAMAAAAJAOz/AAAEAAMAAAAJAO3/AAAEAAMAAAAKAOv/AAAEAAMAAAAKAOz/AAAEAAMAAAAKAO3/AAAEAAMAAAAKAO7/AAAEAAMAAAALAO3/AAAEAAMAAAAMAO3/AAAEAAMAAAANAOv/AAAEAAMAAAANAO7/AAAEAAMAAAAPAOv/AAAEAAMAAAAQAOv/AAAEAAMAAAASAOz/AAAEAAMAAAASAO7/AAAEAAMAAAAUAOv/AAAEAAMAAAAUAO3/AAAEAAMAAAAVAO7/AAAEAAMAAAAWAOv/AAAEAAMAAAAWAO3/AAAEAAMAAAAXAOz/AAAEAAMAAAAXAO7/AAAEAAMAAAAYAO7/AAAEAAMAAAAZAOz/AAAEAAMAAAAdAO7/AAAEAAMAAAAeAO3/AAAEAAMAAAAfAOv/AAAEAAMAAAAfAOz/AAAEAAMAAAAgAO7/AAAEAAMAAAAjAOv/AAAEAAMAAAAjAO3/AAAEAAMAAAAkAOv/AAAEAAMAAAAmAOz/AAAEAAMAAAAmAO3/AAAEAAMAAAAnAO7/AAAEAAMAAAAoAOv/AAAEAAMAAAAhAO//AAAEAAMAAAAhAPD/AAAEAAMAAAAhAPL/AAAEAAMAAAAhAPT/AAAEAAMAAAAhAPX/AAAEAAMAAAAhAPf/AAAEAAMAAAAhAPn/AAAEAAMAAAAhAP3/AAAEAAMAAAAhAAAAAAAEAAMAAAAhAAEAAAAEAAMAAAAiAPD/AAAEAAMAAAAiAPH/AAAEAAMAAAAiAPP/AAAEAAMAAAAiAPf/AAAEAAMAAAAiAPj/AAAEAAMAAAAiAP//AAAEAAMAAAAiAAIAAAAEAAMAAAAiAAMAAAAEAAMAAAAjAPH/AAAEAAMAAAAjAPL/AAAEAAMAAAAjAPX/AAAEAAMAAAAjAPb/AAAEAAMAAAAjAPj/AAAEAAMAAAAjAP7/AAAEAAMAAAAkAO//AAAEAAMAAAAkAPH/AAAEAAMAAAAkAPX/AAAEAAMAAAAkAPb/AAAEAAMAAAAkAPf/AAAEAAMAAAAkAPj/AAAEAAMAAAAkAPn/AAAEAAMAAAAkAP//AAAEAAMAAAAlAPH/AAAEAAMAAAAlAPb/AAAEAAMAAAAlAPr/AAAEAAMAAAAlAPv/AAAEAAMAAAAlAP3/AAAEAAMAAAAlAAEAAAAEAAMAAAAlAAQAAAAEAAMAAAAmAPD/AAAEAAMAAAAmAPP/AAAEAAMAAAAmAPr/AAAEAAMAAAAmAPz/AAAEAAMAAAAmAP//AAAEAAMAAAAmAAIAAAAEAAMAAAD2/+//AAAEAAMAAAD2//H/AAAEAAMAAAD2//X/AAAEAAMAAAD2//b/AAAEAAMAAAD2/wEAAAAEAAMAAAD2/wMAAAAEAAMAAAD3/+//AAAEAAMAAAD3//H/AAAEAAMAAAD3//P/AAAEAAMAAAD3//7/AAAEAAMAAAD3/wAAAAAEAAMAAAD3/wIAAAAEAAMAAAD3/wMAAAAEAAMAAAD4/+//AAAEAAMAAAD4//L/AAAEAAMAAAD4//X/AAAEAAMAAAD4////AAAEAAMAAAD5//D/AAAEAAMAAAD5//P/AAAEAAMAAAD5//f/AAAEAAMAAAD5////AAAEAAMAAAD5/wAAAAAEAAMAAAD6//X/AAAEAAMAAAD6//b/AAAEAAMAAAD6////AAAEAAMAAABiAP3/AAAFAAQAAAA=")
tile_set = SubResource("TileSet_r3m5w")
[node name="Small Foreground layer" type="TileMapLayer" parent="."]
@@ -424,16 +436,16 @@ tile_map_data = PackedByteArray("AAD7//3/AQABAAAAAAD8//3/AQAAAAAAAAD+//3/AQABAAA
tile_set = SubResource("TileSet_fpn3g")
[node name="Brick Player" parent="." instance=ExtResource("4_hetw8")]
-position = Vector2(219, 0)
+position = Vector2(929, -123)
metadata/_edit_group_ = true
[node name="Camera2D" type="Camera2D" parent="."]
physics_interpolation_mode = 1
-position = Vector2(219, -84)
+position = Vector2(929, -123)
limit_left = -176
-limit_top = -368
-limit_right = 672
-limit_bottom = 96
+limit_top = -896
+limit_right = 4128
+limit_bottom = 800
[node name="PhantomCameraHost" type="Node" parent="Camera2D"]
process_priority = 300
@@ -446,6 +458,21 @@ position = Vector2(484, -112)
[node name="Enemy2" parent="." instance=ExtResource("7_qgddg")]
position = Vector2(-14, -34)
+[node name="Enemy3" parent="." instance=ExtResource("7_qgddg")]
+position = Vector2(1433, -224)
+
+[node name="Enemy4" parent="." instance=ExtResource("7_qgddg")]
+position = Vector2(2211, -62)
+
+[node name="Enemy5" parent="." instance=ExtResource("7_qgddg")]
+position = Vector2(3262, -30)
+
+[node name="Enemy6" parent="." instance=ExtResource("7_qgddg")]
+position = Vector2(3710, 2)
+
+[node name="Enemy7" parent="." instance=ExtResource("7_qgddg")]
+position = Vector2(3945, 34)
+
[node name="Bullet" parent="." instance=ExtResource("8_c68mx")]
position = Vector2(68, -89)
@@ -472,7 +499,7 @@ position = Vector2(275, 9)
[node name="PhantomCamera2D" type="Node2D" parent="." node_paths=PackedStringArray("follow_target")]
top_level = true
-position = Vector2(219, -84)
+position = Vector2(929, -123)
script = ExtResource("13_rsy5s")
follow_mode = 1
follow_target = NodePath("../Brick Player")
diff --git a/scripts/components/cage_component.gd b/scripts/components/cage_component.gd
new file mode 100644
index 0000000..e58de2c
--- /dev/null
+++ b/scripts/components/cage_component.gd
@@ -0,0 +1,40 @@
+class_name CageComponent
+extends Node
+
+@export var lever: LeverComponent
+@export var root: Node2D
+@export var move_value: Vector2 = Vector2(0, -100)
+@export var tween_duration: float = 0.5
+@export var should_free: bool = true
+
+
+func _ready() -> void:
+ await get_tree().process_frame
+ if not lever:
+ var levers_nodes := get_tree().get_nodes_in_group("levers")
+ print("Found levers: ", levers_nodes)
+ for lever_node in levers_nodes:
+ var lever_component: LeverComponent = lever_node.get_node_or_null("LeverComponent")
+ print("Lever component: ", lever_component)
+ if lever_component:
+ lever_component.activated.connect(on_lever_activated)
+ else:
+ lever.activated.connect(on_lever_activated)
+
+ if not root:
+ printerr("CageComponent: root is not set.")
+ return
+
+
+func on_lever_activated() -> void:
+ print("Lever activated, moving cage.")
+ var tween: Tween = create_tween()
+ var end_position: Vector2 = root.position + move_value
+ tween.tween_property(root, "position", end_position, tween_duration)
+ tween.tween_callback(_on_tween_completed)
+
+
+func _on_tween_completed() -> void:
+ if not should_free:
+ return
+ root.queue_free()
\ No newline at end of file
diff --git a/scripts/components/can_be_launched_component.gd b/scripts/components/can_be_launched_component.gd
new file mode 100644
index 0000000..5675e50
--- /dev/null
+++ b/scripts/components/can_be_launched_component.gd
@@ -0,0 +1,2 @@
+class_name CanBeLaunchedComponent
+extends Node
\ No newline at end of file
diff --git a/scripts/components/collectable.gd b/scripts/components/collectable.gd
index 3e9837c..a5400f8 100644
--- a/scripts/components/collectable.gd
+++ b/scripts/components/collectable.gd
@@ -6,9 +6,9 @@ var has_fade_away: bool = false
@export var area2d: Area2D
@export var collectable_data: CollectableResource
-
signal collected(amount: int)
+
func _ready() -> void:
if area2d:
area2d.body_entered.connect(_on_area2d_body_entered)
@@ -20,6 +20,7 @@ func _ready() -> void:
if root.has_node("FadeAwayComponent"):
has_fade_away = true
+
func _on_area2d_body_entered(body: Node2D) -> void:
if body.has_node("CanPickUpComponent"):
collected.emit(collectable_data.amount)
diff --git a/scripts/components/jump_pad_component.gd b/scripts/components/jump_pad_component.gd
new file mode 100644
index 0000000..f343ec1
--- /dev/null
+++ b/scripts/components/jump_pad_component.gd
@@ -0,0 +1,40 @@
+class_name JumpPadComponent
+extends Node
+
+@export var jump_force: float = 10.0
+@export var area2d: Area2D
+@export var sprite2d: Sprite2D
+@export var start_animation_index: int = 0
+@export var animation_duration: float = 0.5
+
+
+func _ready() -> void:
+ if not area2d:
+ printerr("JumpPadComponent: area2d is not set.")
+ return
+
+ if not sprite2d:
+ printerr("JumpPadComponent: sprite2d is not set.")
+ return
+
+ area2d.body_entered.connect(_on_body_entered)
+
+
+func _on_body_entered(body: Node2D) -> void:
+ var can_be_launched: CanBeLaunchedComponent = body.get_node_or_null("CanBeLaunchedComponent")
+ if not can_be_launched:
+ return
+
+ if body is PlayerController:
+ handle_launchpad_animation()
+ body.velocity.y = -jump_force
+
+
+func handle_launchpad_animation() -> void:
+ if not sprite2d:
+ return
+
+ var timer := get_tree().create_timer(animation_duration)
+ sprite2d.frame = start_animation_index + 1
+ await timer.timeout
+ sprite2d.frame = start_animation_index
\ No newline at end of file
diff --git a/scripts/components/lever_component.gd b/scripts/components/lever_component.gd
new file mode 100644
index 0000000..d4a8238
--- /dev/null
+++ b/scripts/components/lever_component.gd
@@ -0,0 +1,37 @@
+class_name LeverComponent
+extends Node
+
+@export var area2d: Area2D
+@export var sprite2d: Sprite2D
+@export var start_animation_index: int = 0
+@export var animation_duration: float = 0.5
+signal activated
+
+
+func _ready() -> void:
+ if not area2d:
+ printerr("LeverComponent: area2d is not set.")
+ return
+
+ if not sprite2d:
+ printerr("LeverComponent: sprite2d is not set.")
+ return
+
+ area2d.body_entered.connect(_on_body_entered)
+
+
+func _on_body_entered(body: Node2D) -> void:
+ var trigger_lever: TriggerLeverComponent = body.get_node_or_null("TriggerLeverComponent")
+ if not trigger_lever:
+ return
+
+ activate()
+
+
+func activate() -> void:
+ print("Lever activated.")
+ activated.emit()
+ sprite2d.frame = start_animation_index + 1
+ var timer := get_tree().create_timer(animation_duration)
+ await timer.timeout
+ sprite2d.frame = start_animation_index
diff --git a/scripts/components/trigger_lever_component.gd b/scripts/components/trigger_lever_component.gd
new file mode 100644
index 0000000..1755cce
--- /dev/null
+++ b/scripts/components/trigger_lever_component.gd
@@ -0,0 +1,2 @@
+class_name TriggerLeverComponent
+extends Node
\ No newline at end of file
diff --git a/sprites/grass_tile.svg b/sprites/grass_tile.svg
index 3269780..44ccb27 100644
--- a/sprites/grass_tile.svg
+++ b/sprites/grass_tile.svg
@@ -27,8 +27,8 @@
inkscape:deskcolor="#505050"
inkscape:document-units="px"
inkscape:zoom="4.1513748"
- inkscape:cx="93.703898"
- inkscape:cy="34.566862"
+ inkscape:cx="39.986753"
+ inkscape:cy="96.474064"
inkscape:window-width="2560"
inkscape:window-height="1374"
inkscape:window-x="0"
@@ -99,7 +99,7 @@
diff --git a/sprites/ppc_tileset.png b/sprites/ppc_tileset.png
index 1871edc..ad5d818 100644
Binary files a/sprites/ppc_tileset.png and b/sprites/ppc_tileset.png differ
diff --git a/tileset/sliced/sliced.png b/tileset/sliced/sliced.png
new file mode 100644
index 0000000..fa15c2c
Binary files /dev/null and b/tileset/sliced/sliced.png differ
diff --git a/tileset/sliced/sliced.png.import b/tileset/sliced/sliced.png.import
new file mode 100644
index 0000000..2a68078
--- /dev/null
+++ b/tileset/sliced/sliced.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://i1ebu6ws4clo"
+path="res://.godot/imported/sliced.png-fbb3d940c33e37860c896cb6211fcbe0.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/sliced/sliced.png"
+dest_files=["res://.godot/imported/sliced.png-fbb3d940c33e37860c896cb6211fcbe0.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/sliced/sliced2.png b/tileset/sliced/sliced2.png
new file mode 100644
index 0000000..49a1ac3
Binary files /dev/null and b/tileset/sliced/sliced2.png differ
diff --git a/tileset/sliced/sliced2.png.import b/tileset/sliced/sliced2.png.import
new file mode 100644
index 0000000..79f8a78
--- /dev/null
+++ b/tileset/sliced/sliced2.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dgu78i8vnkyjr"
+path="res://.godot/imported/sliced2.png-f9aca6902388c75f74bd82de896e7917.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/sliced/sliced2.png"
+dest_files=["res://.godot/imported/sliced2.png-f9aca6902388c75f74bd82de896e7917.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/sliced/sliced3.png b/tileset/sliced/sliced3.png
new file mode 100644
index 0000000..932393d
Binary files /dev/null and b/tileset/sliced/sliced3.png differ
diff --git a/tileset/sliced/sliced3.png.import b/tileset/sliced/sliced3.png.import
new file mode 100644
index 0000000..d2b6dd6
--- /dev/null
+++ b/tileset/sliced/sliced3.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bcwsonxig6m3h"
+path="res://.godot/imported/sliced3.png-008c941d9c67ba4be39791e9515f443f.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/sliced/sliced3.png"
+dest_files=["res://.godot/imported/sliced3.png-008c941d9c67ba4be39791e9515f443f.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/sliced/sliced4.png b/tileset/sliced/sliced4.png
new file mode 100644
index 0000000..a89092d
Binary files /dev/null and b/tileset/sliced/sliced4.png differ
diff --git a/tileset/sliced/sliced4.png.import b/tileset/sliced/sliced4.png.import
new file mode 100644
index 0000000..0815c5a
--- /dev/null
+++ b/tileset/sliced/sliced4.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://5csvqfu5cbwm"
+path="res://.godot/imported/sliced4.png-6c8263bf14dc391359ad65783cf1ade5.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/sliced/sliced4.png"
+dest_files=["res://.godot/imported/sliced4.png-6c8263bf14dc391359ad65783cf1ade5.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/sliced/sliced5.png b/tileset/sliced/sliced5.png
new file mode 100644
index 0000000..64eb457
Binary files /dev/null and b/tileset/sliced/sliced5.png differ
diff --git a/tileset/sliced/sliced5.png.import b/tileset/sliced/sliced5.png.import
new file mode 100644
index 0000000..d205ed5
--- /dev/null
+++ b/tileset/sliced/sliced5.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dnott5iefpg8d"
+path="res://.godot/imported/sliced5.png-da25a6508463f9d334b37e9861bb3b03.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/sliced/sliced5.png"
+dest_files=["res://.godot/imported/sliced5.png-da25a6508463f9d334b37e9861bb3b03.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/sliced/sliced6.png b/tileset/sliced/sliced6.png
new file mode 100644
index 0000000..f99d22c
Binary files /dev/null and b/tileset/sliced/sliced6.png differ
diff --git a/tileset/sliced/sliced6.png.import b/tileset/sliced/sliced6.png.import
new file mode 100644
index 0000000..c530ce2
--- /dev/null
+++ b/tileset/sliced/sliced6.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cddhdlrkynekw"
+path="res://.godot/imported/sliced6.png-cd0b0c96ff6ce9ca1f54ef087081285f.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/sliced/sliced6.png"
+dest_files=["res://.godot/imported/sliced6.png-cd0b0c96ff6ce9ca1f54ef087081285f.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/sliced/sliced7.png b/tileset/sliced/sliced7.png
new file mode 100644
index 0000000..e0c6d44
Binary files /dev/null and b/tileset/sliced/sliced7.png differ
diff --git a/tileset/sliced/sliced7.png.import b/tileset/sliced/sliced7.png.import
new file mode 100644
index 0000000..d6e2de3
--- /dev/null
+++ b/tileset/sliced/sliced7.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bk2cnj2adb8ir"
+path="res://.godot/imported/sliced7.png-8c06af759412916ffcd8e6dcfb8274e8.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/sliced/sliced7.png"
+dest_files=["res://.godot/imported/sliced7.png-8c06af759412916ffcd8e6dcfb8274e8.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/sliced/sliced8.png b/tileset/sliced/sliced8.png
new file mode 100644
index 0000000..68e90f9
Binary files /dev/null and b/tileset/sliced/sliced8.png differ
diff --git a/tileset/sliced/sliced8.png.import b/tileset/sliced/sliced8.png.import
new file mode 100644
index 0000000..029d0e4
--- /dev/null
+++ b/tileset/sliced/sliced8.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cghxi204ajn0l"
+path="res://.godot/imported/sliced8.png-12803fb1057c2a107058d028827507ef.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/sliced/sliced8.png"
+dest_files=["res://.godot/imported/sliced8.png-12803fb1057c2a107058d028827507ef.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/sliced/sprite.png b/tileset/sliced/sprite.png
new file mode 100644
index 0000000..93849cb
Binary files /dev/null and b/tileset/sliced/sprite.png differ
diff --git a/tileset/sliced/sprite.png.import b/tileset/sliced/sprite.png.import
new file mode 100644
index 0000000..da3ad16
--- /dev/null
+++ b/tileset/sliced/sprite.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://fblpdrogbn58"
+path="res://.godot/imported/sprite.png-90e20dc272152895adf57a2d0a502e9c.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/sliced/sprite.png"
+dest_files=["res://.godot/imported/sprite.png-90e20dc272152895adf57a2d0a502e9c.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/terrain-Sheet1.png b/tileset/terrain-Sheet1.png
new file mode 100644
index 0000000..471bac1
Binary files /dev/null and b/tileset/terrain-Sheet1.png differ
diff --git a/tileset/terrain-Sheet1.png.import b/tileset/terrain-Sheet1.png.import
new file mode 100644
index 0000000..3996aa3
--- /dev/null
+++ b/tileset/terrain-Sheet1.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://d2cp4rb5bq2pf"
+path="res://.godot/imported/terrain-Sheet1.png-153aac28b0ec8563218b8be3a176dc88.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/terrain-Sheet1.png"
+dest_files=["res://.godot/imported/terrain-Sheet1.png-153aac28b0ec8563218b8be3a176dc88.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/terrain-Sheet2.png b/tileset/terrain-Sheet2.png
new file mode 100644
index 0000000..3ec92f6
Binary files /dev/null and b/tileset/terrain-Sheet2.png differ
diff --git a/tileset/terrain-Sheet2.png.import b/tileset/terrain-Sheet2.png.import
new file mode 100644
index 0000000..e308f88
--- /dev/null
+++ b/tileset/terrain-Sheet2.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://tkw0hrir4315"
+path="res://.godot/imported/terrain-Sheet2.png-ed4f74c168ad9a61e074cf9e8ae3cff7.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/terrain-Sheet2.png"
+dest_files=["res://.godot/imported/terrain-Sheet2.png-ed4f74c168ad9a61e074cf9e8ae3cff7.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/terrain-Sheet3.png b/tileset/terrain-Sheet3.png
new file mode 100644
index 0000000..b18e111
Binary files /dev/null and b/tileset/terrain-Sheet3.png differ
diff --git a/tileset/terrain-Sheet3.png.import b/tileset/terrain-Sheet3.png.import
new file mode 100644
index 0000000..110fe15
--- /dev/null
+++ b/tileset/terrain-Sheet3.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cpdy62eitch3r"
+path="res://.godot/imported/terrain-Sheet3.png-1cb172ff0c6de1cc12dea5142efa3a5c.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/terrain-Sheet3.png"
+dest_files=["res://.godot/imported/terrain-Sheet3.png-1cb172ff0c6de1cc12dea5142efa3a5c.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/terrain-Sheet4.png b/tileset/terrain-Sheet4.png
new file mode 100644
index 0000000..7de70a2
Binary files /dev/null and b/tileset/terrain-Sheet4.png differ
diff --git a/tileset/terrain-Sheet4.png.import b/tileset/terrain-Sheet4.png.import
new file mode 100644
index 0000000..b279c1f
--- /dev/null
+++ b/tileset/terrain-Sheet4.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dl02vtudrlymd"
+path="res://.godot/imported/terrain-Sheet4.png-0ed282c1b3adccbffd4ce65678bcb14b.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/terrain-Sheet4.png"
+dest_files=["res://.godot/imported/terrain-Sheet4.png-0ed282c1b3adccbffd4ce65678bcb14b.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/terrain-Sheet5.png b/tileset/terrain-Sheet5.png
new file mode 100644
index 0000000..b08ee75
Binary files /dev/null and b/tileset/terrain-Sheet5.png differ
diff --git a/tileset/terrain-Sheet5.png.import b/tileset/terrain-Sheet5.png.import
new file mode 100644
index 0000000..ff7fe19
--- /dev/null
+++ b/tileset/terrain-Sheet5.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://di3vjxoqv0j1s"
+path="res://.godot/imported/terrain-Sheet5.png-78baff9808f8e8be4780e1c7bb130aa3.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/terrain-Sheet5.png"
+dest_files=["res://.godot/imported/terrain-Sheet5.png-78baff9808f8e8be4780e1c7bb130aa3.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/terrain-Sheet6.png b/tileset/terrain-Sheet6.png
new file mode 100644
index 0000000..effedfe
Binary files /dev/null and b/tileset/terrain-Sheet6.png differ
diff --git a/tileset/terrain-Sheet6.png.import b/tileset/terrain-Sheet6.png.import
new file mode 100644
index 0000000..35dfca8
--- /dev/null
+++ b/tileset/terrain-Sheet6.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b0b7kpqh13j84"
+path="res://.godot/imported/terrain-Sheet6.png-fadb29960c769391175fb882f02aceb5.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/terrain-Sheet6.png"
+dest_files=["res://.godot/imported/terrain-Sheet6.png-fadb29960c769391175fb882f02aceb5.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/terrain-Sheet7.png b/tileset/terrain-Sheet7.png
new file mode 100644
index 0000000..88e4a2c
Binary files /dev/null and b/tileset/terrain-Sheet7.png differ
diff --git a/tileset/terrain-Sheet7.png.import b/tileset/terrain-Sheet7.png.import
new file mode 100644
index 0000000..de69cc8
--- /dev/null
+++ b/tileset/terrain-Sheet7.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bgqp8ag0p4ag3"
+path="res://.godot/imported/terrain-Sheet7.png-8328b0359058f1c7dec40851f18c5e69.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/terrain-Sheet7.png"
+dest_files=["res://.godot/imported/terrain-Sheet7.png-8328b0359058f1c7dec40851f18c5e69.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/terrain-Sheet8.png b/tileset/terrain-Sheet8.png
new file mode 100644
index 0000000..cc9273e
Binary files /dev/null and b/tileset/terrain-Sheet8.png differ
diff --git a/tileset/terrain-Sheet8.png.import b/tileset/terrain-Sheet8.png.import
new file mode 100644
index 0000000..c3868fc
--- /dev/null
+++ b/tileset/terrain-Sheet8.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bm7uha0ava07u"
+path="res://.godot/imported/terrain-Sheet8.png-97362eb348067439aede9157be7fe901.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/terrain-Sheet8.png"
+dest_files=["res://.godot/imported/terrain-Sheet8.png-97362eb348067439aede9157be7fe901.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/terrain-Sheet9.png b/tileset/terrain-Sheet9.png
new file mode 100644
index 0000000..0218fdb
Binary files /dev/null and b/tileset/terrain-Sheet9.png differ
diff --git a/tileset/terrain-Sheet9.png.import b/tileset/terrain-Sheet9.png.import
new file mode 100644
index 0000000..e525b0d
--- /dev/null
+++ b/tileset/terrain-Sheet9.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bfcnejq0sbglg"
+path="res://.godot/imported/terrain-Sheet9.png-02938eb0b522959794bfbc79ac67ff57.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/terrain-Sheet9.png"
+dest_files=["res://.godot/imported/terrain-Sheet9.png-02938eb0b522959794bfbc79ac67ff57.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village1.png b/tileset/village/village1.png
new file mode 100644
index 0000000..03fd359
Binary files /dev/null and b/tileset/village/village1.png differ
diff --git a/tileset/village/village1.png.import b/tileset/village/village1.png.import
new file mode 100644
index 0000000..7d922ca
--- /dev/null
+++ b/tileset/village/village1.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dnwye5wqyiq8w"
+path="res://.godot/imported/village1.png-6fc7332931f56bd139f6788979dce173.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village1.png"
+dest_files=["res://.godot/imported/village1.png-6fc7332931f56bd139f6788979dce173.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village10.png b/tileset/village/village10.png
new file mode 100644
index 0000000..1b530cc
Binary files /dev/null and b/tileset/village/village10.png differ
diff --git a/tileset/village/village10.png.import b/tileset/village/village10.png.import
new file mode 100644
index 0000000..8f53660
--- /dev/null
+++ b/tileset/village/village10.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b6nxngt1yhmhx"
+path="res://.godot/imported/village10.png-2f670c9fdc8e1f00e37da24288237cfe.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village10.png"
+dest_files=["res://.godot/imported/village10.png-2f670c9fdc8e1f00e37da24288237cfe.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village11.png b/tileset/village/village11.png
new file mode 100644
index 0000000..7aaaa07
Binary files /dev/null and b/tileset/village/village11.png differ
diff --git a/tileset/village/village11.png.import b/tileset/village/village11.png.import
new file mode 100644
index 0000000..f9f9b13
--- /dev/null
+++ b/tileset/village/village11.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dcqbtc0xwkrg5"
+path="res://.godot/imported/village11.png-a4cdc24226728a1433e33c0fc53cb6da.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village11.png"
+dest_files=["res://.godot/imported/village11.png-a4cdc24226728a1433e33c0fc53cb6da.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village13.png b/tileset/village/village13.png
new file mode 100644
index 0000000..526c14a
Binary files /dev/null and b/tileset/village/village13.png differ
diff --git a/tileset/village/village13.png.import b/tileset/village/village13.png.import
new file mode 100644
index 0000000..2114528
--- /dev/null
+++ b/tileset/village/village13.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c5ba7qng80srr"
+path="res://.godot/imported/village13.png-f44c2768d2158e8c72ddcf3fa94d0b99.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village13.png"
+dest_files=["res://.godot/imported/village13.png-f44c2768d2158e8c72ddcf3fa94d0b99.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village14.png b/tileset/village/village14.png
new file mode 100644
index 0000000..da0fcce
Binary files /dev/null and b/tileset/village/village14.png differ
diff --git a/tileset/village/village14.png.import b/tileset/village/village14.png.import
new file mode 100644
index 0000000..5bb930f
--- /dev/null
+++ b/tileset/village/village14.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://nd2y4fb8hjrv"
+path="res://.godot/imported/village14.png-615d02be3e100b96cef6e31a3a56e975.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village14.png"
+dest_files=["res://.godot/imported/village14.png-615d02be3e100b96cef6e31a3a56e975.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village15.png b/tileset/village/village15.png
new file mode 100644
index 0000000..7d5f902
Binary files /dev/null and b/tileset/village/village15.png differ
diff --git a/tileset/village/village15.png.import b/tileset/village/village15.png.import
new file mode 100644
index 0000000..a5c4dd8
--- /dev/null
+++ b/tileset/village/village15.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://vfxm2rc5ener"
+path="res://.godot/imported/village15.png-b4edf98e7bf73422a0fdc757d95f1468.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village15.png"
+dest_files=["res://.godot/imported/village15.png-b4edf98e7bf73422a0fdc757d95f1468.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village16.png b/tileset/village/village16.png
new file mode 100644
index 0000000..2602a09
Binary files /dev/null and b/tileset/village/village16.png differ
diff --git a/tileset/village/village16.png.import b/tileset/village/village16.png.import
new file mode 100644
index 0000000..a4f25a5
--- /dev/null
+++ b/tileset/village/village16.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://htcxc0328j5l"
+path="res://.godot/imported/village16.png-f87e78391e4062f142d743326f6e8456.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village16.png"
+dest_files=["res://.godot/imported/village16.png-f87e78391e4062f142d743326f6e8456.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village17.png b/tileset/village/village17.png
new file mode 100644
index 0000000..6d02f4a
Binary files /dev/null and b/tileset/village/village17.png differ
diff --git a/tileset/village/village17.png.import b/tileset/village/village17.png.import
new file mode 100644
index 0000000..5a5470d
--- /dev/null
+++ b/tileset/village/village17.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dfjacgswyysl6"
+path="res://.godot/imported/village17.png-b3d3463447388e82d12ee335d13f4ca2.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village17.png"
+dest_files=["res://.godot/imported/village17.png-b3d3463447388e82d12ee335d13f4ca2.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village18.png b/tileset/village/village18.png
new file mode 100644
index 0000000..77a422f
Binary files /dev/null and b/tileset/village/village18.png differ
diff --git a/tileset/village/village18.png.import b/tileset/village/village18.png.import
new file mode 100644
index 0000000..175208d
--- /dev/null
+++ b/tileset/village/village18.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dj8cfrbg6mtso"
+path="res://.godot/imported/village18.png-f1f9344b459f38c9bde27b3c8e545f7b.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village18.png"
+dest_files=["res://.godot/imported/village18.png-f1f9344b459f38c9bde27b3c8e545f7b.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village19.png b/tileset/village/village19.png
new file mode 100644
index 0000000..b74a812
Binary files /dev/null and b/tileset/village/village19.png differ
diff --git a/tileset/village/village19.png.import b/tileset/village/village19.png.import
new file mode 100644
index 0000000..989f4af
--- /dev/null
+++ b/tileset/village/village19.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dltrqo2aid0se"
+path="res://.godot/imported/village19.png-e0382c3c2804f5344553fd4558a9975a.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village19.png"
+dest_files=["res://.godot/imported/village19.png-e0382c3c2804f5344553fd4558a9975a.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village2.png b/tileset/village/village2.png
new file mode 100644
index 0000000..03fd359
Binary files /dev/null and b/tileset/village/village2.png differ
diff --git a/tileset/village/village2.png.import b/tileset/village/village2.png.import
new file mode 100644
index 0000000..67dca00
--- /dev/null
+++ b/tileset/village/village2.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://8e7jgldhg6n2"
+path="res://.godot/imported/village2.png-994d9a2b6bd64f3f3b13c61029faf7bb.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village2.png"
+dest_files=["res://.godot/imported/village2.png-994d9a2b6bd64f3f3b13c61029faf7bb.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village20.png b/tileset/village/village20.png
new file mode 100644
index 0000000..4891e90
Binary files /dev/null and b/tileset/village/village20.png differ
diff --git a/tileset/village/village20.png.import b/tileset/village/village20.png.import
new file mode 100644
index 0000000..5ef0dc8
--- /dev/null
+++ b/tileset/village/village20.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bim26wlc0d1od"
+path="res://.godot/imported/village20.png-de4e3f05f88f0bf7e9a8b51b28d234f9.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village20.png"
+dest_files=["res://.godot/imported/village20.png-de4e3f05f88f0bf7e9a8b51b28d234f9.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village21.png b/tileset/village/village21.png
new file mode 100644
index 0000000..7b13e24
Binary files /dev/null and b/tileset/village/village21.png differ
diff --git a/tileset/village/village21.png.import b/tileset/village/village21.png.import
new file mode 100644
index 0000000..098d48f
--- /dev/null
+++ b/tileset/village/village21.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b5hgwffx1b4sx"
+path="res://.godot/imported/village21.png-eafc8ee81ded67c2dd67d7a48a0b03fc.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village21.png"
+dest_files=["res://.godot/imported/village21.png-eafc8ee81ded67c2dd67d7a48a0b03fc.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village22.png b/tileset/village/village22.png
new file mode 100644
index 0000000..3cffdb9
Binary files /dev/null and b/tileset/village/village22.png differ
diff --git a/tileset/village/village22.png.import b/tileset/village/village22.png.import
new file mode 100644
index 0000000..6e38d66
--- /dev/null
+++ b/tileset/village/village22.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dsp4b0g2wgrct"
+path="res://.godot/imported/village22.png-d81f5e919eb215455f4b6411c9d487b3.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village22.png"
+dest_files=["res://.godot/imported/village22.png-d81f5e919eb215455f4b6411c9d487b3.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village24.png b/tileset/village/village24.png
new file mode 100644
index 0000000..21e944a
Binary files /dev/null and b/tileset/village/village24.png differ
diff --git a/tileset/village/village24.png.import b/tileset/village/village24.png.import
new file mode 100644
index 0000000..1f8a794
--- /dev/null
+++ b/tileset/village/village24.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bxnq01t7w1bwd"
+path="res://.godot/imported/village24.png-56c289f98beb64e9846a4add5dd42dd8.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village24.png"
+dest_files=["res://.godot/imported/village24.png-56c289f98beb64e9846a4add5dd42dd8.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village25.png b/tileset/village/village25.png
new file mode 100644
index 0000000..aaaa4af
Binary files /dev/null and b/tileset/village/village25.png differ
diff --git a/tileset/village/village25.png.import b/tileset/village/village25.png.import
new file mode 100644
index 0000000..edde195
--- /dev/null
+++ b/tileset/village/village25.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://4ih7q5xo22y5"
+path="res://.godot/imported/village25.png-28951465a0e967d5da167af1cf38e73d.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village25.png"
+dest_files=["res://.godot/imported/village25.png-28951465a0e967d5da167af1cf38e73d.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village26.png b/tileset/village/village26.png
new file mode 100644
index 0000000..33a2fce
Binary files /dev/null and b/tileset/village/village26.png differ
diff --git a/tileset/village/village26.png.import b/tileset/village/village26.png.import
new file mode 100644
index 0000000..798e503
--- /dev/null
+++ b/tileset/village/village26.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://eb4ct5m3b885"
+path="res://.godot/imported/village26.png-5329b7526936f43864d1b2d0cf51c323.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village26.png"
+dest_files=["res://.godot/imported/village26.png-5329b7526936f43864d1b2d0cf51c323.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village27.png b/tileset/village/village27.png
new file mode 100644
index 0000000..8f80a10
Binary files /dev/null and b/tileset/village/village27.png differ
diff --git a/tileset/village/village27.png.import b/tileset/village/village27.png.import
new file mode 100644
index 0000000..e9593ea
--- /dev/null
+++ b/tileset/village/village27.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://hlkicd0fsxvf"
+path="res://.godot/imported/village27.png-7cdaa6049f7a99a461d3b8a08ee4b7f7.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village27.png"
+dest_files=["res://.godot/imported/village27.png-7cdaa6049f7a99a461d3b8a08ee4b7f7.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village28.png b/tileset/village/village28.png
new file mode 100644
index 0000000..ed5b2e7
Binary files /dev/null and b/tileset/village/village28.png differ
diff --git a/tileset/village/village28.png.import b/tileset/village/village28.png.import
new file mode 100644
index 0000000..3cb790a
--- /dev/null
+++ b/tileset/village/village28.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cda0mqxultiga"
+path="res://.godot/imported/village28.png-263581e88cbbf65b45f82e2271d14aa2.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village28.png"
+dest_files=["res://.godot/imported/village28.png-263581e88cbbf65b45f82e2271d14aa2.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village29.png b/tileset/village/village29.png
new file mode 100644
index 0000000..f65f534
Binary files /dev/null and b/tileset/village/village29.png differ
diff --git a/tileset/village/village29.png.import b/tileset/village/village29.png.import
new file mode 100644
index 0000000..1fdbc1f
--- /dev/null
+++ b/tileset/village/village29.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dvh5ea4b2o8xg"
+path="res://.godot/imported/village29.png-5da56f2606412fa0f4441e1b41c1dab5.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village29.png"
+dest_files=["res://.godot/imported/village29.png-5da56f2606412fa0f4441e1b41c1dab5.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village3.png b/tileset/village/village3.png
new file mode 100644
index 0000000..aba82d2
Binary files /dev/null and b/tileset/village/village3.png differ
diff --git a/tileset/village/village3.png.import b/tileset/village/village3.png.import
new file mode 100644
index 0000000..a216c43
--- /dev/null
+++ b/tileset/village/village3.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c4lr4wydvjhsy"
+path="res://.godot/imported/village3.png-13e88dcc16d82088c7e22e23f295518c.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village3.png"
+dest_files=["res://.godot/imported/village3.png-13e88dcc16d82088c7e22e23f295518c.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village30.png b/tileset/village/village30.png
new file mode 100644
index 0000000..f370708
Binary files /dev/null and b/tileset/village/village30.png differ
diff --git a/tileset/village/village30.png.import b/tileset/village/village30.png.import
new file mode 100644
index 0000000..51ad58e
--- /dev/null
+++ b/tileset/village/village30.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://byqhcxr55ats0"
+path="res://.godot/imported/village30.png-1642a5c62f3675e49dfdda48b6a22065.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village30.png"
+dest_files=["res://.godot/imported/village30.png-1642a5c62f3675e49dfdda48b6a22065.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village31.png b/tileset/village/village31.png
new file mode 100644
index 0000000..7ffec78
Binary files /dev/null and b/tileset/village/village31.png differ
diff --git a/tileset/village/village31.png.import b/tileset/village/village31.png.import
new file mode 100644
index 0000000..bbf8f74
--- /dev/null
+++ b/tileset/village/village31.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://o1bum524qlra"
+path="res://.godot/imported/village31.png-fcc231cbd019a7227df2f069d4241c7d.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village31.png"
+dest_files=["res://.godot/imported/village31.png-fcc231cbd019a7227df2f069d4241c7d.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village32.png b/tileset/village/village32.png
new file mode 100644
index 0000000..26a15a9
Binary files /dev/null and b/tileset/village/village32.png differ
diff --git a/tileset/village/village32.png.import b/tileset/village/village32.png.import
new file mode 100644
index 0000000..85cec1b
--- /dev/null
+++ b/tileset/village/village32.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://jlh84rtme5sn"
+path="res://.godot/imported/village32.png-dbbfc422df305f44c8b26d66e9589935.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village32.png"
+dest_files=["res://.godot/imported/village32.png-dbbfc422df305f44c8b26d66e9589935.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village33.png b/tileset/village/village33.png
new file mode 100644
index 0000000..c7e8486
Binary files /dev/null and b/tileset/village/village33.png differ
diff --git a/tileset/village/village33.png.import b/tileset/village/village33.png.import
new file mode 100644
index 0000000..d9f405f
--- /dev/null
+++ b/tileset/village/village33.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b6olh41l5nxc8"
+path="res://.godot/imported/village33.png-b5085af5ca475bb93de97e2f2ae06c88.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village33.png"
+dest_files=["res://.godot/imported/village33.png-b5085af5ca475bb93de97e2f2ae06c88.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village34.png b/tileset/village/village34.png
new file mode 100644
index 0000000..f2a45fc
Binary files /dev/null and b/tileset/village/village34.png differ
diff --git a/tileset/village/village34.png.import b/tileset/village/village34.png.import
new file mode 100644
index 0000000..96ced5b
--- /dev/null
+++ b/tileset/village/village34.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://4gn8oadx4oyd"
+path="res://.godot/imported/village34.png-fa5d57ca6355046fef43e780bba0adda.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village34.png"
+dest_files=["res://.godot/imported/village34.png-fa5d57ca6355046fef43e780bba0adda.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village35.png b/tileset/village/village35.png
new file mode 100644
index 0000000..09834a2
Binary files /dev/null and b/tileset/village/village35.png differ
diff --git a/tileset/village/village35.png.import b/tileset/village/village35.png.import
new file mode 100644
index 0000000..b33d121
--- /dev/null
+++ b/tileset/village/village35.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bb6hq2minkpqb"
+path="res://.godot/imported/village35.png-779702531696a6aea24a6f538bef1c9a.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village35.png"
+dest_files=["res://.godot/imported/village35.png-779702531696a6aea24a6f538bef1c9a.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village36.png b/tileset/village/village36.png
new file mode 100644
index 0000000..fa07504
Binary files /dev/null and b/tileset/village/village36.png differ
diff --git a/tileset/village/village36.png.import b/tileset/village/village36.png.import
new file mode 100644
index 0000000..45d1aaf
--- /dev/null
+++ b/tileset/village/village36.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://utklc5p23jt3"
+path="res://.godot/imported/village36.png-e4f4c651721371b25c2b4462f601fad0.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village36.png"
+dest_files=["res://.godot/imported/village36.png-e4f4c651721371b25c2b4462f601fad0.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village37.png b/tileset/village/village37.png
new file mode 100644
index 0000000..8093217
Binary files /dev/null and b/tileset/village/village37.png differ
diff --git a/tileset/village/village37.png.import b/tileset/village/village37.png.import
new file mode 100644
index 0000000..56d5d29
--- /dev/null
+++ b/tileset/village/village37.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dldx8qtg8lhrt"
+path="res://.godot/imported/village37.png-4e5c4fc913cdb0d9f0553c70257fba6e.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village37.png"
+dest_files=["res://.godot/imported/village37.png-4e5c4fc913cdb0d9f0553c70257fba6e.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village38.png b/tileset/village/village38.png
new file mode 100644
index 0000000..69367d5
Binary files /dev/null and b/tileset/village/village38.png differ
diff --git a/tileset/village/village38.png.import b/tileset/village/village38.png.import
new file mode 100644
index 0000000..d55ebb5
--- /dev/null
+++ b/tileset/village/village38.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://djpy2m6ketf2n"
+path="res://.godot/imported/village38.png-d19248d97084cadd3e43a6ed4d2354ac.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village38.png"
+dest_files=["res://.godot/imported/village38.png-d19248d97084cadd3e43a6ed4d2354ac.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village39.png b/tileset/village/village39.png
new file mode 100644
index 0000000..d8d6942
Binary files /dev/null and b/tileset/village/village39.png differ
diff --git a/tileset/village/village39.png.import b/tileset/village/village39.png.import
new file mode 100644
index 0000000..7612980
--- /dev/null
+++ b/tileset/village/village39.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cqb7trcqubyie"
+path="res://.godot/imported/village39.png-7c6e1341086ce4106a57028f1015f6df.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village39.png"
+dest_files=["res://.godot/imported/village39.png-7c6e1341086ce4106a57028f1015f6df.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village4.png b/tileset/village/village4.png
new file mode 100644
index 0000000..2819381
Binary files /dev/null and b/tileset/village/village4.png differ
diff --git a/tileset/village/village4.png.import b/tileset/village/village4.png.import
new file mode 100644
index 0000000..8d0d30c
--- /dev/null
+++ b/tileset/village/village4.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dbk8s7a02drcu"
+path="res://.godot/imported/village4.png-f65e17e98baa8e305e68ffe87420b593.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village4.png"
+dest_files=["res://.godot/imported/village4.png-f65e17e98baa8e305e68ffe87420b593.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village40.png b/tileset/village/village40.png
new file mode 100644
index 0000000..6d1316b
Binary files /dev/null and b/tileset/village/village40.png differ
diff --git a/tileset/village/village40.png.import b/tileset/village/village40.png.import
new file mode 100644
index 0000000..a2e2f2b
--- /dev/null
+++ b/tileset/village/village40.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://2orlf7lcxllu"
+path="res://.godot/imported/village40.png-3d29eb4acc9d54853960067cf8fcdaf3.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village40.png"
+dest_files=["res://.godot/imported/village40.png-3d29eb4acc9d54853960067cf8fcdaf3.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village41.png b/tileset/village/village41.png
new file mode 100644
index 0000000..fbf1b79
Binary files /dev/null and b/tileset/village/village41.png differ
diff --git a/tileset/village/village41.png.import b/tileset/village/village41.png.import
new file mode 100644
index 0000000..2e2d4f7
--- /dev/null
+++ b/tileset/village/village41.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bdpl2pgj3omhn"
+path="res://.godot/imported/village41.png-19fad29025df1128610244e8028a64c8.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village41.png"
+dest_files=["res://.godot/imported/village41.png-19fad29025df1128610244e8028a64c8.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village42.png b/tileset/village/village42.png
new file mode 100644
index 0000000..211dbae
Binary files /dev/null and b/tileset/village/village42.png differ
diff --git a/tileset/village/village42.png.import b/tileset/village/village42.png.import
new file mode 100644
index 0000000..f745076
--- /dev/null
+++ b/tileset/village/village42.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://coq6x3x10y5pc"
+path="res://.godot/imported/village42.png-f29f9fd7a502efd8cc76c66a7afbe7a4.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village42.png"
+dest_files=["res://.godot/imported/village42.png-f29f9fd7a502efd8cc76c66a7afbe7a4.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village43.png b/tileset/village/village43.png
new file mode 100644
index 0000000..49846a1
Binary files /dev/null and b/tileset/village/village43.png differ
diff --git a/tileset/village/village43.png.import b/tileset/village/village43.png.import
new file mode 100644
index 0000000..3c6828a
--- /dev/null
+++ b/tileset/village/village43.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b8mee0gaw6u5c"
+path="res://.godot/imported/village43.png-995b847ebf6a47e34c7a2e1e41d57a19.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village43.png"
+dest_files=["res://.godot/imported/village43.png-995b847ebf6a47e34c7a2e1e41d57a19.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village44.png b/tileset/village/village44.png
new file mode 100644
index 0000000..723013b
Binary files /dev/null and b/tileset/village/village44.png differ
diff --git a/tileset/village/village44.png.import b/tileset/village/village44.png.import
new file mode 100644
index 0000000..a92e9a5
--- /dev/null
+++ b/tileset/village/village44.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dt54wda31cqgo"
+path="res://.godot/imported/village44.png-578b41054f82e41ff44046864a9b1bb5.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village44.png"
+dest_files=["res://.godot/imported/village44.png-578b41054f82e41ff44046864a9b1bb5.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village45.png b/tileset/village/village45.png
new file mode 100644
index 0000000..17f1c15
Binary files /dev/null and b/tileset/village/village45.png differ
diff --git a/tileset/village/village45.png.import b/tileset/village/village45.png.import
new file mode 100644
index 0000000..74a416f
--- /dev/null
+++ b/tileset/village/village45.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dhs5g6gbkd36l"
+path="res://.godot/imported/village45.png-96a0f30dff3d2bf2c8a61c6aa3f3b145.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village45.png"
+dest_files=["res://.godot/imported/village45.png-96a0f30dff3d2bf2c8a61c6aa3f3b145.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village5.png b/tileset/village/village5.png
new file mode 100644
index 0000000..7f033df
Binary files /dev/null and b/tileset/village/village5.png differ
diff --git a/tileset/village/village5.png.import b/tileset/village/village5.png.import
new file mode 100644
index 0000000..737750b
--- /dev/null
+++ b/tileset/village/village5.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://diid3abohm730"
+path="res://.godot/imported/village5.png-85b6ead9ad93a9836d68fe7f0e933d9c.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village5.png"
+dest_files=["res://.godot/imported/village5.png-85b6ead9ad93a9836d68fe7f0e933d9c.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village50.png b/tileset/village/village50.png
new file mode 100644
index 0000000..34e8a1d
Binary files /dev/null and b/tileset/village/village50.png differ
diff --git a/tileset/village/village50.png.import b/tileset/village/village50.png.import
new file mode 100644
index 0000000..2b3d5b9
--- /dev/null
+++ b/tileset/village/village50.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dywlgvo8tcott"
+path="res://.godot/imported/village50.png-7e60b9f8eabd52898bdb89a859937cf1.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village50.png"
+dest_files=["res://.godot/imported/village50.png-7e60b9f8eabd52898bdb89a859937cf1.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village51.png b/tileset/village/village51.png
new file mode 100644
index 0000000..bab6c82
Binary files /dev/null and b/tileset/village/village51.png differ
diff --git a/tileset/village/village51.png.import b/tileset/village/village51.png.import
new file mode 100644
index 0000000..7138329
--- /dev/null
+++ b/tileset/village/village51.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bakganarlllc"
+path="res://.godot/imported/village51.png-0354957f1fc9a86350e655984716fd86.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village51.png"
+dest_files=["res://.godot/imported/village51.png-0354957f1fc9a86350e655984716fd86.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village52.png b/tileset/village/village52.png
new file mode 100644
index 0000000..f029224
Binary files /dev/null and b/tileset/village/village52.png differ
diff --git a/tileset/village/village52.png.import b/tileset/village/village52.png.import
new file mode 100644
index 0000000..fba1eae
--- /dev/null
+++ b/tileset/village/village52.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cjjr85d82biwi"
+path="res://.godot/imported/village52.png-0a974aeda48efdd8afd08c0c4f63390b.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village52.png"
+dest_files=["res://.godot/imported/village52.png-0a974aeda48efdd8afd08c0c4f63390b.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village53.png b/tileset/village/village53.png
new file mode 100644
index 0000000..90feda2
Binary files /dev/null and b/tileset/village/village53.png differ
diff --git a/tileset/village/village53.png.import b/tileset/village/village53.png.import
new file mode 100644
index 0000000..44f254f
--- /dev/null
+++ b/tileset/village/village53.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bmtrsodgxgv2t"
+path="res://.godot/imported/village53.png-c6e254d54a670104ea3ecf944adce902.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village53.png"
+dest_files=["res://.godot/imported/village53.png-c6e254d54a670104ea3ecf944adce902.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village54.png b/tileset/village/village54.png
new file mode 100644
index 0000000..772f3ce
Binary files /dev/null and b/tileset/village/village54.png differ
diff --git a/tileset/village/village54.png.import b/tileset/village/village54.png.import
new file mode 100644
index 0000000..acda01e
--- /dev/null
+++ b/tileset/village/village54.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://djfv7b84smgg0"
+path="res://.godot/imported/village54.png-2682428ca7dcfc7baba54691952d6caa.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village54.png"
+dest_files=["res://.godot/imported/village54.png-2682428ca7dcfc7baba54691952d6caa.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village6.png b/tileset/village/village6.png
new file mode 100644
index 0000000..fd297fc
Binary files /dev/null and b/tileset/village/village6.png differ
diff --git a/tileset/village/village6.png.import b/tileset/village/village6.png.import
new file mode 100644
index 0000000..7d450da
--- /dev/null
+++ b/tileset/village/village6.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cdh51d84twh82"
+path="res://.godot/imported/village6.png-0b41435e89aeb3c3d7231e630428a753.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village6.png"
+dest_files=["res://.godot/imported/village6.png-0b41435e89aeb3c3d7231e630428a753.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village7.png b/tileset/village/village7.png
new file mode 100644
index 0000000..34d3f62
Binary files /dev/null and b/tileset/village/village7.png differ
diff --git a/tileset/village/village7.png.import b/tileset/village/village7.png.import
new file mode 100644
index 0000000..e601af4
--- /dev/null
+++ b/tileset/village/village7.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dxrjquqixlvuj"
+path="res://.godot/imported/village7.png-a3d0f08ee457ba9c9ac9c968812d0bf7.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village7.png"
+dest_files=["res://.godot/imported/village7.png-a3d0f08ee457ba9c9ac9c968812d0bf7.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village8.png b/tileset/village/village8.png
new file mode 100644
index 0000000..179684b
Binary files /dev/null and b/tileset/village/village8.png differ
diff --git a/tileset/village/village8.png.import b/tileset/village/village8.png.import
new file mode 100644
index 0000000..8b92a7d
--- /dev/null
+++ b/tileset/village/village8.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dcacwrcnawh0l"
+path="res://.godot/imported/village8.png-b7db40f18b0ada953d94a69089ce8587.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village8.png"
+dest_files=["res://.godot/imported/village8.png-b7db40f18b0ada953d94a69089ce8587.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/tileset/village/village9.png b/tileset/village/village9.png
new file mode 100644
index 0000000..b33b83f
Binary files /dev/null and b/tileset/village/village9.png differ
diff --git a/tileset/village/village9.png.import b/tileset/village/village9.png.import
new file mode 100644
index 0000000..48b1a01
--- /dev/null
+++ b/tileset/village/village9.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dyhcw3w0dg355"
+path="res://.godot/imported/village9.png-afb2388b9419e75a8c264d77c9439ce9.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://tileset/village/village9.png"
+dest_files=["res://.godot/imported/village9.png-afb2388b9419e75a8c264d77c9439ce9.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1