projektluzid/addons/vizpath/visualized_path.gd
2025-06-07 19:10:56 +02:00

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