200 lines
6.8 KiB
GDScript
200 lines
6.8 KiB
GDScript
## Ths class is designed to create a mesh that
|
|
## will display a path between pairs of VisualizationSpots.
|
|
##
|
|
## To use a VisualizedPath it must be populated with an array
|
|
## of spots which define the local object space positions and
|
|
## normals for the path at that point.
|
|
##
|
|
## The spots array can be defined programmatically such as by
|
|
## user action (such as clicking) or defined in the editor using
|
|
## the VizPath gizmo.
|
|
##
|
|
## Not all paths are valid. Putting the spots too close together
|
|
## or with normals that do not leave enough room for bends to be
|
|
## created with the defined path characteristics (width, inner radius,
|
|
## etc.), will result in the path not being displayed and the "error"
|
|
## attribute being set to the underlying problem.
|
|
##
|
|
|
|
@tool
|
|
@icon("res://addons/vizpath/images/path.png")
|
|
extends Node3D
|
|
class_name VisualizedPath
|
|
|
|
signal changed_layout
|
|
|
|
## The spots array defines where the path will go between.
|
|
## See [VisualizationSpot]
|
|
@export var spots : Array[VisualizationSpot] :
|
|
set(p_spots):
|
|
spots = p_spots
|
|
for spot in spots:
|
|
if not spot.changed.is_connected(_rebuild):
|
|
spot.changed.connect(_rebuild)
|
|
_rebuild()
|
|
|
|
## The path_width defines the width of the path, which indirectly
|
|
## determines how tight the path can turn at a midpoint spot.
|
|
@export_range(0.005, 100, 0.001) var path_width := 0.1 :
|
|
set(w):
|
|
path_width = w
|
|
_rebuild()
|
|
|
|
## The inner_curve_radius defines how tight a turn can be made
|
|
## at a midpoint spot within the path.
|
|
@export_range(0.005, 100, 0.001) var inner_curve_radius := 0.1 :
|
|
set(r):
|
|
inner_curve_radius = r
|
|
_rebuild()
|
|
|
|
## The num_curve_segs defines how many points will be defined in the
|
|
## arc that are used to create a turn. More segments will
|
|
## make a smoother UV mapping and therefore the display of associated material
|
|
## more accurate.
|
|
@export_range(4, 128) var num_curve_segs := 32 :
|
|
set(r):
|
|
num_curve_segs = r
|
|
_rebuild()
|
|
|
|
## The bend_segs defines the segments in a bend of the path when transitioning
|
|
## between two spots with normals not in the same plane.
|
|
@export_range(4, 128) var bend_segs := 6 :
|
|
set(r):
|
|
bend_segs = r
|
|
_rebuild()
|
|
|
|
## The bend_lip defines the distance that a segment extends from the intersection
|
|
## of the planes defined by the beginning and ending normals when those normals
|
|
## are not in the same plane. Making this a smaller value will make the
|
|
## segment flatter for more of its length.
|
|
@export_range(0.005, 100, 0.001) var bend_lip := 0.1 :
|
|
set(r):
|
|
bend_lip = r
|
|
_rebuild()
|
|
|
|
## The bend_sharpness defines how sharp the bend in a path. Making this
|
|
## smaller will result in a tighter bend.
|
|
@export var bend_sharpness := 1.0 :
|
|
set(r):
|
|
bend_sharpness = r
|
|
_rebuild()
|
|
|
|
## The path_mat is the material that will be applied to the underlying mesh
|
|
## that displays the path. The initial spot in the path will have texture coordinates
|
|
## that start with U equal to 0.0 and ending at the value calculated as the actual length of the path
|
|
## in local space. The V will range between 0.0 at the left side of the path, and 1.0 at the
|
|
## right side of the path.
|
|
@export var path_mat : Material :
|
|
set(m):
|
|
path_mat = m
|
|
_rebuild()
|
|
|
|
## The path_head is a resource (optional) that defines the end of the path by
|
|
## providing an "apply" method. The provided [VizHead] resource is an example
|
|
## that draws an arrow head on the end.
|
|
@export var path_head : VizHead :
|
|
set(m):
|
|
path_head = m
|
|
path_head.changed.connect(_rebuild)
|
|
_rebuild()
|
|
|
|
## The path_tail is a resource (optional) that defines the start of the path by
|
|
## providing an "apply" method. The provided [VizTail] resource is an example
|
|
## that draws a rounded cap on the end.
|
|
@export var path_tail : VizTail :
|
|
set(m):
|
|
path_tail = m
|
|
path_tail.changed.connect(_rebuild)
|
|
_rebuild()
|
|
|
|
@export var suppress_warnings := false :
|
|
set(s):
|
|
suppress_warnings = s
|
|
_rebuild()
|
|
|
|
var _mesh_instance : MeshInstance3D
|
|
var _errors : Array[String]
|
|
|
|
func get_triangle_mesh() -> TriangleMesh:
|
|
if _mesh_instance.mesh != null:
|
|
return _mesh_instance.mesh.generate_triangle_mesh()
|
|
return null
|
|
|
|
func _ready():
|
|
_mesh_instance = MeshInstance3D.new()
|
|
add_child(_mesh_instance)
|
|
_rebuild()
|
|
|
|
func _get_configuration_warnings():
|
|
if not suppress_warnings:
|
|
return PackedStringArray(_errors)
|
|
return PackedStringArray()
|
|
|
|
func get_errors() -> Array[String]:
|
|
return _errors
|
|
|
|
func _rebuild():
|
|
_errors = []
|
|
if _mesh_instance == null:
|
|
return
|
|
if spots.size() < 2:
|
|
_errors.append("the spots array must have at least 2 entries")
|
|
_mesh_instance.mesh = null
|
|
var u := 0.0
|
|
var segments : Array[VizSegment] = []
|
|
for idx in range(1, spots.size()):
|
|
segments.push_back(VizSegment.new(spots[idx-1], spots[idx], path_width, bend_lip))
|
|
for idx in range(1, segments.size()):
|
|
if segments[idx-1].is_invalid():
|
|
_errors.push_back(segments[idx-1].get_error())
|
|
break
|
|
var midpoint := VizMid.new(segments[idx-1], segments[idx], path_width, inner_curve_radius)
|
|
if midpoint.is_invalid():
|
|
_errors.push_back(midpoint.get_error())
|
|
break
|
|
# Creating midpoint may invalidate prior segment
|
|
if segments[idx-1].is_invalid():
|
|
_errors.push_back(segments[idx-1].get_error())
|
|
break
|
|
if idx == 1:
|
|
u = _add_tail(segments[idx-1], u)
|
|
u = segments[idx-1].update_mesh(_mesh_instance, u, bend_segs, bend_sharpness, path_mat)
|
|
u = midpoint.update_mesh(_mesh_instance, u, num_curve_segs, path_mat)
|
|
if _errors.size() == 0:
|
|
if segments[segments.size()-1].is_invalid():
|
|
_errors.push_back(segments[segments.size()-1].get_error())
|
|
else:
|
|
if segments.size() == 1:
|
|
u = _add_tail(segments[0], u)
|
|
_add_head(segments[segments.size()-1], u)
|
|
update_configuration_warnings()
|
|
changed_layout.emit()
|
|
|
|
func _add_head(head : VizSegment, u : float):
|
|
if path_head != null:
|
|
var end := head.get_end().point
|
|
var binormal := head.get_end_binormal()
|
|
var left := end - binormal * path_width / 2.0
|
|
var right := end + binormal * path_width / 2.0
|
|
var normal := head.get_end().normal
|
|
var direction := head.get_end_ray()
|
|
var offset := path_head.get_offset(left, right, normal, direction)
|
|
head.adjust_end(offset)
|
|
u = head.update_mesh(_mesh_instance, u, bend_segs, bend_sharpness, path_mat)
|
|
path_head.apply(_mesh_instance, u, left - direction * offset, right - direction * offset, normal, direction, path_mat)
|
|
else:
|
|
head.update_mesh(_mesh_instance, u, bend_segs, bend_sharpness, path_mat)
|
|
|
|
func _add_tail(tail : VizSegment, u : float) -> float:
|
|
if path_tail != null:
|
|
var begin := tail.get_begin().point
|
|
var binormal := tail.get_begin_binormal()
|
|
var left := begin - binormal * path_width / 2.0
|
|
var right := begin + binormal * path_width / 2.0
|
|
var normal := tail.get_begin().normal
|
|
var direction := tail.get_begin_ray()
|
|
var offset := path_tail.get_offset(left, right, normal, direction)
|
|
tail.adjust_begin(offset)
|
|
path_tail.apply(_mesh_instance, u, left - direction * offset, right - direction * offset, normal, direction, path_mat)
|
|
return u + offset
|
|
return u
|