528 lines
16 KiB
GDScript
528 lines
16 KiB
GDScript
@tool
|
|
extends Node3D
|
|
## Dynamic ramp/staircase shape
|
|
##
|
|
## This node can generate ramps and staircases with a variety of parameters.
|
|
|
|
## Called when the anchor is changed. Used by the `proto_gizmo.dg` script to update gizmo handler positions.
|
|
signal anchor_changed
|
|
|
|
## Called when the width is changed.
|
|
signal width_changed
|
|
|
|
## Called when the height is changed.
|
|
signal height_changed
|
|
|
|
## Called when the depth is changed.
|
|
signal depth_changed
|
|
|
|
## Called when step count is changed.
|
|
signal step_count_changed
|
|
|
|
## Called when type is changed.
|
|
signal type_changed
|
|
|
|
## Called when fill is changed.
|
|
signal fill_changed
|
|
|
|
## Calculation mode for the staircase.
|
|
enum Calculation {
|
|
STAIRCASE_DIMENSIONS, ## Width, depth and height are the same size as the whole staircase.
|
|
STEP_DIMENSIONS, ## Width, depth and height are the same size as a single step.
|
|
}
|
|
|
|
## Anchor point for the ramp. Used to position the ramp in the world.
|
|
enum Anchor {
|
|
BOTTOM_CENTER, ## Default anchor point. The anchor is at the bottom of the ramp. The ramp is positioned in the middle.
|
|
BOTTOM_LEFT, ## The anchor is at the bottom left of the ramp. The ramp is shifted right.
|
|
BOTTOM_RIGHT, ## The anchor is at the bottom right of the ramp. The ramp is shifted left.
|
|
TOP_CENTER, ## The anchor is at the top of the ramp. The ramp is positioned in the middle.
|
|
TOP_LEFT, ## The anchor is at the top left of the ramp. The ramp is shifted right.
|
|
TOP_RIGHT, ## The anchor is at the top right of the ramp. The ramp is shifted left.
|
|
BASE_CENTER, ## The anchor is at the base of the ramp. The ramp is positioned in the middle.
|
|
BASE_LEFT, ## The anchor is at the base left of the ramp. The ramp is shifted right.
|
|
BASE_RIGHT, ## The anchor is at the base right of the ramp. The ramp is shifted left.
|
|
}
|
|
|
|
# TODO: RAMP VS STAIRCASE WRONG WIDTH
|
|
## Act as a ramp without stairs or a staircase with stairs.
|
|
enum Type {
|
|
RAMP, ## Simple CSGPolygon3D shape.
|
|
STAIRCASE, ## Staircase with stairs combined of CSGBox3D shapes.
|
|
}
|
|
|
|
## Used to avoid setting properties traditionally on initialization to avoid bugs.
|
|
var is_entered_tree := false
|
|
|
|
## Storing CSG shapes for easy access without interfering with children.
|
|
var shape_polygon: CSGPolygon3D = null
|
|
|
|
## Used to avoid z-fighting and incorrect snapping between steps.
|
|
var epsilon: float = 0.0001
|
|
|
|
## Default public values
|
|
const _default_calculation := Calculation.STAIRCASE_DIMENSIONS
|
|
const _default_steps: int = 8
|
|
const _default_width: float = 1.0
|
|
const _default_height: float = 1.0
|
|
const _default_depth: float = 1.0
|
|
const _default_fill: float = 1.0
|
|
const _default_type := Type.RAMP
|
|
const _default_anchor := Anchor.BOTTOM_CENTER
|
|
const _default_anchor_fixed := true
|
|
const _default_collisions_enabled := true
|
|
|
|
## Default private values
|
|
var _calculation := _default_calculation
|
|
var _steps := _default_steps
|
|
var _width := _default_width
|
|
var _height := _default_height
|
|
var _depth := _default_depth
|
|
var _fill := _default_fill
|
|
var _type := _default_type
|
|
var _anchor := _default_anchor
|
|
var _anchor_fixed := _default_anchor_fixed
|
|
var _collisions_enabled := _default_collisions_enabled
|
|
|
|
@export_category("Proto Ramp")
|
|
## Calculation method of width, depth and height.
|
|
var calculation: Calculation: set = set_calculation, get = get_calculation
|
|
|
|
## Number of steps in the staircase.
|
|
var steps: int: set = set_steps, get = get_steps
|
|
|
|
## Width of the ramp/staircase.
|
|
var width: float: set = set_width, get = get_width
|
|
|
|
## Height of the ramp/staircase.
|
|
var height: float: set = set_height, get = get_height
|
|
|
|
## Depth of the ramp/staircase.
|
|
var depth: float: set = set_depth, get = get_depth
|
|
|
|
## Percentage of non-empty space under the ramp/staircase.
|
|
var fill: float: set = set_fill, get = get_fill
|
|
|
|
## Act as a ramp or staircase with steps.
|
|
var type: Type: set = set_type, get = get_type
|
|
|
|
## Anchor point of the ramp/staircase.
|
|
var anchor: Anchor: set = set_anchor, get = get_anchor
|
|
|
|
## Collisions enabled for the ramp/staircase.
|
|
var collisions_enabled: bool: set = set_collisions_enabled, get = get_collisions_enabled
|
|
|
|
## If true, the anchor point will not move in global space changed when the anchor is changed.
|
|
## Instead, the ramp/staircase will move in local space.
|
|
var anchor_fixed: bool: set = set_anchor_fixed, get = get_anchor_fixed
|
|
|
|
var material: Variant: set = set_material, get = get_material
|
|
|
|
func _get_property_list() -> Array[Dictionary]:
|
|
var list: Array[Dictionary] = [
|
|
{"name": "type", "type": TYPE_INT, "hint": PROPERTY_HINT_ENUM, "hint_string": "Ramp,Staircase"},
|
|
{"name": "collisions_enabled", "type": TYPE_BOOL},
|
|
{"name": "width", "type": TYPE_FLOAT, "hint": PROPERTY_HINT_RANGE, "hint_string": "0.001,100,0.01,or_greater"},
|
|
{"name": "height", "type": TYPE_FLOAT, "hint": PROPERTY_HINT_RANGE, "hint_string": "0.001,100,0.01,or_greater"},
|
|
{"name": "depth", "type": TYPE_FLOAT, "hint": PROPERTY_HINT_RANGE, "hint_string": "0.001,100,0.01,or_greater"},
|
|
{"name": "anchor", "type": TYPE_INT, "hint": PROPERTY_HINT_ENUM, "hint_string": "Bottom Center,Bottom Left,Bottom Right,Top Center,Top Left,Top Right,Base Center,Base Left,Base Right"},
|
|
{"name": "anchor_fixed", "type": TYPE_BOOL},
|
|
{"name": "fill", "type": TYPE_FLOAT, "hint": PROPERTY_HINT_RANGE, "hint_string": "0.000,1.000,0.001"},
|
|
{"name": "material","class_name": &"BaseMaterial3D,ShaderMaterial", "type": 24, "hint": 17, "hint_string": "BaseMaterial3D,ShaderMaterial", "usage": 6 }
|
|
]
|
|
|
|
# Staircase exclusive properties
|
|
if type == Type.STAIRCASE:
|
|
list += [
|
|
{"name": "calculation", "type": TYPE_INT, "hint": PROPERTY_HINT_ENUM, "hint_string": "Staircase Dimensions,Step Dimensions"},
|
|
{"name": "steps", "type": TYPE_INT, "hint": PROPERTY_HINT_RANGE, "hint_string": "1,100,1,or_greater"},
|
|
]
|
|
|
|
return list
|
|
|
|
func _set(property: StringName, value: Variant) -> bool:
|
|
match property:
|
|
"type":
|
|
set_type(value)
|
|
return true
|
|
"calculation":
|
|
set_calculation(value)
|
|
return true
|
|
"steps":
|
|
set_steps(value)
|
|
return true
|
|
"width":
|
|
set_width(value)
|
|
return true
|
|
"height":
|
|
set_height(value)
|
|
return true
|
|
"depth":
|
|
set_depth(value)
|
|
return true
|
|
"fill":
|
|
set_fill(value)
|
|
return true
|
|
"anchor":
|
|
set_anchor(value)
|
|
return true
|
|
"anchor_fixed":
|
|
set_anchor_fixed(value)
|
|
return true
|
|
"material":
|
|
set_material(value)
|
|
return true
|
|
"collisions_enabled":
|
|
set_collisions_enabled(value)
|
|
return true
|
|
|
|
return false
|
|
|
|
func _property_can_revert(property: StringName) -> bool:
|
|
if property in ["type", "calculation", "steps", "width", "height", "depth", "fill", "anchor", "anchor_fixed", "material", "collisions_enabled"]:
|
|
return true
|
|
return false
|
|
|
|
func _property_get_revert(property: StringName) -> Variant:
|
|
match property:
|
|
"type":
|
|
return _default_type
|
|
"calculation":
|
|
return _default_calculation
|
|
"steps":
|
|
return _default_steps
|
|
"width":
|
|
return _default_width
|
|
"height":
|
|
return _default_height
|
|
"depth":
|
|
return _default_depth
|
|
"fill":
|
|
return _default_fill
|
|
"anchor":
|
|
return _default_anchor
|
|
"anchor_fixed":
|
|
return _default_anchor_fixed
|
|
"material":
|
|
return null
|
|
"collisions_enabled":
|
|
return _default_collisions_enabled
|
|
return null
|
|
|
|
func get_type() -> Type:
|
|
return _type
|
|
|
|
func get_calculation() -> Calculation:
|
|
return _calculation
|
|
|
|
func get_width() -> float:
|
|
return _width
|
|
|
|
func get_height() -> float:
|
|
return _height
|
|
|
|
func get_depth() -> float:
|
|
return _depth
|
|
|
|
func get_fill() -> float:
|
|
return _fill
|
|
|
|
func get_steps() -> int:
|
|
return _steps
|
|
|
|
func get_anchor() -> Anchor:
|
|
return _anchor
|
|
|
|
func get_collisions_enabled() -> bool:
|
|
return _collisions_enabled
|
|
|
|
func get_anchor_fixed() -> bool:
|
|
return _anchor_fixed
|
|
|
|
func get_material() -> Variant:
|
|
return material
|
|
|
|
## Get the step depth of the staircase.
|
|
func get_true_step_depth() -> float:
|
|
if type == Type.RAMP or calculation == Calculation.STEP_DIMENSIONS:
|
|
return depth
|
|
else:
|
|
return depth / steps
|
|
|
|
## Get the whole depth of the ramp/staircase.
|
|
func get_true_depth() -> float:
|
|
if type == Type.STAIRCASE:
|
|
return get_true_step_depth() * steps
|
|
else:
|
|
return get_true_step_depth()
|
|
|
|
## Get the step height of the staircase.
|
|
func get_true_step_height() -> float:
|
|
if type == Type.RAMP or calculation == Calculation.STEP_DIMENSIONS:
|
|
return height
|
|
else:
|
|
return height / steps
|
|
|
|
## Get the whole height of the ramp/staircase.
|
|
func get_true_height() -> float:
|
|
if type == Type.STAIRCASE:
|
|
return get_true_step_height() * steps
|
|
else:
|
|
return get_true_step_height()
|
|
|
|
## Get the anchor offset for a specific anchor according to the dimensions of the ramp/staircase.
|
|
func get_anchor_offset(anchor: Anchor) -> Vector3:
|
|
var offset := Vector3()
|
|
var depth: float = get_true_depth()
|
|
var height: float = get_true_height()
|
|
match anchor:
|
|
Anchor.BOTTOM_CENTER:
|
|
offset = Vector3(0, 0, 0)
|
|
Anchor.BOTTOM_LEFT:
|
|
offset = Vector3(-width / 2.0, 0, 0)
|
|
Anchor.BOTTOM_RIGHT:
|
|
offset = Vector3(width / 2.0, 0, 0)
|
|
Anchor.TOP_CENTER:
|
|
offset = Vector3(0, -height, -depth)
|
|
Anchor.TOP_LEFT:
|
|
offset = Vector3(-width / 2.0, -height, -depth)
|
|
Anchor.TOP_RIGHT:
|
|
offset = Vector3(width / 2.0, -height, -depth)
|
|
Anchor.BASE_CENTER:
|
|
offset = Vector3(0, 0, -depth)
|
|
Anchor.BASE_LEFT:
|
|
offset = Vector3(-width / 2.0, 0, -depth)
|
|
Anchor.BASE_RIGHT:
|
|
offset = Vector3(width / 2.0, 0, -depth)
|
|
return offset
|
|
|
|
func set_type(value: Type) -> void:
|
|
_type = value
|
|
notify_property_list_changed()
|
|
if is_entered_tree:
|
|
# Staircase: dimensions are reset from forced STAIRCASE_DIMENSIONS calculation
|
|
# Ramp: dimensions are forced to STAIRCASE_DIMENSIONS calculation
|
|
if calculation == Calculation.STEP_DIMENSIONS:
|
|
match type:
|
|
Type.STAIRCASE:
|
|
_height /= steps
|
|
_depth = (_depth + epsilon) / steps
|
|
Type.RAMP:
|
|
_height *= steps
|
|
_depth = (_depth + epsilon) * steps
|
|
refresh_shape()
|
|
type_changed.emit()
|
|
update_gizmos()
|
|
|
|
## Sets the calculation method and recalculates the dimensions of the ramp/staircase.
|
|
func set_calculation(value: Calculation) -> void:
|
|
_calculation = value
|
|
# Calculate current step or staircase dimensions
|
|
# Only affecting dimensions when in STAIRCASE mode
|
|
if is_entered_tree:
|
|
match calculation:
|
|
Calculation.STAIRCASE_DIMENSIONS:
|
|
if type == Type.STAIRCASE:
|
|
_height *= steps
|
|
_depth = (_depth + epsilon) * steps
|
|
Calculation.STEP_DIMENSIONS:
|
|
if type == Type.STAIRCASE:
|
|
_height /= steps
|
|
_depth = (_depth + epsilon) / steps
|
|
|
|
func set_width(value: float) -> void:
|
|
_width = value
|
|
refresh_shape()
|
|
width_changed.emit()
|
|
update_gizmos()
|
|
|
|
func set_height(value: float) -> void:
|
|
_height = value
|
|
refresh_shape()
|
|
height_changed.emit()
|
|
update_gizmos()
|
|
|
|
func set_depth(value: float) -> void:
|
|
_depth = value
|
|
refresh_shape()
|
|
depth_changed.emit()
|
|
update_gizmos()
|
|
|
|
func set_fill(value: float) -> void:
|
|
_fill = max(0.0, min(1.0, value))
|
|
refresh_shape()
|
|
fill_changed.emit()
|
|
update_gizmos()
|
|
|
|
## Translates the ramp/staircase to a new anchor point in local space.
|
|
## Then recalculates the stairs/ramp with the new offset.
|
|
func set_anchor(value: Anchor) -> void:
|
|
# Transform node to new anchor
|
|
translate_anchor(anchor, value)
|
|
_anchor = value
|
|
refresh_shape()
|
|
anchor_changed.emit()
|
|
update_gizmos()
|
|
|
|
func set_collisions_enabled(value: bool) -> void:
|
|
_collisions_enabled = value
|
|
notify_property_list_changed()
|
|
refresh_shape()
|
|
|
|
## Translates the ramp/staircase to a new anchor point in local space if anchor is not fixed.
|
|
func translate_anchor(from_anchor: Anchor, to_anchor: Anchor) -> void:
|
|
if not anchor_fixed:
|
|
translate_object_local(get_anchor_offset(from_anchor) - get_anchor_offset(to_anchor))
|
|
|
|
func set_anchor_fixed(value: bool) -> void:
|
|
_anchor_fixed = value
|
|
|
|
func set_material(value: Variant) -> void:
|
|
material = value
|
|
refresh_shape()
|
|
|
|
func set_steps(value: int) -> void:
|
|
_steps = value
|
|
refresh_shape()
|
|
step_count_changed.emit()
|
|
update_gizmos()
|
|
|
|
## Deletes all children and generates new steps/ramp.
|
|
func refresh_shape() -> void:
|
|
var offset := get_anchor_offset(anchor)
|
|
var polygon_offset := Vector3(offset.z, offset.y, -offset.x) + Vector3(0, 0, width / 2.0)
|
|
|
|
# Resetting anchor offset to 0,0,0 to avoid problems during step calculations
|
|
translate_anchor(anchor, Anchor.BOTTOM_CENTER)
|
|
|
|
if shape_polygon != null:
|
|
remove_child(shape_polygon)
|
|
shape_polygon.queue_free()
|
|
|
|
shape_polygon = CSGPolygon3D.new()
|
|
shape_polygon.use_collision = false
|
|
|
|
match type:
|
|
Type.STAIRCASE:
|
|
shape_polygon.polygon = create_staircase_array()
|
|
Type.RAMP:
|
|
shape_polygon.polygon = create_ramp_array()
|
|
|
|
shape_polygon.rotate(Vector3.UP, -PI / 2.0)
|
|
shape_polygon.translate(polygon_offset)
|
|
shape_polygon.depth = width
|
|
|
|
shape_polygon.use_collision = collisions_enabled
|
|
if collisions_enabled and material == null:
|
|
var shape_material := StandardMaterial3D.new()
|
|
shape_material.albedo_color = Color.AQUA
|
|
shape_polygon.material = shape_material
|
|
else:
|
|
shape_polygon.material = material
|
|
|
|
add_child(shape_polygon)
|
|
|
|
# Restore anchor offset
|
|
translate_anchor(Anchor.BOTTOM_CENTER, anchor)
|
|
|
|
## Adds a new ramp based on current dimensions (without any anchor offset).
|
|
func create_ramp_array() -> PackedVector2Array:
|
|
# Create a single CSGPolygon3D
|
|
var array := PackedVector2Array()
|
|
if fill == 1:
|
|
array.append(Vector2(0, 0))
|
|
array.append(Vector2(get_true_depth(), 0))
|
|
array.append(Vector2(get_true_depth(), get_true_height()))
|
|
|
|
if fill == 0:
|
|
array.append(Vector2(0, 0))
|
|
array.append(Vector2(get_true_depth() * 0.001, 0))
|
|
array.append(Vector2(get_true_depth(), get_true_height() * 0.999))
|
|
array.append(Vector2(get_true_depth(), get_true_height()))
|
|
|
|
if fill < 1.0 and fill > 0.0:
|
|
array.append(Vector2(0, 0))
|
|
array.append(Vector2(get_true_depth() * fill, 0))
|
|
array.append(Vector2(get_true_depth(), get_true_height() * (1 - fill)))
|
|
array.append(Vector2(get_true_depth(), get_true_height()))
|
|
|
|
return array
|
|
|
|
func create_staircase_array() -> PackedVector2Array:
|
|
# Create a staircase with CSGBox3Ds
|
|
var array := PackedVector2Array()
|
|
|
|
if fill == 1:
|
|
# Base:
|
|
# 4
|
|
# |
|
|
# | 1
|
|
# | |
|
|
# 3--------2
|
|
array.append(Vector2(0, get_true_step_height())) # 1
|
|
array.append(Vector2(0, 0)) # 2
|
|
array.append(Vector2(get_true_depth(), 0)) # 3
|
|
array.append(Vector2(get_true_depth(), get_true_height())) # 4
|
|
|
|
if fill == 0:
|
|
# Base:
|
|
# 4
|
|
# \
|
|
# \ 1
|
|
# \ |
|
|
# 2
|
|
array.append(Vector2(0, get_true_step_height())) # 1
|
|
array.append(Vector2(0, 0)) # 2
|
|
# No #3 present
|
|
array.append(Vector2(get_true_depth(), get_true_height())) # 4
|
|
|
|
if fill < 1.0 and fill > 0.0:
|
|
# Base:
|
|
# 4
|
|
# |
|
|
# 3b 1
|
|
# \ |
|
|
# 3a---2
|
|
array.append(Vector2(0, get_true_step_height())) # 1
|
|
array.append(Vector2(0, 0)) # 2
|
|
array.append(Vector2(get_true_depth() * fill, 0)) # 3a
|
|
array.append(Vector2(get_true_depth(), get_true_height() * (1 - fill))) # 3b
|
|
array.append(Vector2(get_true_depth(), get_true_height())) # 4
|
|
|
|
# Steps:
|
|
# 4---5
|
|
# | |
|
|
# | 6---7
|
|
# | |
|
|
# | 8---1
|
|
# | |
|
|
# 3-----------2
|
|
for i in range(steps - 1):
|
|
array.append(Vector2(get_true_depth() - get_true_step_depth() * (i + 1), get_true_height() - get_true_step_height() * i))
|
|
array.append(Vector2(get_true_depth() - get_true_step_depth() * (i + 1), get_true_height() - get_true_step_height() * (i + 1)))
|
|
|
|
return array
|
|
|
|
## Using dynamic type for gizmos to avoid packaging errors.
|
|
## See proto_ramp_gizmos.gd for more information.
|
|
var gizmos = null
|
|
|
|
func _enter_tree() -> void:
|
|
# is_entered_tree is used to avoid setting properties traditionally on initialization
|
|
refresh_shape()
|
|
if material:
|
|
set_material(material)
|
|
if Engine.is_editor_hint():
|
|
var ProtoRampGizmos = load("res://addons/proto_shape/proto_ramp/proto_ramp_gizmos.gd")
|
|
gizmos = ProtoRampGizmos.new()
|
|
gizmos.attach_ramp(self)
|
|
|
|
is_entered_tree = true
|
|
|
|
func _exit_tree() -> void:
|
|
# Remove all children
|
|
remove_child(shape_polygon)
|
|
shape_polygon.queue_free()
|
|
if Engine.is_editor_hint():
|
|
gizmos.remove_ramp()
|