├── .gitignore ├── LICENSE ├── README.md ├── VectorCollisionPolygon.gd ├── VectorControl.gd ├── VectorFill.gd ├── VectorHandle.gd ├── VectorPath.gd ├── VectorPoint.gd ├── VectorPointGroup.gd ├── VectorStroke.gd └── icon2.png /.gitignore: -------------------------------------------------------------------------------- 1 | *.import 2 | 3 | # Godot 4+ specific ignores 4 | .godot/ 5 | 6 | # Godot-specific ignores 7 | .import/ 8 | export.cfg 9 | export_presets.cfg 10 | 11 | # Imported translations (automatically generated from CSV files) 12 | *.translation 13 | 14 | # Mono-specific ignores 15 | .mono/ 16 | data_*/ 17 | mono_crash.*.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Peter Chaplin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VectorNode 2 | Create shapes with fill/stroke from Node2D control points 3 | 4 | Note: As of 1.7.0 I recommend using with the Bit Flags Editor plugin: https://github.com/SquiggelSquirrel/BitFlagEditor 5 | 6 | ## VectorPath 7 | - Defines the path to fill/stroke 8 | - Add VectorPoint nodes for each point on the path 9 | - Emits the `shape_changed` signal when the path changes 10 | - Emits the `stroke_data_changed` signal when the stroke widths and/or colors change 11 | - Sets the default bake interval for the shape 12 | - Has editor-preview-only color settings for handle-in, handle-out and path 13 | - Uses VectorPoint to define stroke widths & colors, and specific bake intervals 14 | - Can define an array of tag name strings (only for editor UI purposes), for use with VectorPoint "tags" bitmask 15 | 16 | ## VectorPoint 17 | - Defines a single point in a vector path 18 | - Use as child of VectorPath or VectorPointGroup 19 | - Add VectorHandle nodes to define the vector in/out 20 | - Can specify a stroke width and/or color 21 | - (will only apply to VectorStroke nodes with `use_data_nodes_width` or `use_data_nodes_color` set 22 | - Can specify a bake interval for outgoing edge 23 | - When set to 0.0, use default value specified on VectorPath node 24 | - Supports different handle types: 25 | - NONE: no handles 26 | - IN: one handle, defines vector in only 27 | - OUT: one handle, defines vector out only 28 | - BOTH: two handles, first defines vector in, second defines vector out 29 | - MIRROR IN: one handle, defines vector in. Vector out is reflection of vector in 30 | - MIRROR OUT: one handle, defines vector out. Vector in is reflection of vector out 31 | - IN OUT: one handle, defines vector in and vector out as same vector 32 | - Points and handles use position relative to VectorPath, so rotating/scaling a VectorPoint can change the handle position 33 | - Has an integer bitmask of tags, which can be used by fill and stroke to filter for different sub-paths 34 | 35 | ## VectorHandle 36 | - Defines vector in/out for a VectorPoint (see above) 37 | - Has an integer bitmask of tags, which can be used by fill and stroke to filter for different sub-paths 38 | 39 | ## VectorControl 40 | - Abstract class defining shared behaviour of VectorPoint and VectorHandle 41 | 42 | ## VectorPointGroup 43 | - Use as child of VectorPath or another VectorPointGroup 44 | - Usually contains one or more VectorPoints and/or other VectorPointGroups 45 | - Allows one or more VectorPoints to be moved, rotated, and/or scaled together 46 | 47 | ## VectorFill 48 | - Fills a VectorPath 49 | - Set `path_node_path` to the NodePath of the VectorPath 50 | - Call `update_fill` to update 51 | - Commonly you would connect the VectorPath `shape_changed` signal to the `update_fill` method 52 | - Has an integer mask (normally I would only set one bit) to specify which VectorControls to use 53 | 54 | ## VectorStroke 55 | - Strokes a VectorPath 56 | - Set `path_node_path` to the NodePath of the VectorPath 57 | - Call `update_shape` to update the shape 58 | - Call `update_data` to update stroke widths and/or colors: 59 | - (only if using `use_data_nodes_width` or `use_data_nodes_color`) 60 | - Extends Line2D, so also accepts textures, etc. 61 | - Can stroke part of a path - use start/end to define start and end points 62 | - These accept negative values and wrap 63 | - These also now accept floating values, and will interpolate along the path between points 64 | - Has `start_pinch length` and `end_pinch_length`, interpolate the width to 0.0 towards the start/end 65 | - Lengths are defined as fraction of total stroke length 66 | - Has `use_close_fix`, attempts to work around the fact that Line2D cannot be closed by creating 67 | a small overlap when the stroke is closed 68 | - Commonly you would connect the VectorPath `shape_changed` signal to the `update_shape` method 69 | - If using width/color, connect `stroke_data_changed` to `update_data` for automatic updates 70 | - Has an integer mask (normally I would only set one bit) to specify which VectorControls to use 71 | 72 | ## VectorCollisionPolygon 73 | - Creates a collision polygon from a VectorPath 74 | - Almost identical to VectorFill 75 | - Call `upadate_polygon` to update 76 | - Connect `shape_changed` to `update_polygon` for automatic updates 77 | -------------------------------------------------------------------------------- /VectorCollisionPolygon.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends CollisionPolygon2D 3 | class_name VectorCollisionPolygon 4 | # Collision Polygon with points updated from a VectorPath 5 | 6 | export(NodePath) var path_node_path setget set_path_node_path 7 | export(int, FLAGS, "") var mask := 1 8 | 9 | var is_vector_fill := true 10 | 11 | onready var is_ready := true 12 | 13 | 14 | func _get_configuration_warning(): 15 | if ! _get_path_node(): 16 | return "No valid path set" 17 | return "" 18 | 19 | 20 | func set_path_node_path(new_path :NodePath) -> void: 21 | path_node_path = new_path 22 | update_configuration_warning() 23 | if is_ready and path_node_path: 24 | update_polygon() 25 | 26 | 27 | func update_polygon(): 28 | var node = _get_path_node() 29 | if ! node: 30 | return 31 | polygon = node.get_shape(0,0,mask) 32 | 33 | 34 | func _get_path_node(): 35 | if path_node_path == "": 36 | return null 37 | var node = get_node(path_node_path) 38 | if ! node: 39 | return null 40 | if node.get('is_vector_path'): 41 | return node 42 | return null 43 | 44 | 45 | func _get_layer_names(property_name :String) -> Array: 46 | if property_name != "mask": 47 | return [] 48 | var path_node = _get_path_node() 49 | if ! path_node: 50 | return [] 51 | return path_node._get_layer_names("tags") 52 | -------------------------------------------------------------------------------- /VectorControl.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends Node2D 3 | class_name VectorControl 4 | # Abstract class for code shared by VectorPoint and VectorHandle 5 | # Tracks whether or not transform has changed, 6 | # since has_changed was last set to false 7 | 8 | export(int, FLAGS, "") var tags := 1 9 | var has_changed :bool setget set_has_changed, get_has_changed 10 | onready var _cached_transform = null 11 | 12 | 13 | func get_has_changed() -> bool: 14 | var parent = get_parent() 15 | if parent.get('is_control_point_group') and parent.has_changed: 16 | return true 17 | return ! _cached_transform or _cached_transform != transform 18 | 19 | 20 | func set_has_changed(new_value: bool) -> void: 21 | if new_value: 22 | _cached_transform = null 23 | else: 24 | _cached_transform = transform 25 | 26 | 27 | func get_position_in(reference_node: Node2D) -> Vector2: 28 | return reference_node.to_local(global_position) 29 | 30 | 31 | func matches_mask(mask :int) -> bool: 32 | return bool(tags & mask) 33 | 34 | 35 | func _get_layer_names(property_name :String) -> Array: 36 | if property_name != "tags": 37 | return [] 38 | var parent = get_parent() 39 | if parent.has_method("_get_layer_names"): 40 | return parent._get_layer_names("tags") 41 | return [] 42 | -------------------------------------------------------------------------------- /VectorFill.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends Polygon2D 3 | class_name VectorFill 4 | # Polygon2D used as "fill" for a VectorPath 5 | 6 | export(NodePath) var path_node_path setget set_path_node_path 7 | export(int, FLAGS, "") var mask := 1 8 | 9 | var is_vector_fill := true 10 | 11 | onready var is_ready := true 12 | 13 | 14 | func _get_configuration_warning(): 15 | if ! _get_path_node(): 16 | return "No valid path set" 17 | return "" 18 | 19 | 20 | func set_path_node_path(new_path :NodePath) -> void: 21 | path_node_path = new_path 22 | update_configuration_warning() 23 | if is_ready and path_node_path: 24 | update_fill() 25 | 26 | 27 | func update_fill(): 28 | var node = _get_path_node() 29 | if ! node: 30 | return 31 | polygon = node.get_shape(0,0,mask) 32 | 33 | 34 | func _get_path_node(): 35 | if path_node_path == "": 36 | return null 37 | var node = get_node(path_node_path) 38 | if ! node: 39 | return null 40 | if node.get('is_vector_path'): 41 | return node 42 | return null 43 | 44 | 45 | func _get_layer_names(property_name :String) -> Array: 46 | if property_name != "mask": 47 | return [] 48 | var path_node = _get_path_node() 49 | if ! path_node: 50 | return [] 51 | return path_node._get_layer_names("tags") 52 | -------------------------------------------------------------------------------- /VectorHandle.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends VectorControl 3 | class_name VectorHandle 4 | # Control handle for VectorPoint 5 | # No additional code yet, just a class to identify 6 | # node type/purpose 7 | 8 | var is_control_handle := true 9 | -------------------------------------------------------------------------------- /VectorPath.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends Node2D 3 | class_name VectorPath 4 | # Vector Shape Path container 5 | # Monitors for changes to the points & handles 6 | # Emits a "changed" signal when changes occur 7 | # Exposes a curve for each path segment, 8 | # Can provide an array of points for any range of segments 9 | 10 | signal shape_changed 11 | signal stroke_data_changed 12 | 13 | const ALL_TAGS = ~0 14 | 15 | export(float, 1.0, 100.0) var bake_interval = 10.0 16 | export(Color) var color_handle_in := Color.red setget set_color_handle_in 17 | export(Color) var color_handle_out := Color.green setget set_color_handle_out 18 | export(Color) var color_path := Color.lightgray setget set_color_path 19 | export(Array, String) var tag_names := [] 20 | 21 | var is_vector_path = true 22 | 23 | var _curves := {} 24 | var _cached_bake_interval = 10.0 25 | 26 | 27 | func _process(_delta :float) -> void: 28 | if get_shape_has_changed(): 29 | _expire_changed_curves() 30 | emit_signal("shape_changed") 31 | set_shape_has_changed(false) 32 | update() 33 | if get_stroke_data_has_changed(): 34 | emit_signal("stroke_data_changed") 35 | set_stroke_data_has_changed(false) 36 | 37 | 38 | func _draw() -> void: 39 | if ! Engine.editor_hint: 40 | return 41 | for point in get_point_nodes(): 42 | var start :Vector2 = point.get_position_in(self) 43 | var end :Vector2 = point.get_position_in(self) + point.get_handle_in(self) 44 | draw_line(start, end, color_handle_in) 45 | end = point.get_position_in(self) + point.get_handle_out(self) 46 | draw_line(start, end, color_handle_out) 47 | var points := get_shape() 48 | if points.size() > 2: 49 | points.append(points[0]) 50 | draw_polyline(points, color_path) 51 | 52 | 53 | func set_color_handle_in(new_color :Color) -> void: 54 | color_handle_in = new_color 55 | update() 56 | 57 | 58 | func set_color_handle_out(new_color :Color) -> void: 59 | color_handle_out = new_color 60 | update() 61 | 62 | 63 | func set_color_path(new_color :Color) -> void: 64 | color_path = new_color 65 | update() 66 | 67 | 68 | func get_shape(start := 0.0, end := 0.0, mask :int = 1) -> Array: 69 | var shape := [] 70 | var curves := _get_curves(start, end, mask) 71 | var start_fraction := fposmod(start, 1.0) 72 | var end_fraction := fposmod(end, 1.0) 73 | var last_point 74 | 75 | for i in curves.size(): 76 | var curve = curves[i] 77 | if curve is Curve2D: 78 | var baked_points := Array(curves[i].get_baked_points()) 79 | var first := 0 80 | var last := -2 81 | var prepend_point 82 | last_point = baked_points[-1] 83 | 84 | if i == 0 and start_fraction > 0.0: 85 | var f := start_fraction * (baked_points.size() -1) 86 | var before := int(f) 87 | first = before + 1 88 | prepend_point = lerp( 89 | baked_points[before], 90 | baked_points[first], 91 | f - before) 92 | 93 | if i + 1 == curves.size() and end_fraction > 0.0: 94 | var f := end_fraction * (baked_points.size() -1) 95 | last = int(f) 96 | if ! range_is_closed(start, end): 97 | last_point = lerp( 98 | baked_points[last], 99 | baked_points[last + 1], 100 | f - last) 101 | 102 | if prepend_point: 103 | shape.append(prepend_point) 104 | shape += baked_points.slice(first, last) 105 | 106 | else: 107 | # Straight line 108 | var start_point = curve[0] 109 | var end_point = curve[1] 110 | if i == 0 and start_fraction > 0.0: 111 | start_point = lerp( 112 | curve[0], 113 | curve[1], 114 | start_fraction) 115 | if i + 1 == curves.size() and end_fraction > 0.0: 116 | end_point = lerp( 117 | curve[0], 118 | curve[1], 119 | end_fraction) 120 | 121 | shape.append(start_point) 122 | last_point = end_point 123 | 124 | if ! range_is_closed(start, end) and curves.size() > 0: 125 | shape.append(last_point) 126 | 127 | return shape 128 | 129 | 130 | func get_baked_lengths(start := 0.0, end := 0.0, mask :int = 1) -> Array: 131 | var lengths := [] 132 | var curves := _get_curves(start, end, mask) 133 | for i in curves.size(): 134 | var curve = curves[i] 135 | 136 | var length :float 137 | if curve is Curve2D: 138 | length = curve.get_baked_length() 139 | else: 140 | length = curve[0].distance_to(curve[1]) 141 | 142 | var start_fraction := 0.0 143 | if i == 0: 144 | start_fraction = fposmod(start, 1.0) 145 | var head_length := length * start_fraction 146 | 147 | var end_fraction := 1.0 148 | if i + 1 == curves.size(): 149 | end_fraction = fposmod(end, 1.0) 150 | end_fraction = stepify(end_fraction, 0.001) 151 | if end_fraction == 0.0: 152 | end_fraction = 1.0 153 | var tail_length := length * (1.0 - end_fraction) 154 | 155 | length -= head_length + tail_length 156 | lengths.append(length) 157 | 158 | return lengths 159 | 160 | 161 | func get_colors(start := 0.0, end := 0.0, mask :int = 1) -> Array: 162 | var colors := [] 163 | for node in get_point_nodes(start, end, mask): 164 | colors.append(node.stroke_color) 165 | return colors 166 | 167 | 168 | func get_widths(start := 0.0, end := 0.0, mask :int = 1) -> Array: 169 | var widths := [] 170 | for node in get_point_nodes(start, end, mask): 171 | widths.append(node.stroke_width) 172 | return widths 173 | 174 | 175 | func get_shape_has_changed() -> bool: 176 | if _cached_bake_interval != bake_interval: 177 | return true 178 | for point in get_point_nodes(ALL_TAGS): 179 | if point.get_shape_has_changed(): 180 | return true 181 | return false 182 | 183 | 184 | func get_stroke_data_has_changed() -> bool: 185 | for point in get_point_nodes(ALL_TAGS): 186 | if point.get_stroke_data_has_changed(): 187 | return true 188 | return false 189 | 190 | 191 | func set_shape_has_changed(new_value: bool) -> void: 192 | if new_value: 193 | _cached_bake_interval = null 194 | else: 195 | _cached_bake_interval = bake_interval 196 | for point in get_point_nodes(ALL_TAGS): 197 | point.set_shape_has_changed(new_value) 198 | 199 | 200 | func set_stroke_data_has_changed(new_value: bool) -> void: 201 | for point in get_point_nodes(ALL_TAGS): 202 | point.set_stroke_data_has_changed(new_value) 203 | 204 | 205 | func get_point_nodes(start := 0.0, end := 0.0, mask :int = 1) -> Array: 206 | var points := [] 207 | for child in get_children(): 208 | if child.get("is_control_point") and child.matches_mask(mask): 209 | points.append(child) 210 | if child.get("is_control_point_group"): 211 | for node in child.get_point_nodes(mask): 212 | points.append(node) 213 | return _get_array_range(points, start, end) 214 | 215 | 216 | func range_is_closed(start :float, end:float) -> bool: 217 | var size := float(get_point_nodes().size()) 218 | return wrapf(start, 0.0, size) == wrapf(end, 0.0, size) 219 | 220 | 221 | func _get_array_range(array :Array, start :float, end:float) -> Array: 222 | # Given an array of nodes, and start/end points, return the array of nodes 223 | # required to encompass those points 224 | if array.size() == 0: 225 | return array 226 | var fsize = float(array.size()) 227 | 228 | start = fposmod(start, fsize) 229 | end = wrapf(end, start, start + fsize) 230 | end = stepify(end, 0.001) # stepify to elimitate floating-point error 231 | if end == start: 232 | end += fsize 233 | 234 | var int_start := int(floor(start)) 235 | var int_end := int(ceil(end)) 236 | 237 | var result := [] 238 | for i in range(int_start, int_end + 1): 239 | result.append(array[wrapi(i, 0, array.size())]) 240 | 241 | return result 242 | 243 | 244 | func _get_curves(start := 0.0, end := 0.0, mask :int = 1) -> Array: 245 | var curves := [] 246 | var point_nodes = get_point_nodes(start, end, mask) 247 | for i in range(0, point_nodes.size() - 1): 248 | curves.append(_get_curve(point_nodes[i], point_nodes[i+1], mask)) 249 | return curves 250 | 251 | 252 | func _get_curve(start_point :Node, end_point :Node, mask :int = 1): 253 | var handle_in = start_point.get_VectorHandle_out(mask) 254 | var handle_out = end_point.get_VectorHandle_in(mask) 255 | if _curves.has([start_point,handle_in,end_point,handle_out]): 256 | return _curves[[start_point,handle_in,end_point,handle_out]] 257 | else: 258 | var curve = _get_new_curve(start_point, end_point, mask) 259 | _curves[[start_point,handle_in,end_point,handle_out]] = curve 260 | return curve 261 | 262 | 263 | func _is_straight(start_point :Node, end_point :Node, mask :int = 1) -> bool: 264 | return (start_point.get_handle_out(self, mask) == Vector2.ZERO 265 | and 266 | end_point.get_handle_in(self, mask) == Vector2.ZERO) 267 | 268 | 269 | func _get_new_curve(start_point :Node, end_point :Node, mask :int = 1): 270 | if _is_straight(start_point, end_point, mask): 271 | return [ 272 | start_point.get_position_in(self), 273 | end_point.get_position_in(self)] 274 | var curve := Curve2D.new() 275 | if start_point.bake_interval == 0.0: 276 | curve.bake_interval = bake_interval 277 | else: 278 | curve.bake_interval = start_point.bake_interval 279 | curve.add_point( 280 | start_point.get_position_in(self), 281 | start_point.get_handle_in(self, mask), 282 | start_point.get_handle_out(self, mask)) 283 | curve.add_point( 284 | end_point.get_position_in(self), 285 | end_point.get_handle_in(self, mask), 286 | end_point.get_handle_out(self, mask)) 287 | return curve 288 | 289 | 290 | func _expire_changed_curves() -> void: 291 | var bake_interval_has_changed = _cached_bake_interval != bake_interval 292 | for key in _curves.keys(): 293 | if ( 294 | (key[0].bake_interval < 1.0 and bake_interval_has_changed) 295 | or key[0].get_has_changed() 296 | or ( key[1] and key[1].get_has_changed() ) 297 | or key[2].get_has_changed() 298 | or ( key[3] and key[3].get_has_changed() ) 299 | ): 300 | # warning-ignore:return_value_discarded 301 | _curves.erase(key) 302 | 303 | 304 | func _get_layer_names(property_name :String) -> Array: 305 | if property_name != "tags": 306 | return [] 307 | return tag_names 308 | -------------------------------------------------------------------------------- /VectorPoint.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends VectorControl 3 | class_name VectorPoint 4 | # Point for a VectorPath 5 | 6 | enum HandlesType {NONE, IN, OUT, BOTH, MIRROR_IN, MIRROR_OUT, IN_OUT} 7 | 8 | const ALL_FLAGS :int = ~0 9 | 10 | export(HandlesType) var handles_type = HandlesType.BOTH setget set_handles_type 11 | export(float, 0.0, 1.0) var stroke_width := 1.0 12 | export(Color) var stroke_color 13 | export(float, 0.0, 100.0) var bake_interval := 0.0 14 | 15 | var is_control_point := true 16 | 17 | var _cached_stroke_width = 1.0 18 | var _cached_stroke_color 19 | var _cached_bake_interval = 0.0 20 | 21 | 22 | func _get_configuration_warning() -> String: 23 | var count_handles := get_handles().size() 24 | var expected_handle_count :int = { 25 | HandlesType.NONE: 0, 26 | HandlesType.IN: 1, 27 | HandlesType.OUT: 1, 28 | HandlesType.BOTH: 2, 29 | HandlesType.MIRROR_IN: 1, 30 | HandlesType.MIRROR_OUT: 1, 31 | HandlesType.IN_OUT: 1 32 | }[handles_type] 33 | if ( 34 | count_handles < expected_handle_count 35 | or (expected_handle_count == 0 and count_handles > 0) 36 | ): 37 | return ( 38 | "Handle type " 39 | + String(HandlesType.keys()[handles_type]) 40 | + " expects " 41 | + String(expected_handle_count) 42 | + " handles" 43 | ); 44 | return "" 45 | 46 | 47 | func get_has_changed() -> bool: 48 | if _cached_bake_interval != bake_interval: 49 | return true 50 | if .get_has_changed(): 51 | return true 52 | return false 53 | 54 | 55 | func get_shape_has_changed(mask :int = ALL_FLAGS) -> bool: 56 | if get_has_changed(): 57 | return true 58 | for handle in get_handles(mask): 59 | if handle.get_has_changed(): 60 | return true 61 | return false 62 | 63 | 64 | func get_handle_in_has_changed(mask :int = 1) -> bool: 65 | if _cached_transform == null: 66 | return true 67 | match handles_type: 68 | HandlesType.NONE, HandlesType.OUT: 69 | return false 70 | _: 71 | var handles := get_handles(mask) 72 | if handles.size() == 0: 73 | return false 74 | return handles[0].get_has_changed() 75 | 76 | 77 | func get_handle_out_has_changed(mask :int = 1) -> bool: 78 | if _cached_transform == null: 79 | return true 80 | match handles_type: 81 | HandlesType.NONE, HandlesType.IN: 82 | return false 83 | HandlesType.BOTH: 84 | var handles := get_handles(mask) 85 | if handles.size() < 2: 86 | return false 87 | return handles[1].get_has_changed() 88 | _: 89 | var handles := get_handles(mask) 90 | if handles.size() == 0: 91 | return false 92 | return handles[0].get_has_changed() 93 | 94 | 95 | func set_shape_has_changed(new_value: bool) -> void: 96 | if new_value: 97 | _cached_bake_interval = null 98 | else: 99 | _cached_bake_interval = bake_interval 100 | .set_has_changed(new_value) 101 | for handle in get_handles(): 102 | handle.set_has_changed(new_value) 103 | 104 | 105 | func get_stroke_data_has_changed() -> bool: 106 | return (stroke_width != _cached_stroke_width 107 | or stroke_color != _cached_stroke_color) 108 | 109 | 110 | func set_stroke_data_has_changed(new_value :bool) -> void: 111 | if new_value: 112 | _cached_stroke_color = null 113 | _cached_stroke_width = null 114 | else: 115 | _cached_stroke_color = stroke_color 116 | _cached_stroke_width = stroke_width 117 | 118 | 119 | func get_handle_in(base_node: Node2D, mask :int = 1) -> Vector2: 120 | match handles_type: 121 | HandlesType.NONE, HandlesType.OUT: 122 | return Vector2.ZERO 123 | HandlesType.MIRROR_OUT: 124 | return _get_handle(base_node, 0, mask) * -1 125 | _: 126 | return _get_handle(base_node, 0, mask) 127 | 128 | 129 | func get_handle_out(base_node: Node2D, mask :int = 1) -> Vector2: 130 | match handles_type: 131 | HandlesType.NONE, HandlesType.IN: 132 | return Vector2.ZERO 133 | HandlesType.MIRROR_IN: 134 | return _get_handle(base_node, 0, mask) * -1 135 | HandlesType.BOTH: 136 | return _get_handle(base_node, 1, mask) 137 | _: 138 | return _get_handle(base_node, 0, mask) 139 | 140 | 141 | func get_VectorHandle_in(mask :int = 1): 142 | match handles_type: 143 | HandlesType.NONE, HandlesType.OUT: 144 | return null 145 | HandlesType.MIRROR_OUT: 146 | return _get_VectorHandle(0, mask) * -1 147 | _: 148 | return _get_VectorHandle(0, mask) 149 | 150 | 151 | func get_VectorHandle_out(mask :int = 1): 152 | match handles_type: 153 | HandlesType.NONE, HandlesType.IN: 154 | return null 155 | HandlesType.MIRROR_IN: 156 | return _get_VectorHandle(0, mask) * -1 157 | HandlesType.BOTH: 158 | return _get_VectorHandle(1, mask) 159 | _: 160 | return _get_VectorHandle(0, mask) 161 | 162 | 163 | func set_handles_type(new_type: int) -> void: 164 | handles_type = new_type 165 | set_has_changed(true) 166 | update_configuration_warning() 167 | 168 | 169 | func get_handles(mask :int = ALL_FLAGS) -> Array: 170 | var handles := [] 171 | for child in get_children(): 172 | if child.get("is_control_handle") and child.matches_mask(mask): 173 | handles.append(child) 174 | return handles 175 | 176 | 177 | func _get_handle(base_node :Node2D, index :int, mask :int = 1) -> Vector2: 178 | var vectorHandle = _get_VectorHandle(index, mask) 179 | if ! vectorHandle: 180 | return Vector2.ZERO 181 | var handle_position := vectorHandle.get_position_in(base_node) as Vector2 182 | var own_position := get_position_in(base_node) 183 | return handle_position - own_position 184 | 185 | 186 | func _get_VectorHandle(index :int, mask :int = 1): 187 | var handles := get_handles(mask) 188 | if index >= handles.size(): 189 | return null 190 | return (handles[index] as VectorHandle) 191 | -------------------------------------------------------------------------------- /VectorPointGroup.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends Node2D 3 | class_name VectorPointGroup 4 | 5 | var is_control_point_group := true 6 | var has_changed :bool setget set_has_changed, get_has_changed 7 | 8 | onready var _cached_transform = null 9 | 10 | 11 | func get_has_changed() -> bool: 12 | var parent = get_parent() 13 | if parent.get('is_control_point_group') and parent.has_changed: 14 | return true 15 | return ! _cached_transform or _cached_transform != transform 16 | 17 | 18 | func set_has_changed(new_value: bool) -> void: 19 | if new_value: 20 | _cached_transform = null 21 | else: 22 | _cached_transform = transform 23 | 24 | 25 | func get_point_nodes(mask :int = 1) -> Array: 26 | var points := [] 27 | for child in get_children(): 28 | if child.get("is_control_point") and child.matches_mask(mask): 29 | points.append(child) 30 | if child.get("is_control_point_group"): 31 | points += child.get_point_nodes(mask); 32 | return points 33 | 34 | 35 | func _get_layer_names(property_name :String) -> Array: 36 | if property_name != "tags": 37 | return [] 38 | var parent = get_parent() 39 | if parent.has_method("_get_layer_names"): 40 | return parent._get_layer_names("tags") 41 | return [] 42 | -------------------------------------------------------------------------------- /VectorStroke.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends Line2D 3 | class_name VectorStroke 4 | # Line2D that provides a "stroke" for a VectorPath 5 | 6 | export(NodePath) var path_node_path setget set_path_node_path 7 | export(int, FLAGS, "") var mask := 1 8 | export(float) var start := 0.0 setget set_start 9 | export(float) var end := 0.0 setget set_end 10 | export(bool) var use_data_nodes_width = false setget set_use_data_nodes_width 11 | export(bool) var use_data_nodes_color = false setget set_use_data_nodes_color 12 | export(bool) var use_close_fix = false setget set_use_close_fix 13 | export(float, 1.0) var start_pinch_length := 0.0 setget set_start_pinch_length 14 | export(float, 1.0) var end_pinch_length := 0.0 setget set_end_pinch_length 15 | 16 | var is_vector_stroke := true 17 | 18 | var _needs_shape_update := false 19 | var _needs_data_update := false 20 | var _segment_ratios := [] 21 | 22 | onready var _is_ready = true 23 | 24 | 25 | func _ready() -> void: 26 | set_process( !! _get_path_node() ) 27 | 28 | 29 | func _process(_delta) -> void: 30 | var path_node = _get_path_node() 31 | if ! path_node: 32 | return 33 | if _needs_shape_update: 34 | update_shape() 35 | if _needs_data_update: 36 | update_data() 37 | 38 | 39 | func _get_configuration_warning() -> String: 40 | if ! _get_path_node(): 41 | return "No path node specified" 42 | return "" 43 | 44 | 45 | func set_path_node_path(new_path :NodePath) -> void: 46 | path_node_path = new_path 47 | update_configuration_warning() 48 | if _is_ready: 49 | set_process(!! _get_path_node()) 50 | _needs_shape_update = true 51 | _needs_data_update = true 52 | 53 | 54 | func set_start(new_start :float) -> void: 55 | start = new_start 56 | _needs_shape_update = true 57 | _needs_data_update = true 58 | 59 | 60 | func set_end(new_end :float) -> void: 61 | end = new_end 62 | _needs_shape_update = true 63 | _needs_data_update = true 64 | 65 | 66 | func set_start_pinch_length(length :float) -> void: 67 | start_pinch_length = length 68 | _needs_data_update = true 69 | 70 | 71 | func set_end_pinch_length(length :float) -> void: 72 | end_pinch_length = length 73 | _needs_data_update = true 74 | 75 | 76 | func set_use_data_nodes_width(new_use_data_nodes_width: bool) -> void: 77 | use_data_nodes_width = new_use_data_nodes_width 78 | if new_use_data_nodes_width: 79 | _needs_data_update = true 80 | 81 | 82 | func set_use_data_nodes_color(new_use_data_nodes_color: bool) -> void: 83 | use_data_nodes_color = new_use_data_nodes_color 84 | if new_use_data_nodes_color: 85 | _needs_data_update = true 86 | 87 | 88 | func set_use_close_fix(new_use_close_fix :bool) -> void: 89 | use_close_fix = new_use_close_fix 90 | _needs_shape_update = true 91 | 92 | 93 | func update_shape() -> void: 94 | var path_node = _get_path_node() 95 | if ! path_node: 96 | return 97 | points = path_node.get_shape(start,end,mask) 98 | if path_node.range_is_closed(start,end): 99 | _close() 100 | _needs_shape_update = false 101 | 102 | 103 | func update_data() -> void: 104 | var path_node = _get_path_node() 105 | if ! path_node: 106 | return 107 | _update_segment_ratios(path_node, mask) 108 | if use_data_nodes_color: 109 | _update_color(path_node, mask) 110 | if use_data_nodes_width: 111 | _update_width(path_node, mask) 112 | _needs_data_update = false 113 | 114 | 115 | func _get_path_node(): 116 | if path_node_path: 117 | var node := get_node(path_node_path) 118 | if node.get('is_vector_path'): 119 | return node 120 | return null 121 | 122 | 123 | func _update_segment_ratios(path_node :Node, mask :int = 1) -> void: 124 | var length_offsets := [0.0] 125 | var total_length := 0.0 126 | for length in path_node.get_baked_lengths(start, end, mask): 127 | total_length += length 128 | length_offsets.append(total_length) 129 | _segment_ratios = [] 130 | for length in length_offsets: 131 | _segment_ratios.append(length / total_length) 132 | 133 | 134 | func _update_color(data_node :Node, mask :int = 1) -> void: 135 | var offsets := [] 136 | var colors := [] 137 | var point_colors :Array = data_node.get_colors(start, end, mask) 138 | if _segment_ratios.size() != point_colors.size(): 139 | return 140 | 141 | var start_fraction := fposmod(start, 1.0) 142 | var end_fraction := fposmod(end, 1.0) 143 | 144 | for i in point_colors.size(): 145 | offsets.append(_segment_ratios[i]) 146 | 147 | if i == 0 and start_fraction != 0.0: 148 | colors.append(lerp( 149 | point_colors[0], 150 | point_colors[1], 151 | start_fraction)) 152 | elif i + 1 == point_colors.size() and end_fraction != 0.0: 153 | colors.append(lerp( 154 | point_colors[i-1], 155 | point_colors[i], 156 | end_fraction)) 157 | else: 158 | colors.append(point_colors[i]) 159 | 160 | if ! gradient: 161 | gradient = Gradient.new() 162 | gradient.offsets = offsets 163 | gradient.colors = colors 164 | 165 | 166 | func _update_width(data_node :Node, mask :int = 1) -> void: 167 | var widths :Array = data_node.get_widths(start, end, mask) 168 | if _segment_ratios.size() != widths.size(): 169 | return 170 | if width_curve: 171 | width_curve.clear_points() 172 | else: 173 | width_curve = Curve.new() 174 | 175 | var start_fraction := fposmod(start, 1.0) 176 | var end_fraction := fposmod(end, 1.0) 177 | 178 | for i in widths.size(): 179 | var width 180 | if i == 0 and start_fraction != 0.0: 181 | width = lerp(widths[0], widths[1], start_fraction) 182 | elif i + 1 == widths.size() and end_fraction != 0.0: 183 | width = lerp(widths[i-1], widths[i], end_fraction) 184 | else: 185 | width = widths[i] 186 | 187 | # warning-ignore:return_value_discarded 188 | width_curve.add_point(Vector2(_segment_ratios[i],width)) 189 | 190 | if start_pinch_length > 0.0: 191 | var width := width_curve.interpolate(start_pinch_length) 192 | for i in width_curve.get_point_count(): 193 | for j in width_curve.get_point_count(): 194 | if width_curve.get_point_position(j)[0] <= start_pinch_length: 195 | width_curve.remove_point(j) 196 | break 197 | # warning-ignore:return_value_discarded 198 | width_curve.add_point(Vector2.ZERO) 199 | # warning-ignore:return_value_discarded 200 | width_curve.add_point(Vector2(start_pinch_length,width)) 201 | 202 | if end_pinch_length > 0.0: 203 | var end_pinch := 1.0 - end_pinch_length 204 | var width := width_curve.interpolate(end_pinch) 205 | for i in width_curve.get_point_count(): 206 | for j in width_curve.get_point_count(): 207 | if width_curve.get_point_position(j)[0] >= end_pinch: 208 | width_curve.remove_point(j) 209 | break 210 | # warning-ignore:return_value_discarded 211 | width_curve.add_point(Vector2.RIGHT) 212 | # warning-ignore:return_value_discarded 213 | width_curve.add_point(Vector2(end_pinch, width)) 214 | 215 | 216 | func _close() -> void: 217 | # Godot doesn't currently support closed Line2D nodes; 218 | # create a closed-like effect with an extra point overlapping 219 | # the start line 220 | var _points = points 221 | _points.append(points[0]) 222 | if use_close_fix: 223 | var start_vector := (points[1] - points[0]).normalized() 224 | var close_point := points[0] + start_vector * 0.1 225 | _points.append(close_point) 226 | points = _points 227 | 228 | 229 | func _get_layer_names(property_name :String) -> Array: 230 | if property_name != "mask": 231 | return [] 232 | var path_node = _get_path_node() 233 | if ! path_node: 234 | return [] 235 | return path_node._get_layer_names("tags") 236 | -------------------------------------------------------------------------------- /icon2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SquiggelSquirrel/VectorNode/ab3accd46fb592153ab9f1308b37b35834cde2d9/icon2.png --------------------------------------------------------------------------------