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